%% OpenGlyph LaTeX -- named, font-backed historical glyphs.
%% Copyright 2026 OpenGlyph contributors. MIT; see LICENSE.
\NeedsTeXFormat{LaTeX2e}[2020-10-01]
\ProvidesExplPackage{openglyph}{2026-10-28}{0.2.1}{Named font-backed glyphs for historical texts}
\RequirePackage{iftex}
\ifXeTeX\else\ifLuaTeX\else
  \PackageError{openglyph}{XeLaTeX or LuaLaTeX is required}{Use XeLaTeX, LuaLaTeX, or Tectonic, not pdfLaTeX.}
  \expandafter\endinput
\fi\fi
\RequirePackage{fontspec}
\RequirePackage{xparse}
\ExplSyntaxOn
\tl_new:N \l__openglyph_base_tl
\tl_new:N \l__openglyph_path_tl
\tl_new:N \l__openglyph_catalog_tl
\prop_new:N \g__openglyph_loaded_prop
\msg_new:nnn {openglyph}{unknown}{Unknown~glyph~'#1'.~Load~its~catalogue~and~check~the~ASCII~ID.}
\msg_new:nnn {openglyph}{duplicate}{Glyph~or~font~ID~'#1'~is~already~defined;~use~a~different~namespace.}
\msg_new:nnn {openglyph}{missing-file}{Cannot~find~'#1'.~Keep~the~catalogue~and~font~files~together.}
\msg_new:nnn {openglyph}{missing-char}{The~selected~font~does~not~contain~glyph~'#1'.~Check~the~bundle~version.}
\msg_new:nnn {openglyph}{invalid}{Invalid~ASCII~identifier~'#1'.~Use~letters,~digits,~colon,~dot,~underscore,~or~hyphen.}
\cs_new_protected:Npn \__openglyph_validate:n #1
  {\regex_match:nnF {\A[A-Za-z]([A-Za-z0-9:._]|-)*\Z}{#1}{\msg_fatal:nnn{openglyph}{invalid}{#1}}}
\NewDocumentCommand \OpenGlyphLoad {m}
  {
    \prop_if_in:NnF \g__openglyph_loaded_prop {#1}
      {
        \file_if_exist:nTF {#1/catalog.tex}
          {\file_get_full_name:nN{#1/catalog.tex}\l__openglyph_catalog_tl
           \exp_args:NV\filename@parse\l__openglyph_catalog_tl
           \tl_set:Nx\l__openglyph_base_tl{\filename@area}
           \exp_args:NV\file_input:n\l__openglyph_catalog_tl
           \tl_clear:N\l__openglyph_base_tl
           \prop_gput:Nnn\g__openglyph_loaded_prop{#1}{loaded}}
          {\str_if_eq:nnTF{#1}{ebag}
             {\file_if_exist:nTF{openglyph-ebag.def}
                {\tl_clear:N\l__openglyph_base_tl
                 \file_input:n{openglyph-ebag.def}
                 \prop_gput:Nnn\g__openglyph_loaded_prop{#1}{loaded}}
                {\msg_fatal:nnn{openglyph}{missing-file}{openglyph-ebag.def}}}
             {\msg_fatal:nnn{openglyph}{missing-file}{#1/catalog.tex}}}
      }
  }
\NewDocumentCommand \OpenGlyphDeclareFont {m m}
  {
    \__openglyph_validate:n{#1}
    \cs_if_exist:cTF{__openglyph_font_#1:}
      {\msg_fatal:nnn{openglyph}{duplicate}{#1}}
      {
        \tl_set:Nx\l__openglyph_path_tl{\l__openglyph_base_tl#2}
        % LuaTeX caches font data across documents. Keep concrete paths so a
        % relative custom-font filename never refers to a later document's cwd.
        \sys_if_engine_luatex:T
          {\directlua{
             local~p=token.get_macro("l__openglyph_path_tl");~
             if~token.get_macro("l__openglyph_base_tl")==""~then~
               p=kpse.find_file(p,"opentype~fonts")~or~p;~
             end;~
             if~not(p:sub(1,1)=="/")~and~not~p:match("^[A-Za-z]:")~then~
               p=lfs.currentdir().."/"..p;~
             end;~
             if~lfs.attributes(p,"mode")=="file"~then~
               token.set_macro("l__openglyph_path_tl",p);~
             end;~
           }}
        \exp_args:NV\file_if_exist:nTF\l__openglyph_path_tl
          {\exp_args:NV\file_get_full_name:nN\l__openglyph_path_tl\l__openglyph_path_tl
           \exp_args:NV\filename@parse\l__openglyph_path_tl
           \use:x{\exp_not:N\newfontfamily\exp_not:c{__openglyph_font_#1:}
             [Path={\filename@area}]{\filename@base.\filename@ext}}}
          {\tl_if_empty:NTF\l__openglyph_base_tl
             {% An installed OTF is found by the engine's font search, not
              % necessarily by LaTeX's tex-file search. Keep filename lookup.
              \IfFontExistsTF{#2}
                {\exp_args:Nc\newfontfamily{__openglyph_font_#1:}{#2}}
                {\msg_fatal:nnn{openglyph}{missing-file}{#2}}}
             {% A project bundle must not borrow a different installed font.
              \msg_fatal:nnn{openglyph}{missing-file}{#2}}}
      }
  }
\NewDocumentCommand \OpenGlyphDeclare {m m m}
  {
    \__openglyph_validate:n{#1}
    \regex_match:nnF{\A[0-9A-Fa-f]{4,6}\Z}{#3}{\msg_fatal:nnn{openglyph}{invalid}{#3}}
    \OpenGlyphDeclareEntry{#1}{#2}{#3}
  }
% Generated catalogues have already validated every ID and scalar in Python.
% Retain runtime collision/font checks without 45,000 regular-expression runs.
\cs_new_protected:Npn \OpenGlyphDeclareEntry #1#2#3
  {
    \cs_if_exist:cTF{__openglyph_glyph_#1:}
      {\msg_fatal:nnn{openglyph}{duplicate}{#1}}
      {
        \cs_if_exist:cF{__openglyph_font_#2:}{\msg_fatal:nnn{openglyph}{unknown}{font:#2}}
        \cs_gset_protected:cpn{__openglyph_glyph_#1:}{\__openglyph_render:nnn{#1}{#2}{#3}}
      }
  }
\cs_new_protected:Npn \__openglyph_render:nnn #1#2#3
  {
    \mode_leave_vertical:
    \hbox:n
      {
        \ifXeTeX\XeTeXinterchartokenstate=0\relax\fi
        \cs_if_exist:NT\ltjsetparameter{\ltjsetparameter{autospacing=false,autoxspacing=false}}
        \cs:w __openglyph_font_#2:\cs_end:
        \tex_iffontchar:D\tex_font:D"#3\scan_stop:
          \tex_char:D"#3\scan_stop:
        \else:\msg_error:nnn{openglyph}{missing-char}{#1}\texttt{[#1]}\fi:
      }
  }
\NewDocumentCommand \OpenGlyph {m}
  {\cs_if_exist:cTF{__openglyph_glyph_#1:}{\use:c{__openglyph_glyph_#1:}}
    {\msg_error:nnn{openglyph}{unknown}{#1}\texttt{[#1]}}}
\ProvideDocumentCommand \gwglyph {m}{\OpenGlyph{#1}}
\NewDocumentCommand \ebag {m}{\OpenGlyph{ebag:#1}}
\NewDocumentCommand \OpenGlyphAlias {m m}
  {\__openglyph_validate:n{#1}
   \cs_if_exist:cT{__openglyph_glyph_#1:}{\msg_fatal:nnn{openglyph}{duplicate}{#1}}
   \cs_if_exist:cTF{__openglyph_glyph_#2:}
     {\cs_gset_eq:cc{__openglyph_glyph_#1:}{__openglyph_glyph_#2:}}
     {\msg_fatal:nnn{openglyph}{unknown}{#2}}}
\NewDocumentCommand \OpenGlyphIfExistsTF {m m m}
  {\cs_if_exist:cTF{__openglyph_glyph_#1:}{#2}{#3}}
\NewDocumentCommand \OpenGlyphSequence {m}
  {\clist_map_inline:nn{#1}{\OpenGlyph{##1}}}
\AtBeginDocument{
  \cs_if_exist:NT\pdfstringdefDisableCommands{
    \pdfstringdefDisableCommands{\def\OpenGlyph#1{[#1]}\def\gwglyph#1{[#1]}\def\ebag#1{[ebag:#1]}}
  }
}
\ExplSyntaxOff
\endinput
