%%
%% chemidentifier-v2.sty
%%
%% Numerotation logique et hyperliens pour les composes chimiques.
%% Logical numbering and hyperlinking of chemical compounds.
%%
%% Copyright (C) 2026 Aliocha SKRZYPCZAK <aliochask.pro@gmail.com>
%%
%% 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.
%%
%% This work has the LPPL maintenance status `maintained'.
%% The Current Maintainer of this work is Aliocha SKRZYPCZAK.
%%
\ProvidesExplFile {chemidentifier-v2.sty} {2026-08-19} {2.0.0}
  {Logical numbering and hyperlinking of chemical compounds}

%% ------------------------------------------------------------------
%% 1. Variants used throughout
%% ------------------------------------------------------------------

\cs_generate_variant:Nn \prop_gput:Nnn      { NVn, NVV, NVe, Nne, Nen }
\cs_generate_variant:Nn \prop_get:NnN       { NVN }
\cs_generate_variant:Nn \prop_item:Nn       { NV, Ne }
\cs_generate_variant:Nn \prop_if_in:NnTF    { NVTF, NeTF }
\cs_generate_variant:Nn \prop_if_in_p:Nn    { NV }
\cs_generate_variant:Nn \prop_gremove:Nn    { NV }
\cs_generate_variant:Nn \tl_if_blank_p:n    { V }
\cs_generate_variant:Nn \seq_set_split:Nnn  { NnV }
\cs_generate_variant:Nn \msg_error:nnn      { nnV }
\cs_generate_variant:Nn \msg_error:nnnn     { nnVV }
\cs_generate_variant:Nn \msg_warning:nnn    { nnV }
\cs_generate_variant:Nn \msg_info:nnnn      { nnVV }
\cs_generate_variant:Nn \str_case:nnF       { VnF }

%% ------------------------------------------------------------------
%% 2. Variables
%% ------------------------------------------------------------------

% Main counter: incremented once per new parent key.
\int_new:N \g__chemid_main_int

% key -> main number
\prop_new:N \g__chemid_num_prop
% key -> sub index (0 when the key is a parent)
\prop_new:N \g__chemid_sub_prop
% key -> parent part of the key
\prop_new:N \g__chemid_parent_prop
% key -> rich (typeset) custom name, empty when the key is auto-numbered
\prop_new:N \g__chemid_rich_prop
% key -> raw (PDF-string) custom name
\prop_new:N \g__chemid_raw_prop
% key -> number of \chemidhere anchors placed so far
\prop_new:N \g__chemid_anchor_prop
% key -> anchored only because \chemidhere{parent} anchors the whole
% family; an anchor of the key's own removes it from here
\prop_new:N \g__chemid_weakanchor_prop
% parent -> first of its children to have been anchored: where a link
% to the parent goes when the parent itself is never anchored
\prop_new:N \g__chemid_firstkid_prop
% parent -> current sub counter
\prop_new:N \g__chemid_subcount_prop
% --- templated families (\chemidfamily) ---------------------------
% parent -> how a child's value is dressed up, as code calling
% \chemidvalue (e.g. '\textsubscript{\chemidvalue}').  Non-empty for a
% family parent, empty for every other key: this *is* the flag saying
% 'this parent is a family'.
\prop_new:N \g__chemid_famformat_prop
% parent -> style numbering its automatic children (arabic, alph,
% lower-greek...), overriding the document-wide 'sub-style'
\prop_new:N \g__chemid_famstyle_prop
% parent -> what \chemid{parent} alone shows in place of a value
\prop_new:N \g__chemid_famholder_prop
\prop_new:N \g__chemid_famholderraw_prop
% parent -> 1 when its children carry values of their own, 0 when they
% are numbered automatically.  The two cannot be mixed, and this is
% what says which of the two the family settled on.
\prop_new:N \g__chemid_famexplicit_prop
% child -> its value, the part that varies inside the family
\prop_new:N \g__chemid_value_prop
\prop_new:N \g__chemid_valueraw_prop
% parent -> created implicitly by a child, not yet declared on its own
\prop_new:N \g__chemid_implicit_prop
% key -> used at least once by \chemid
\prop_new:N \g__chemid_used_prop
% key -> seen unknown by \__chemid_record_one:n, verdict deferred to
% \end{document} (\__chemid_check_pending: raises the real error only
% if the key is STILL unknown then).  Repurposed from a simple
% "already reported" flag: either way a given key is only ever added
% to it once.
\prop_new:N \g__chemid_reported_prop
% key -> anchored during the *previous* run (read back from the .aux
% file), whichever way; and the subset anchored *explicitly*, which is
% what tells \chemidhere{parent} to leave that child alone
\prop_new:N \g__chemid_prevanchor_prop
\prop_new:N \g__chemid_prevexplicit_prop
% parent -> first anchored child, likewise read back from the .aux
\prop_new:N \g__chemid_prev_firstkid_prop
% babel/polyglossia language name -> separator before the last family,
% e.g. { french } -> { ~et~ }, { german } -> { ~und~ }.
\prop_new:N \g__chemid_lang_lastsep_prop

% "Previous pass" mirrors of the five tables above, read back from the
% .aux file at \begin{document} (\chemidregister, written once per key
% at \end{document}).  They let \chemid / \herechemid be used *before*
% the corresponding \chemid* in the source, exactly the way \ref
% resolves against \label -- a second compilation away, and stable
% afterwards, since the declaration order itself never depends on where
% the uses sit.
\prop_new:N \g__chemid_prev_num_prop
\prop_new:N \g__chemid_prev_sub_prop
\prop_new:N \g__chemid_prev_parent_prop
\prop_new:N \g__chemid_prev_rich_prop
\prop_new:N \g__chemid_prev_raw_prop
\prop_new:N \g__chemid_prev_famformat_prop
\prop_new:N \g__chemid_prev_famstyle_prop
\prop_new:N \g__chemid_prev_famholder_prop
\prop_new:N \g__chemid_prev_famholderraw_prop
\prop_new:N \g__chemid_prev_famexplicit_prop
\prop_new:N \g__chemid_prev_value_prop
\prop_new:N \g__chemid_prev_valueraw_prop

% Declaration order == logical order of synthesis.
\seq_new:N \g__chemid_order_seq

% Scratch
\seq_new:N  \l__chemid_parts_seq
\clist_new:N \l__chemid_pendingkeys_clist
\str_new:N  \l__chemid_key_str
\str_new:N  \l__chemid_parent_str
\str_new:N  \l__chemid_child_str
\str_new:N  \l__chemid_name_str
\str_new:N  \l__chemid_herekey_str
\str_new:N  \l__chemid_herechild_str
\tl_new:N   \l__chemid_tmp_tl
\tl_new:N   \l__chemid_rich_tl
\tl_new:N   \l__chemid_raw_tl
\tl_new:N   \l__chemid_valuerich_tl
\tl_new:N   \l__chemid_valueraw_tl
\tl_new:N   \l__chemid_curstyle_tl
\tl_new:N   \l__chemid_word_tl
\bool_new:N \l__chemid_ok_bool
\dim_new:N  \l__chemid_prevdepth_dim

% True while a .toc/.lof/.lot file is being typeset: links must be
% suppressed there, the entry being already a link to the section.
\bool_new:N \l__chemid_toc_bool
% True when hyperref is available.
\bool_new:N \g__chemid_hyper_bool
% True once the \herechemid -> \chemidhere rename has been mentioned.
\bool_new:N \g__chemid_saidrename_bool

%% ------------------------------------------------------------------
%% 3. Package options
%% ------------------------------------------------------------------

\tl_new:N   \l__chemid_listsep_tl
\tl_new:N   \l__chemid_lastsep_tl
\tl_new:N   \l__chemid_subsep_tl
\tl_new:N   \l__chemid_rangesep_tl
\tl_new:N   \l__chemid_format_tl
\tl_new:N   \l__chemid_mainstyle_tl
\tl_new:N   \l__chemid_substyle_tl
\tl_new:N   \l__chemid_prefix_tl
\tl_new:N   \l__chemid_unknown_tl
\int_new:N  \l__chemid_threshold_int
\bool_new:N \l__chemid_purify_bool
\bool_new:N \l__chemid_implicit_bool
\bool_new:N \g__chemid_strict_bool
\bool_new:N \l__chemid_autolang_bool
\bool_new:N \l__chemid_links_bool
\bool_new:N \l__chemid_anchorkids_bool
\tl_new:N   \l__chemid_pdftexfont_tl

\keys_define:nn { chemidentifier }
  {
    list-sep         .tl_set:N    = \l__chemid_listsep_tl ,
    list-sep         .initial:n   = { ,~ } ,
    last-sep         .tl_set:N    = \l__chemid_lastsep_tl ,
    last-sep         .initial:n   = { ~and~ } ,
    sub-sep          .tl_set:N    = \l__chemid_subsep_tl ,
    sub-sep          .initial:n   = { , } ,
    range-sep        .tl_set:N    = \l__chemid_rangesep_tl ,
    range-sep        .initial:n   = { - } ,
    range-threshold  .int_set:N   = \l__chemid_threshold_int ,
    range-threshold  .initial:n   = { 3 } ,
    format           .tl_set:N    = \l__chemid_format_tl ,
    format           .initial:n   = { } ,
    main-style       .tl_set:N    = \l__chemid_mainstyle_tl ,
    main-style       .initial:n   = { arabic } ,
    sub-style        .tl_set:N    = \l__chemid_substyle_tl ,
    sub-style        .initial:n   = { alph } ,
    prefix           .tl_set:N    = \l__chemid_prefix_tl ,
    prefix           .initial:n   = { chemid. } ,
    unknown-text     .tl_set:N    = \l__chemid_unknown_tl ,
    unknown-text     .initial:n   = { ?? } ,
    purify           .bool_set:N  = \l__chemid_purify_bool ,
    purify           .initial:n   = { true } ,
    implicit-parent  .bool_set:N  = \l__chemid_implicit_bool ,
    implicit-parent  .initial:n   = { true } ,
    strict-anchors   .bool_gset:N = \g__chemid_strict_bool ,
    strict-anchors   .initial:n   = { true } ,
    links            .bool_set:N  = \l__chemid_links_bool ,
    links            .initial:n   = { true } ,
    anchor-children  .bool_set:N  = \l__chemid_anchorkids_bool ,
    anchor-children  .initial:n   = { true } ,
    auto-lang        .bool_set:N  = \l__chemid_autolang_bool ,
    auto-lang        .initial:n   = { true } ,
    pdftex-font      .tl_set:N    = \l__chemid_pdftexfont_tl ,
    pdftex-font      .initial:n   = { \normalfont } ,
    % Already consumed by the loader (chemidentifier.sty), which picked
    % this file on the strength of it: accepted and ignored here.
    version          .code:n      = { } ,
    unknown          .code:n      =
      { \msg_warning:nnV { chemidentifier } { unknown-option } \l_keys_key_str } ,
  }

% Les options du paquetage sont traitees plus bas, une fois les messages
% declares : l'option inconnue doit pouvoir etre signalee.

\NewDocumentCommand \chemidsetup { m }
  { \keys_set:nn { chemidentifier } {#1} }

% --- Multilingue : "et" / "and" suivant la langue babel courante ------
%
% \languagename est mise a jour par babel a chaque \selectlanguage : une
% simple macro, lue ici de facon pleinement expansible, sans imposer
% babel comme dependance.
%
% \languagename existe desormais TOUJOURS (le noyau LaTeX la definit,
% valeur 'english' par defaut, meme sans babel) : elle ne peut donc pas
% servir a detecter babel.  \bbl@error, elle, n'est definie que par
% babel.sty lui-meme : c'est ce qu'on teste.  Le nom contient '@', qui
% n'est pas touche par \ExplSyntaxOn (seuls _ et : le sont) : il faut
% donc \makeatletter autour de cette seule ligne.
\group_begin:
  \char_set_catcode_letter:N \@
  \cs_new:Npn \__chemid_babel_loaded_p: { \cs_if_exist_p:N \bbl@error }
\group_end:

\NewDocumentCommand \chemidaddlanguage { m m }
  { \prop_gput:Nnn \g__chemid_lang_lastsep_prop {#1} {#2} }

% Anglais (defaut du paquetage)
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { english }    { ~and~ }
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { american }   { ~and~ }
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { british }    { ~and~ }
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { australian } { ~and~ }
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { UKenglish }  { ~and~ }
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { USenglish }  { ~and~ }
% Francais
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { french }     { ~et~ }
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { francais }   { ~et~ }
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { acadian }    { ~et~ }
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { canadien }   { ~et~ }
% Allemand
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { german }     { ~und~ }
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { ngerman }    { ~und~ }
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { austrian }   { ~und~ }
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { naustrian }  { ~und~ }
% Espagnol
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { spanish }    { ~y~ }
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { mexican }    { ~y~ }
% Italien
\prop_gput:Nnn \g__chemid_lang_lastsep_prop { italian }    { ~e~ }

% Fully expandable: usable inside the render engine (text, ToC, bookmarks
% alike).  Falls back to the static 'last-sep' when auto-lang is off,
% babel is absent, or the current language has no registered entry.
\cs_new:Npn \__chemid_effective_lastsep:
  {
    \bool_if:NTF \l__chemid_autolang_bool
      {
        \bool_if:nTF { \__chemid_babel_loaded_p: }
          {
            \prop_if_in:NeTF \g__chemid_lang_lastsep_prop { \languagename }
              { \prop_item:Ne \g__chemid_lang_lastsep_prop { \languagename } }
              { \l__chemid_lastsep_tl }
          }
          { \l__chemid_lastsep_tl }
      }
      { \l__chemid_lastsep_tl }
  }

%% ------------------------------------------------------------------
%% 4. Messages
%% ------------------------------------------------------------------

\msg_new:nnnn { chemidentifier } { undefined-key }
  { Compound~key~'#1'~has~not~been~declared. }
  {
    Every~key~must~be~declared~with~\iow_char:N \\chemid*{#1}~*before*~it~is~
    used;~ '\l__chemid_unknown_tl'~is~printed~instead.
  }

\msg_new:nnnn { chemidentifier } { duplicate-key }
  { Compound~key~'#1'~is~declared~twice. }
  { The~second~declaration~is~ignored;~the~first~one~keeps~its~number. }

\msg_new:nnnn { chemidentifier } { key-too-deep }
  { Compound~key~'#1'~has~more~than~one~level~of~hierarchy. }
  {
    Only~'parent'~and~'parent.child'~are~supported;~
    the~declaration~is~ignored.
  }

\msg_new:nnnn { chemidentifier } { invalid-key }
  { Compound~key~'#1'~is~empty~or~malformed. }
  { A~key~and~each~of~its~parts~must~be~non-empty;~the~declaration~is~ignored. }

\msg_new:nnnn { chemidentifier } { comma-in-key }
  { Compound~key~'#1'~contains~a~comma. }
  {
    Commas~separate~keys~in~\iow_char:N \\chemid*{key1,key2,...}~and~
    \iow_char:N \\chemid{key1,key2,...};~a~single~key~cannot~contain~one~
    itself.~The~declaration~is~ignored.
  }

\msg_new:nnnn { chemidentifier } { multi-key-custom-name }
  { A~custom~name~cannot~be~given~to~several~keys~at~once~('#1'). }
  {
    \iow_char:N \\chemid*{key1,key2}{rich~name}~would~be~ambiguous:~which~
    key~gets~the~name?~Declare~that~key~on~its~own,~or~drop~the~name~and~
    let~every~key~be~numbered~automatically.~The~whole~declaration~is~
    ignored.
  }

\msg_new:nnnn { chemidentifier } { missing-anchor }
  { Compound~'#1'~is~used~but~has~no~\iow_char:N \\chemidhere~anchor. }
  {
    Add~\iow_char:N \\chemidhere{#1}~in~the~scheme~or~figure~showing~
    this~compound;~printed~without~an~anchor,~it~is~not~a~link~at~all.
  }

\msg_new:nnnn { chemidentifier } { duplicate-anchor }
  { Compound~'#1'~has~more~than~one~\iow_char:N \\chemidhere~anchor. }
  {
    Only~one~anchor~per~key~is~meaningful:~the~PDF~target~is~ambiguous~
    and~most~viewers~keep~the~last~one.
  }

\msg_new:nnnn { chemidentifier } { anchor-undefined-key }
  { \iow_char:N \\chemidhere~used~with~the~undeclared~key~'#1'. }
  { Declare~it~first~with~\iow_char:N \\chemid*{#1};~the~anchor~is~not~placed. }

\msg_new:nnnn { chemidentifier } { renamed-herechemid }
  { \iow_char:N \\herechemid~is~now~called~\iow_char:N \\chemidhere. }
  {
    The~old~name~still~works,~and~will~keep~working;~but~every~command~
    of~the~package~now~begins~with~'chemid'~(\iow_char:N \\chemidhere,~
    \iow_char:N \\chemidkey,~\iow_char:N \\chemidscheme...).~
    Mentioned~once~per~document.
  }

\msg_new:nnnn { chemidentifier } { implicit-parent }
  { Parent~key~'#1'~created~implicitly~by~the~declaration~of~'#2'. }
  {
    Declaring~the~parent~first~makes~the~numbering~easier~to~follow.~
    Set~'implicit-parent=false'~to~turn~this~into~an~error.
  }

\msg_new:nnnn { chemidentifier } { no-implicit-parent }
  { Parent~key~'#1'~of~'#2'~has~not~been~declared. }
  { 'implicit-parent'~is~false,~so~the~declaration~of~'#2'~is~ignored. }

\msg_new:nnnn { chemidentifier } { no-hyperref }
  { Package~'hyperref'~is~not~loaded. }
  {
    chemidentifier~still~numbers~everything,~but~nothing~is~clickable.~
    Load~hyperref~to~get~the~links.
  }

\msg_new:nnn { chemidentifier } { unknown-option }
  { Unknown~package~option~'#1'~ignored. }

\msg_new:nnnn { chemidentifier } { family-needs-parent }
  { \iow_char:N \\chemidfamily~needs~a~parent~key,~not~'#1'. }
  {
    A~family~is~a~parent~and~the~children~hanging~off~it:~declare~it~as~
    \iow_char:N \\chemidfamily{parent}{...},~then~its~children~as~
    \iow_char:N \\chemid*{parent.child}.~This~declaration~is~ignored.
  }

\msg_new:nnnn { chemidentifier } { family-twice }
  { Compound~'#1'~is~already~a~family. }
  { The~second~\iow_char:N \\chemidfamily~is~ignored. }

\msg_new:nnnn { chemidentifier } { family-mixed-values }
  { Child~'#1'~#2~a~value~of~its~own,~unlike~the~rest~of~the~family. }
  {
    Inside~one~family,~either~every~child~carries~its~own~value~
    (\iow_char:N \\chemid*{key}{4})~or~none~does~and~they~are~all~
    numbered~automatically~--~mixing~the~two~leaves~no~sane~reading~for~
    the~ones~in~between.~The~child~is~declared~anyway,~with~whatever~
    value~it~could~be~given.
  }

\msg_new:nnnn { chemidentifier } { unknown-family-option }
  { Unknown~\iow_char:N \\chemidfamily~option~'#1'~ignored. }
  {
    The~options~of~a~family~are~'name',~'raw',~'sub-style',~
    'child-format'~and~'placeholder'~(plus~'placeholder-raw').
  }

%% Toutes les options peuvent maintenant etre traitees.
\ProcessKeyOptions [ chemidentifier ]

%% ------------------------------------------------------------------
%% 5. hyperref interface
%% ------------------------------------------------------------------

% hyperref is detected, not loaded: the user keeps full control over its
% options and over the loading order.
\hook_gput_code:nnn { begindocument } { chemidentifier }
  {
    \cs_if_exist:NTF \hyperlink
      { \bool_gset_true:N \g__chemid_hyper_bool }
      {
        \bool_gset_false:N \g__chemid_hyper_bool
        \msg_warning:nn { chemidentifier } { no-hyperref }
      }
  }

% Bookmarks and PDF metadata: \chemid must expand to plain text there.
% \pdfstringdefDisableCommands is hyperref's sanctioned hook for this;
% the replacement is fully expandable, as \pdfstringdef uses \edef.
\hook_gput_code:nnn { begindocument/before } { chemidentifier }
  {
    \cs_if_exist:NT \pdfstringdefDisableCommands
      {
        \pdfstringdefDisableCommands
          {
            \cs_set:Npn \chemid #1 { \__chemid_render:nn { plain } {#1} }
            % Expandable here, and here only: this is where the Greek
            % letter must become the characters hyperref reads.
            % Expandable here, and unsealed twice over: what
            % \tl_item:Nn hands back is protected from expansion just
            % as a property-list value is, and hyperref must be free
            % to read those bytes its own way.
            \cs_set:Npn \chemidgreekchar #1
              { \__chemid_unseal:e { \tl_item:Nn \g__chemid_greekraw_tl {#1} } }
            \cs_set:Npn \chemidhere #1 { }
            \cs_set:Npn \herechemid #1 { }
            \cs_set:Npn \chemidreset { }
          }
      }
  }

% Links must not be nested inside the link of a table-of-contents entry.
% \@starttoc covers \tableofcontents, \listoffigures and \listoftables in
% one single place, whatever the class does to \contentsline.
\hook_gput_code:nnn { begindocument/end } { chemidentifier }
  {
    \cs_if_exist:NT \@starttoc
      {
        \cs_gset_eq:NN \__chemid_orig_starttoc:n \@starttoc
        \cs_gset_protected:Npn \@starttoc #1
          {
            \group_begin:
              \bool_set_true:N \l__chemid_toc_bool
              \__chemid_orig_starttoc:n {#1}
            \group_end:
          }
      }
  }

%% ------------------------------------------------------------------
%% 6. Key parsing and validation
%% ------------------------------------------------------------------

% #1 : raw key.  Sets \l__chemid_key_str, \l__chemid_parent_str and
% \l__chemid_child_str (the latter empty for a parent key).  Returns
% false on a malformed key, the caller then giving up silently.
\prg_new_protected_conditional:Npnn \__chemid_parse:n #1 { TF }
  {
    \bool_set_false:N \l__chemid_ok_bool
    \str_set:Ne \l__chemid_key_str { \tl_trim_spaces:n {#1} }
    % Commas are the list separator of \chemid and \chemid* alike: a key
    % should never see one at this point, since both callers already
    % split on commas before reaching here.  Caught here nonetheless, as
    % a safety net against a key built by macro expansion.
    \str_if_in:NnTF \l__chemid_key_str { , }
      { \msg_error:nnV { chemidentifier } { comma-in-key } \l__chemid_key_str }
      { \__chemid_parse_aux: }
    \bool_if:NTF \l__chemid_ok_bool
      { \prg_return_true: }
      { \prg_return_false: }
  }

% Sets \l__chemid_ok_bool ; split out of the conditional above only to
% keep the comma safety-net readable.
\cs_new_protected:Npn \__chemid_parse_aux:
  {
    \seq_set_split:NnV \l__chemid_parts_seq { . } \l__chemid_key_str
    \int_case:nnF { \seq_count:N \l__chemid_parts_seq }
      {
        { 1 }
          {
            \str_set_eq:NN \l__chemid_parent_str \l__chemid_key_str
            \str_clear:N \l__chemid_child_str
            \str_if_empty:NTF \l__chemid_parent_str
              { \msg_error:nnV { chemidentifier } { invalid-key } \l__chemid_key_str }
              { \bool_set_true:N \l__chemid_ok_bool }
          }
        { 2 }
          {
            \str_set:Ne \l__chemid_parent_str
              { \seq_item:Nn \l__chemid_parts_seq { 1 } }
            \str_set:Ne \l__chemid_child_str
              { \seq_item:Nn \l__chemid_parts_seq { 2 } }
            \bool_lazy_or:nnTF
              { \str_if_empty_p:N \l__chemid_parent_str }
              { \str_if_empty_p:N \l__chemid_child_str }
              { \msg_error:nnV { chemidentifier } { invalid-key } \l__chemid_key_str }
              { \bool_set_true:N \l__chemid_ok_bool }
          }
      }
      { \msg_error:nnV { chemidentifier } { key-too-deep } \l__chemid_key_str }
  }

%% ------------------------------------------------------------------
%% 7. Declaration -- \chemid*{key}
%% ------------------------------------------------------------------

% Registers a brand new parent key with the next main number.
\cs_new_protected:Npn \__chemid_new_parent:N #1
  {
    \int_gincr:N \g__chemid_main_int
    \prop_gput:NVe \g__chemid_num_prop #1 { \int_use:N \g__chemid_main_int }
    \prop_gput:NVn \g__chemid_sub_prop #1 { 0 }
    \prop_gput:NVV \g__chemid_parent_prop #1 #1
    \prop_gput:NVn \g__chemid_rich_prop #1 { }
    \prop_gput:NVn \g__chemid_raw_prop #1 { }
    \prop_gput:NVn \g__chemid_anchor_prop #1 { 0 }
    \prop_gput:NVn \g__chemid_subcount_prop #1 { 0 }
    \seq_gput_right:NV \g__chemid_order_seq #1
  }

% #1 key, #2 rich name, #3 raw name
\cs_new_protected:Npn \__chemid_declare:nnn #1#2#3
  {
    \__chemid_parse:nTF {#1}
      {
        \bool_lazy_and:nnTF
          { \prop_if_in_p:NV \g__chemid_num_prop \l__chemid_key_str }
          { ! \prop_if_in_p:NV \g__chemid_implicit_prop \l__chemid_key_str }
          { \msg_error:nnV { chemidentifier } { duplicate-key } \l__chemid_key_str }
          {
            \str_if_empty:NTF \l__chemid_child_str
              { \__chemid_declare_parent: }
              { \__chemid_declare_child: }
            % Inside a templated family, the argument that would name a
            % key outright is read as the *value* the family varies on:
            % \chemid*{lc.a}{4} is 'Lcoum-C' with a subscript 4, not a
            % compound called '4'.
            \bool_lazy_and:nnTF
              { ! \str_if_empty_p:N \l__chemid_child_str }
              { \__chemid_isfam_p:V \l__chemid_parent_str }
              { \__chemid_store_value:nn {#2} {#3} }
              { \__chemid_store_names:nn {#2} {#3} }
          }
      }
      { }
  }

% A parent key.  It may already exist, having been created implicitly by
% one of its children: it then keeps its number and is merely completed.
\cs_new_protected:Npn \__chemid_declare_parent:
  {
    \prop_if_in:NVTF \g__chemid_implicit_prop \l__chemid_key_str
      { \prop_gremove:NV \g__chemid_implicit_prop \l__chemid_key_str }
      { \__chemid_new_parent:N \l__chemid_key_str }
  }

\cs_new_protected:Npn \__chemid_declare_child:
  {
    \prop_if_in:NVTF \g__chemid_num_prop \l__chemid_parent_str
      { } { \__chemid_implicit_parent: }
    % the parent may have been refused, hence the second test
    \prop_if_in:NVTF \g__chemid_num_prop \l__chemid_parent_str
      {
        \prop_get:NVN \g__chemid_subcount_prop \l__chemid_parent_str
          \l__chemid_tmp_tl
        \tl_set:Ne \l__chemid_tmp_tl { \int_eval:n { \l__chemid_tmp_tl + 1 } }
        \prop_gput:NVV \g__chemid_subcount_prop \l__chemid_parent_str
          \l__chemid_tmp_tl
        \prop_gput:NVe \g__chemid_num_prop \l__chemid_key_str
          { \prop_item:NV \g__chemid_num_prop \l__chemid_parent_str }
        \prop_gput:NVV \g__chemid_sub_prop \l__chemid_key_str \l__chemid_tmp_tl
        \prop_gput:NVV \g__chemid_parent_prop \l__chemid_key_str
          \l__chemid_parent_str
        \prop_gput:NVn \g__chemid_rich_prop \l__chemid_key_str { }
        \prop_gput:NVn \g__chemid_raw_prop \l__chemid_key_str { }
        \prop_gput:NVn \g__chemid_anchor_prop \l__chemid_key_str { 0 }
        \seq_gput_right:NV \g__chemid_order_seq \l__chemid_key_str
      }
      { }
  }

\cs_new_protected:Npn \__chemid_implicit_parent:
  {
    \bool_if:NTF \l__chemid_implicit_bool
      {
        \msg_info:nnVV { chemidentifier } { implicit-parent }
          \l__chemid_parent_str \l__chemid_key_str
        \__chemid_new_parent:N \l__chemid_parent_str
        \prop_gput:NVn \g__chemid_implicit_prop \l__chemid_parent_str { }
      }
      {
        \msg_error:nnVV { chemidentifier } { no-implicit-parent }
          \l__chemid_parent_str \l__chemid_key_str
      }
  }

% Rich name and its PDF-string counterpart.
\cs_new_protected:Npn \__chemid_store_names:nn #1#2
  {
    \bool_lazy_and:nnT
      { \prop_if_in_p:NV \g__chemid_num_prop \l__chemid_key_str }
      { ! \tl_if_blank_p:n {#1} }
      {
        \prop_gput:NVn \g__chemid_rich_prop \l__chemid_key_str {#1}
        \tl_if_blank:nTF {#2}
          {
            \bool_if:NTF \l__chemid_purify_bool
              { \tl_set:Ne \l__chemid_raw_tl { \text_purify:n {#1} } }
              { \tl_set:Nn \l__chemid_raw_tl {#1} }
          }
          { \tl_set:Nn \l__chemid_raw_tl {#2} }
        \prop_gput:NVV \g__chemid_raw_prop \l__chemid_key_str \l__chemid_raw_tl
      }
  }

\cs_generate_variant:Nn \__chemid_store_names:nn { VV }

% The value of one child of a family: given by hand, or produced by the
% family's own numbering style.  Both spellings are worked out here,
% once, so that the renderer has only to pick one.
\cs_new_protected:Npn \__chemid_store_value:nn #1#2
  {
    \prop_if_in:NVTF \g__chemid_num_prop \l__chemid_key_str
      {
        \tl_if_blank:nTF {#1}
          { \__chemid_value_auto: }
          { \__chemid_value_given:nn {#1} {#2} }
        \prop_gput:NVV \g__chemid_value_prop
          \l__chemid_key_str \l__chemid_valuerich_tl
        \prop_gput:NVV \g__chemid_valueraw_prop
          \l__chemid_key_str \l__chemid_valueraw_tl
      }
      { }
  }

\cs_new_protected:Npn \__chemid_value_auto:
  {
    \__chemid_family_mode:n { 0 }
    \tl_set:Ne \l__chemid_valuerich_tl
      {
        \__chemid_style:enn { \__chemid_famstyle:V \l__chemid_parent_str }
          { rich } { \__chemid_sub:V \l__chemid_key_str }
      }
    \tl_set:Ne \l__chemid_valueraw_tl
      {
        \__chemid_style:enn { \__chemid_famstyle:V \l__chemid_parent_str }
          { plain } { \__chemid_sub:V \l__chemid_key_str }
      }
  }

\cs_new_protected:Npn \__chemid_value_given:nn #1#2
  {
    \__chemid_family_mode:n { 1 }
    \tl_set:Nn \l__chemid_valuerich_tl {#1}
    \tl_if_blank:nTF {#2}
      {
        \bool_if:NTF \l__chemid_purify_bool
          { \tl_set:Ne \l__chemid_valueraw_tl { \text_purify:n {#1} } }
          { \tl_set:Nn \l__chemid_valueraw_tl {#1} }
      }
      { \tl_set:Nn \l__chemid_valueraw_tl {#2} }
  }

% Automatic and given values cannot be mixed inside one family: the
% first child to be declared settles which of the two the family is
% made of, and any later child disagreeing with it is an error.  It is
% still declared -- the numbering of everything after it must not shift
% because of a diagnostic.
\cs_new_protected:Npn \__chemid_family_mode:n #1
  {
    \prop_if_in:NVTF \g__chemid_famexplicit_prop \l__chemid_parent_str
      {
        \str_if_eq:eeTF
            { \prop_item:NV \g__chemid_famexplicit_prop \l__chemid_parent_str }
            {#1}
          { }
          {
            \str_if_eq:nnTF {#1} { 1 }
              { \tl_set:Nn \l__chemid_word_tl { carries } }
              { \tl_set:Nn \l__chemid_word_tl { lacks } }
            \msg_error:nnVV { chemidentifier } { family-mixed-values }
              \l__chemid_key_str \l__chemid_word_tl
          }
      }
      { \prop_gput:NVn \g__chemid_famexplicit_prop \l__chemid_parent_str {#1} }
  }

% Keep the *content* of \textsubscript & friends when deriving the raw
% name automatically.
\text_declare_purify_equivalent:Nn \textsubscript   { \use:n }
\text_declare_purify_equivalent:Nn \textsuperscript { \use:n }

%% ------------------------------------------------------------------
%% 8. Expandable accessors
%% ------------------------------------------------------------------

% Each accessor reads the current pass first, and falls back to the
% previous pass's registration when the key has not been declared *yet*
% this time round -- see the \g__chemid_prev_*_prop block above.
\cs_new:Npn \__chemid_num:n #1
  {
    \prop_if_in:NnTF \g__chemid_num_prop {#1}
      { \prop_item:Nn \g__chemid_num_prop {#1} }
      { \prop_item:Nn \g__chemid_prev_num_prop {#1} }
  }
\cs_new:Npn \__chemid_sub:n #1
  {
    \prop_if_in:NnTF \g__chemid_sub_prop {#1}
      { \prop_item:Nn \g__chemid_sub_prop {#1} }
      { \prop_item:Nn \g__chemid_prev_sub_prop {#1} }
  }
\cs_new:Npn \__chemid_parent:n #1
  {
    \prop_if_in:NnTF \g__chemid_parent_prop {#1}
      { \prop_item:Nn \g__chemid_parent_prop {#1} }
      { \prop_item:Nn \g__chemid_prev_parent_prop {#1} }
  }
\cs_new:Npn \__chemid_rich:n #1
  {
    \prop_if_in:NnTF \g__chemid_rich_prop {#1}
      { \prop_item:Nn \g__chemid_rich_prop {#1} }
      { \prop_item:Nn \g__chemid_prev_rich_prop {#1} }
  }
\cs_new:Npn \__chemid_raw:n #1
  {
    \prop_if_in:NnTF \g__chemid_raw_prop {#1}
      { \prop_item:Nn \g__chemid_raw_prop {#1} }
      { \prop_item:Nn \g__chemid_prev_raw_prop {#1} }
  }
\cs_generate_variant:Nn \__chemid_sub:n { V, e }
\cs_generate_variant:Nn \__chemid_num:n { e }

% Same again for the templated families.  A family is a parent with a
% 'child-format': that format, being always non-empty for one and
% always empty for anything else, doubles as the flag.
\cs_new:Npn \__chemid_famformat:n #1
  {
    \prop_if_in:NnTF \g__chemid_famformat_prop {#1}
      { \prop_item:Nn \g__chemid_famformat_prop {#1} }
      { \prop_item:Nn \g__chemid_prev_famformat_prop {#1} }
  }
\cs_new:Npn \__chemid_famstyle:n #1
  {
    \prop_if_in:NnTF \g__chemid_famstyle_prop {#1}
      { \prop_item:Nn \g__chemid_famstyle_prop {#1} }
      { \prop_item:Nn \g__chemid_prev_famstyle_prop {#1} }
  }
\cs_new:Npn \__chemid_famholder:n #1
  {
    \prop_if_in:NnTF \g__chemid_famholder_prop {#1}
      { \prop_item:Nn \g__chemid_famholder_prop {#1} }
      { \prop_item:Nn \g__chemid_prev_famholder_prop {#1} }
  }
\cs_new:Npn \__chemid_famholderraw:n #1
  {
    \prop_if_in:NnTF \g__chemid_famholderraw_prop {#1}
      { \prop_item:Nn \g__chemid_famholderraw_prop {#1} }
      { \prop_item:Nn \g__chemid_prev_famholderraw_prop {#1} }
  }
\cs_new:Npn \__chemid_famexplicit:n #1
  {
    \prop_if_in:NnTF \g__chemid_famexplicit_prop {#1}
      { \prop_item:Nn \g__chemid_famexplicit_prop {#1} }
      { \prop_item:Nn \g__chemid_prev_famexplicit_prop {#1} }
  }
\cs_new:Npn \__chemid_value:n #1
  {
    \prop_if_in:NnTF \g__chemid_value_prop {#1}
      { \prop_item:Nn \g__chemid_value_prop {#1} }
      { \prop_item:Nn \g__chemid_prev_value_prop {#1} }
  }
\cs_new:Npn \__chemid_valueraw:n #1
  {
    \prop_if_in:NnTF \g__chemid_valueraw_prop {#1}
      { \prop_item:Nn \g__chemid_valueraw_prop {#1} }
      { \prop_item:Nn \g__chemid_prev_valueraw_prop {#1} }
  }
\cs_generate_variant:Nn \__chemid_famformat:n    { V, e }
\cs_generate_variant:Nn \__chemid_famstyle:n     { V, e }
\cs_generate_variant:Nn \__chemid_famholder:n    { V, e }
\cs_generate_variant:Nn \__chemid_famholderraw:n { V, e }
\cs_generate_variant:Nn \__chemid_famexplicit:n  { V, e }
\cs_generate_variant:Nn \__chemid_value:n        { V, e }
\cs_generate_variant:Nn \__chemid_valueraw:n     { V, e }

% Is this parent a family?
\prg_new_conditional:Npnn \__chemid_isfam:n #1 { p, TF }
  {
    \tl_if_blank:eTF { \__chemid_famformat:n {#1} }
      { \prg_return_false: } { \prg_return_true: }
  }
\prg_generate_conditional_variant:Nnn \__chemid_isfam:n { V, e } { p, TF }

% Is this key a child *of* a family?  Such a child always has a value,
% automatic or given, and nothing else ever has one.
\prg_new_conditional:Npnn \__chemid_isfamkid:n #1 { p, TF }
  {
    \tl_if_blank:eTF { \__chemid_value:n {#1} }
      { \prg_return_false: } { \prg_return_true: }
  }
\prg_generate_conditional_variant:Nnn \__chemid_isfamkid:n { V, e } { p, TF }

% Whether a run of children of this family may be contracted into a
% range: only when they are numbered automatically.  Values given by
% hand (4, 8, al) have no notion of 'consecutive', so '4-al' would be
% a lie -- they are always listed one by one.
\prg_new_conditional:Npnn \__chemid_rangeable:n #1 { p, TF }
  {
    \str_if_eq:eeTF { \__chemid_famexplicit:e { \__chemid_parent:n {#1} } } { 1 }
      { \prg_return_false: } { \prg_return_true: }
  }

\prg_new_conditional:Npnn \__chemid_known:n #1 { p, TF }
  {
    \bool_lazy_or:nnTF
      { \prop_if_in_p:Nn \g__chemid_num_prop {#1} }
      { \prop_if_in_p:Nn \g__chemid_prev_num_prop {#1} }
      { \prg_return_true: } { \prg_return_false: }
  }
\cs_generate_variant:Nn \__chemid_known:nTF { V }

% A key may be merged with its neighbours only if it is a plain,
% auto-numbered child: a custom name always stands alone.
\prg_new_conditional:Npnn \__chemid_groupable:n #1 { p, TF }
  {
    \bool_lazy_all:nTF
      {
        { \__chemid_known_p:n {#1} }
        { \int_compare_p:nNn { 0 \__chemid_sub:n {#1} } > { 0 } }
        { \tl_if_blank_p:e { \__chemid_rich:n {#1} } }
      }
      { \prg_return_true: } { \prg_return_false: }
  }

% Symmetric test for a plain, auto-numbered *parent* (sub = 0): used to
% contract a run of several such top-level citations into a range, e.g.
% \chemid{mol1,mol2,mol3} -> '1-3' instead of '1, 2 and 3'.  A custom
% name still stands alone, exactly as for children.
\prg_new_conditional:Npnn \__chemid_topgroupable:n #1 { p, TF }
  {
    \bool_lazy_all:nTF
      {
        { \__chemid_known_p:n {#1} }
        { \int_compare_p:nNn { 0 \__chemid_sub:n {#1} } = { 0 } }
        { \tl_if_blank_p:e { \__chemid_rich:n {#1} } }
      }
      { \prg_return_true: } { \prg_return_false: }
  }

% #1 style, #2 integer
\cs_new:Npn \__chemid_format_int:nn #1#2
  {
    \str_case:nnF {#1}
      {
        { arabic } { \int_to_arabic:n {#2} }
        { alph }   { \int_to_alph:n   {#2} }
        { Alph }   { \int_to_Alph:n   {#2} }
        { roman }  { \int_to_roman:n  {#2} }
        { Roman }  { \int_to_Roman:n  {#2} }
      }
      { \int_to_arabic:n {#2} }
  }

% The Greek alphabet as a numbering style.  Two spellings of the same
% letter are needed: the math one to typeset, and the character itself
% for PDF strings, where hyperref has no maths to offer -- bookmarks
% being Unicode, it takes the letter perfectly well.
%
% That second table cannot simply be typed here.  Under \ExplSyntaxOn a
% byte above 127 is 'other', not the active byte LaTeX's UTF-8 reader
% expects, so an alpha written literally in this file reaches hyperref
% as its two raw bytes and comes out of the bookmark as mojibake.  It
% is therefore built from code points: one character on the Unicode
% engines, the two UTF-8 bytes with the catcodes of a real source file
% on the others.  Omicron is left as the Latin 'o' it is
% indistinguishable from, and which every font actually has.
\tl_const:Nn \c__chemid_greek_tl
  {
    {\alpha}   {\beta}    {\gamma}   {\delta} {\epsilon} {\zeta}
    {\eta}     {\theta}   {\iota}    {\kappa} {\lambda}  {\mu}
    {\nu}      {\xi}      {o}        {\pi}    {\rho}     {\sigma}
    {\tau}     {\upsilon} {\phi}     {\chi}   {\psi}     {\omega}
  }

\cs_new:Npn \__chemid_utfchar:n #1
  {
    \bool_lazy_or:nnTF
      { \sys_if_engine_xetex_p: } { \sys_if_engine_luatex_p: }
      { \char_generate:nn {#1} { 12 } }
      {
        \char_generate:nn { \int_div_truncate:nn {#1} { 64 } + 192 } { 13 }
        \char_generate:nn { \int_mod:nn {#1} { 64 } + 128 } { 12 }
      }
  }

\tl_new:N \g__chemid_greekraw_tl
% alpha to omega, final sigma (962) left out; 959 is omicron, spelt as
% the Latin letter to match \c__chemid_greek_tl above.
\clist_map_inline:nn
  {
    945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956,
    957, 958, 959, 960, 961, 963, 964, 965, 966, 967, 968, 969
  }
  {
    \int_compare:nNnTF {#1} = { 959 }
      { \tl_gput_right:Nn \g__chemid_greekraw_tl { { o } } }
      {
        \tl_gput_right:Ne \g__chemid_greekraw_tl
          { { \__chemid_utfchar:n {#1} } }
      }
  }

% The character itself is deliberately *not* handed over as those two
% bytes: a value is stored, written to the .aux and read back again
% through full expansions, and an active byte expanding on the way
% comes out as mojibake at the far end.  This robust command carries
% it instead, untouched, and unfolds into the character in the one
% place that wants it -- inside \pdfstringdef, where hyperref has its
% own reading of those bytes (see the 'begindocument/before' hook).
\NewDocumentCommand \chemidgreekchar { m }
  { \tl_item:Nn \g__chemid_greekraw_tl {#1} }

% #1 mode, #2 integer
\cs_new:Npn \__chemid_greek:nn #1#2
  {
    \int_compare:nNnTF {#2} > { 24 }
      { \int_to_arabic:n {#2} }
      {
        \str_if_eq:nnTF {#1} { plain }
          { \chemidgreekchar {#2} }
          { \ensuremath { \tl_item:Nn \c__chemid_greek_tl {#2} } }
      }
  }

% #1 style, #2 mode, #3 integer.  Same as \__chemid_format_int:nn for
% every style version 1 already knew; 'lower-greek' is the one that
% needs to know which of the two spellings is being asked for.
\cs_new:Npn \__chemid_style:nnn #1#2#3
  {
    \str_if_eq:nnTF {#1} { lower-greek }
      { \__chemid_greek:nn {#2} {#3} }
      { \__chemid_format_int:nn {#1} {#3} }
  }
\cs_generate_variant:Nn \__chemid_style:nnn { Vnn, enn }

% #1 mode, #2 key.  A child of a family has no number of its own to
% show: the family name stands in front of the whole group, printed
% once, and only the values follow.
\cs_new:Npn \__chemid_main_text:nn #1#2
  {
    \__chemid_isfamkid:nTF {#2}
      { }
      { \__chemid_style:Vnn \l__chemid_mainstyle_tl {#1} { \__chemid_num:n {#2} } }
  }
\cs_new:Npn \__chemid_sub_text:nn #1#2
  {
    \__chemid_isfamkid:nTF {#2}
      { \__chemid_value_text:nn {#1} {#2} }
      { \__chemid_style:Vnn \l__chemid_substyle_tl {#1} { \__chemid_sub:n {#2} } }
  }
\cs_new:Npn \__chemid_value_text:nn #1#2
  {
    \str_if_eq:nnTF {#1} { plain }
      { \__chemid_unseal:e { \__chemid_valueraw:n {#2} } }
      { \__chemid_value:n {#2} }
  }

% A value fetched from a property list comes back sealed against
% expansion -- \prop_item:Nn protects what it returns, and inside the
% \edef of \pdfstringdef that seal holds for good.  Passing it on as an
% *argument* breaks it, the body of a macro being ordinary tokens
% again: this is what lets \chemidgreekchar unfold into its character
% there, and it is the only reason this otherwise pointless-looking
% detour exists.
\cs_new:Npn \__chemid_unseal:n #1 {#1}
\cs_generate_variant:Nn \__chemid_unseal:n { e }

%% ------------------------------------------------------------------
%% 9. Expandable renderer
%% ------------------------------------------------------------------
%%
%% Rendering is *fully expandable* so that one single implementation
%% serves the three contexts a compound identifier may end up in:
%%   (a) ordinary typesetting,
%%   (b) a .toc / .lof / .lot entry, where links must be dropped,
%%   (c) hyperref's \pdfstringdef, which is an \edef (bookmarks).
%%
%% Modes: 'rich'  -> typeset material, links unless we sit in a ToC
%%        'plain' -> raw text, never any link

\prg_new_conditional:Npnn \__chemid_links:n #1 { p, TF }
  {
    \bool_lazy_all:nTF
      {
        { \str_if_eq_p:nn {#1} { rich } }
        { ! \bool_if_p:N \l__chemid_toc_bool }
        { \bool_if_p:N \g__chemid_hyper_bool }
        { \bool_if_p:N \l__chemid_links_bool }
      }
      { \prg_return_true: } { \prg_return_false: }
  }

% Number of anchors laid on this key so far, this run.
\cs_new:Npn \__chemid_anchorcount:n #1
  { \int_eval:n { 0 \prop_item:Nn \g__chemid_anchor_prop {#1} } }

% Whether an anchor for this key is known to exist at all: laid already
% this run, or recorded in the .aux file by the previous one.
\prg_new_conditional:Npnn \__chemid_hasanchor:n #1 { p, TF }
  {
    \bool_lazy_or:nnTF
      { \int_compare_p:nNn { \__chemid_anchorcount:n {#1} } > { 0 } }
      { \prop_if_in_p:Nn \g__chemid_prevanchor_prop {#1} }
      { \prg_return_true: } { \prg_return_false: }
  }
\prg_generate_conditional_variant:Nnn \__chemid_hasanchor:n { e } { p, TF }

% First anchored child of a parent, current run first, previous run as
% a fallback -- empty for anything that is not a parent with an
% anchored child.
\cs_new:Npn \__chemid_firstkid:n #1
  {
    \prop_if_in:NnTF \g__chemid_firstkid_prop {#1}
      { \prop_item:Nn \g__chemid_firstkid_prop {#1} }
      { \prop_item:Nn \g__chemid_prev_firstkid_prop {#1} }
  }

% Where a link on this key should actually land: on the key itself
% whenever it is anchored, otherwise -- for a parent whose own anchor
% was never laid -- on the first of its children that is.  A scheme
% that anchors only the children thus still catches \chemid{parent},
% instead of leaving it pointing at nothing.
\cs_new:Npn \__chemid_targetkey:n #1
  {
    \bool_lazy_or:nnTF
      { \__chemid_hasanchor_p:n {#1} }
      { \tl_if_blank_p:e { \__chemid_firstkid:n {#1} } }
      {#1}
      { \__chemid_firstkid:n {#1} }
  }

% Whether this key may carry a link.  Under 'strict-anchors' (the
% default), only if an anchor is known to exist: laid already, or
% recorded in the .aux file by the previous run.  Fed the *resolved*
% target key, never the cited one.
\prg_new_conditional:Npnn \__chemid_anchored:n #1 { p, TF }
  {
    \bool_lazy_or:nnTF
      { ! \bool_if_p:N \g__chemid_strict_bool }
      { \__chemid_hasanchor_p:n {#1} }
      { \prg_return_true: } { \prg_return_false: }
  }

% Anchor names are always built from a catcode-12 string, so that the
% target laid by \herechemid and the link laid by \chemid match whatever
% the catcodes of the key were (an underscore in a key, typically).
\cs_new:Npn \__chemid_name:n #1
  { \l__chemid_prefix_tl \tl_to_str:n {#1} }

% #1 mode, #2 key, #3 text
\cs_new:Npn \__chemid_link:nnn #1#2#3
  { \__chemid_link_ii:enn { \__chemid_targetkey:n {#2} } {#1} {#3} }

% #1 target key (already resolved), #2 mode, #3 text
\cs_new:Npn \__chemid_link_ii:nnn #1#2#3
  {
    \bool_lazy_and:nnTF
      { \__chemid_links_p:n {#2} }
      { \__chemid_anchored_p:n {#1} }
      { \exp_args:Ne \__chemid_hyperlink:nn { \__chemid_name:n {#1} } {#3} }
      {#3}
  }
\cs_generate_variant:Nn \__chemid_link_ii:nnn { enn }
\cs_new:Npn \__chemid_hyperlink:nn #1#2 { \hyperlink {#1} {#2} }

% #1 mode, #2 text: the user format (bold numbers...) except in PDF strings
\cs_new:Npn \__chemid_fmt:nn #1#2
  {
    \str_if_eq:nnTF {#1} { plain }
      {#2}
      { \l__chemid_format_tl {#2} }
  }

% Full identifier (number + letter).
\cs_new:Npn \__chemid_full:nn #1#2
  {
    \__chemid_link:nnn {#1} {#2}
      {
        \__chemid_fmt:nn {#1}
          { \__chemid_main_text:nn {#1} {#2} \__chemid_subtext:nn {#1} {#2} }
      }
  }
\cs_new:Npn \__chemid_subtext:nn #1#2
  {
    \int_compare:nNnT { \__chemid_sub:n {#2} } > { 0 }
      { \__chemid_sub_text:nn {#1} {#2} }
  }

% Letter alone, for the continuation of a family: the '1' of '1a,b'.
\cs_new:Npn \__chemid_letter:nn #1#2
  {
    \__chemid_link:nnn {#1} {#2}
      { \__chemid_fmt:nn {#1} { \__chemid_sub_text:nn {#1} {#2} } }
  }

% The name at the head of a family: the one given to \chemidfamily, or
% the parent's plain number when it was given none.
\cs_new:Npn \__chemid_famname:nn #1#2
  {
    \tl_if_blank:eTF { \__chemid_rich:n {#2} }
      { \__chemid_style:Vnn \l__chemid_mainstyle_tl {#1} { \__chemid_num:n {#2} } }
      {
        \str_if_eq:nnTF {#1} { plain }
          { \__chemid_raw:n {#2} }
          { \__chemid_rich:n {#2} }
      }
  }
\cs_generate_variant:Nn \__chemid_famname:nn { ne }

% Dress a value (or a whole list of them) the way the family asks:
% \textsubscript{...} unless told otherwise.  Nothing to dress in a PDF
% string, which has no subscripts to offer.
% #1 mode, #2 any key of the family, #3 material
\cs_new:Npn \__chemid_famwrap:nnn #1#2#3
  {
    \str_if_eq:nnTF {#1} { plain }
      {#3}
      {
        \exp_args:Ne \__chemid_famwrap_ii:nn
          { \__chemid_famformat:e { \__chemid_parent:n {#2} } } {#3}
      }
  }
\cs_generate_variant:Nn \__chemid_famwrap:nnn { nen }

% The format is code calling \chemidvalue, which is bound here rather
% than substituted into it: no pattern matching, and the code may use
% the value as many times as it likes.  The group is what keeps that
% binding from leaking; it is opened in the token stream, never inside
% an \edef, PDF strings taking the branch above.
\cs_new_protected:Npn \__chemid_famwrap_ii:nn #1#2
  {
    \group_begin:
      \cs_set:Npn \chemidvalue {#2}
      #1
    \group_end:
  }

% A lone key: family child, family parent, custom name, unknown key, or
% plain identifier.
\cs_new:Npn \__chemid_single:nn #1#2
  {
    \__chemid_known:nTF {#2}
      {
        \__chemid_isfamkid:nTF {#2}
          { \__chemid_famsingle:nn {#1} {#2} }
          {
            \__chemid_isfam:nTF {#2}
              { \__chemid_famparent:nn {#1} {#2} }
              {
                \tl_if_blank:eTF { \__chemid_rich:n {#2} }
                  { \__chemid_full:nn {#1} {#2} }
                  {
                    \__chemid_link:nnn {#1} {#2}
                      {
                        \str_if_eq:nnTF {#1} { plain }
                          { \__chemid_raw:n {#2} }
                          { \__chemid_fmt:nn {#1} { \__chemid_rich:n {#2} } }
                      }
                  }
              }
          }
      }
      { \l__chemid_unknown_tl }
  }

% One child of a family, on its own: the family name and its value, all
% under a single link -- 'Lcoum-C' + subscript '4'.
\cs_new:Npn \__chemid_famsingle:nn #1#2
  {
    \__chemid_link:nnn {#1} {#2}
      {
        \__chemid_fmt:nn {#1}
          {
            \__chemid_famname:ne {#1} { \__chemid_parent:n {#2} }
            \__chemid_famwrap:nnn {#1} {#2} { \__chemid_value_text:nn {#1} {#2} }
          }
      }
  }

% The family parent cited on its own: its name, followed by the
% placeholder standing for 'any of them' when one was given --
% 'Lcoum-C' with a subscript 'n'.
\cs_new:Npn \__chemid_famparent:nn #1#2
  {
    \__chemid_link:nnn {#1} {#2}
      {
        \__chemid_fmt:nn {#1}
          {
            \__chemid_famname:nn {#1} {#2}
            \tl_if_blank:eF { \__chemid_famholder:n {#2} }
              {
                \__chemid_famwrap:nnn {#1} {#2}
                  { \__chemid_famholder_text:nn {#1} {#2} }
              }
          }
      }
  }
\cs_new:Npn \__chemid_famholder_text:nn #1#2
  {
    \str_if_eq:nnTF {#1} { plain }
      { \__chemid_famholderraw:n {#2} }
      { \__chemid_famholder:n {#2} }
  }

\cs_generate_variant:Nn \__chemid_full:nn   { nf }
\cs_generate_variant:Nn \__chemid_letter:nn { nf, ne }
\cs_generate_variant:Nn \__chemid_single:nn { nf }

%% -- entry point ---------------------------------------------------

\cs_new:Npn \__chemid_render:nn #1#2
  {
    \use:e
      {
        \exp_not:N \__chemid_start:nnn {#1} { }
        \clist_map_function:nN {#2} \__chemid_brace:n
        { \exp_not:N \q_recursion_tail } \exp_not:N \q_recursion_stop
      }
  }
% Keys are expanded here exactly as \__chemid_parse:n expands them at
% declaration time, so that a key built by a macro resolves on both
% sides.  (The whole thing already sits inside \use:e.)
\cs_new:Npn \__chemid_brace:n #1 { {#1} }

% #1 mode, #2 empty (keeps the signature regular), #3 first key
\cs_new:Npn \__chemid_start:nnn #1#2#3
  {
    \quark_if_recursion_tail_stop_do:nn {#3} { }
    \__chemid_scan:nnnnn {#1} { } { {#3} } { \__chemid_newparent:n {#3} }
  }

% Split the list into families.  #1 mode, #2 completed groups,
% #3 current group, #4 current family (empty = "do not merge"), #5 key.
\cs_new:Npn \__chemid_scan:nnnnn #1#2#3#4#5
  {
    \quark_if_recursion_tail_stop_do:nn {#5}
      { \__chemid_join:ne {#1} { \__chemid_topcontract:n { #2 {#3} } } }
    \bool_lazy_and:nnTF
      { \__chemid_groupable_p:n {#5} }
      { \str_if_eq_p:ee { \__chemid_parent:n {#5} } {#4} }
      { \__chemid_scan:nnnnn {#1} {#2} { #3 {#5} } {#4} }
      { \__chemid_scan:nnnnn {#1} { #2 {#3} } { {#5} } { \__chemid_newparent:n {#5} } }
  }

\cs_new:Npn \__chemid_newparent:n #1
  { \__chemid_groupable:nTF {#1} { \__chemid_parent:n {#1} } { } }

%% -- contracting plain top-level runs into a range ------------------
%%
%% Children of one parent are already cut into runs and contracted by
%% \__chemid_group:nn / \__chemid_runs_*:  (\S 'one family' below).  This
%% does the same for *parents* cited several times in a row: it walks
%% the family list produced by \__chemid_scan:nnnnn and merges any
%% maximal run of plain, auto-numbered, consecutively-numbered singleton
%% families into one two-key {first}{last} family, which
%% \__chemid_group:nn then renders as a range.  A run shorter than
%% 'range-threshold', a custom name, an unknown key, or an existing
%% multi-key (children) family is passed through untouched -- so with no
%% qualifying run anywhere, the family list this produces is token-for-
%% token identical to the input, and \chemid renders exactly as before.

% #1 family list (as built by \__chemid_scan:nnnnn)
\cs_new:Npn \__chemid_topcontract:n #1
  { \__chemid_topcontract_loop:nnn { } { } {#1} }

% #1 finished families, #2 pending run of plain keys, #3 remaining
% families
\cs_new:Npn \__chemid_topcontract_loop:nnn #1#2#3
  {
    \tl_if_empty:nTF {#3}
      { \__chemid_topflush:nn {#1} {#2} }
      { \__chemid_topcontract_ii:nnff {#1} {#2} { \tl_head:n {#3} } { \tl_tail:n {#3} } }
  }

% #3 = next family's key list (unwrapped, already forced plain by the
% ':nnff' call above), #4 = families still after it (likewise plain).
\cs_new:Npn \__chemid_topcontract_ii:nnnn #1#2#3#4
  { \__chemid_topcontract_iii:nnnnf {#1} {#2} {#3} {#4} { \tl_head:n {#3} } }
\cs_generate_variant:Nn \__chemid_topcontract_ii:nnnn { nnff }

% #5 = that family's sole key, forced plain by the call above -- only
% meaningful once the count test below confirms #3 *is* a singleton.
\cs_new:Npn \__chemid_topcontract_iii:nnnnn #1#2#3#4#5
  {
    \bool_lazy_and:nnTF
      { \int_compare_p:nNn { \tl_count:n {#3} } = { 1 } }
      { \__chemid_topgroupable_p:n {#5} }
      {
        \tl_if_empty:nTF {#2}
          { \__chemid_topcontract_loop:nnn {#1} { {#5} } {#4} }
          {
            \int_compare:nNnTF
                { \__chemid_num:n {#5} }
                = { \__chemid_num:e { \tl_item:nn {#2} { -1 } } + 1 }
              { \__chemid_topcontract_loop:nnn {#1} { #2 {#5} } {#4} }
              {
                \__chemid_topcontract_loop:nnn
                  { \__chemid_topflush:nn {#1} {#2} } { {#5} } {#4}
              }
          }
      }
      {
        \__chemid_topcontract_loop:nnn
          { \__chemid_topflush:nnn {#1} {#2} {#3} } { } {#4}
      }
  }
\cs_generate_variant:Nn \__chemid_topcontract_iii:nnnnn { nnnnf }

% Close the pending run: below the threshold, explode it back into one
% singleton family per key (untouched-looking, list-sep/last-sep join
% applies to them exactly as if they had never been merged); at or
% above the threshold, contract it into one {first}{last} range family.
\cs_new:Npn \__chemid_topflush:nn #1#2
  {
    \tl_if_empty:nTF {#2}
      { #1 }
      {
        \int_compare:nNnTF { \tl_count:n {#2} } < { \l__chemid_threshold_int }
          { #1 \__chemid_topexplode:n {#2} }
          { #1 \__chemid_toprange:ff { \tl_head:n {#2} } { \tl_item:nn {#2} { -1 } } }
      }
  }
% Same, plus a family #3 (already forced plain) to carry over unchanged
% right after.
\cs_new:Npn \__chemid_topflush:nnn #1#2#3
  { \__chemid_topflush:nn {#1} {#2} { #3 } }

\cs_new:Npn \__chemid_toprange:nn #1#2 { { {#1} {#2} } }
\cs_generate_variant:Nn \__chemid_toprange:nn { ff }

\cs_new:Npn \__chemid_topexplode:n #1
  {
    \tl_if_empty:nF {#1}
      {
        \__chemid_topsingle:f { \tl_head:n {#1} }
        \__chemid_topexplode:f { \tl_tail:n {#1} }
      }
  }
\cs_generate_variant:Nn \__chemid_topexplode:n { f }
\cs_new:Npn \__chemid_topsingle:n #1 { { {#1} } }
\cs_generate_variant:Nn \__chemid_topsingle:n { f }

%% -- joining the families ------------------------------------------

\cs_new:Npn \__chemid_join:nn #1#2
  {
    \tl_if_empty:nF {#2}
      {
        \__chemid_group:nf {#1} { \tl_head:n {#2} }
        \__chemid_join_ii:nf {#1} { \tl_tail:n {#2} }
      }
  }
\cs_generate_variant:Nn \__chemid_join:nn { ne }

\cs_new:Npn \__chemid_join_ii:nn #1#2
  {
    \tl_if_empty:nF {#2}
      {
        \int_compare:nNnTF { \tl_count:n {#2} } = { 1 }
          { \__chemid_effective_lastsep: }
          { \l__chemid_listsep_tl }
        \__chemid_join:nn {#1} {#2}
      }
  }
\cs_generate_variant:Nn \__chemid_join_ii:nn { nf }

%% -- one family ----------------------------------------------------

% #1 mode, #2 braced list of keys belonging to one family
\cs_new:Npn \__chemid_group:nn #1#2
  {
    \int_compare:nNnTF { \tl_count:n {#2} } = { 1 }
      { \__chemid_single:nf {#1} { \tl_head:n {#2} } }
      {
        % A two-key family whose first key has no sub-index can only be
        % a range synthesised by \__chemid_topcontract:n: a genuine
        % children family always has sub > 0.  Render it as a number
        % range instead of running it through the letter-run logic.
        \int_compare:nNnTF { \__chemid_sub:e { \tl_head:n {#2} } } = { 0 }
          {
            \__chemid_full:nf {#1} { \tl_head:n {#2} }
            \l__chemid_rangesep_tl
            \__chemid_full:nf {#1} { \tl_item:nn {#2} { -1 } }
          }
          {
            % A family with a 'child-format' prints its name once and
            % puts every value inside one subscript --
            % 'Lcoum-C_{4,8}' -- where a plain family repeats the
            % number as needed: '1a,b'.
            \__chemid_isfamkid:eTF { \tl_head:n {#2} }
              { \__chemid_famgroup:nn {#1} {#2} }
              { \__chemid_runs_start:nff {#1} { \tl_head:n {#2} } { \tl_tail:n {#2} } }
          }
      }
  }
\cs_generate_variant:Nn \__chemid_group:nn { nf }

% #1 mode, #2 keys of one templated family, two or more of them
\cs_new:Npn \__chemid_famgroup:nn #1#2
  {
    \__chemid_famprefix:nf {#1} { \tl_head:n {#2} }
    \__chemid_famwrap:nen {#1} { \tl_head:n {#2} }
      { \__chemid_runs_start:nff {#1} { \tl_head:n {#2} } { \tl_tail:n {#2} } }
  }

% The name in front of the group, linked like the first value it
% introduces -- the same way the '1' of '1a,b' is.
\cs_new:Npn \__chemid_famprefix:nn #1#2
  {
    \__chemid_link:nnn {#1} {#2}
      {
        \__chemid_fmt:nn {#1}
          { \__chemid_famname:ne {#1} { \__chemid_parent:n {#2} } }
      }
  }
\cs_generate_variant:Nn \__chemid_famprefix:nn { nf }

\cs_new:Npn \__chemid_runs_start:nnn #1#2#3
  { \__chemid_runs:nnn {#1} { {#2} } {#3} }
\cs_generate_variant:Nn \__chemid_runs_start:nnn { nff }

% Cut the family into maximal runs of consecutive letters.
% #1 mode, #2 current run, #3 remaining keys
\cs_new:Npn \__chemid_runs:nnn #1#2#3
  {
    \tl_if_empty:nTF {#3}
      { \__chemid_run:nn {#1} {#2} }
      { \__chemid_runs_ii:nnff {#1} {#2} { \tl_head:n {#3} } { \tl_tail:n {#3} } }
  }

% #1 mode, #2 current run, #3 next key, #4 rest
\cs_new:Npn \__chemid_runs_ii:nnnn #1#2#3#4
  {
    \bool_lazy_and:nnTF
      {
        \int_compare_p:nNn
            { \__chemid_sub:n {#3} }
            = { \__chemid_sub:e { \tl_item:nn {#2} { -1 } } + 1 }
      }
      { \__chemid_rangeable_p:n {#3} }
      { \__chemid_runs:nnn {#1} { #2 {#3} } {#4} }
      {
        \__chemid_run:nn {#1} {#2}
        \l__chemid_subsep_tl
        \__chemid_runs:nnn {#1} { {#3} } {#4}
      }
  }
\cs_generate_variant:Nn \__chemid_runs_ii:nnnn { nnff }

% One run of consecutive letters: 1a / 1a,b / 1a-c
\cs_new:Npn \__chemid_run:nn #1#2
  {
    \__chemid_full:nf {#1} { \tl_head:n {#2} }
    \int_compare:nNnF { \tl_count:n {#2} } = { 1 }
      {
        \int_compare:nNnTF { \tl_count:n {#2} } < { \l__chemid_threshold_int }
          { \__chemid_run_all:nf {#1} { \tl_tail:n {#2} } }
          {
            \l__chemid_rangesep_tl
            \__chemid_letter:ne {#1} { \tl_item:nn {#2} { -1 } }
          }
      }
  }

\cs_new:Npn \__chemid_run_all:nn #1#2
  {
    \tl_if_empty:nF {#2}
      {
        \l__chemid_subsep_tl
        \__chemid_letter:nf {#1} { \tl_head:n {#2} }
        \__chemid_run_all:nf {#1} { \tl_tail:n {#2} }
      }
  }
\cs_generate_variant:Nn \__chemid_run_all:nn { nf }

%% ------------------------------------------------------------------
%% 10. Usage -- \chemid{key,key,...}
%% ------------------------------------------------------------------

\cs_new_protected:Npn \__chemid_record_one:n #1
  {
    \__chemid_known:nTF {#1}
      { \prop_gput:Nnn \g__chemid_used_prop {#1} { } }
      {
        % Not known *yet* this pass: indistinguishable, from here, from
        % a genuine typo -- but also indistinguishable from a forward
        % reference to a \chemid* that has not run yet this same pass
        % (e.g. \tableofcontents replaying a stale .toc from a previous,
        % erroring run, before the \include that declares the key gets
        % a chance to run). Raising here unconditionally would make that
        % second, resolvable case a hard (nonzero-exit) error on every
        % single run until a full clean -- \end{document} is reached,
        % all of the document's \chemid* have long since run, and
        % latexmk's own "rerun automatically" heuristic never gets to
        % see the state settle, because it stops rerunning the moment
        % *any* pass in the chain reported an error.
        % So: record the key, once, and let \__chemid_check_pending:
        % (\end{document}) deliver the actual verdict once every
        % declaration in the document has had its chance to run --
        % exactly like \ref on an undefined label.
        \prop_if_in:NnF \g__chemid_reported_prop {#1}
          { \prop_gput:Nnn \g__chemid_reported_prop {#1} { } }
      }
  }
\cs_generate_variant:Nn \__chemid_record_one:n { V }

% Book-keeping pass: record the use, complain once per unknown key.
\cs_new_protected:Npn \__chemid_record:n #1
  {
    \clist_map_inline:nn {#1}
      {
        \tl_if_blank:nF {##1}
          {
            \str_set:Ne \l__chemid_key_str {##1}
            \__chemid_record_one:V \l__chemid_key_str
          }
      }
  }

\cs_new_protected:Npn \__chemid_use:n #1
  {
    \__chemid_record:n {#1}
    \cs_if_exist:NTF \texorpdfstring
      {
        \texorpdfstring
          { \__chemid_render:nn { rich } {#1} }
          { \__chemid_render:nn { plain } {#1} }
      }
      { \__chemid_render:nn { rich } {#1} }
  }

%% ------------------------------------------------------------------
%% 11. Anchor -- \chemidhere{key,key,...}
%% ------------------------------------------------------------------

\cs_new_protected:Npn \__chemid_hypertarget:n #1 { \hypertarget {#1} { } }
\cs_generate_variant:Nn \__chemid_hypertarget:n { V }

% A zero-sized, layout-neutral hypertarget, in horizontal mode as well
% as in vertical mode (inside a figure, next to the \caption...).
\cs_new_protected:Npn \__chemid_target:n #1
  {
    \bool_lazy_and:nnT
      { \bool_if_p:N \g__chemid_hyper_bool }
      { \bool_if_p:N \l__chemid_links_bool }
      {
        \str_set:Ne \l__chemid_name_str { \__chemid_name:n {#1} }
        \mode_if_vertical:TF
          {
            \dim_set:Nn \l__chemid_prevdepth_dim { \tex_prevdepth:D }
            \nointerlineskip
            \vbox_to_zero:n
              { \hbox:n { \__chemid_hypertarget:V \l__chemid_name_str } \vss }
            \tex_prevdepth:D \l__chemid_prevdepth_dim
          }
          { \hbox_overlap_right:n { \__chemid_hypertarget:V \l__chemid_name_str } }
      }
  }
\cs_generate_variant:Nn \__chemid_target:n { V }

% One \chemidhere{...}: a list of keys, exactly the comma convention
% \chemid and \chemid* already use.
\cs_new_protected:Npn \__chemid_here_list:n #1
  {
    \clist_map_inline:nn {#1}
      { \tl_if_blank:nF {##1} { \__chemid_here:n {##1} } }
  }

% One key of that list.  A *parent* key anchors its whole family: the
% parent itself, plus every child that has no anchor of its own -- one
% \chemidhere{cmbr} where the scheme draws cmbr.4, cmbr.6 and cmbr.8,
% instead of one \chemidhere per compound drawn.
\cs_new_protected:Npn \__chemid_here:n #1
  {
    \__chemid_parse:nTF {#1}
      {
        % 'known' accepts a key seen only in the previous pass (forward
        % declaration) -- see the accessors above -- so the anchor count
        % read below cannot assume \g__chemid_anchor_prop was pre-seeded
        % at declaration time: it may not have happened yet this pass.
        \__chemid_known:VTF \l__chemid_key_str
          {
            % \l__chemid_key_str and friends belong to \__chemid_parse:n
            % and are about to be needed again by the loop over the
            % children: take a copy before anything else runs.
            \str_set_eq:NN \l__chemid_herekey_str   \l__chemid_key_str
            \str_set_eq:NN \l__chemid_herechild_str \l__chemid_child_str
            \__chemid_anchor_own:V \l__chemid_herekey_str
            \bool_lazy_and:nnT
              { \str_if_empty_p:N \l__chemid_herechild_str }
              { \bool_if_p:N \l__chemid_anchorkids_bool }
              { \__chemid_anchor_children:V \l__chemid_herekey_str }
          }
          {
            \msg_error:nnV { chemidentifier } { anchor-undefined-key }
              \l__chemid_key_str
          }
      }
      { }
  }

% An anchor the document asked for by name.  Two of those on one key is
% the ambiguity the warning is about; an anchor of one's own landing on
% top of a family anchor is not -- it is the very case the family
% anchor is meant to give way to, and it is silent.
\cs_new_protected:Npn \__chemid_anchor_own:n #1
  {
    \bool_lazy_and:nnT
      { \int_compare_p:nNn { \__chemid_anchorcount:n {#1} } > { 0 } }
      { ! \prop_if_in_p:Nn \g__chemid_weakanchor_prop {#1} }
      { \msg_warning:nnn { chemidentifier } { duplicate-anchor } {#1} }
    \prop_gremove:Nn \g__chemid_weakanchor_prop {#1}
    \__chemid_anchor_place:nn {#1} { own }
  }
\cs_generate_variant:Nn \__chemid_anchor_own:n { V }

% An anchor laid on a child by \chemidhere{parent}.  Skipped when the
% child is anchored by name -- already this run, or, from the .aux, on
% the previous one, which is what keeps the two from piling two PDF
% targets of the same name on top of each other once the document has
% settled.
\cs_new_protected:Npn \__chemid_anchor_children:n #1
  {
    \seq_map_inline:Nn \g__chemid_order_seq
      { \__chemid_anchor_kid:nn {#1} {##1} }
    % Children declared *after* this point in the source are only known
    % from the previous run: taking them from there too is what makes a
    % family anchor independent of where the family is declared.
    \prop_map_inline:Nn \g__chemid_prev_parent_prop
      { \__chemid_anchor_kid:nn {#1} {##1} }
  }
\cs_generate_variant:Nn \__chemid_anchor_children:n { V }

% #1 parent being anchored, #2 candidate key
\cs_new_protected:Npn \__chemid_anchor_kid:nn #1#2
  {
    \bool_lazy_all:nT
      {
        { \int_compare_p:nNn { 0 \__chemid_sub:n {#2} } > { 0 } }
        { \str_if_eq_p:ee { \__chemid_parent:n {#2} } {#1} }
        { \int_compare_p:nNn { \__chemid_anchorcount:n {#2} } = { 0 } }
        { ! \prop_if_in_p:Nn \g__chemid_prevexplicit_prop {#2} }
      }
      {
        \prop_gput:Nnn \g__chemid_weakanchor_prop {#2} { }
        \__chemid_anchor_place:nn {#2} { family }
      }
  }

% Lay the target and book-keep it, whichever of the two ways asked for
% it.  #1 key, #2 'own' or 'family'.
\cs_new_protected:Npn \__chemid_anchor_place:nn #1#2
  {
    \prop_gput:Nne \g__chemid_anchor_prop {#1}
      { \int_eval:n { \__chemid_anchorcount:n {#1} + 1 } }
    \__chemid_note_firstkid:n {#1}
    \__chemid_target:n {#1}
    \__chemid_write_anchor:nn {#1} {#2}
  }

% The first anchored child of a parent is where a link to that parent
% goes when the parent has no anchor of its own (\__chemid_targetkey:n).
% First one wins, and it is recorded for the next run.
\cs_new_protected:Npn \__chemid_note_firstkid:n #1
  {
    \int_compare:nNnT { 0 \__chemid_sub:n {#1} } > { 0 }
      {
        \prop_if_in:NeTF \g__chemid_firstkid_prop { \__chemid_parent:n {#1} }
          { }
          {
            \prop_gput:Nen \g__chemid_firstkid_prop
              { \__chemid_parent:n {#1} } {#1}
          }
      }
  }

% Anchors are remembered in the .aux so that the next run knows, at the
% moment it prints a number, whether that number has a target at all --
% \chemid may well come before its \chemidhere in the source.  This is
% what makes the links settle on the second compilation, like \ref.
% An anchor laid on a child by its parent is written apart: it counts
% as a target all the same, but it must not stop the child from
% claiming an anchor of its own next time round.
\cs_new_protected:Npn \__chemid_write_anchor:nn #1#2
  {
    \legacy_if:nT { @filesw }
      {
        \iow_now:Ne \@auxout
          {
            \str_if_eq:nnTF {#2} { own }
              { \token_to_str:N \chemidanchored }
              { \token_to_str:N \chemidanchoredfamily }
            {#1}
          }
      }
  }

\ProvideDocumentCommand \chemidanchoredfamily { m }
  { \prop_gput:Nnn \g__chemid_prevanchor_prop {#1} { } }

\ProvideDocumentCommand \chemidfirstchild { m m }
  { \prop_gput:Nnn \g__chemid_prev_firstkid_prop {#1} {#2} }

\cs_new_protected:Npn \__chemid_write_firstkids:
  {
    \legacy_if:nT { @filesw }
      {
        \prop_map_inline:Nn \g__chemid_firstkid_prop
          {
            \iow_now:Ne \@auxout
              { \token_to_str:N \chemidfirstchild {##1} {##2} }
          }
      }
  }

% Written by \chemidhere{key} itself (and by version 1 of the package,
% with the same meaning and the same signature: a .aux left over from
% it still reads).
\ProvideDocumentCommand \chemidanchored { m }
  {
    \prop_gput:Nnn \g__chemid_prevanchor_prop   {#1} { }
    \prop_gput:Nnn \g__chemid_prevexplicit_prop {#1} { }
  }

% Forward declaration: every key gets its final state (number, letter,
% parent, rich/raw name) written to the .aux at \end{document}, and read
% back automatically at the next \begin{document} -- same mechanism and
% same trade-off as \label/\ref.  \chemid / \herechemid may then be used
% *before* the corresponding \chemid* in the source: undefined and '??'
% on the run that first introduces the forward reference, resolved from
% the second run on, and stable afterwards, because the declaration
% order itself never depends on where the uses sit in the document.
\ProvideDocumentCommand \chemidregister { m m m m m m }
  {
    \prop_gput:Nnn \g__chemid_prev_num_prop    {#1} {#2}
    \prop_gput:Nnn \g__chemid_prev_sub_prop    {#1} {#3}
    \prop_gput:Nnn \g__chemid_prev_parent_prop {#1} {#4}
    \prop_gput:Nnn \g__chemid_prev_rich_prop   {#1} {#5}
    \prop_gput:Nnn \g__chemid_prev_raw_prop    {#1} {#6}
  }

% Deliberately *not* extra arguments of \chemidregister: a .aux file
% written by an earlier run must keep reading, and a command that
% grows an argument would eat whatever followed it there instead.
\ProvideDocumentCommand \chemidregisterfamily { m m m m m m }
  {
    \prop_gput:Nnn \g__chemid_prev_famformat_prop    {#1} {#2}
    \prop_gput:Nnn \g__chemid_prev_famstyle_prop     {#1} {#3}
    \prop_gput:Nnn \g__chemid_prev_famholder_prop    {#1} {#4}
    \prop_gput:Nnn \g__chemid_prev_famholderraw_prop {#1} {#5}
    \prop_gput:Nnn \g__chemid_prev_famexplicit_prop  {#1} {#6}
  }

\ProvideDocumentCommand \chemidregistervalue { m m m }
  {
    \prop_gput:Nnn \g__chemid_prev_value_prop    {#1} {#2}
    \prop_gput:Nnn \g__chemid_prev_valueraw_prop {#1} {#3}
  }

\cs_new_protected:Npn \__chemid_write_register:n #1
  {
    \iow_now:Ne \@auxout
      {
        \token_to_str:N \chemidregister
          {#1}
          { \__chemid_num:n {#1} }
          { \__chemid_sub:n {#1} }
          { \__chemid_parent:n {#1} }
          { \__chemid_rich:n {#1} }
          { \__chemid_raw:n {#1} }
      }
    % What makes a family a family, and what each of its children is
    % worth, travel the same way and for the same reason: \chemid{lc.a}
    % may well come before the \chemidfamily that shapes it.
    \prop_if_in:NnT \g__chemid_famformat_prop {#1}
      {
        \iow_now:Ne \@auxout
          {
            \token_to_str:N \chemidregisterfamily
              {#1}
              { \__chemid_famformat:n {#1} }
              { \__chemid_famstyle:n {#1} }
              { \__chemid_famholder:n {#1} }
              { \__chemid_famholderraw:n {#1} }
              { \__chemid_famexplicit:n {#1} }
          }
      }
    \prop_if_in:NnT \g__chemid_value_prop {#1}
      {
        \iow_now:Ne \@auxout
          {
            \token_to_str:N \chemidregistervalue
              {#1}
              { \__chemid_value:n {#1} }
              { \__chemid_valueraw:n {#1} }
          }
      }
  }

\cs_new_protected:Npn \__chemid_write_all_registers:
  {
    \legacy_if:nT { @filesw }
      { \seq_map_function:NN \g__chemid_order_seq \__chemid_write_register:n }
  }

%% ------------------------------------------------------------------
%% 12. Reset
%% ------------------------------------------------------------------

\NewDocumentCommand \chemidreset { }
  { \int_gzero:N \g__chemid_main_int }

%% ------------------------------------------------------------------
%% 13. End of document check
%% ------------------------------------------------------------------

% Delivers the verdict deferred by \__chemid_record_one:n: a key seen
% unknown at some point during the run may since have been declared
% (a forward reference, resolved once every \chemid* in the document
% has run) or may genuinely never appear in a \chemid* anywhere --
% indistinguishable from inside \__chemid_record_one:n itself, but
% settled by now.  Resolved after all -> counts as used, same as any
% other key (so \__chemid_check_anchors: below still asks for its
% anchor). Still unknown -> that was a genuine undeclared key; the
% error simply appears here instead of at first use.
% Hook code is stored verbatim, so the inline mapping takes #1, not ##1.
\cs_new_protected:Npn \__chemid_check_pending:
  {
    \prop_map_inline:Nn \g__chemid_reported_prop
      {
        \__chemid_known:nTF {##1}
          { \prop_gput:Nnn \g__chemid_used_prop {##1} { } }
          { \msg_error:nnn { chemidentifier } { undefined-key } {##1} }
      }
  }

% A compound printed with nowhere to jump to.  Silent under
% 'links=false', where no link was going to be drawn anyway, and silent
% for a parent whose family carries the anchor: \__chemid_targetkey:n
% sends the link to the first anchored child, so nothing is missing.
% Hook code is stored verbatim, so the inline mapping takes #1, not ##1.
\cs_new_protected:Npn \__chemid_check_anchors:
  {
    \bool_if:NT \l__chemid_links_bool
      {
        \seq_map_inline:Nn \g__chemid_order_seq
          {
            \bool_lazy_and:nnT
              { \prop_if_in_p:Nn \g__chemid_used_prop {##1} }
              { ! \__chemid_hasanchor_p:e { \__chemid_targetkey:n {##1} } }
              { \msg_warning:nnn { chemidentifier } { missing-anchor } {##1} }
          }
      }
  }

\hook_gput_code:nnn { enddocument } { chemidentifier }
  {
    \__chemid_check_pending:
    \__chemid_check_anchors:
    \__chemid_write_all_registers:
    \__chemid_write_firstkids:
  }

%% ------------------------------------------------------------------
%% 14. User interface
%% ------------------------------------------------------------------

\NewDocumentCommand \chemid { s m }
  {
    \IfBooleanTF {#1}
      { \__chemid_declare_peek:n {#2} }
      { \__chemid_use:n {#2} }
  }

% Only the *starred* form takes further arguments, and they are peeked
% at here rather than declared in the signature: a brace group following
% a plain \chemid{...} is thus never swallowed.  Accepted forms:
%     \chemid*{key}              \chemid*{key}{rich}
%     \chemid*{key}{rich}[raw]   \chemid*{key}[rich][raw]
%
% The peeks are strict (no \ignore_spaces): {rich} or [rich] is only
% captured when it is glued directly to the key, with *zero* intervening
% space or newline -- exactly the style used in every example of the
% manual.  Any space (a single end of line included) makes the peek
% fail, and everything that follows is left untouched as ordinary body
% text.  This is what rules out the classic footgun of a paragraph that
% happens to start with a brace on the very next source line.
\tl_new:N \l__chemid_pendingkey_tl

\cs_new_protected:Npn \__chemid_declare_peek:n #1
  {
    \tl_set:Nn \l__chemid_pendingkey_tl {#1}
    \tl_clear:N \l__chemid_rich_tl
    \tl_clear:N \l__chemid_raw_tl
    \peek_catcode:NTF \c_group_begin_token
      { \__chemid_grab_rich:n }
      { \__chemid_peek_bracket_i: }
  }

% {rich} present: grab it, then look for an optional [raw].
\cs_new_protected:Npn \__chemid_grab_rich:n #1
  {
    \tl_set:Nn \l__chemid_rich_tl {#1}
    \__chemid_peek_bracket_ii:
  }

% No brace group: [rich] may still come, followed by [raw].
\cs_new_protected:Npn \__chemid_peek_bracket_i:
  {
    \peek_charcode:NTF [
      { \__chemid_grab_rich_bracket:w }
      { \__chemid_declare_now: }
  }
\cs_new_protected:Npn \__chemid_grab_rich_bracket:w [#1]
  {
    \tl_set:Nn \l__chemid_rich_tl {#1}
    \__chemid_peek_bracket_ii:
  }

\cs_new_protected:Npn \__chemid_peek_bracket_ii:
  {
    \peek_charcode:NTF [
      { \__chemid_grab_raw_bracket:w }
      { \__chemid_declare_now: }
  }
\cs_new_protected:Npn \__chemid_grab_raw_bracket:w [#1]
  {
    \tl_set:Nn \l__chemid_raw_tl {#1}
    \__chemid_declare_now:
  }

\cs_generate_variant:Nn \__chemid_declare:nnn { VVV }

% \chemid*{key} declares one key ; \chemid*{key1,key2,...} declares
% several at once, each auto-numbered in turn -- exactly the comma
% convention already used by \chemid.  A custom {rich}[raw] name cannot
% be attached to a multi-key declaration: it would be ambiguous which
% key it belongs to.
\cs_new_protected:Npn \__chemid_declare_now:
  {
    \clist_set:NV \l__chemid_pendingkeys_clist \l__chemid_pendingkey_tl
    \int_compare:nNnTF { \clist_count:N \l__chemid_pendingkeys_clist } > { 1 }
      {
        \bool_lazy_or:nnTF
          { ! \tl_if_blank_p:V \l__chemid_rich_tl }
          { ! \tl_if_blank_p:V \l__chemid_raw_tl }
          {
            \msg_error:nnV { chemidentifier } { multi-key-custom-name }
              \l__chemid_pendingkey_tl
          }
          {
            \clist_map_inline:Nn \l__chemid_pendingkeys_clist
              { \__chemid_declare:nnn {##1} { } { } }
          }
      }
      {
        \__chemid_declare:VVV
          \l__chemid_pendingkey_tl \l__chemid_rich_tl \l__chemid_raw_tl
      }
  }

\NewDocumentCommand \chemidhere { m }
  { \__chemid_here_list:n {#1} }

% \herechemid was version 1's name for it, and the odd one out among
% \chemid, \chemidkey, \chemidnote, \chemidscheme, \chemidsetup: kept
% working for the documents that use it, mentioned once, never twice.
\NewDocumentCommand \herechemid { m }
  {
    \bool_if:NF \g__chemid_saidrename_bool
      {
        \bool_gset_true:N \g__chemid_saidrename_bool
        \msg_warning:nn { chemidentifier } { renamed-herechemid }
      }
    \__chemid_here_list:n {#1}
  }

%% ------------------------------------------------------------------
%% 15. Templated families -- \chemidfamily{parent}{options}
%% ------------------------------------------------------------------
%%
%% An ordinary parent numbers its children with letters: 1a, 1b, 1c.
%% A *family* names them instead, from a stem it holds and a value each
%% child carries:
%%
%%   \chemidfamily{lc}{ name = Lcoum-C, child-format = subscript,
%%                      sub-style = arabic, placeholder = n }
%%     \chemid*{lc.four}{4}      -> Lcoum-C_4
%%     \chemid*{lc.eight}{8}     -> Lcoum-C_8
%%     \chemid{lc}               -> Lcoum-C_n
%%     \chemid{lc.four,lc.eight} -> Lcoum-C_{4,8}
%%
%% The stem is printed once for the whole group, exactly as the '1' of
%% '1a,b' is, and the values are gathered in one subscript.
%%
%% Children may equally carry no value at all, and be numbered by the
%% family's own 'sub-style' -- lower-greek gives alpha, beta, gamma...
%% What they may not do is mix the two: see 'family-mixed-values'.

\tl_new:N \l__chemid_fam_name_tl
\tl_new:N \l__chemid_fam_raw_tl
\tl_new:N \l__chemid_fam_style_tl
\tl_new:N \l__chemid_fam_format_tl
\tl_new:N \l__chemid_fam_code_tl
\tl_new:N \l__chemid_fam_holder_tl
\tl_new:N \l__chemid_fam_holderraw_tl

% Bound to the value being printed while a 'child-format' is applied,
% and defined here as a no-op so that a format sitting in the .aux, or
% a stray use of it in the document, expands to nothing rather than
% blowing up.  It is \NewDocumentCommand, hence robust, which is what
% lets a format survive the \iow_now:Ne that writes it out.
\NewDocumentCommand \chemidvalue { } { }

\keys_define:nn { chemidentifier / family }
  {
    name            .tl_set:N = \l__chemid_fam_name_tl ,
    raw             .tl_set:N = \l__chemid_fam_raw_tl ,
    sub-style       .tl_set:N = \l__chemid_fam_style_tl ,
    child-format    .tl_set:N = \l__chemid_fam_format_tl ,
    placeholder     .tl_set:N = \l__chemid_fam_holder_tl ,
    placeholder-raw .tl_set:N = \l__chemid_fam_holderraw_tl ,
    unknown         .code:n   =
      {
        \msg_warning:nnV { chemidentifier } { unknown-family-option }
          \l_keys_key_str
      } ,
  }

\NewDocumentCommand \chemidfamily { m m }
  { \__chemid_family:nn {#1} {#2} }

\cs_new_protected:Npn \__chemid_family:nn #1#2
  {
    \__chemid_parse:nTF {#1}
      {
        \str_if_empty:NTF \l__chemid_child_str
          { \__chemid_family_aux:n {#2} }
          {
            \msg_error:nnV { chemidentifier } { family-needs-parent }
              \l__chemid_key_str
          }
      }
      { }
  }

% Declares the parent if it is new, completes it if it already exists
% -- a child declared first creates its parent implicitly, and the
% family options may perfectly well arrive after that.  Being a family
% twice over, though, is a contradiction: the second set of options
% would silently rewrite every child already declared.
\cs_new_protected:Npn \__chemid_family_aux:n #1
  {
    \prop_if_in:NVTF \g__chemid_famformat_prop \l__chemid_key_str
      { \msg_error:nnV { chemidentifier } { family-twice } \l__chemid_key_str }
      {
        \tl_clear:N \l__chemid_fam_name_tl
        \tl_clear:N \l__chemid_fam_raw_tl
        \tl_clear:N \l__chemid_fam_holder_tl
        \tl_clear:N \l__chemid_fam_holderraw_tl
        \tl_set_eq:NN \l__chemid_fam_style_tl \l__chemid_substyle_tl
        \tl_set:Nn \l__chemid_fam_format_tl { subscript }
        \keys_set:nn { chemidentifier / family } {#1}
        \prop_if_in:NVTF \g__chemid_num_prop \l__chemid_key_str
          { \prop_gremove:NV \g__chemid_implicit_prop \l__chemid_key_str }
          { \__chemid_new_parent:N \l__chemid_key_str }
        \__chemid_store_names:VV \l__chemid_fam_name_tl \l__chemid_fam_raw_tl
        \__chemid_family_code:
        \__chemid_family_store:
      }
  }

% 'child-format' takes a keyword or, for anything the three keywords
% cannot express, the code itself -- any material calling \chemidvalue
% where the value belongs.  Both end up stored as such code.
\cs_new_protected:Npn \__chemid_family_code:
  {
    \str_case:VnF \l__chemid_fam_format_tl
      {
        { subscript }
          { \tl_set:Nn \l__chemid_fam_code_tl { \textsubscript { \chemidvalue } } }
        { superscript }
          { \tl_set:Nn \l__chemid_fam_code_tl { \textsuperscript { \chemidvalue } } }
        { plain }
          { \tl_set:Nn \l__chemid_fam_code_tl { \chemidvalue } }
      }
      { \tl_set_eq:NN \l__chemid_fam_code_tl \l__chemid_fam_format_tl }
  }

\cs_new_protected:Npn \__chemid_family_store:
  {
    \tl_if_empty:NT \l__chemid_fam_holderraw_tl
      {
        \bool_if:NTF \l__chemid_purify_bool
          {
            \tl_set:Ne \l__chemid_fam_holderraw_tl
              { \text_purify:n { \l__chemid_fam_holder_tl } }
          }
          { \tl_set_eq:NN \l__chemid_fam_holderraw_tl \l__chemid_fam_holder_tl }
      }
    \prop_gput:NVV \g__chemid_famformat_prop
      \l__chemid_key_str \l__chemid_fam_code_tl
    \prop_gput:NVV \g__chemid_famstyle_prop
      \l__chemid_key_str \l__chemid_fam_style_tl
    \prop_gput:NVV \g__chemid_famholder_prop
      \l__chemid_key_str \l__chemid_fam_holder_tl
    \prop_gput:NVV \g__chemid_famholderraw_prop
      \l__chemid_key_str \l__chemid_fam_holderraw_tl
  }

%% ------------------------------------------------------------------
%% 16. Text substitution in Inkscape .pdf_tex figures (LuaLaTeX only)
%% ------------------------------------------------------------------
%%
%% A ChemDraw/Inkscape scheme is exported once, with a plain-text
%% placeholder ("TMP1", "cpdA"...) standing in for each compound label.
%% \chemidscheme reads the .pdf_tex line by line and substitutes every
%% placeholder for the corresponding \chemid{key} -- or any other
%% replacement text -- *as the file is read*, without ever touching the
%% file on disk: re-exporting from Inkscape loses nothing. This mirrors
%% what psfrag once did for .eps figures, without the .eps baggage: a
%% .pdf_tex is plain LaTeX + \includegraphics{...pdf}, so the trick is
%% just a literal, per-line text substitution done in Lua, which needs
%% LuaLaTeX. No support is provided for the old .eps/psfrag route.
%%
%% USAGE
%%   \chemidsetup{ pdftex-font = \sffamily\small }   % base style, once
%%   \chemid*{precursor}  \chemid*{product}
%%   ...
%%   \begin{figure}
%%     \chemidkey{TMP1}{precursor}   % TMP1 -> \chemid{precursor}
%%     \chemidkey[1.4]{TMP2}{product}         % 1.4x bigger than the rest
%%     \chemidnote{TMPCOND}{K$_2$CO$_3$, acetone, 70~\si{\celsius}}
%%     \chemidscheme[0.8]{figures/scheme.pdf_tex}   % scale is optional
%%     \herechemid{precursor}\herechemid{product}
%%     \caption{...}
%%   \end{figure}
%%
%% The placeholder must sit ISOLATED in the figure text (not a substring
%% of another word): the substitution is literal and global on each
%% line. \chemidscheme consumes the pending list as it reads the file,
%% so the next figure starts from an empty list automatically -- there
%% is no separate "clear" step to remember.
%%
%% SIZE OF THE SUBSTITUTED TEXT
%% Three factors multiply together, so a figure scaled down still reads
%% fine without retouching every label by hand:
%%   1. pdftex-font (\chemidsetup)     -- the base style, set once
%%   2. the [scale] of \chemidscheme   -- same number as the figure's
%%      own \svgscale, so labels shrink/grow together with the drawing
%%   3. the optional [factor] of \chemidkey/\chemidnote -- one label
%%      singled out, relative to the other labels of the same figure
%% The combined factor is applied with \scalebox (graphicx, already
%% needed by the .pdf_tex itself): a geometric zoom on top of whatever
%% pdftex-font selected, so this works regardless of what that style
%% declaration contains.

\bool_new:N \g__chemid_pdftex_repl_bool % true once the Lua table exists
\tl_new:N   \l__chemid_pdftex_dir_tl

\msg_new:nnnn { chemidentifier } { pdftex-needs-lualatex }
  { \iow_char:N \\chemidscheme~requires~LuaLaTeX. }
  {
    Text~substitution~in~a~.pdf_tex~figure~is~done~by~a~small~Lua~
    routine,~so~this~document~must~be~compiled~with~lualatex~(or~any~
    engine~providing~\iow_char:N \\directlua).~Compile~with~
    'lualatex'~instead,~or~drop~\iow_char:N \\chemidscheme~and~
    \iow_char:N \\includegraphics~the~figure~as~usual.
  }

\msg_new:nnnn { chemidentifier } { pdftex-file-not-found }
  { File~'#1'~not~found. }
  { Check~the~path~given~to~\iow_char:N \\chemidscheme. }

% Letter-only name (no '_' or ':'), on purpose: this is what Lua prints
% back into the document to report a missing file, and at that point
% TeX is reading it under whatever catcodes the *document* uses -- not
% expl3's -- so an expl3-named function such as \msg_error:nnn would
% not even tokenize as one control sequence there.
\cs_new_protected:Npn \__chemid_pdftex_filenotfound:n #1
  { \msg_error:nnn { chemidentifier } { pdftex-file-not-found } {#1} }
\cs_generate_variant:Nn \__chemid_pdftex_filenotfound:n { V }
\ProvideDocumentCommand \chemidpdftexfilenotfound { m }
  { \__chemid_pdftex_filenotfound:n {#1} }

% expl3 gives the space character catcode 9 (ignored): harmless for
% ordinary expl3 code, fatal for the Lua source embedded below, whose
% keywords ('if', 'then', 'end'...) would otherwise run together with
% no separator at all. Restore the ordinary catcode 10 (space) until
% the end of this section, then set it back; no \group_begin: here on
% purpose -- \NewDocumentCommand defines its macros *locally*, and a
% group would silently discard them all again at \group_end:. Only the
% space character is touched, so expl3 names (letters '_' and ':')
% read below are completely unaffected.
%
% A second, sneakier consequence of the same toggle: for a *raw*
% \cs_new(_protected):Npn definition (\NewDocumentCommand is immune --
% see below), any whitespace between the parameter text and the body's
% opening brace -- a same-line space, or even just a line break, since
% crossing a line end inserts a space token exactly like a literal one
% typed there -- now becomes a REAL token instead of vanishing, and
% TeX reads it as a trailing DELIMITER on the last parameter: the
% macro silently stops taking that parameter as "the next {...} group"
% and starts taking it as "everything up to the next space", which is
% *empty* the moment two such calls sit next to each other as
% \foo{a} {b}. Every \cs_new(_protected):Npn below therefore glues its
% opening brace directly onto the parameter text (or the bare name, if
% it takes none), with zero characters in between -- ugly, but the
% only reliably safe spelling here. \NewDocumentCommand does not have
% this problem: its body is just an ordinary argument grabbed by
% \NewDocumentCommand itself, not raw \def parameter-text syntax.
\catcode`\ =10\relax

% Lazily create the Lua replacement table: keeps this section a no-op,
% including under a non-Lua engine, until \chemidkey/\chemidnote is
% actually used.
\cs_new_protected:Npn \__chemid_pdftex_init:{
    \bool_if:NF \g__chemid_pdftex_repl_bool
      {
        \bool_gset_true:N \g__chemid_pdftex_repl_bool
        \directlua{ __chemidentifier_pdftex_repl = { } }
      }
  }

% \chemidnote[factor]{motif}{replacement} -- free-form substitution:
% any TeX material is allowed on the right-hand side, expanded when the
% figure is read (so \chemid{key} used here reflects its value at that
% point). [factor] singles this one label out, on top of pdftex-font
% and the \chemidscheme scale -- see "SIZE OF THE SUBSTITUTED TEXT"
% above; it defaults to 1, i.e. no effect beyond the other two factors.
\NewDocumentCommand \chemidnote { O{1} m m }
  {
    \sys_if_engine_luatex:T
      {
        \__chemid_pdftex_init:
        \directlua{
          table.insert(__chemidentifier_pdftex_repl, {
            from   = [[\detokenize{#2}]],
            to     = [[\detokenize{#3}]],
            factor = [[\detokenize{#1}]]
          })
        }
      }
  }

% \chemidkey[factor]{motif}{key} -- shorthand for
% \chemidnote[factor]{motif}{\chemid{key}}: the placeholder becomes the
% compound's current number (and its clickable link), without declaring
% or renumbering it.
\NewDocumentCommand \chemidkey { O{1} m m }
  {
    \sys_if_engine_luatex:T
      {
        \__chemid_pdftex_init:
        \directlua{
          table.insert(__chemidentifier_pdftex_repl, {
            from   = [[\detokenize{#2}]],
            to     = [[\noexpand\chemid{\detokenize{#3}}]],
            factor = [[\detokenize{#1}]]
          })
        }
      }
  }

% Engine-agnostic core: read #2 line by line, apply the pending
% replacements -- each one scaled by #1 (the \chemidscheme scale) times
% its own [factor] -- typeset the result, then empty the list, so the
% next \chemidscheme starts clean with no separate "clear" step to
% forget.
%
% No Lua comment ('--') appears in this block, on purpose: \directlua
% folds a multi-line brace argument into one logical Lua line (each
% newline becomes a plain space, not a real \n), so a '--' comment
% would swallow everything typed after it, all the way to the end of
% the chunk. Two more characters get the same "must not appear
% literally" treatment for a different reason: '%' is still a *TeX*
% comment marker even inside \directlua{...} (it would eat the rest of
% that physical line, braces included, before Lua ever sees it), and
% '~' is active in this file already (the non-breaking space used
% elsewhere in the package) -- string.char(37) and 'not (eff == 1)'
% below sidestep the two of them.
\cs_new_protected:Npn \__chemid_pdftex_apply:nn #1#2{
    \group_begin:
      \l__chemid_pdftexfont_tl \selectfont
      \directlua{
        local scheme_scale = tonumber([[\detokenize{#1}]]) or 1
        local filename = [[\detokenize{#2}]]
        local repl = __chemidentifier_pdftex_repl

        local function replace_plain(s, needle, replacement)
          if needle == "" then return s end
          local out, i = {}, 1
          while true do
            local j, k = s:find(needle, i, true)
            if not j then table.insert(out, s:sub(i)); break end
            table.insert(out, s:sub(i, j - 1))
            table.insert(out, replacement)
            i = k + 1
          end
          return table.concat(out)
        end

        local f = io.open(filename, "r")
        if not f then
          tex.print("\\chemidpdftexfilenotfound{" .. filename .. "}")
        else
          for line in f:lines() do
            if line:sub(-1) == string.char(13) then
              line = line:sub(1, -2)
            end
            for _, r in ipairs(repl) do
              local eff = scheme_scale * (tonumber(r.factor) or 1)
              local to = r.to
              if not (eff == 1) then
                local fmt = string.char(37) .. ".5f"
                to = "\\scalebox{" .. string.format(fmt, eff) .. "}{" .. to .. "}"
              end
              line = replace_plain(line, r.from, to)
            end
            tex.print(line)
          end
          f:close()
        end
        __chemidentifier_pdftex_repl = { }
      }
    \group_end:
  }

% Directory part of #1, as a detokenized string (Lua pattern match) --
% used to add the figure's own folder to \graphicspath, since a
% .pdf_tex calls \includegraphics{<name>.pdf} without any path. Unlike
% \__chemid_pdftex_apply:nn and \__chemid_pdftex_init: above, this
% function's *result* is used as data (spliced into \graphicspath by
% the caller), not just executed for effect: any stray leading or
% trailing space token baked in from the usual multi-line indented
% layout -- harmless for those two, invisible in typeset output --
% would corrupt this one, turning "figures/" into " figures/ " and
% breaking every file lookup in the figure. One line, glued tight at
% both ends, avoids that entirely.
\cs_new:Npn \__chemid_pdftex_dirof:n #1{\directlua{tex.sprint((string.match([[\detokenize{#1}]], "^.*/") or ""))}}

% Space goes back to being ignored here: \chemidscheme below contains
% no raw Lua of its own (it only orchestrates calls to the functions
% above, already safely captured), so it belongs back in expl3's usual
% regime -- catcode 10 would otherwise bake the cosmetic spaces of
% '{ \__chemid_pdftex_dirof:n {#2} }' straight into the *value* that
% \tl_set:Ne (a full, \edef-like expansion) assigns to
% \l__chemid_pdftex_dir_tl, silently turning "figures/" into
% "  figures/  " and breaking every \includegraphics lookup in the
% figure.
\catcode`\ =9\relax

% \chemidscheme[scale]{path} -- the command to actually use. [scale]
% defaults to 1: it both feeds \svgscale (the figure's own geometry --
% up to the .pdf_tex file to use it) and multiplies into the font size
% of every substituted label, so the two stay in step.
\NewDocumentCommand \chemidscheme { O{1} m }
  {
    \sys_if_engine_luatex:TF
      {
        \group_begin:
          \def\svgscale {#1}
          \tl_set:Ne \l__chemid_pdftex_dir_tl { \__chemid_pdftex_dirof:n {#2} }
          \tl_if_empty:NF \l__chemid_pdftex_dir_tl
            { \graphicspath { { \l__chemid_pdftex_dir_tl } { ./ } } }
          \__chemid_pdftex_apply:nn {#1} {#2}
        \group_end:
      }
      { \msg_error:nn {chemidentifier} {pdftex-needs-lualatex} }
  }
