% \iffalse meta-comment
% arara: lualatex: { branch: developer, draft: true }
% arara: lualatex: { branch: developer, interaction: batchmode, shell: false }
% arara: lualatex: { branch: developer, interaction: batchmode, shell: false }
% arara: clean: { extensions: [ aux, log, out, glo, gls, hd, ind, ilg, idx, toc, atfi ] }
% arara: clean: { files: [ userdoc.idx, userdoc.ilg, userdoc.ind ] }
% arara: halt
%
% File: spanish-mathml-intent.dtx
%
% Copyright (C) 2026 by Pablo González L <pablgonz@educarchile.cl>
%
% This work may be distributed and/or modified under the conditions of the
% LaTeX Project Public License, either version 1.3c of this license or (at
% your option) any later version. The latest version of this license is in
%
%    https://www.latex-project.org/lppl.txt
%
% and version 1.3c or later is part of all distributions of LaTeX version
% 2005/12/01 or later.
%
% This work is "maintained" (as per the LPPL maintenance status)
% by Pablo González L.
%
% This work consists of the files spanish-mathml-intent.dtx,
% spanish-mathml-intent.ins, spanish-mathml-intent.sty, spintent.lua,
% spmmli-tool.lua and spmmli-tool.1.
%
% \fi
%
% \iffalse
%<*driver>
\documentclass[full]{l3doc}
\usepackage[svgnames]{xcolor}
\usepackage[sf,bf,compact,medium,pagestyles]{titlesec}
\usepackage{lastpage,microtype,attachfile2,multicol,listings,titletoc,booktabs, longtable, fontawesome5}
\usepackage{scalerel} % for \spmQ sample
\usepackage[english]{babel} % \selectlanguage{english}
\usepackage[top=0.5in,bottom=0.3in,left=1.70in,right=0.7in,footskip=0.2in,headheight=1cm,headsep=0.27cm]{geometry}
% Set \parindent and \parskip
\setlength{\parindent}{0pt}
\setlength{\parskip}{\smallskipamount}
% Extra stretch before overfull lines
\setlength{\emergencystretch}{2em}
% Fonts
\usepackage{luacode}
% For documentation and examples
\begin{luacode*}
local SCALE = 0.85
local fallback_cache = {}
local lookup_cache = {}

luatexbase.add_to_callback("luaotfload.patch_font", function(fontdata, specification, id)
  if type(fontdata) ~= "table" then return end
  local meta = fontdata.shared and fontdata.shared.rawdata and fontdata.shared.rawdata.metadata
  local familyname = meta and meta.familyname
  if type(familyname) ~= "string" then return end
  if familyname:lower():find("libertinus serif", 1, true) then
    local size = fontdata.size
    local key  = tostring(size)
    local fb_id = fallback_cache[key]
    if not fb_id then
      local path = kpse.find_file("Carlito-Regular.ttf", "truetype fonts")
      if path then
        local scaled_size = math.floor(size * SCALE)
        local ok, result = pcall(luaotfload.define_font, "[" .. path .. "]", scaled_size)
        if ok then
          fb_id = result
          fallback_cache[key] = fb_id
        end
      end
    end
    if fb_id then
      local lookup = lookup_cache[fb_id]
      if not lookup then
        local fb_font = font.getfont(fb_id)
        lookup = {}
        for uni, _ in next, fb_font.characters do
          lookup[uni] = fb_id
        end
        lookup_cache[fb_id] = lookup
      end
      fontdata.fallback_lookup = lookup
    end
  end
end, "shekel_fallback_patch")
\end{luacode*}

\usepackage[osf,nomath,mono=false,ScaleSF=0.95,ScaleRM=0.95]{libertinus-otf}

\begin{luacode*}
  local dejavu_path = kpse.find_file("DejaVuSansMono.ttf", "truetype fonts")
  luaotfload.add_fallback("shekelfallback", { "[" .. dejavu_path .. "]" })
\end{luacode*}
\defaultfontfeatures[\ttfamily]
  {
    Numbers   = OldStyle,
    Scale     = 0.84249,
    Extension = .otf,
  }
\setmonofont[
    UprightFont       = *-Regular,
    ItalicFont        = *-RegularIt,
    BoldFont          = *-Medium,
    BoldItalicFont    = *-MediumIt,
    RawFeature        = {+zero,+ss01,fallback=shekelfallback}]{SourceCodePro}

\newfontfamily\mysourcecodeprolight{SourceCodePro-Regular.otf}[
    Scale = 0.80,FakeStretch =0.75,
    FontFace = {m}{it}{Font =SourceCodePro-RegularIt.otf,Color=FF0000}]
\RenewDocumentCommand{\MacroLongFont}{}
  {
    \mysourcecodeprolight\small
  }
% Internal
\newfontfamily\mylmrotf{lmroman10-regular.otf}[NFSSFamily=lmrotf,Scale = 0.95]
% The character of visible space is now taken from Latin Modern Mono
% to prevent fonts in T1. The original definition for xetex/luatex is
% \def\verbvisiblespace{\usefont{OT1}{cmtt}{m}{n}\asciispace}
\def\verbvisiblespace{{\fontfamily{lmrotf}\fontshape{n}\selectfont\char"2423}}
% Math font
\usepackage{unicode-math}
\setmathfont{Erewhon-Math}[Scale = 0.95]
\setmathfont{Erewhon-Math}[range={cal,bfcal},StylisticSet={1,4}]
\setmathfont{NewCMMath-Regular.otf}[Scale = 0.95, range={"299C}] % \rightanglesqr
\providecommand*{\widearc}{\Umathaccent 7 \symoperators "023DC}
\newfontfamily\fetamontotf{ffmw10.otf}[
   Scale             = 0.95,%
   RawFeature        = {+latn,+rand,+kern,+size},%
   FontFace          = {b}{n}{ffmw10.otf},% fix replace font
   ]
% Remove `"` and `|` for short verbatim defined in l3doc, then set short |...|
\AtBeginDocument
  {
    \DeleteShortVerb \"
    \DeleteShortVerb \|
    \lstMakeShortInline[language=spintent-doc,basicstyle=\ttfamily]\|
  }
% Index
\usepackage{imakeidx}
\indexsetup{level=\section,firstpagestyle=myheader,othercode=\pagestyle{myheader}}
% imakeidx also opens a write stream for the .idx file, and that conflicts
% with the one opened by l3doc. Here we close that write stream:
\ExplSyntaxOn
\iow_close:c { @indexfile }
% and later we will copy the write stream opened by imakeidx into \@indexfile
% so that entries written to both streams end up in the same file.
\makeindex[name=userdoc,options={-q ~ -s ~ gind.ist},columnsep=15pt,title={Índice ~ de ~ la ~ Documentación}]
\makeindex[options={-q ~ -s ~ gind.ist},columnsep=15pt,title={Índice ~ de ~ la ~ Implementación}]
\EnableCrossrefs
% \PageIndex, \CodelineIndex undoes what \PageIndex does \CodelineIndex
% tries to open another write stream for the index file. We don't want
% that, so we temporarily make \makeindex a no-op:
\cs_set_eq:NN \l__mydoc_tmp_makeindex_cs \makeindex
\cs_set_nopar:Npn \makeindex { }
\CodelineIndex
\cs_set_eq:NN \makeindex \l__mydoc_tmp_makeindex_cs
\DoNotIndex{\ , \1,\^}
\exp_args:Ne \DoNotIndex { \tl_to_str:n { \{ } }
\exp_args:Ne \DoNotIndex { \tl_to_str:n { \} } }
\exp_args:Ne \DoNotIndex { \tl_to_str:n { \begin } }
% \StartImplementation
\cs_new_protected:Npn \StartImplementation
  {
    \bool_set_true:N \l__codedoc_in_implementation_bool
  }
\bool_set_false:N \l__codedoc_in_implementation_bool
% Links and index
\newcommand{\HP}[1]{\emph{\hyperpage{#1}}\normalsize}
\cs_new_protected:Nn \__mydoc_sort_index:nn
  {
    \bool_if:NTF \l__codedoc_in_implementation_bool
      {
        \index{#1\actualchar#2|HP}
      }
      {
        \index[userdoc]{#1\actualchar#2|HP}
      }
  }
% The text of the entry in Spanish (#2) for the user part and in English (#3)
% for the implementation, chosen when the command is called.
\cs_new_protected:Nn \__mydoc_sort_index:nnn
  {
    \bool_if:NTF \l__codedoc_in_implementation_bool
      { \__mydoc_sort_index:nn {#1} {#3} }
      { \__mydoc_sort_index:nn {#1} {#2} }
  }
% Now, after imakeidx opens the write stream for the index file, we copy
% the reference to \@indexfile:
\cs_set_eq:cc { @indexfile } { \c_sys_jobname_str @idxfile }
\ExplSyntaxOff
% LuaLaTeX syntax highlighter for l3doc
\usepackage{mylhmc}
\setmylhmc
  {
    module  = spintent,
    provide =
      { setspintent, spnum, spunit, spnewunit, spunitalias, spmoney,
        spshort, spsiglo, sptime, spdate, spread, spdsym, spmsym,
        spusep, spdiv, spmul, spnM, spnD, spmcd, spmcm, spnZ, spang,
        spmQ, spPoint, sprecta, sprayo, spside, spmside, spangle, spmangle,
        sparc, spmarc, spPoly, spAfig, spPfig, sprad, spdiam, spcirc, spAcirc,
        spPcirc, spcoord, spvec, sprazon, spProp, spnewread, spVol, spinterval, spfun, spfuncomp, spdom, sprec, spcod, %
        spProm, spmedia, spmediana, spmoda, spdesv, spvarz, %
        spcuartil, spdecil, spPerc, sprango, spriq, spmin, spmax, spFrec, spcase, spOmega, spPval, spProb, spcomp, spcomb, spfact, spabs, %
      }
  }
% Colors
\definecolor{rulecolor}{rgb}{0.96,0.96,0.96}
\definecolor{mathcatgreen}{rgb}{0.165, 0.478, 0.157} % MathCAT green (#2A7A28)
\definecolor{mathmlblue}{HTML}{1F5FBF}               % Blue for MathML "Math"
% File info
\def\fileversion{v1.4}
\def\filedate{2026-11-02}
\def\fileinfo{Spanish MathML intent}

% Config hyperref
\definecolor{newlinkcolor}{HTML}{0A602C}
\hypersetup
  {
    allcolors          = newlinkcolor,
    colorlinks         = true,
    linktoc            = all,
    pdftitle           = {.:: The spanish-mathml-intent package --- \fileinfo{} ::.},
    pdfauthor          = {Pablo González L},
    pdfsubject         = {Documentation for \fileversion{} [\filedate] },
    pdfkeywords        = {MathCAT, NVDA, tagged PDF, spanish, accesibilidad},
    bookmarksopenlevel = 2,
  }
% User level commands
\ExplSyntaxOn
% Logo
\NewDocumentCommand \pkglogo {}
  {
    \group_begin:
      \LibertinusSansOsF{\textcolor{module_name}{spanish}\textcolor{gray}{-}\textcolor{module_name}{mathml}\textcolor{gray}{-}\textcolor{OrangeRed}{intent}}
    \group_end:
  }
% Custom \meta[...]{...}, \marg[...]{...} and \oarg[...]{...} with color
\NewDocumentCommand \mymeta { O{} m }
  {
    \__mydoc_meta_generic:Nnn \__mydoc_meta:n { #1 } { #2 }
  }
\NewDocumentCommand \mymarg { O{} m }
  {
    \__mydoc_meta_generic:Nnn \__mydoc_marg:n { #1 } { #2 }
  }
\NewDocumentCommand \myoarg { O{} m }
  {
    \__mydoc_meta_generic:Nnn \__mydoc_oarg:n { #1 } { #2 }
  }
\NewDocumentCommand \myparg { O{} m }
  {
    \__mydoc_meta_generic:Nnn \__mydoc_parg:n { #1 } { #2 }
  }
% Rows with two columns inside the syntax environment
\dim_new:N \l_userdoc_col_dim
\NewDocumentCommand \mycolrow { m }
  {
    \mode_leave_vertical:
    \dim_set:Nn \l_userdoc_col_dim { #1 }
    \tex_hangindent:D \l_userdoc_col_dim
    \tex_hbox:D to \l_userdoc_col_dim \c_group_begin_token
  }
\NewDocumentCommand \mycolsep { }
  {
    \tex_hfil:D
    \c_group_end_token
  }
% Variables and keys
\tl_new:N \l_userdoc_meta_font_tl
\keys_define:nn { userdoc / mymeta }
  {
    type .choice:,
    type / tt .code:n = \tl_set:Nn \l_userdoc_meta_font_tl { \ttfamily },
    type / rm .code:n = \tl_set:Nn \l_userdoc_meta_font_tl { \rmfamily },
    type .initial:n = tt,
    cf   .tl_set:N = \l_userdoc_meta_color_tl,
    cf   .initial:n = keyname,
    ac   .tl_set:N = \l_userdoc_meta_anglecolor_tl,
    ac   .initial:n = lightgray,
    sbc  .tl_set:N = \l_userdoc_meta_brackcolor_tl,
    sbc  .initial:n = gray,
    cbc  .tl_set:N = \l_userdoc_meta_bracecolor_tl,
    cbc  .initial:n = gray,
    pbc  .tl_set:N = \l_userdoc_meta_parencolor_tl,
    pbc  .initial:n = gray,
  }
% Internal commands
\cs_new_protected:Npn \__mydoc_meta_generic:Nnn #1 #2 #3
  {
    \group_begin:
      \keys_set:nn { userdoc / mymeta } { #2 }
      \color{ \l_userdoc_meta_color_tl }
      \l_userdoc_meta_font_tl
      #1 { #3 } % #1 is \__mydoc_meta:n, \__mydoc_marg:n or \__mydoc_oarg:n
    \group_end:
  }
\cs_new_protected:Npn \__mydoc_meta:n #1
  {
    \__mydoc_meta_angle:n { \textlangle }
    \__mydoc_meta_meta:n { #1 }
    \__mydoc_meta_angle:n { \textrangle }
  }
\cs_new_protected:Npn \__mydoc_marg:n #1
  {
    \__mydoc_meta_brace:n { \textbraceleft }
    \__mydoc_meta:n { #1 }
    \__mydoc_meta_brace:n { \textbraceright }
  }
\cs_new_protected:Npn \__mydoc_oarg:n #1
  {
    \__mydoc_meta_brack:n { [ }
    \__mydoc_meta:n { #1 }
    \__mydoc_meta_brack:n { ] }
  }
\cs_new_protected:Npn \__mydoc_parg:n #1
  {
    \__mydoc_meta_brace:n { ( }
    \__mydoc_meta:n { #1 }
    \__mydoc_meta_brace:n { ) }
  }
\cs_new_protected:Npn \__mydoc_meta_meta:n #1
  {
    \textnormal{\textit{#1}}
  }
\cs_new_protected:Npn \__mydoc_meta_angle:n #1
  {
    \group_begin:
      \fontfamily{lmrotf}\fontshape{n}\selectfont
      \textcolor{\l_userdoc_meta_anglecolor_tl}{#1}
    \group_end:
  }
\cs_new_protected:Npn \__mydoc_meta_brace:n #1
  {
    \group_begin:
      \color{\l_userdoc_meta_bracecolor_tl}
        #1
    \group_end:
  }
\cs_new_protected:Npn \__mydoc_meta_brack:n #1
  {
    \textcolor{\l_userdoc_meta_brackcolor_tl}{#1}
  }
% \keyexampcmd{s m m m o}
\NewDocumentCommand \keyexampcmd { s m m m o }
  {
    \group_begin:
      \noindent
      \mode_leave_vertical:
      \hbox_to_wd:nn { -\marginparsep }
        {
          \tex_hss:D
          \textcolor { keyname } { \small \ttfamily {#2} }
        }
      \IfBooleanTF {#1}
        {
          \hphantom { \textcolor { white } { \, \bfseries \texttt{=} } \, {} }
          \mymeta [ type=tt, cbc=gray, ac=lightgray, cf=lightgray ] { \small {#3} }
        }
        {
          \textcolor { gray } { \, \bfseries \texttt{=} } \, {}
          \mymarg [ type=tt, cbc=gray, ac=lightgray, cf=keyname ] { \small {#3} }
        }
      \hfill
      \textcolor { gray }
        {
          \small
          \textsf
            {
              \bool_if:NTF \l__codedoc_in_implementation_bool
                { default } { por~defecto }
            }
          :~ \emph{#4}
        }
      \par
      \IfValueT {#5}
        {
          \clist_map_inline:nn {#5}
            {
              \__mydoc_sort_index:nnn {Keys}
                {
                  Llaves~de~ \texttt { \textbackslash ##1 } ~ provistas~por~ \pkglogo :> \texttt {#2}
                }
                {
                  Keys~for~ \texttt { \textbackslash ##1 } ~ provided~by~ \pkglogo :> \texttt {#2}
                }
            }
        }
    \group_end:
  }

% \mykey{o m}
\NewDocumentCommand \mykey { o m }
  {
    \group_begin:
      \textcolor{keyname}{\ttfamily #2}
      \IfValueT {#1}
        {
          \clist_map_inline:nn {#1}
            {
              \__mydoc_sort_index:nnn {Keys}
                {Llaves~de~\texttt{\textbackslash ##1}~provistas~por~\pkglogo :> \texttt {#2}}
                {Keys~for~\texttt{\textbackslash ##1}~provided~by~\pkglogo :> \texttt {#2}}
            }
        }
    \group_end:
  }
% \mypkg{sm}
\NewDocumentCommand \mypkg { s O{} m }
  {
    \group_begin:
    \IfBooleanTF {#1}
      {
        \pkglogo
      }
      {
        \textcolor{pkgname} { \textbf{\texttt{#3} } }
      }
    \str_if_eq:nnF {#2} { no-index }
      {
        \__mydoc_sort_index:nnn { packages } { Paquetes~:>~\texttt{#3} } { Packages~:>~\texttt{#3} }
      }
    \group_end:
  }
% \myclass{m}
\NewDocumentCommand \myclass { m }
  {
    \textcolor{clsname} { \textbf{\texttt{#1} } }
    \__mydoc_sort_index:nnn {class} {Clases~de~documento :>\texttt{#1}} {Document~class :>\texttt{#1}}
  }
% \mynvmath{m}
\tl_new:N \l__mynvmath_input_tl
\NewDocumentCommand \mynvmath { m }
  {
    \group_begin:
      \tl_set:Nn \l__mynvmath_input_tl { #1 }
      \regex_replace_case_all:nN
        {
          { NVDA }   { \c{textcolor}\cB\{percent\cE\}\cB\{NVDA\cE\} }
          { MathML } { \c{textcolor}\cB\{mathmlblue\cE\}\cB\{Math\cE\}\c{textcolor}\cB\{mathcatgreen\cE\}\cB\{ML\cE\} }
          { Math }   { \c{textcolor}\cB\{percent\cE\}\cB\{Math\cE\} }
          { CAT|ML } { \c{textcolor}\cB\{comment\cE\}\cB\{\0\cE\} }
          { / }      { \c{textcolor}\cB\{backtick\cE\}\cB\{/\cE\} }
        } \l__mynvmath_input_tl
      \textbf{\texttt{ \tl_use:N \l__mynvmath_input_tl } }
    \group_end:
  }
% \ics{sm}
\NewDocumentCommand \ics {s m}
  {
    \IfBooleanTF{#1}
      {
        \tl_set:Nn \l_tmpa_tl { #2 }
        \regex_replace_once:nnN { (\*) } { \c{textcolor}\cB\{MediumOrchid\cE\}\cB\{\1\cE\} } \l_tmpa_tl
        \regex_replace_once:nnN { (!) } { \c{textcolor}\cB\{MediumOrchid\cE\}\cB\{\1\cE\} } \l_tmpa_tl
        \textcolor{module_name}{\ttfamily\textbackslash{\tl_use:N \l_tmpa_tl}}
        \__mydoc_sort_index:nnn {Commands}
          {Comandos~provistos~por~\pkglogo :>\texttt{\textbackslash#2}}
          {Commands~provided~by~\pkglogo :>\texttt{\textbackslash#2}}
      }
      {
        \textcolor{latex_cmd}{\ttfamily\textbackslash{#2}}
        \__mydoc_sort_index:nn {#2} {\texttt{\textbackslash#2}}
      }
  }
% \mydim{m}
\NewDocumentCommand \mydim { m }
  {
    \group_begin:
      \tl_set:Nn \l_tmpa_tl { #1 }
      \regex_replace_all:nnN { (\d) } { \c{textcolor}\cB\{number\cE\}\cB\{\1\cE\} } \l_tmpa_tl
    \textcolor{unit}{ \ttfamily \tl_use:N \l_tmpa_tl }
    \group_end:
  }
% \myluamod{}
\tl_new:N \l__mydoc_luamod_tl
\NewDocumentCommand \myluamod { }
  {
    \group_begin:
      \tl_set:Nn \l__mydoc_luamod_tl { spintent.lua }
      \regex_replace_all:nnN { spintent }
        {
          \c{textcolor}\cB\{module_name\cE\}\cB\{sp\cE\}
          \c{textcolor}\cB\{OrangeRed\cE\}\cB\{intent\cE\}
        }
        \l__mydoc_luamod_tl
      \regex_replace_once:nnN { \. ( .* ) \Z }
        {
          \c{textcolor}\cB\{MediumOrchid\cE\}\cB\{\.\cE\}
          \c{textcolor}\cB\{gray\cE\}\cB\{\1\cE\}
        }
        \l__mydoc_luamod_tl
      \texttt{ \l__mydoc_luamod_tl }
      \__mydoc_sort_index:nnn {Module}
        {Módulo~Lua~provisto~por~\pkglogo :>\texttt{spintent.lua}}
        {Lua~module~provided~by~\pkglogo :>\texttt{spintent.lua}}
    \group_end:
  }
% \myspitool{o}
\NewDocumentCommand \myspitool { O{} }
  {
    \group_begin:
      \texttt
        {
          \textcolor { module_name } { sp }
          \textcolor { OrangeRed } { mmli }
          \textcolor { gray }      { - }
          \textcolor { OrangeRed } { tool }
        }
      \str_if_eq:nnF {#1} { no-index }
        {
          \__mydoc_sort_index:nnn { Tool }
            { Herramienta~provista~por~\pkglogo :>\texttt{spmmli-tool} }
            { Tool~provided~by~\pkglogo :>\texttt{spmmli-tool} }
        }
    \group_end:
  }
% \myfile{m}
\NewDocumentCommand \myfile { m }
  {
    \group_begin:
      \tl_clear:N \l_tmpa_tl
      \tl_set:Nn \l_tmpa_tl { #1 }
      \regex_replace_all:nnN { (\d) } { \c{textcolor}\cB\{MediumOrchid\cE\}\cB\{\1\cE\} } \l_tmpa_tl
      \regex_replace_all:nnN  { (\.) } { \c{textcolor}\cB\{MediumOrchid\cE\}\cB\{\1\cE\} } \l_tmpa_tl
      \textcolor{filename}{ \ttfamily \tl_use:N \l_tmpa_tl }
    \group_end:
  }
% \myattachfile{m}
\NewDocumentCommand \myattachfile{ m }
  {
    \group_begin:
      \IfFileExists{#1.pdf}{Archivo~fuente~
      \textattachfile[color=newlinkcolor,print=false]{#1.tex}{\faFile*[regular]},~
      \emph{tagged}~\texttt{PDF}~\texttt{mathml-SE}~
      \textattachfile[color=newlinkcolor,print=false]{#1.pdf}{\faFilePdf[regular]}
      \ y~\texttt{mathml-AF}~\textattachfile[color=newlinkcolor,print=false]{#1-AF.pdf}{\faFilePdf[regular]}.}{}
    \group_end:
  }
% Margin icons for the important environment (internal use)
\cs_new_protected:Npn \__mydoc_margin_icon:n #1
  {
    \par
    \mode_leave_vertical:
    \skip_horizontal:n { -0.5 \marginparsep }
    \hbox_overlap_left:n
      {
        \hbox_to_wd:nn { \marginparsep }
          {
            \tex_hss:D
            \textcolor { module_at } { #1 } % privatefun
          }
      }
    \skip_horizontal:n { 0.5 \marginparsep }
  }
% Space before the function environment
\hook_gput_code:nnn { env/function/before } { mydoc }
  {
    \vspace* { -\baselineskip }
  }
% Long tables at full line width
\skip_set:Nn \LTleft { 0pt }
\skip_set:Nn \LTright { 0pt }
% Notes environment
\NewDocumentEnvironment { important } { s }
  {
    \par\addvspace { \smallskipamount }
    \small\sffamily
    \IfBooleanTF { #1 }
      {
        \__mydoc_margin_icon:n { \footnotesize \faIcon { bomb } }
      }
      {
        \__mydoc_margin_icon:n { \scriptsize \reflectbox { \faIcon { tag } } }
      }
    \ignorespaces
  }
  { }
% Email parsing and formatting
\seq_new:N \l__mydoc_email_seq
\cs_new_protected:Npn \__mydoc_format_email:n #1
  {
    \seq_set_split:Nnn \l__mydoc_email_seq { @ } { #1 }
    \href { mailto:#1 }
      {
        \ttfamily
        \seq_item:Nn \l__mydoc_email_seq { 1 }
        \textcolor { arroba } { \footnotesize @ }
        \seq_item:Nn \l__mydoc_email_seq { 2 }
      }
  }
\NewDocumentCommand \email { m }
  {
    \__mydoc_format_email:n { #1 }
  }
% Table of Contents (TOC) override
\cs_new_eq:Nc \__mydoc_mkboth:nn { @mkboth }
\cs_new_eq:Nc \__mydoc_starttoc:n { @starttoc }
\RenewDocumentCommand \tableofcontents { }
  {
    \group_begin:
    % Restore the l3doc \parskip locally (only for the TOC)
    \skip_set:Nn \parskip { 0pt~plus~3pt~minus~0pt }
    \section*
      {
        \contentsname \quad
          {
            \color { gray }
            \tex_leaders:D \tex_hrule:D height 5pt depth -4.4pt \tex_hfill:D
          }
      }
    \__mydoc_mkboth:nn { \text_uppercase:n { \contentsname } } { \text_uppercase:n { \contentsname } }
    \vspace* { -14pt }
    \dim_set:Nn \columnsep { 10pt }
    \begin{multicols}{2}
      \__mydoc_starttoc:n { toc }
    \end{multicols}
    \vspace* { -3pt }
      {
        \color { gray }
        \tex_hrule:D height 0.6pt
      }
    \vspace* { 5pt }
    \group_end:
  }
% \thanks
\cs_new:Npn \__mydoc_fnsymbol:n #1
  {
    \ensuremath
      {
        \int_case:nnF { #1 }
          {
            { 1 } { \textcolor { module_name } { * } }
            { 2 } { \textcolor { module_name } { \dagger } }
            { 3 } { \ddagger }
            { 4 } { \mathsection }
            { 5 } { \mathparagraph }
            { 6 } { \| }
            { 7 } { ** }
            { 8 } { \dagger \dagger }
            { 9 } { \ddagger \ddagger }
          }
          { \@ctrerr }
      }
  }
\hook_gput_code:nnn { begindocument } { spintent }
  {
    \cs_set_eq:cN { @fnsymbol } \__mydoc_fnsymbol:n
  }

% Don't copy numbers in code example
\newcommand*{\noaccsupp}[1]
  {
    \__mylhmc_luafun_actual_text_begin: #1 \__mylhmc_luafun_actual_text_end:
  }
\ExplSyntaxOff

% Create a language for documentation
\lstdefinelanguage{spintent-doc}{
    texcsstyle    = *,%
    escapechar    =`,%
    extendedchars = true,%
    keepspaces    = true,%
    stringstyle   = {\color{red}},%
    basicstyle    = \small\ttfamily,%
    alsoletter    = {-,.},%
% comments
    morecomment   = [l]{\%},%
    commentstyle  = \ttfamily\small\color{comment},%
% Important \NeedsTeXFormat \ProvidesExplPackage \documentclass \DocumentMetadata
    keywordstyle  = [1]{\bfseries\color{structure}},%
    keywords      = [1]{documentclass, newcommand, %
                    begin, end, document, %
                    },%
% Other words \usepackage,
    keywordstyle  = [2]{\color{pkgstruct}},%
    keywords      = [2]{usepackage,},%
% Other words 3 MathML/tagpdf
    keywordstyle  = [3]{\color{mathtag}},%
    keywords      = [3]{MathMLintent,MathMLarg, luamml_attribute, DocumentMetadata,
                     mrow, msub, mo, mi, pre_mlist_to_hlist_filter, mathml_filter,},%
% Other words 4 (LaTeX2e)
    keywordstyle  = [4]{\color{latex_cmd}},%
    keywords      = [4]{frac, dfrac, section, emph, textbf, footnote, bigodot, %
                    circ, ast, sptext, href, label, ref, %
                    mathord, mathopen, mathclose, parallel, mathstyle, text, textsuperscript, %
                    Umathchardef,mathrm, mathbf, textstyle, addfontfeatures, mdlgwhtsquare, %
                    left, right, Uleft, Uright, overleftrightarrow, overrightarrow, overline, triangle, rightangle,%
                    rightanglesqr, operatorname, angle, measuredangle, measuredrightangle,alpha, mathcal, pi, %
                    attribute, vec, vphantom, widearc, cong, bigl, bigr, Bigl, Bigr, biggl, biggr,Biggl, Biggr, %
                    cup, cap, mid, setminus, infty, lvert, rvert, xcuad, Omega, %
                    chardef, mathchardef, ell, lparen, rparen, textsc, mathexclam, },%
% Other words 5
    keywordstyle  = [5]{\bfseries\color{pkgname}},%
    keywords      = [5]{babel,spanish-mathml-intent,libertinus-otf,sourcecodepro,fourier-otf,Erewhon-Math,hyperref,},%
% Reserved words 6 (document class)
    keywordstyle  = [6]{\bfseries\color{clsname}},%
    keywords      = [6]{article, l3doc,},%
% Reserved words 7 (spintent cmd)
    keywordstyle  = [7]{\color{module_name}},%
    keywords      = [7]{setspintent, spnum, spunit, spnewunit, spunitalias, spmoney, spshort, spsiglo, sptime, %
                        spdate, spread, spdsym, spmsym, spusep, spdiv, spmul, spnM, spnD, spmcd, spmcm, spnZ, %
                        spang, spmQ, spPoint, sprecta, sprayo, spside, spmside, spangle, spmangle, sparc, spmarc,%
                        spPoly, spAfig, spPfig, sprad, spdiam, spcirc, spAcirc, spPcirc, spcoord, spvec, sprazon,%
                        spProp, spnewread, spVol, spinterval, spfun, spfuncomp, spdom, sprec, spcod, %
                        spProm, spmedia, spmediana, spmoda, spdesv, spvarz, %
                        spcuartil, spdecil, spPerc, sprango, spriq, spmin, spmax, spFrec, spcase, spOmega, spPval, spProb, spcomp, spcomb, spfact, spabs, },%
% Reserved words 8 (keyname) KEYS
    keywordstyle  = [8]{\color{keyname}},%
    keywords      = [8]{print-era, none, ac, dc, currency, sub-units, clear, terse, times, cdot, true, false, unknown, valid, %
                    multislash, coma, punto, result, join-word, entero, enteros, y, error, periódico, postfix, infix, nofix,%
                    two-dots, old-style, slash, dividido, dividido-por, dividido-entre, por,%
                    multiplicado-por, veces, cross, dot, asterisk, cifras-sep, read-cmd, %
                    osf, nomath, mono, spanish, shorthands, no-text, semibold, colorlinks, pdftitle, pdfdisplaydoctitle, %
                    pdfstartview, FitH, pdfinfo, Title, Subject, range, StylisticSet, small-caps, read-arg, píxeles, px, caudal,%
                    read-period-sep, read-sign, notation-sym, partido-por, Numbers, Lining, OldStyle, size, read-sty, segmento,%
                    lado, trazo, mathcat,school, rayo, semirrecta, print-sym, recto, silent, math, mathml-SE, es-CL, on, off, %
                    setup, tagging, tagging-setup, lang, pdfversion, pdfstandard, arg, auto, read-sub, radio-sty, diameter, %
                    circun, circulo, side, mside, usual, sub-index, odot, concept, upper-case, prefix, big, Big, bigg, Bigg, sigma, %
                    use-dfrac, use-spnZ, wrap-parens, read-parens, read-cdot, read-slash, read-sym, set-sym, print-cdot, %
                    money-math, autofit, suffix, result-num, alt, tag, AFghost, kind, repeat, circular, print-sty, close, card, bar, %
                    inline, square, permutation, combination, variation, arrangement, binom, letter, mathml-AF, },%
% Reserved words 9 (intent values)
    keywordstyle  = [9]{\color{ltcmd_arg}},%
    keywords      = [9]{number,intent,above,below, E, cal,bfcal, silent, %
                    title, table, on,XXI, setup, mathvariant, normal, },%
% Reserved words 10 (signature)
    keywordstyle  = [10]{\color{sigargs}},%
    keywords      = [10]{een, },%
% Reserved words 11 (support)
    keywordstyle  = [11]{\color{support}},%
    keywords      = [11]{hypersetup, setmathfont,},%
% Reserved words 12 (AF ghost - MathML tags)
    keywordstyle  = [12]{\color{other_np}},%
    keywords      = [12]{sptmp, inner, outer, },%
% Reserved words 13 (message names)
    keywordstyle  = [13]{\color{red}},%
    keywords      = [13]{cmd-key-unknown, cmd-key-value-unknown, env-success,%
                    fatal-engine-requires-luatex, fatal-metadata-missing, mathcal-fallback,%
                    mc-too-few-args, md-invalid-arg, need-math-mode, need-text-mode,%
                    package-not-load, right-angle-sqr-fallback,%
                    spAcirc-invalid-arg, spAfig-invalid-arg, spAfig-invalid-points,%
                    spAfig-read-arg-key, spAfig-sym-cardinality-conflict,%
                    spAfig-sym-letter-conflict, spFrec-invalid-sub-index, spFrec-invalid-symbol,%
                    spPcirc-invalid-arg, spPfig-invalid-arg, spPfig-invalid-points,%
                    spPfig-read-arg-key, spPfig-sym-cardinality-conflict,%
                    spPfig-sym-letter-conflict, spPoint-invalid-point, spPoly-invalid-points,%
                    spProb-invalid-arg, spabs-empty-arg, spang-invalid-arg, spangle-invalid-arg,%
                    sparc-invalid-points, spcase-invalid-letter, spcirc-invalid-arg,%
                    spcomb-extra-arg, spcomb-invalid-arg, spcomb-invalid-keys, spcomb-missing-arg,%
                    spcomp-empty-arg, spcoord-invalid-arg, spdate-invalid-arg, spdiv-empty-arg,%
                    spdom-invalid-arg, spfact-empty-arg, spfun-invalid-arg, spfuncomp-invalid-arg,%
                    spinterval-invalid-arg, spmQ-not-integer, spmQ-signed-part,%
                    spmQ-zero-denominator, spmangle-invalid-arg, spmarc-invalid-points,%
                    spmoney-invalid-currency, spmoney-no-subunits, spmside-invalid-points,%
                    spmul-empty-arg, spnZ-not-integer, spnewread-duplicate, spnewread-empty-args,%
                    spnewunit-duplicate, spnum-has-units, spnum-no-digits, sprayo-invalid-points,%
                    spread-empty-sym, spread-unknown-sym, sprecta-invalid-points,%
                    spshort-unknown-abbreviation, spside-invalid-points, spsiglo-invalid-arg,%
                    sptime-invalid-arg, spunit-inline-numeric-denominator, spunit-invalid-unit,%
                    spunit-no-units, spunitalias-duplicate, spunitalias-invalid-expr,%
                    spusep-empty-arg, spusep-invalid-arg, spvec-invalid-arg, stat-pos-invalid-index,%
                    unknown-choice,%
                    unknown-command-setup,%
                    },%
    literate      =*{\{}{{{\color{brace}{\char`\{}}}}{1}
                    {\}}{{{\color{brace}{\char`\}}}}}{1}
                    {\|}{{{\color{gray}{\char`\|}}}}{1}
                    {\$}{{{\color{math}{\char`\$}}}}{1}
                    {\#}{{{\color{argument}{\char`\#}}}}{1}
                    {(}{{\textcolor{gray}{(}}}{1}
                    {)}{{\textcolor{gray}{)}}}{1}
                    {[}{{\textcolor{bracket}{[}}}{1}
                    {]}{{\textcolor{bracket}{]}}}{1}
                    {,}{{\textcolor{gray}{,}}}{1}
                    {;}{{\textcolor{gray}{;}}}{1}
                    {.}{{\textcolor{signature}{.}}}{1}
                    {:}{{\textcolor{signature}{:}}}{1}
                    {\&}{{\textcolor{gray}{\&}}}{1}
                    {>}{{\textcolor{percent}{>}}}{1}
                    {<}{{\textcolor{percent}{<}}}{1}
                    {*}{{\textcolor{star}{*}}}{1}
                    {"}{{\textcolor{quote}{"}}}{1}
                    {ua-2}{{\textcolor{keyname}{ua-2}}}{4} % UA-2
                    {!}{{\textcolor{MediumOrchid}{!}}}{1}
                    {@}{{\textcolor{arroba}{@}}}{1}%
                    {\^}{{\textcolor{keyname}{\textasciicircum}}}{1}
                    {0}{{\textcolor{argnum}{0}}}{1}
                    {1}{{\textcolor{argnum}{1}}}{1}
                    {2}{{\textcolor{argnum}{2}}}{1}
                    {3}{{\textcolor{argnum}{3}}}{1}
                    {4}{{\textcolor{argnum}{4}}}{1}
                    {5}{{\textcolor{argnum}{5}}}{1}
                    {6}{{\textcolor{argnum}{6}}}{1}
                    {7}{{\textcolor{argnum}{7}}}{1}
                    {8}{{\textcolor{argnum}{8}}}{1}
                    {9}{{\textcolor{argnum}{9}}}{1}
                    {.0}{{\textcolor{argnum}{.0}}}{2}% Following is to ensure that only periods
                    {.1}{{\textcolor{argnum}{.1}}}{2}% followed by a digit are changed.
                    {.2}{{\textcolor{argnum}{.2}}}{2}
                    {.3}{{\textcolor{argnum}{.3}}}{2}
                    {.4}{{\textcolor{argnum}{.4}}}{2}
                    {.5}{{\textcolor{argnum}{.5}}}{2}
                    {.6}{{\textcolor{argnum}{.6}}}{2}
                    {.7}{{\textcolor{argnum}{.7}}}{2}
                    {.8}{{\textcolor{argnum}{.8}}}{2}
                    {.9}{{\textcolor{argnum}{.9}}}{2}
                    {=}{{\textcolor{module_eq}{=}}}{1},%
}[keywords,tex,comments,strings]% end language

% \begin{examplecode}[key=val]...\end{examplecode}
\lstnewenvironment{examplecode}[1][]{%
\lstset{
    language    = spintent-doc,%
    stringstyle = {\color{red}},%
    basicstyle  = \ttfamily\small,%
    columns     = fullflexible,%
    fontadjust  = true,%
    aboveskip   = 8pt,%
    belowskip   = 5pt,%
    rulecolor   = \color{gray!50},%
    framesep    = \fboxsep,%
    framerule   = \fboxrule,%
    xleftmargin = \dimexpr\fboxsep+\fboxrule\relax,%
    xrightmargin= \dimexpr\fboxsep+\fboxrule\relax,%
           #1,%
    }% close lstset
}{\vspace{-\parskip}}% close examplecode
% \lstinline[style=inline]|...|
\lstdefinestyle{inline}
  {
    language    = spintent-doc,%
    upquote     = true,%
    numbersep   = 1em,%
    escapechar  =`,%
    basicstyle  =\ttfamily,%
    numberstyle = \tiny\color{lightgray}\noaccsupp,%
    literate    =*{\%}{{\textcolor{percent}{\%}}}{1}
                  {i}{{\textcolor{keyname}{i}}}{1}
                  {ii}{{\textcolor{keyname}{ii}}}{2}
                  {iii}{{\textcolor{keyname}{iii}}}{3}
                  {iv}{{\textcolor{keyname}{iv}}}{2}
                  {v}{{\textcolor{keyname}{v}}}{1}
                  {vi}{{\textcolor{keyname}{vi}}}{1}
                  {>}{{\textcolor{mathtag}{>}}}{1}
                  {<}{{\textcolor{mathtag}{<}}}{1}
  }
% Set default \lstinline style
\lstset{style=inline}

% Configuration titleps and titlesec
\settitlemarks{section}
\renewpagestyle{plain}[\color{keyname}\small\sffamily]{%
\setfoot{\rlap{\hskip\dimexpr-\oddsidemargin-1in\relax%
         \parbox{1.93\paperwidth}{\hfil\thepage\,/\,\pageref{LastPage}}}}%
        {}%
        {}%
}
\newpagestyle{myheader}[\color{keyname}\small\sffamily]{%
\renewcommand\makeheadrule{%
\rlap{\hskip\dimexpr-\oddsidemargin-1in\relax
      \color{rulecolor}\rule[0.3cm]{\paperwidth}{0.7cm}}\hss
}
\setfoot{\rlap{\hskip\dimexpr-\oddsidemargin-1in\relax%
         \parbox{1.93\paperwidth}{\hfil\thepage\,/\,\pageref{LastPage}}}}%
        {\parbox{\textwidth}{\raggedright \textcolor{gray}{\raisebox{-1pt}{\textcopyright}{}2026 by Pablo González L}}}%
        {}%
\sethead{\llap{\raisebox{0.55cm}{\parbox{\dimexpr\oddsidemargin+1in\relax}{\makebox[0pt][l]{\hspace{15pt}\pkglogo\space\fileversion}}}}}
        {\raisebox{0.55cm}{\parbox{\textwidth}{\hspace*{-\oddsidemargin}\centering\small\S.\thesection\space\sectiontitle}}}%
        {}%
}
\titlecontents{section}[0mm]{}%
    {\bfseries\contentspush{\makebox[5mm][l]{\thecontentslabel\hfill}}}%
    {\hspace*{-5mm}}% numberless
    {\hspace{0.25em}\titlerule*[6pt]{.}\contentspage}%
\titlecontents{subsection}[5mm]{}%
    {\contentspush{\makebox[6mm][l]{\thecontentslabel\hfill}}}
    {\hspace*{-11mm}}% numberless
    {\hspace{0.25em}\titlerule*[6pt]{.}\contentspage}%
\titlecontents{subsubsection}[11mm]{}%
    {\contentspush{\makebox[8mm][l]{\thecontentslabel\hfill}}}
    {\hspace*{-19mm}}% numberless
    {\hspace{0.25em}\titlerule*[6pt]{.}\contentspage}%
\begin{document}
  \DocInput{\jobname.dtx}
\end{document}
%</driver>
% \fi
%
% \selectlanguage{spanish}
% \setlocalecaption{spanish}{table}{Tabla}
%
% \title{
%    \scalebox{1.25}{\pkglogo}\\[-5pt]
%    {\Large\textcolor{guard_angle}{\fetamontotf{mathml intent en español}}}\\[-5pt]
%    \fileversion{} \quad \filedate\thanks{
%     Este archivo describe la documentación para la versión \fileversion{}
%     revisada por última vez el
%    \filedate.}\\[25pt]
%    \author{
%    \large
%    \raisebox{-1pt}{\textcolor{guard_angle}{\textcopyright{}2026 by Pablo González L}}\thanks{
%    E-mail: \textcolor{guard_angle}{\textsf{\guillemotleft}}\email{pablgonz@educarchile.cl}\textcolor{guard_angle}{\textsf{\guillemotright}}.
%       }%
%    }
% \small
% \vspace*{-5pt}
% \textsc{ctan}: \url{https://www.ctan.org/pkg/spanish-mathml-intent}\\
% \textcolor{gray}{\scriptsize\faIcon[regular]{github}}\,\,\,\url{https://github.com/pablgonz/spanish-mathml-intent}
% \vspace*{-2cm}
% }
% \date{}
% \maketitle
%
% \begin{abstract}
%  El paquete \mypkg*[no-index]{spanish-mathml-intent} agrega el atributo
%  \emph{intent} de \mynvmath{MathML} a las matemáticas escritas en español
%  con \hologo{LuaLaTeX}. El atributo le indica a un lector de pantalla qué
%  concepto es una expresión y cómo se debe leer, de modo que
%  \mynvmath{NVDA} con \mynvmath{MathCAT} lea correctamente las fórmulas en
%  documentos \texttt{PDF} accesibles (\emph{tagged} \texttt{PDF}). Sus
%  comandos escriben y etiquetan números, unidades, dinero, fechas y horas,
%  geometría, álgebra y estadística, cada uno con la lectura en español que le
%  corresponde. Está pensado para docentes de educación primaria y secundaria.
% \end{abstract}
%
% \tableofcontents
%
% \section*{Motivación y agradecimientos}
%
% Desde que desarrollé \mypkg{scontents}\cite{scontents} en 2019 y, más
% tarde, \mypkg{enumext}\cite{enumext} en 2024, me he ido interesando
% cada vez más por la accesibilidad en los documentos \texttt{PDF}. Al
% principio era un tema prácticamente desconocido para mí, pero a medida
% que fui aprendiendo sobre él comprendí la importancia que tiene,
% especialmente en el ámbito educativo.
%
%
% El equipo del proyecto de \hologo{LaTeX}\cite{tagged-project} ha
% realizado un trabajo extraordinario en este campo. Hoy es posible
% generar documentos etiquetados, validarlos con herramientas como
% \href{https://dev.verapdf-rest.duallab.com}{\texttt{veraPDF}}\cite{verapdf}
% o inspeccionar su estructura con
% \href{https://ngpdf.com}{\texttt{ngpdf}}\cite{ngpdf}. Sin embargo,
% siempre me quedó una duda: cuando un estudiante utiliza \mynvmath{NVDA}\cite{nvda}
% junto con \mynvmath{MathCAT}\cite{mathcat}, ¿realmente escucha lo que
% el o la docente quiso expresar?
%
% Basta con generar un documento accesible siguiendo las recomendaciones
% de \href{https://latex3.github.io/tagging-project/}{\texttt{The LaTeX
% Tagged PDF Project}} y escucharlo con \mynvmath{NVDA/MathCAT} para darse
% cuenta de que, en muchos casos, la \emph{verbalización} no coincide con lo
% que uno espera. Esto no resulta extraño: las reglas del español, y en
% particular el lenguaje matemático, dependen en gran medida del contexto
% en que se utilizan.
%
% El objetivo de \mypkg*[no-index]{spintent} nace precisamente de esta necesidad:
% proporcionar herramientas para que el o la docente pueda indicar
% explícitamente el contexto de determinadas expresiones matemáticas
% desde el propio código fuente de \hologo{LaTeX}, de modo que el
% documento \texttt{PDF} resultante no solo sea \emph{accesible}, sino que
% también produzca una \emph{verbalización más natural} y adecuada para el
% estudiante al utilizar \mynvmath{NVDA/MathCAT}.
%
% El nuevo estándar \texttt{PDF} |2.0|\cite{pdf20,pdf20-impact,pdf20-errata},
% las especificaciones de \texttt{WTPDF}\cite{wtpdf} y la guía más reciente de la
% \texttt{PDF} Association, «Best Practice Guide: Math in
% \texttt{PDF}»\cite{bpg-math},
% coinciden en que el camino hacia una accesibilidad matemática de calidad
% pasa por el uso de \mynvmath{MathML}. En ese contexto,
% \hologo{LuaLaTeX} ofrece una plataforma especialmente adecuada para
% incorporar esta tecnología.
%
% Debo agradecer a todo el equipo de \hologo{LaTeX} que desarrolló el proyecto
% de \emph{tagged} \texttt{PDF} y a los diversos miembros de la comunidad
% \href{https://tex.stackexchange.com}{TeX-SX} que han respondido mis muchas
% dudas en el chat sobre este tema; destaco entre ellos a David Carlisle,
% Ulrike Fischer, Clea F. Rees y Max Chernoff, quien me mostró el camino de Lua.
%
% Un agradecimiento especial a mi esposa Javiera, quien me acompañó
% incontables horas en las escuchas de \mynvmath{NVDA/MathCAT} durante el
% desarrollo de \mypkg*[no-index]{spintent}.
%
%
% \thispagestyle{plain}
%
% \section*{Licencia y requerimientos}
%
% El paquete \mypkg*[no-index]{spintent} carga y requiere el paquete \mypkg{unicode-math}\cite{unicode-math}
% y que se ejecute bajo \hologo{LuaLaTeX}, por lo que es necesario contar con
% una distribución moderna de \hologo{TeX} como \hologo{TeX} Live 2026. Ha sido probado con la clase estándar
% \myclass{article} proporcionada por \hologo{LaTeX} en tamaño \mydim{10pt}, usando
% \mynvmath{NVDA} 2026.2 y Adobe Acrobat Reader\cite{acrobat} actualizado 2026; además se utilizó
% \href{https://daisy.github.io/MathCATDemo/}{MathCATDemo}\cite{mathcatdemo}.
% No se ha probado con otros lectores de pantalla.
%
% Para profundizar en el etiquetado de documentos, es decir, |\DocumentMetadata|,
% \mykey{tagging} y \mykey{tagging-setup}, consulte la documentación de
% \href{https://latex3.github.io/tagging-project/documentation/}{\texttt{The LaTeX
% Tagged PDF Project}}\cite{tagged-project,userguide}, que está en inglés.
% Quien da sus primeros pasos en \hologo{LaTeX} puede apoyarse en
% \href{https://www.learnlatex.org/es/}{Learn \hologo{LaTeX}}\cite{learnlatex}, un sitio
% respaldado por el equipo de \hologo{LaTeX} con lecciones breves y ejemplos que
% se pueden probar en línea; como referencia general sirve el manual no oficial
% de \hologo{LaTeX2e}\cite{latexref}.
%
% Se otorga permiso para copiar, distribuir y/o modificar este paquete bajo
% los términos de \LaTeX{} Project Public License (\textsc{lppl}), versión
% 1.3 o posterior (\url{https://www.latex-project.org/lppl.txt}). El paquete
% tiene la condición de mantenido al estilo \emph{«A Work in Progress\footnote{%
% \emph{A Work in Progress} es un álbum en vivo de Neil Peart
% ($\ast$~1952--\textdied~2020), baterista y principal letrista de Rush.
% El nombre no pretende formar parte de la terminología de la
% \textsc{lppl}; simplemente describe el estado permanente de este paquete
% mientras sigo aprendiendo sobre \emph{accesibilidad} en \texttt{PDF}.}»}.
%
% \begin{important}*
%  El requerimiento mínimo es \hologo{LaTeX}\cite{latex2e} release 2026-11-01.
% \end{important}
%
% Este paquete solo se puede utilizar con \hologo{LuaLaTeX} y
% \emph{tagged} \texttt{PDF} activo. Está presente en \hologo{TeX} Live, para
% instalarlo utilice el gestor de paquetes con |tlmgr install| \texttt{spanish-mathml-intent}.
% Si desea probar la versión de desarrollo, que puede contener cambios aún no
% publicados, clone el repositorio y ejecute |l3build install|:
%
% \iffalse
%<*example>
% \fi
\begin{examplecode}[frame=single,language={}]
  git clone https://github.com/pablgonz/spanish-mathml-intent.git
  cd spanish-mathml-intent
  l3build install
\end{examplecode}
% \iffalse
%</example>
% \fi
%
% \pagestyle{myheader}
%
% \section{Descripción}
%
% El nombre \mypkg*[no-index]{spanish-mathml-intent} resume lo que hace el
% paquete: \emph{«Spanish»} por el idioma, \emph{«MathML»} por el lenguaje en
% que se escriben las fórmulas accesibles e \emph{«intent»} por el atributo
% del mismo nombre que indica cómo se lee cada fórmula. El nombre corto
% original, \emph{«Spanish Parse Intent»}, sobrevive en el prefijo \texttt{sp}
% de los comandos y en el módulo interno \texttt{spintent}. El paquete está
% pensado para docentes de educación primaria y secundaria.
%
% Un documento \emph{tagged} \texttt{PDF}, en español \emph{«PDF etiquetado»},
% es un \texttt{PDF} que, además de componer la página, guarda su estructura:
% qué es un título, un párrafo, una lista, una tabla o una fórmula, y en qué
% orden se leen. Los lectores de pantalla, como \mynvmath{NVDA}, usan esa
% información para leer el documento a estudiantes ciegos o con baja visión;
% sin ella solo disponen del texto suelto, sin saber qué es cada parte.
% Cómo activarlo se explica en la~\S\ref{subsec:carga} y un documento
% completo en la~\S\ref{subsec:primer-documento}.
%
% El paquete está pensado para crear \emph{«apuntes»}, \emph{«hojas de ejercicios»}
% y \emph{«clases escritas»} enfocadas en el estudiante. Solo un docente sabe cuál es
% el nivel de aprendizaje del estudiante. El paquete intenta sobrescribir
% muchas de las reglas dictadas por la heurística de \mynvmath{NVDA/MathCAT}
% teniendo esto presente.
%
% \begin{important}*
%  El paquete \mypkg*[no-index]{spintent} usa y abusa del atributo |:intent| de \mynvmath{MathML}
%  por medio del comando |\MathMLintent| definido en \mypkg{latex-lab-mathintent}\cite{latexlab},
%  un encapsulado de |\luamml_attribute:een| provista por \mypkg{luamml}\cite{luamml}. ¡Gracias Marcel!
% \end{important}
%
% \subsection{Carga del paquete}\label{subsec:carga}
%
% El paquete \mypkg*[no-index]{spintent} trabaja solo con \hologo{LuaLaTeX} y
% \emph{tagged} \texttt{PDF} activo usando |tagging=on| dentro del comando
% |\DocumentMetadata| al inicio del archivo.
% Las llaves \mymeta{keys} aceptadas por |\DocumentMetadata| y en
% general para el código de \emph{tagged} \texttt{PDF} están documentadas
% en los paquetes \mypkg{tagpdf}\cite{tagpdf} y \mypkg{documentmetadata-support}\cite{latexlab};
% es deber del usuario revisar la documentación asociada a ellos.
%
% \iffalse
%<*example>
% \fi
\begin{examplecode}[frame=single]
\DocumentMetadata
  {
    lang=es-CL,
    pdfversion=2.0,
    pdfstandard=ua-2,
    tagging=on,
    tagging-setup={math/setup={mathml-SE}},
  }
\documentclass{article}
\usepackage[spanish]{babel}
\usepackage{spanish-mathml-intent}
\end{examplecode}
% \iffalse
%</example>
% \fi
%
% \subsection{El paquete \texttt{babel}}
%
% El paquete \mypkg{babel-spanish}\cite{babel-spanish} está marcado como
% «parcialmente compatible» para \emph{tagged} \texttt{PDF}, pero es
% absolutamente necesario para la creación de documentos en español.
% En caso de encontrar problemas de compatibilidad con otros paquetes
% se puede desactivar usando:
%
% \iffalse
%<*example>
% \fi
\begin{examplecode}[frame=single]
\usepackage[spanish,shorthands=off]{babel}
\end{examplecode}
% \iffalse
%</example>
% \fi
%
% Muchas de las «shorthands» provistas por \mypkg{babel-spanish} hoy no
% son necesarias, más allá de |\sptext| para las abreviaturas y ordinales,
% el resto son solo atajos de teclas para escritura que hoy en día no se usan.
% El paquete \mypkg*[no-index]{spintent} provee el comando |\spshort| (\S\ref{subsec:spshort})
% que cubre estos casos, de esta manera podemos cargar \mypkg{babel}\cite{babel}
% con todas sus funcionalidades sin problemas.
%
% \subsection{Fuentes tipográficas}\label{subsec:fuentes}
%
% Como \mypkg*[no-index]{spintent} es un paquete \hologo{LuaLaTeX} debemos
% distinguir entre las fuentes tipográficas \texttt{OpenType/TrueType}
% versus las fuentes de \texttt{8bit} estándar usadas por
% \hologo{LaTeX} para el \emph{modo texto} y el \emph{modo matemático}.
% Para evitar problemas con \emph{tagged} \texttt{PDF} preferiremos no
% combinar fuentes \texttt{OpenType/TrueType} con fuentes \texttt{8bit} y
% trabajaremos solo con fuentes \texttt{OpenType/TrueType} que tengan
% soporte para matemáticas.
%
% La carga de las fuentes tipográficas para \hologo{LuaLaTeX} se
% maneja por medio del paquete \mypkg{fontspec}\cite{fontspec}, si no se especifica
% ninguna se usarán las fuentes \texttt{Latin Modern} para el \emph{modo texto} y
% \texttt{Latin Modern Math} para el \emph{modo matemático}.
%
% El tipo de fuente tipográfica escogida es preferencia de cada usuario, en lo
% personal utilizo la fuente \texttt{Libertinus} para el \emph{modo
% texto} por medio del paquete
% \mypkg{libertinus-otf}\cite{libertinus-otf} y la fuente
% \texttt{Erewhon-Math} para el modo matemático cargada por el paquete
% \mypkg{fourier-otf}\cite{erewhon-math}. Adicionalmente cargo la fuente \texttt{Source Code Pro}
% para el texto tipo \texttt{«máquina de escribir»} por medio del paquete
% \mypkg{sourcecodepro}\cite{sourcecodepro}.
%
% \iffalse
%<*example>
% \fi
\begin{examplecode}[frame=single]
% \DocumentMetadata{...}
\documentclass{article}
\usepackage[spanish]{babel}
\usepackage[osf,nomath,mono=false]{libertinus-otf}
\usepackage[osf,semibold]{sourcecodepro}
\usepackage[no-text]{fourier-otf}
\usepackage{spanish-mathml-intent}
\end{examplecode}
% \iffalse
%</example>
% \fi
%
% \subsubsection{La llave \texttt{mathcal}}
%
% Algunos comandos, como |\spAfig| y |\spPfig|, usan
% |\mathcal| en su salida. Por defecto, con \mykey[usepackage]{mathcal}|=true|, el paquete
% \mypkg*[no-index]{spintent} busca la fuente \texttt{NewCMMath-Regular.otf},
% parte del paquete \mypkg[no-index]{newcomputermodern}\cite{newcomputermodern}, para imprimir esas
% letras caligráficas con el estilo clásico; se recomienda tenerla instalada.
% Si no la encuentra, avisa con un \enquote{warning} y usa el
% |\mathcal| de la fuente matemática cargada. Con
% \mykey{mathcal}|=false| se usa siempre el |\mathcal| de la
% fuente matemática cargada, y la apariencia de la letra cambia. La misma
% fuente se usa para el símbolo del ángulo recto de |\spangle|
% (v.\,pág.\,\pageref{subsec:spangle}). La llave se pasa al cargar el
% paquete:
%
% \iffalse
%<*example>
% \fi
\begin{examplecode}[frame=single]
\usepackage[mathcal=false]{spanish-mathml-intent}
\end{examplecode}
% \iffalse
%</example>
% \fi
%
% \subsection{El paquete \texttt{hyperref}}
%
% El paquete \mypkg{hyperref}\cite{hyperref} es \emph{«casi obligatorio»}
% cuando se crean documentos \emph{tagged} \texttt{PDF}, se encarga de las
% referencias cruzadas |\label| y |\ref|, las notas al pie |\footnote|,
% los hipervínculos |\href| y muchos otros elementos como marcadores, índices
% y metadatos.
%
% \iffalse
%<*example>
% \fi
\begin{examplecode}[frame=single]
\DocumentMetadata
  {
    lang=es-CL,
    pdfversion=2.0,
    pdfstandard=ua-2,
    tagging=on,
    tagging-setup={math/setup={mathml-SE}},
  }
\documentclass{article}
\usepackage[spanish]{babel}
\usepackage[osf,nomath,mono=false]{libertinus-otf}
\usepackage[osf,semibold]{sourcecodepro}
\usepackage[no-text]{fourier-otf}
\setmathfont{Erewhon-Math}[range={cal,bfcal},StylisticSet=1]
\usepackage{spanish-mathml-intent, hyperref}
\hypersetup
  {
    colorlinks         = true,
    pdfstartview       = FitH,
    pdfdisplaydoctitle = true,
    pdftitle           = {Test para el paquete spanish-mathml-intent},
    pdfinfo            =
      {
        Title   = {Test},
        Subject = {spanish-mathml-intent},
      },
  }
\end{examplecode}
% \iffalse
%</example>
% \fi
%
% \subsection{Interfaz de usuario}\label{subsec:interfaz}
%
% La interfaz de usuario de \mypkg*[no-index]{spintent} está formada solo por
% \emph{comandos}: el comando de configuración |\setspintent|
% (\S\ref{sec:setspintent}) y 73 utilidades para escribir y verbalizar
% matemática escolar. El paquete no define entornos, contadores ni
% dimensiones públicas.
%
% \subsubsection{Lo que ofrece el paquete}
%
% Cada utilidad hace dos cosas a la vez: imprime la expresión en la salida
% \texttt{PDF} con el formato correcto y establece cómo la verbalizará
% \mynvmath{NVDA/MathCAT}, para que la lea como la diría un docente y no
% según la heurística general de \mynvmath{NVDA/MathCAT}.
% Las utilidades que reciben llaves \myoarg{key \textnormal{\textcolor{gray}{=}} val}
% las toman de tres niveles, de menor a mayor prioridad: los valores por
% defecto del paquete, los establecidos de forma global con |\setspintent| y
% los del argumento opcional de cada comando. Un argumento que no cumple la
% forma documentada devuelve un \enquote{error} y se detiene la compilación
% del documento.
%
% Los cuatro grandes (\S\ref{subsec:spnum}, \S\ref{subsec:spunit}, \S\ref{subsec:spmoney} y
% \S\ref{subsec:spshort}) son la base del paquete: |\spnum| escribe
% números, |\spunit| unidades de medida, |\spmoney| cifras con moneda y
% |\spshort| abreviaturas, acrónimos y ordinales. Los comandos |\spnewunit| y
% |\spunitalias| amplían el diccionario de |\spunit|. Muchas utilidades de los
% demás ejes procesan sus argumentos numéricos bajo |\spnum| y |\spunit|, por
% ejemplo |\spdiv|, |\spmul|, |\spcoord| y |\spang|.
%
% \subsubsection{Lo que ofrece cada eje}
%
% El resto de las utilidades se agrupan por eje temático, en el mismo orden en
% que se describen en este documento:
%
% \begin{itemize}[left=0pt,itemsep=\parskip]
%   \item \emph{Generales} (\S\ref{sec:utilidades}): |\spsiglo| escribe
%     siglos en números arábigos o romanos, |\sptime| horas y |\spdate| fechas.
%   \item \emph{Símbolos} (\S\ref{sec:simbolos}): |\spnewread| y |\spread|
%     registran y usan la lectura de un símbolo, |\spusep| escribe paréntesis,
%     corchetes y llaves sueltos, y |\spdsym| y |\spmsym| fijan cómo se
%     imprime y se lee el símbolo de división y el de multiplicación.
%   \item \emph{Números} (\S\ref{sec:numeros}): operaciones con |\spdiv| y
%     |\spmul|, divisores y múltiplos con |\spnD| y |\spnM|, máximo común
%     divisor y mínimo común múltiplo con |\spmcd| y |\spmcm|, números enteros
%     con |\spnZ|, números mixtos con |\spmQ|, razones y proporciones con
%     |\sprazon| y |\spProp|.
%   \item \emph{Geometría} (\S\ref{sec:geometria}): puntos y coordenadas
%     con |\spPoint| y |\spcoord|, vectores con |\spvec|, rectas, rayos y lados
%     con |\sprecta|, |\sprayo|, |\spside| y |\spmside|, ángulos con |\spangle|,
%     |\spmangle| y |\spang|, arcos con |\sparc| y |\spmarc|, polígonos
%     con |\spPoly|, áreas y perímetros de figuras con |\spAfig| y |\spPfig|,
%     circunferencias con |\spcirc|, |\spAcirc| y |\spPcirc|, radio y diámetro
%     con |\sprad| y |\spdiam|, y volumen con |\spVol|.
%   \item \emph{Álgebra} (\S\ref{sec:algebra}): intervalos con |\spinterval|,
%     funciones con |\spfun|, dominio, recorrido y codominio con |\spdom|,
%     |\sprec| y |\spcod|, composición de funciones con |\spfuncomp| y valor
%     absoluto con |\spabs|.
%   \item \emph{Estadística} (\S\ref{sec:estadistica}): promedio con |\spProm|,
%     medidas de tendencia central y de dispersión con |\spmedia|,
%     |\spmediana|, |\spmoda|, |\spdesv|, |\spvarz|, |\sprango|, |\spriq|,
%     |\spmin| y |\spmax|, cuartiles, deciles y percentiles con |\spcuartil|,
%     |\spdecil| y |\spPerc|, frecuencias con |\spFrec|, probabilidad con
%     |\spcase|, |\spOmega|, |\spPval|, |\spProb| y |\spcomp|, y conteo con
%     |\spcomb| y |\spfact|.
% \end{itemize}
%
% \subsubsection{Reglas para argumentos}\label{subsubsec:reglas-args}
%
% Casi todas las utilidades envían su argumento a \myluamod{}, que lo lee como
% \emph{texto plano} y reconoce números, letras, signos y la sintaxis propia de
% cada comando. Por eso, salvo lo que se documente en cada comando, el
% \mymarg{argumento} NO puede contener comandos de formato o de estilo, con o
% sin argumentos, como |\textstyle|, |\mathrm{#1}| o |\mathbf{#1}|, tal como
% se advierte en |\spnum|, |\spunit| y |\spmoney|. Un argumento que no cumple
% devuelve un \enquote{error} y se detiene la compilación del documento.
%
% Hacen excepción las utilidades que tipografían su argumento directamente,
% sin procesamiento semántico adicional, y por eso aceptan cualquier expresión
% matemática: |\sprazon|, |\spProp|, |\spPval|, |\spfact|, |\spProm| y
% |\spabs|. En |\spinterval|, los extremos que no son numéricos, como una
% letra, un subíndice, una fracción o una raíz, también se imprimen tal cual.
%
% \subsubsection{Consulta rápida}\label{subsubsec:consulta-rapida}
%
% La tabla~\ref{tab:consulta-rapida} reúne, para lo que un docente escribe con más
% frecuencia, el comando que corresponde y la página donde se describe, con
% su sintaxis completa y sus llaves.
%
% \begingroup\small
% \setlength{\LTleft}{\fill}\setlength{\LTright}{\fill}
% \begin{longtable}{@{}p{5cm} l r@{}}
% \caption{Consulta rápida: qué quiero escribir y qué comando usar} \label{tab:consulta-rapida} \\
% \toprule
% \textbf{Quiero escribir} & \textbf{Uso} & \multicolumn{1}{c}{\textbf{Página}} \\
% \midrule
% \endfirsthead
% \caption[]{\emph{(Continuación: Consulta rápida)}} \\
% \toprule
% \textbf{Quiero escribir} & \textbf{Uso} & \multicolumn{1}{c}{\textbf{Página}} \\
% \midrule
% \endhead
% \midrule
% \multicolumn{3}{r}{\emph{Continúa en la siguiente página...}} \\
% \endfoot
% \bottomrule
% \endlastfoot
% \multicolumn{3}{c}{\textbf{Generales}} \\
% \midrule
% un siglo & |\spsiglo{XXI}| & \pageref{subsec:spsiglo} \\
% una hora & |$\sptime{23:45}$| & \pageref{subsec:sptime} \\
% una fecha & |$\spdate{1981-01-02}$| & \pageref{subsec:spdate} \\
% \midrule
% \multicolumn{3}{c}{\textbf{Los cuatro grandes}} \\
% \midrule
% un número & |$\spnum{3,14}$| & \pageref{subsec:spnum} \\
% una medida & |$\spunit{23 m}$| & \pageref{subsec:spunit} \\
% una cantidad de dinero & |$\spmoney{500}$| & \pageref{subsec:spmoney} \\
% una abreviatura, sigla u ordinal & |\spshort{prof.a}| & \pageref{subsec:spshort} \\
% \midrule
% \multicolumn{3}{c}{\textbf{Símbolos}} \\
% \midrule
% la lectura de un símbolo & |$\spread{\cong}$| & \pageref{subsec:spread} \\
% \midrule
% \multicolumn{3}{c}{\textbf{Números}} \\
% \midrule
% una división & |$\spdiv{20}{26}$| & \pageref{subsec:spdiv} \\
% un máximo común divisor & |$\spmcd(2, 3, 6)$| & \pageref{subsec:spmcd} \\
% un número entero & |$\spnZ{-23}$| & \pageref{subsec:spnZ} \\
% una razón & |$\sprazon{3}{4}$| & \pageref{subsec:sprazon} \\
% una proporción & |$\spProp{3}{4}{6}{8}$| & \pageref{subsec:spProp} \\
% un número mixto & |$\spmQ{2}{5}{7}$| & \pageref{subsec:spmQ} \\
% \midrule
% \multicolumn{3}{c}{\textbf{Geometría}} \\
% \midrule
% un punto & |$\spPoint{A}$| & \pageref{subsec:spPoint} \\
% unas coordenadas & |$\spcoord{-3, 4}$| & \pageref{subsec:spcoord} \\
% un vector & |$\spvec{AB}$| & \pageref{subsec:spvec} \\
% una recta & |$\sprecta{AB}$| & \pageref{subsec:sprecta} \\
% un ángulo & |$\spangle{A}$| & \pageref{subsec:spangle} \\
% un arco & |$\sparc{AB}$| & \pageref{subsec:sparc} \\
% un polígono & |$\spPoly{ABC}$| & \pageref{subsec:spPoly} \\
% \midrule
% \multicolumn{3}{c}{\textbf{Álgebra}} \\
% \midrule
% un intervalo & |$\spinterval{(1,5)}$| & \pageref{subsec:spinterval} \\
% una función & |$\spfun{f(x)}$| & \pageref{subsec:spfun} \\
% un dominio & |$\spdom{f}$| & \pageref{subsec:spdom} \\
% un valor absoluto & |$\spabs{-5}$| & \pageref{subsec:spabs} \\
% \midrule
% \multicolumn{3}{c}{\textbf{Estadística}} \\
% \midrule
% un promedio & |$\spProm{x}$| & \pageref{subsec:spProm} \\
% un cuartil & |$\spcuartil{1}$| & \pageref{subsec:spcuartil} \\
% una probabilidad & |$\spProb{A \cap B}$| & \pageref{subsec:spProb} \\
% una combinatoria & |$\spcomb{5,2}$| & \pageref{subsec:spcomb} \\
% un factorial & |$\spfact{5}$| & \pageref{subsec:spfact} \\
% \end{longtable}
% \endgroup
%
% \subsection{Un primer documento}\label{subsec:primer-documento}
%
% El siguiente documento está completo: se puede copiar, guardar con
% extensión \texttt{.tex} y compilar con \hologo{LuaLaTeX}. Usa comandos de
% varios ejes temáticos de este documento y los metadatos mínimos que necesita
% un \emph{tagged} \texttt{PDF}.
%
% \iffalse
%<*ignore>
% \fi
\begin{examplecode}[frame=single]
\DocumentMetadata
  {
    lang=es-CL,
    pdfversion=2.0,
    pdfstandard=ua-2,
    tagging=on,
    tagging-setup={math/setup={mathml-SE}},
  }
\documentclass{article}
\usepackage[spanish]{babel}
\usepackage{spanish-mathml-intent, hyperref}
\hypersetup
  {
    pdfdisplaydoctitle = true,
    pdftitle           = {Guía de ejercicios},
  }

\begin{document}

\section*{Guía de ejercicios}

El número $\spnum{3,14}$ es una aproximación de $\pi$. El pasillo del
colegio mide $\spunit{23 m}$ de largo y un cuaderno cuesta $\spmoney{500}$.

Dibuje el triángulo $\spPoly{ABC}$ y marque el ángulo $\spangle{A}$. Sea
$\spfun{f(x)}$ una función y sean $A$ y $B$ dos sucesos: calcule
$\spProb{A \cap B}$.

Material preparado por la \spshort{prof.a} Pérez.

\end{document}
\end{examplecode}
% \iffalse
%</ignore>
% \fi
%
% La tabla~\ref{tab:primer-documento} muestra qué se imprime en el \texttt{PDF}
% y qué lee \mynvmath{NVDA/MathCAT} en cada caso; los valores salen de los
% ejemplos documentados en las secciones de cada comando.
%
% \begingroup\small
% \setlength{\LTleft}{\fill}\setlength{\LTright}{\fill}
% \begin{longtable}{@{}l l p{5cm}@{}}
% \caption{Lo que escribe el docente y lo que escucha el estudiante} \label{tab:primer-documento} \\
% \toprule
% \textbf{Entrada} & \textbf{Salida PDF} & \multicolumn{1}{c}{\textbf{Lectura NVDA}} \\
% \midrule
% \endfirsthead
% \caption[]{\emph{(Continuación: Lo que escribe el docente y lo que escucha el estudiante)}} \\
% \toprule
% \textbf{Entrada} & \textbf{Salida PDF} & \multicolumn{1}{c}{\textbf{Lectura NVDA}} \\
% \midrule
% \endhead
% \midrule
% \multicolumn{3}{r}{\emph{Continúa en la siguiente página...}} \\
% \endfoot
% \bottomrule
% \endlastfoot
% |$\spnum{3,14}$| & $3{,}14$ & \emph{«tres coma catorce»} \\
% |$\spunit{23 m}$| & $23\,\mathrm{m}$ & \emph{«veintitrés metros»} \\
% |$\spmoney{500}$| & $\$500$ & \emph{«quinientos pesos»} \\
% |\spshort{prof.a}| & Prof.$^{\text{a}}$ & \emph{«profesora»} \\
% |$\spPoly{ABC}$| & $\triangle ABC$ & \emph{«triángulo A B C»} \\
% |$\spfun{f(x)}$| & $f(x)$ & \emph{«f de x»} \\
% |$\spProb{A \cap B}$| & $P(A \cap B)$ & \emph{«probabilidad de A y B»} \\
% \end{longtable}
% \endgroup
%
% \subsection{Cómo comprobar el resultado}\label{subsec:comprobar}
%
% Hay dos formas de comprobar un documento: \emph{escucharlo} y
% \emph{validar su estructura}. La primera dice si la lectura es la que se
% quiso; la segunda, si el \texttt{PDF} está bien etiquetado.
%
% \subsubsection{Escuchar la lectura}
%
% Abra el \texttt{PDF} con \mynvmath{NVDA}\cite{nvda} y
% \mynvmath{MathCAT}\cite{mathcat} instalados, recorra el texto hasta la
% fórmula y compare lo que escucha con la columna «Lectura NVDA» de la
% tabla~\ref{tab:primer-documento} y de las secciones de cada comando. Si una
% lectura no es la esperada, vea la~\S\ref{subsec:si-algo-falla}.
%
% \subsubsection{Validar la estructura}
%
% Las siguientes órdenes, que se escriben en una terminal y no dentro del
% documento, extraen las etiquetas del \texttt{PDF} y las comprueban con
% \texttt{show-pdf-tags} y \texttt{rnv-wrapp}, y validan la norma
% \texttt{PDF/UA-2} con \texttt{veraPDF}\cite{verapdf}:
%
% \iffalse
%<*ignore>
% \fi
\begin{examplecode}[frame=single,language={}]
show-pdf-tags --xml archivo.pdf | rnv-wrapp
verapdf --flavour ua2 --format text archivo.pdf
\end{examplecode}
% \iffalse
%</ignore>
% \fi
%
% \texttt{show-pdf-tags} está en \href{https://ctan.org/pkg/show-pdf-tags}{\texttt{CTAN}}; si su
% distribución no lo incluye, instálelo con el gestor de paquetes.
%
% Si tiene instaladas estas herramientas, la orden |spmmli-tool -c archivo.pdf|
% ejecuta ambas comprobaciones de una vez (\S\ref{sec:af}).
%
% \subsubsection{Instalar \texttt{rnv-wrapp} y \texttt{veraPDF}}
%
% Ambas herramientas se instalan con los scripts del repositorio
% \href{https://github.com/pablgonz/rnv-vera-pdf-cli}{\texttt{rnv-vera-pdf-cli}}.
% \texttt{veraPDF} necesita tener \texttt{Java} instalado. En \texttt{Linux} se
% ejecuta, en una terminal:
%
% \iffalse
%<*ignore>
% \fi
\begin{examplecode}[frame=single,language={},breaklines=true]
curl -sSL https://raw.githubusercontent.com/pablgonz/rnv-vera-pdf-cli/main/install-rnv.sh | bash -s -- --unattended
curl -sSL https://raw.githubusercontent.com/pablgonz/rnv-vera-pdf-cli/main/install-verapdf.sh | bash -s -- --unattended
\end{examplecode}
% \iffalse
%</ignore>
% \fi
%
% En \texttt{Windows}, en \texttt{PowerShell}:
%
% \iffalse
%<*ignore>
% \fi
\begin{examplecode}[frame=single,language={},breaklines=true]
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/pablgonz/rnv-vera-pdf-cli/main/install-rnv.ps1" -OutFile install-rnv.ps1
.\install-rnv.ps1 --unattended
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/pablgonz/rnv-vera-pdf-cli/main/install-verapdf.ps1" -OutFile install-verapdf.ps1
.\install-verapdf.ps1 --unattended
\end{examplecode}
% \iffalse
%</ignore>
% \fi
%
% La opción |--unattended| agrega las herramientas al \texttt{PATH}; después
% de instalar, abra una terminal nueva para que el cambio tenga efecto. El
% script de \texttt{veraPDF} también sirve en \texttt{macOS}; para
% \texttt{rnv-wrapp} el repositorio ofrece solo \texttt{Linux} y \texttt{Windows}.
%
%
% \subsection{Si algo no suena como esperaba}\label{subsec:si-algo-falla}
%
% Revise, en este orden:
%
% \begin{enumerate}[left=0pt,itemsep=\parskip]
%   \item \emph{El mensaje de error.} Si la compilación se detiene con un
%     \enquote{error}, el mensaje indica el comando y el argumento que no
%     cumple la forma documentada (\S\ref{sec:setspintent}).
%   \item \emph{La llave} \mykey{read-cmd}. Varios comandos ofrecen más de una
%     lectura; |$\spPoint[read-cmd=school]{A_{1}}$| verbaliza
%     «sub uno». Los valores posibles se describen en la sección de cada comando.
%   \item \emph{La llave} \mykey{read-arg}. Reemplaza la lectura del argumento,
%     |$\spread[read-arg={es-paralela-a}]{\parallel}$|; su valor siempre
%     va entre llaves.
%   \item \emph{La llave} \mykey{silent}. Con \mykey{silent}|=true| se silencia la lectura
%     sin cambiar lo que se imprime; la validación del argumento se mantiene.
%   \item \emph{La configuración global.} Se puede fijar para todo
%     el documento, |\setspintent{spPoint}{read-cmd=school}|; la llave dada
%     en el argumento opcional de un comando tiene prioridad sobre
%     ella (\S\ref{sec:setspintent}).
% \end{enumerate}
%
% \section{El comando \cs[no-index]{setspintent}}\label{sec:setspintent}
%
% \begin{function}{\setspintent}
%   \begin{syntax}
%     |\setspintent|\mymarg{comando}\mymarg{key-uno \textnormal{\textcolor{gray}{=}} valor\textnormal{\textcolor{gray}{,}} key-dos \textnormal{\textcolor{gray}{=}} valor\textnormal{\textcolor{gray}{,}} key-tres \textnormal{\textcolor{gray}{=}} valor\textnormal{\textcolor{gray}{,}} \dots}
%   \end{syntax}
%   El comando \ics*{setspintent} establece las llaves \mymeta{keys} de
%   forma global para los comandos provistos por \mypkg*[no-index]{spintent}. Puede
%   utilizarse tanto en el preámbulo como en el cuerpo del documento
%   tantas veces como se desee.
% \end{function}
%
% Las llaves \mymeta{keys} definidas en el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val} de los comandos
% tienen prioridad sobre las establecidas por este, anulando las
% configuraciones globales de |\setspintent|.
%
% Muchas de las utilidades que provee el paquete \mypkg*[no-index]{spintent} implementan
% el sistema \myoarg{key \textnormal{\textcolor{gray}{=}} val} y están pensadas
% para ser utilizadas en la generación de \emph{«hojas de ejercicios»} o \emph{«clases
% escritas»}. Se recomienda usar |\setspintent| y establecerlas de manera
% global para cada documento, para mantener la coherencia en la verbalización
% de \mynvmath{NVDA/MathCAT} a lo largo del mismo.
%
% El sistema \myoarg{key \textnormal{\textcolor{gray}{=}} val} utilizado
% por \mypkg*[no-index]{spintent} se implementa usando el módulo \mypkg{l3keys}
% de \mypkg{expl3}\cite{expl3,inter3}. Por diseño, las llaves que reciben un
% \emph{valor} después del signo `|=|' NO aceptan \emph{valores vacíos}
% y es obligatorio pasar uno válido; las llaves documentadas como \mymeta{valor prohibido}
% no reciben \emph{ningún valor}.
% En caso de omitir un \emph{valor} válido o pasar un \emph{valor vacío}
% a una llave que acepte \emph{valor} o pasar un \emph{valor} a una llave
% del tipo \mymeta{valor prohibido} se devuelve un \enquote{error}
% y se detiene la compilación del documento.
%
% \begin{important}*
%  En el modo \texttt{mathml-AF} (\S\ref{sec:af}), dos fórmulas con
%  \emph{exactamente la misma fuente} reciben el mismo \mynvmath{MathML}
%  adjunto, aunque una de ellas esté dentro de un grupo con otra configuración
%  de |\setspintent|.
%
%  Por ejemplo, tras |{\setspintent{spmedia}{sub-index={x}}$\spmedia$}| la
%  fórmula siguiente |$\spmedia$| se verbaliza igual que la primera. Esto se
%  debe a que el núcleo de \hologo{LaTeX} asocia el \mynvmath{MathML} por el
%  \emph{hash} de la fuente y usa el primer fragmento que coincide; no ocurre
%  en el modo \texttt{mathml-SE}. Para evitarlo, haga distinta la fuente de
%  ambas fórmulas, por ejemplo pasando las llaves en el argumento opcional
%  de cada comando.
% \end{important}
%
% \section{The big four}
%
% El paquete \mypkg*[no-index]{spintent} provee cuatro grandes comandos
% \emph{«the big four»}\footnote{Los \emph{«cuatro grandes»} del
% Thrash Metal: Metallica, Megadeth, Slayer y Anthrax, que me acompañan
% mientras desarrollo \mypkg*[no-index]{spintent}.}:
% |\spnum| para el manejo de números, |\spunit| para el manejo
% de unidades, |\spmoney| para el manejo de dinero y |\spshort|
% para el manejo de abreviaturas, acrónimos y números ordinales.
%
% \begin{important}*
%  También se incluyen las utilidades |\spnewunit| y |\spunitalias| asociadas
%  al comando |\spunit|.
% \end{important}
%
% \subsection{El comando \cs[no-index]{spnum}}\label{subsec:spnum}
%
% \begin{function}{\spnum}
%   \begin{syntax}
%     |\spnum|\mymarg{número}\hfill|\spnum|\mymarg{número\textnormal{\textcolor{red}{{,}}}número}\hfill|\spnum|\mymarg{número\textnormal{\textcolor{red}{{.}}}número}\hfill|\spnum|\mymarg{número\textnormal{\textcolor{red}{{;}}}número}\par\noindent
%     |\spnum|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{número\textnormal{\textcolor{red}{{,}}}número\textnormal{\textcolor{red}{{;}}}número}\hfill|\spnum|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{número\textnormal{\textcolor{red}{{,}}}número\textnormal{\textcolor{red}{{;}}}número \textnormal{\textcolor{red}{{e}}} exponente}\par\noindent
%     |\spnum|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{número\textnormal{\textcolor{red}{{.}}}número\textnormal{\textcolor{red}{{;}}}número}\hfill|\spnum|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{número\textnormal{\textcolor{red}{{.}}}número\textnormal{\textcolor{red}{{;}}}número \textnormal{\textcolor{red}{{e}}} exponente}
%   \end{syntax}
%   El comando \ics*{spnum} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento obligatorio} \mymarg{número}, donde la \emph{«parte entera»}
%   puede iniciar con los signos `$\mathcolor{red}{+}$' o `$\mathcolor{red}{-}$',
%   la \emph{«parte decimal finita»} \textbf{debe} iniciar con una \emph{coma}
%   `\textcolor{red}{\texttt{,}}' o con un \emph{punto} `\textcolor{red}{\texttt{.}}',
%   y la \emph{«parte decimal infinita (periódica)»} \textbf{debe} iniciar
%   con un \emph{punto y coma} `\textcolor{red}{\texttt{;}}'.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
% y se ejecuta |$\spnum{+23}$| se imprimirá `$+23$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«más veintitrés»}, si se
% ejecuta |$\spnum{3,14}$| se imprimirá `$3{,}14$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«tres coma catorce»}, si
% se ejecuta |$\spnum{3,1;4}$| se imprimirá `$3{,}1\overline{4}$' en la
% salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«tres coma
% uno con cuatro periódico»}.
%
% Además, el \emph{argumento obligatorio} \mymarg{número} admite al final
% de este un sufijo de \emph{«notación científica»}. Este se indica mediante las
% letras `\textcolor{red}{\texttt{e}}' o `\textcolor{red}{\texttt{E}}',
% acompañadas de los signos opcionales `$\mathcolor{red}{+}$' o
% `$\mathcolor{red}{-}$' seguidos por los dígitos del exponente.
% Por ejemplo:~|$\spnum{3,14e3}$| imprimirá `$3{,}14\cdot 10^{3}$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«tres coma catorce por diez al cubo»}.
%
% \begin{important}*
%  La \emph{coma} `\textcolor{red}{\texttt{,}}' o el \emph{punto} `\textcolor{red}{\texttt{.}}'
%  son \emph{separadores decimales} y NO de millares, por lo tanto solo
%  pueden aparecer una «única vez» dentro del \mymarg{argumento}.
%  El \mymarg{argumento} puede contener espacios matemáticos válidos, pero
%  NO puede contener comandos con o sin argumentos como |\textstyle|, |\mathrm{#1}| o
%  |\mathbf{#1}| por ejemplo. Cualquier entrada fuera de esta sintaxis
%  devuelve un \enquote{error} y se detiene la compilación del documento.
% \end{important}
%
% \subsubsection{Llaves para \cs[no-index]{spnum}}\label{subsubsec:key-spnum}
%
% \keyexampcmd{cifras-sep}{true \textnormal{\textcolor{lightgray}{\textbar}} false}{true}[spnum]
%
% Establece el \emph{espaciado} utilizado para agrupar los dígitos en
% bloques de tres, tanto en la parte entera como en la parte decimal finita.
% Si se establece en |true| se siguen las normas de la \textsc{rae} y se
% utiliza un \emph{espacio fino} `\textcolor{blue!75}{\textbackslash}|,|'
% entre las cifras; si se establece en |false|, se respetan los espacios
% matemáticos introducidos en el argumento. Por ejemplo:~|$\spnum{100000}$|
% imprimirá `$100\,000$' en la salida \texttt{PDF} y |$\spnum[cifras-sep=false]{10\,00\,00}$|
% imprimirá `$10\,00\,00$' en la salida \texttt{PDF}.
%
% \keyexampcmd{read-sign}{terse \textnormal{\textcolor{lightgray}{\textbar}} clear}{terse}[spnum]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará los signos
% `$\mathcolor{red}{+}$' o `$\mathcolor{red}{-}$'.
% Si se establece en |terse|, \mynvmath{NVDA/MathCAT} verbalizará \emph{«más»} o
% \emph{«menos»} \textbf{antes} de leer el número; si se establece en |clear|, \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«positivo»} o \emph{«negativo»} \textbf{después} de leer el número.
% Por ejemplo:~|$\spnum{-23}$| imprimirá `$-23$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«menos veintitrés»} y |$\spnum[read-sign=clear]{-23}$|
% imprimirá `$-23$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«veintitrés negativo»}.
%
% \keyexampcmd{read-period-sep}{coma \textnormal{\textcolor{lightgray}{\textbar}} punto}{coma}[spnum]
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará e imprimirá
% el signo de separación `\textcolor{red}{\texttt{;}}' de la \emph{«parte
% decimal infinita (periódica)»} cuando no se tiene la \emph{«parte
% decimal finita»}. Si se establece en |coma|, se imprimirá `\textcolor{red}{\texttt{,}}'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«coma»};
% si se establece en |punto|, se imprimirá `\textcolor{red}{\texttt{.}}'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«punto»}.
% Por ejemplo:~|$\spnum{3;4}$| imprimirá `$3{,}\overline{4}$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«tres coma cuatro periódico»}
% y |$\spnum[read-period-sep=punto]{3;4}$| imprimirá `$3{.}\overline{4}$'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«tres punto cuatro periódico»}.
%
% \begin{important}*
%  Esta llave es necesaria SOLO cuando se utilizan \emph{«decimales periódicos puros»},
%  es decir, cuando no se ha incluido en el \mymarg{argumento} una \emph{coma}
%  `\textcolor{red}{\texttt{,}}' o un \emph{punto} `\textcolor{red}{\texttt{.}}'
%  antes del signo de separación `\textcolor{red}{\texttt{;}}' de la parte periódica.
% \end{important}
%
% \keyexampcmd{notation-sym}{times \textnormal{\textcolor{lightgray}{\textbar}} cdot}{cdot}[spnum]
%
% Establece el \emph{símbolo} que se utilizará para imprimir
% la \emph{«notación científica»} al usar `\textcolor{red}{\texttt{e}}' o
% `\textcolor{red}{\texttt{E}}'. Si se establece en |times| imprimirá
% `$\mathcolor{red}{\times}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«por»}; si se establece en |cdot| imprimirá
% `$\mathcolor{red}{\cdot}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«por»}. Por ejemplo:~|$\spnum{3,14e3}$| imprimirá `$3{,}14\cdot 10^{3}$'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«tres coma catorce por diez al cubo»} y |$\spnum[notation-sym=times]{3,14e3}$|
% imprimirá `$3{,}14\times 10^{3}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«tres coma catorce por diez al cubo»}.
%
% \begin{important}*
%  Decir \emph{«notación científica»} aquí es un poco exagerado, en realidad
%  es \emph{«multiplicación por potencias de $10$»}, no se verifica si la
%  entrada cumple o no con la condición de estar escrita en \emph{«notación científica»}.
% \end{important}
%
% \subsection{El comando \cs[no-index]{spunit}}\label{subsec:spunit}
%
% \begin{function}{\spunit}
%   \begin{syntax}
%     |\spunit|\mymarg{unidad}\hfill|\spunit|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{unidad}\hfill|\spunit|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{número unidad}\par\noindent
%     |\spunit|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{número unidad\textnormal{\textcolor{red}{{\textasteriskcentered}}}unidad}\hfill|\spunit|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{número unidad\textnormal{\textcolor{red}{{\textasteriskcentered}}}unidad\textnormal{\textcolor{red}{{/}}}unidad}\par\noindent
%     |\spunit|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{número unidad\textnormal{\textcolor{red}{{\textasteriskcentered}}}unidad\textnormal{\textcolor{red}{{/}}}unidad\textnormal{\textcolor{red}{{\textasteriskcentered}}}\dots}\hfill|\spunit|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{número \textnormal{\textcolor{red}{{e}}} exponente unidad \dots}
%   \end{syntax}
%   El comando \ics*{spunit} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento obligatorio} \mymarg{unidad}, el cual puede contener
%   una \emph{«parte numérica»} opcional la cual se procesa
%   internamente por el comando |\spnum| al inicio del \mymarg{argumento} seguido
%   de una \mymarg{unidad} o un \mymarg{alias} válido descritos en~\S\ref{subsubsec:arg-spunit}
%   o una \mymarg{unidad} creada por el comando |\spnewunit| (\S\ref{subsubsec:spnewunit})
%   o un \mymarg{alias} creado por el comando |\spunitalias| (\S\ref{subsubsec:spunitalias}).
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spunit{23 m}$| imprimirá `$23\,\mathrm{m}$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«veintitrés metros»}.
%
% La multiplicación entre las \mymarg{unidades} se realiza mediante un
% \emph{asterisco} `$\mathcolor{red}{\ast}$' y las potencias de estas
% se indican con un \emph{circunflejo seguido del exponente entre llaves}
% `\textcolor{red}{\texttt{\^{}}}\mymarg{exponente}'. Por ejemplo, si
% se ejecuta |$\spunit{23 m*s^{2}}$| se imprimirá `$23\,\mathrm{m}\,\mathrm{s}^{2}$'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«veintitrés metros segundos cuadrado»}.
%
% Se pueden usar \emph{fracciones lineales} con las \mymarg{unidades}
% utilizando una \emph{«única barra»} `$\mathcolor{red}{/}$' para la
% separación entre las \mymarg{unidades} del numerador y el denominador,
% teniendo presente que el denominador NO puede contener números de ningún
% tipo. Por ejemplo:~|$\spunit{23 m/s^{2}}$| imprimirá `$23\,\mathrm{m}/\mathrm{s}^{2}$'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«veintitrés metros por segundos cuadrado»}.
%
% \begin{important}*
%  Solo imprimiremos \emph{fracciones lineales}, si se requiere otra forma
%  fraccionaria para explicaciones y otros usos se debe usar el comando
%  por separado. El \mymarg{argumento} puede contener espacios matemáticos
%  válidos, pero NO puede contener comandos con o sin argumentos como
%  |\textstyle|, |\mathrm{#1}| o |\mathbf{#1}| por ejemplo, NO puede contener
%  más de una \emph{«única barra»} `$\mathcolor{red}{/}$' y NO puede contener
%  números en el denominador. Cualquier entrada fuera de esta sintaxis
%  devuelve un \enquote{error} y se detiene la compilación del documento.
% \end{important}
%
% \subsubsection{Llaves para \cs[no-index]{spunit}}
%
% El comando |\spunit| posee las \mymeta{meta-keys} \mykey[spunit]{cifras-sep},
% \mykey[spunit]{read-period-sep} y \mykey[spunit]{notation-sym} pasadas
% al comando |\spnum| (\S\ref{subsubsec:key-spnum}).
%
% \keyexampcmd{print-cdot}{true \textnormal{\textcolor{lightgray}{\textbar}} false}{false}[spunit]
%
% Establece si se \emph{imprime} |\cdot| `$\mathcolor{red}{\cdot}$' o un \emph{espacio fino}
% `\textcolor{blue!75}{\textbackslash}|,|' para la multiplicación de \mymarg{unidades}.
% Si se establece en |true|, se imprimirá `$\mathcolor{red}{\cdot}$' entre las \mymarg{unidades}
% en la salida \texttt{PDF}; si se establece en |false| se imprimirá un \emph{espacio fino}
% `\textcolor{blue!75}{\textbackslash}|,|' entre las \mymarg{unidades} en
% la salida \texttt{PDF}.
%
% \keyexampcmd{read-cdot}{true \textnormal{\textcolor{lightgray}{\textbar}} false}{false}[spunit]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará la
% multiplicación de \mymarg{unidades}. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} verbalizará «por» entre las \mymarg{unidades}; si se
% establece en |false|, \mynvmath{NVDA/MathCAT} no verbalizará nada entre las
% \mymarg{unidades}.
%
% \keyexampcmd{read-slash}{por \textnormal{\textcolor{lightgray}{\textbar}} partido-por}{por}[spunit]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará la
% \emph{«barra»} `$\mathcolor{red}{/}$' de separación entre el
% numerador y el denominador. Si se establece en |por|, \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«por»}; si se establece en |partido-por|, \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«partido por»}.
%
% \subsubsection{Argumentos para \cs[no-index]{spunit}}\label{subsubsec:arg-spunit}
%
% La tabla~\ref{tab:spunit-unidades} resume las \mymarg{unidades} y los \mymarg{alias}
% soportados por |\spunit|.
%
% \begingroup\small
% \begin{longtable}{@{\extracolsep{\fill}}cllcl@{}}
%   \caption{Unidades soportadas} \label{tab:spunit-unidades} \\
%   \toprule
%   \textbf{Entrada} & \textbf{Alias / Variantes} & \textbf{Nombre} & \textbf{Salida PDF} & \multicolumn{1}{c}{\textbf{Lectura NVDA}} \\
%   \midrule
%   \endfirsthead
%   %
%   \caption[]{\emph{(Continuación: Unidades soportadas)}} \\
%   \toprule
%   \textbf{Entrada} & \textbf{Alias / Variantes} & \textbf{Nombre} & \textbf{Salida PDF} & \multicolumn{1}{c}{\textbf{Lectura NVDA}} \\
%   \midrule
%   \endhead
%   %
%   \midrule
%   \multicolumn{5}{r}{\emph{Continúa en la siguiente página...}} \\
%   \endfoot
%   %
%   \bottomrule
%   \endlastfoot
%   %
%   m   & metro, metros             & Metro               & $\mathrm{m}$   & \emph{«metro»}               \\
%   g   & gramo, gramos             & Gramo               & $\mathrm{g}$   & \emph{«gramo»}               \\
%   s   & segundo, segundos         & Segundo (Tiempo)    & $\mathrm{s}$   & \emph{«segundo»}             \\
%   l   & litro, l-litro            & Litro (minúscula)   & $\mathrm{l}$   & \emph{«litro»}               \\
%   h   & hora, horas               & Hora                & $\mathrm{h}$   & \emph{«hora»}                \\
%   d   & dia, dias                 & Día                 & $\mathrm{d}$   & \emph{«día»}                 \\
%   A   & amperio, ampere           & Amperio             & $\mathrm{A}$   & \emph{«amperio»}             \\
%   V   & voltio, volt              & Voltio              & $\mathrm{V}$   & \emph{«voltio»}              \\
%   W   & vatio, watt               & Vatio               & $\mathrm{W}$   & \emph{«vatio»}               \\
%   J   & julio, joule              & Julio               & $\mathrm{J}$   & \emph{«julio»}               \\
%   N   & newton                    & Newton              & $\mathrm{N}$   & \emph{«newton»}              \\
%   Pa  & pascal                    & Pascal              & $\mathrm{Pa}$  & \emph{«pascal»}              \\
%   Hz  & hercio, hertz             & Hercio              & $\mathrm{Hz}$  & \emph{«hercio»}              \\
%   Ω   & ohmio, ohm, Omega         & Ohmio               & $\mathrm{Ω}$   & \emph{«ohmio»}               \\
%   F   & faradio                   & Faradio             & $\mathrm{F}$   & \emph{«faradio»}             \\
%   C   & culombio                  & Culombio            & $\mathrm{C}$   & \emph{«culombio»}            \\
%   K   & kelvin, Kelvin            & Kelvin              & $\mathrm{K}$   & \emph{«kelvin»}              \\
%   °C  & celsius, degC, ℃          & Grados Celsius      & $\mathrm{°C}$  & \emph{«grados celsius»}      \\
%   °F  & fahrenheit, degF, ℉       & Grados Fahrenheit   & $\mathrm{°F}$  & \emph{«grados fahrenheit»}   \\
%   cal & caloria, calorias         & Caloría             & $\mathrm{cal}$ & \emph{«caloría»}             \\
%   b   & bit                       & Bit                 & $\mathrm{b}$   & \emph{«bit»}                 \\
%   B   & byte, bytes               & Byte                & $\mathrm{B}$   & \emph{«byte»}                \\
%   in  & pulgada, pulgadas         & Pulgada             & $\mathrm{in}$  & \emph{«pulgada»}             \\
%   ft  & pie, pies                 & Pie                 & $\mathrm{ft}$  & \emph{«pie»}                 \\
%   yd  & yarda, yardas             & Yarda               & $\mathrm{yd}$  & \emph{«yarda»}               \\
%   mi  & milla, millas             & Milla               & $\mathrm{mi}$  & \emph{«milla»}               \\
%   lb  & libra, libras             & Libra               & $\mathrm{lb}$  & \emph{«libra»}               \\
%   oz  & onza, onzas               & Onza                & $\mathrm{oz}$  & \emph{«onza»}                \\
%   gal & galon, galones            & Galón               & $\mathrm{gal}$ & \emph{«galón»}               \\
%   Å   & angstrom                  & Angstrom            & $\mathrm{Å}$   & \emph{«angstrom»}            \\
%   µ   & micro                     & Micro (Prefijo)     & $\mathrm{µ}$   & \emph{«micro»}               \\
%   ℧   & mho                       & Mho / Siemens       & $\mathrm{℧}$   & \emph{«mho / siemens»}       \\
%   ′   & minmin                    & Minuto (Arco)       & $\mathrm{′}$   & \emph{«minuto»}              \\
%   ″   & segseg                    & Segundo (Arco)      & $\mathrm{″}$   & \emph{«segundo»}             \\
%   u   & u-masa                    & Unidad masa atómica & $\mathrm{u}$   & \emph{«unidad masa atómica»} \\
%   \midrule
%   \multicolumn{5}{c}{\textbf{Unidades directas (sin alias)}} \\
%   \midrule
%   L   & ---                         & Litro (Mayúscula)   & $\mathrm{L}$   & \emph{«litro»}               \\
%   ℓ   & ---                         & Litro (Cursiva)     & $\mathrm{ℓ}$   & \emph{«litro»}               \\
%   w   & ---                         & Vatio (Minúscula)   & $\mathrm{w}$   & \emph{«vatio»}               \\
%   °K  & ---                         & Grado Kelvin        & $\mathrm{°K}$  & \emph{«grado kelvin»}        \\
%   Bq  & ---                         & Becquerel           & $\mathrm{Bq}$  & \emph{«becquerel»}           \\
%   Da  & ---                         & Dalton              & $\mathrm{Da}$  & \emph{«dalton»}              \\
%   Gy  & ---                         & Gray                & $\mathrm{Gy}$  & \emph{«gray»}                \\
%   S   & ---                         & Siemens             & $\mathrm{S}$   & \emph{«siemens»}             \\
%   Sv  & ---                         & Sievert             & $\mathrm{Sv}$  & \emph{«sievert»}             \\
%   T   & ---                         & Tesla               & $\mathrm{T}$   & \emph{«tesla»}               \\
%   Wb  & ---                         & Weber               & $\mathrm{Wb}$  & \emph{«weber»}               \\
%   atm & ---                         & Atmósfera           & $\mathrm{atm}$ & \emph{«atmósfera»}           \\
%   au  & ---                         & Unidad astronómica  & $\mathrm{au}$  & \emph{«unidad astronómica»}  \\
%   bar & ---                         & Bar                 & $\mathrm{bar}$ & \emph{«bar»}                 \\
%   cd  & ---                         & Candela             & $\mathrm{cd}$  & \emph{«candela»}             \\
%   dB  & ---                         & Decibelio           & $\mathrm{dB}$  & \emph{«decibelio»}           \\
%   dyn & ---                         & Dina                & $\mathrm{dyn}$ & \emph{«dina»}                \\
%   eV  & ---                         & Electronvoltio      & $\mathrm{eV}$  & \emph{«electronvoltio»}      \\
%   erg & ---                         & Ergio               & $\mathrm{erg}$ & \emph{«ergio»}               \\
%   ha  & ---                         & Hectárea            & $\mathrm{ha}$  & \emph{«hectárea»}            \\
%   kat & ---                         & Katal               & $\mathrm{kat}$ & \emph{«katal»}               \\
%   kg  & ---                         & Kilogramo           & $\mathrm{kg}$  & \emph{«kilogramo»}           \\
%   km  & ---                         & Kilómetro           & $\mathrm{km}$  & \emph{«kilómetro»}           \\
%   lm  & ---                         & Lumen               & $\mathrm{lm}$  & \emph{«lumen»}               \\
%   lx  & ---                         & Lux                 & $\mathrm{lx}$  & \emph{«lux»}                 \\
%   cm  & ---                         & Centímetro          & $\mathrm{cm}$  & \emph{«centímetro»}          \\
%   min & ---                         & Minuto (Tiempo)     & $\mathrm{min}$ & \emph{«minuto»}              \\
%   mm  & ---                         & Milímetro           & $\mathrm{mm}$  & \emph{«milímetro»}           \\
%   mol & ---                         & Mol                 & $\mathrm{mol}$ & \emph{«mol»}                 \\
%   pc  & ---                         & Pársec              & $\mathrm{pc}$  & \emph{«pársec»}              \\
%   rad & ---                         & Radián              & $\mathrm{rad}$ & \emph{«radián»}              \\
%   rpm & ---                         & Rev. por minuto     & $\mathrm{rpm}$ & \emph{«rev. por minuto»}     \\
%   sr  & ---                         & Estereorradián      & $\mathrm{sr}$  & \emph{«estereorradián»}      \\
%   t   & ---                         & Tonelada            & $\mathrm{t}$   & \emph{«tonelada»}            \\
%   a   & ---                         & Año / Área (are)    & $\mathrm{a}$   & \emph{«año / área»}          \\
%   y   & ---                         & Año (Year)          & $\mathrm{y}$   & \emph{«año»}                 \\
%   °   & ---                         & Grado (Arco)        & $\mathrm{°}$   & \emph{«grado»}               \\
% \end{longtable}
% \endgroup
%
% \subsubsection{El comando \cs[no-index]{spnewunit}}\label{subsubsec:spnewunit}
%
% \begin{function}{\spnewunit}
%   \begin{syntax}
%     \ics*{spnewunit}\mymarg{nueva unidad}\mymarg{verbalización \textnormal{NVDA}}
%   \end{syntax}
%   El comando \ics*{spnewunit} trabaja en \emph{modo texto} y recibe dos
%   \emph{argumentos obligatorios}: \mymarg{nueva unidad} que establece
%   lo que se imprimirá en la salida \texttt{PDF} y \mymarg{verbalización \textnormal{NVDA}}
%   que establece cómo \mynvmath{NVDA/MathCAT} \emph{verbalizará} la \mymarg{nueva unidad}.
% \end{function}
%
% La \mymarg{nueva unidad} no debe estar registrada como \mymarg{unidad}
% o \mymarg{alias} para |\spunit| descritos en~\S\ref{subsubsec:arg-spunit}.
% Si la \mymarg{nueva unidad} ya está registrada se devuelve un \enquote{error}
% y se detiene la compilación del documento.
%
% La declaración de la \mymarg{nueva unidad} es «global» y se almacena
% en el módulo \myluamod{} como una \mymarg{unidad} válida para pasar como
% \mymarg{argumento} a |\spunit|. Por ejemplo:~|\spnewunit{px}{píxeles}|
% creará la \mymarg{nueva unidad} `|px|' y cuando se utilice |$\spunit{100 px}$|
% se imprimirá `$100\,\mathrm{px}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«cien píxeles»}.
%
% \subsubsection{El comando \cs[no-index]{spunitalias}}\label{subsubsec:spunitalias}
%
% \begin{function}{\spunitalias}
%   \begin{syntax}
%     \ics*{spunitalias}\mymarg{nuevo alias}\mymarg{expresión de unidades}
%   \end{syntax}
%   El comando \ics*{spunitalias} trabaja en \emph{modo texto} y recibe
%   dos \emph{argumentos obligatorios}: \mymarg{nuevo alias} que es el nuevo \mymarg{alias} de \mymarg{argumento} para |\spunit| y
%   \mymarg{expresión de unidades} que puede contener multiplicación o
%   división de \mymarg{unidades} ya registradas que se imprimirán en la
%   salida \texttt{PDF}.
% \end{function}
%
% El \mymarg{nuevo alias} no debe estar registrado como \mymarg{unidad}
% o \mymarg{alias} para |\spunit| descritos en~\S\ref{subsubsec:arg-spunit}
% o estar definido como \mymarg{unidad} por medio del comando |\spnewunit|.
% Si el \mymarg{nuevo alias} ya está registrado se devuelve un \enquote{error}
% y se detiene la compilación del documento.
%
% La declaración de un \mymarg{nuevo alias} es «global» y se almacena
% en el módulo \myluamod{} como un \mymarg{alias} válido para pasar como
% \mymarg{argumento} a |\spunit|. Por ejemplo:~|\spunitalias{caudal}{m^{3}/s}|
% creará el \mymarg{alias} `|caudal|' y cuando se utilice |$\spunit{50 caudal}$|
% se imprimirá `$50\,\mathrm{m}^{3} / \mathrm{s}$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«cincuenta metros al cubo por segundos»}.
%
% \subsection{El comando \cs[no-index]{spmoney}}\label{subsec:spmoney}
%
% \begin{function}{\spmoney}
%   \begin{syntax}
%     \mycolrow{19.7em}|\spmoney|\mymarg{cifra}\mycolsep |\spmoney|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{cifra}
%     \mycolrow{19.7em}|\spmoney|\mymarg{cifra símbolo moneda}\mycolsep |\spmoney|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{cifra símbolo moneda}
%     \mycolrow{19.7em}|\spmoney|\mymarg{cifra alias moneda}\mycolsep |\spmoney|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{cifra alias moneda}
%   \end{syntax}
%   El comando \ics*{spmoney} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento obligatorio} \mymarg{cifra} que contiene la
%   \emph{«cifra numérica»} positiva o negativa la cual se procesa internamente
%   por el comando |\spnum| acompañada de \emph{«símbolo moneda»} o
%   \emph{«alias moneda»} opcionales descritos en~\S\ref{subsubsec:arg-spmoney}.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spmoney{500}$| imprimirá `$\$500$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«quinientos pesos»}.
%
%  El comportamiento de |\spmoney| depende de cómo se pase \emph{«símbolo moneda»} o
%  \emph{«alias moneda»} en el \emph{argumento obligatorio} donde tienen
%  prioridad por sobre los valores por defecto. Por ejemplo:~%
%  |$\spmoney{500 dolares}$| imprimirá `$\$500$' en la salida \texttt{PDF}
%  y \mynvmath{NVDA/MathCAT} verbalizará \emph{«quinientos dólares»}
%  aunque la moneda por defecto sean pesos.
%
% Si se pasa un \emph{«alias moneda»} del tipo \texttt{ISO}, el código de divisas,
% como |CLP| o |USD| escrito en mayúsculas, se modifica la verbalización en \mynvmath{NVDA/MathCAT}
% agregando el país y se ajusta la salida \texttt{PDF} al estándar \texttt{ISO}. Por ejemplo:~%
% |$\spmoney{500 CLP}$| imprimirá `$\mathrm{CLP}500$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«quinientos pesos chilenos»}.
%
% Si se pasa un \emph{«alias moneda»} del tipo \texttt{ISO}
% como |clp| o |usd| escrito en minúsculas, se modifica la verbalización en \mynvmath{NVDA/MathCAT}
% agregando el país, pero NO se modifica la salida \texttt{PDF} imprimiendo
% el \emph{«símbolo de moneda»} correspondiente. Por ejemplo:~|$\spmoney{500 clp}$|
% imprimirá `$\$500$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«quinientos pesos chilenos»}.
%
% Pasar de manera directa en el \emph{argumento obligatorio} un \emph{«símbolo moneda»}
% como `$\mathcolor{red}{\mathdollar}$', `$\mathcolor{red}{\euro}$', `$\mathcolor{red}{\mathsterling}$'
% o un \emph{«alias moneda»} como |peso|, |euro| o |libra| produce
% la salida \texttt{PDF} y verbalización estándar esperada para \mynvmath{NVDA/MathCAT}.
% Los \emph{«alias moneda»} se pueden pasar con o sin tilde, por ejemplo |dolares| o |dólares|.
%
% \begin{important}*
%  Los \emph{«alias moneda»} se deben escribir en minúsculas y los códigos
%  \texttt{ISO} en minúsculas o todo en mayúsculas. Cualquier otra
%  escritura, por ejemplo |PESO| o |Usd|, devuelve un \enquote{error} y
%  se detiene la compilación del documento.
% \end{important}
%
% \begin{important}*
%  La posición del \emph{«símbolo moneda»} a la izquierda o a la derecha
%  de la \emph{«cifra numérica»} y la verbalización para \mynvmath{NVDA/MathCAT}
%  se manejan desde el módulo Lua \myluamod{} y no se proveen llaves para
%  modificar este comportamiento.
% \end{important}
%
% \begin{important}*
%  El \emph{«símbolo moneda»} es tomado de la fuente tipográfica definida
%  para el \emph{modo matemático} al momento de ejecutar el comando, o del
%  \emph{modo texto} si se establece \mykey{money-math}|=false|, se
%  debe tener presente en caso de querer usar un \emph{«símbolo moneda»}
%  poco habitual que esté soportado por \mypkg*[no-index]{spintent}. El \mymarg{argumento}
%  puede contener espacios matemáticos válidos, pero NO puede contener
%  comandos con o sin argumentos como |\textstyle|, |\mathrm{#1}| o
%  |\mathbf{#1}| por ejemplo, con la única excepción de |\$|. La \emph{coma} `\textcolor{red}{\texttt{,}}'
%  o el \emph{punto} `\textcolor{red}{\texttt{.}}' como \emph{separador decimal} solo
%  pueden aparecer una «única vez». Cualquier entrada fuera de esta sintaxis
%  devuelve un \enquote{error} y se detiene la compilación del documento.
% \end{important}
%
% \subsubsection{Llaves para \cs[no-index]{spmoney}}
%
% El comando |\spmoney| posee la \mymeta{meta-key} \mykey[spmoney]{cifras-sep}
% pasada al comando |\spnum| (\S\ref{subsubsec:key-spnum}).
%
% \keyexampcmd{currency}{moneda \textnormal{\textcolor{lightgray}{\textbar}} alias}{pesos}[spmoney]
%
% Establece el \emph{símbolo moneda} por defecto que se imprimirá en la
% salida \texttt{PDF} y cómo lo verbalizará \mynvmath{NVDA/MathCAT} cuando
% SOLO se pasa una \emph{«cifra numérica»} como \mymarg{argumento} al comando |\spmoney|.
% El \emph{«símbolo moneda»} o el \emph{«alias moneda»} pasado a esta
% llave debe ser alguno de los descritos en~\S\ref{subsubsec:arg-spmoney}.
%
% \keyexampcmd{sub-units}{true \textnormal{\textcolor{lightgray}{\textbar}} false}{false}[spmoney]
%
% Establece si se usarán \emph{subunidades} de la moneda en curso para la
% verbalización en \mynvmath{NVDA/MathCAT}. La moneda en curso debe aceptar
% \emph{subunidades}. Por ejemplo:~|$\spmoney[sub-units=true]{100,5 dolares}$|
% imprimirá `$\$100{,}5$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«cien dólares con cincuenta centavos»}.
%
% \begin{important}*
%  Si la \emph{«cifra numérica»} no tiene parte decimal, la llave
%  \mykey{sub-units} no tiene efecto. Si la moneda en curso no admite
%  \emph{subunidades} se emite una advertencia y la llave tampoco tiene efecto.
% \end{important}
%
% \keyexampcmd{money-math}{true \textnormal{\textcolor{lightgray}{\textbar}} false}{true}[spmoney]
%
% Establece si el \emph{símbolo moneda} se imprimirá en \emph{modo texto}
% o en \emph{modo matemático}. Si se establece en |true|, se usará la
% fuente tipográfica definida para el \emph{modo matemático} en curso
% para imprimir el \emph{símbolo moneda} en la salida \texttt{PDF}; si se
% establece en |false|, se usará la fuente tipográfica definida para el
% \emph{modo texto} para imprimir el \emph{símbolo moneda} en la salida
% \texttt{PDF}.
%
% \subsubsection{Monedas y alias para \cs[no-index]{spmoney}}\label{subsubsec:arg-spmoney}
%
% La tabla~\ref{tab:spmoney-divisas} resume los \emph{«símbolos moneda»} y los
% \emph{«alias moneda»} soportados en el \mymarg{argumento} de |\spmoney|.
%
% \begingroup\small
% \begin{longtable}{@{\extracolsep{\fill}}c >{\raggedright\arraybackslash}p{2.75cm} l c l@{}}
%   \caption{Monedas y alias soportados} \label{tab:spmoney-divisas} \\
%   \toprule
%   \textbf{Entrada} & \textbf{Alias / Variantes} & \textbf{Nombre} & \textbf{Salida PDF} & \multicolumn{1}{c}{\textbf{Lectura NVDA}} \\
%   \midrule
%   \endfirsthead
%   \caption[]{\emph{(Continuación: Monedas y alias soportados)}} \\
%   \toprule
%   \textbf{Entrada} & \textbf{Alias / Variantes} & \textbf{Nombre} & \textbf{Salida PDF} & \multicolumn{1}{c}{\textbf{Lectura NVDA}} \\
%   \midrule
%   \endhead
%   \midrule
%   \multicolumn{5}{r}{\emph{Continúa en la siguiente página...}} \\
%   \endfoot
%   \bottomrule
%   \endlastfoot
%   \texttt{\$} & \texttt{peso}, \texttt{pesos} & peso & \$ & \emph{«peso / pesos»} \\
%   \texttt{dolar} & \texttt{dolares}, \texttt{dólar}, \texttt{dólares} & dólar & \$ & \emph{«dólar / dólares»} \\
%   \texttt{€} & \texttt{euro}, \texttt{euros} & euro & € & \emph{«euro / euros»} \\
%   \texttt{£} & \texttt{libra}, \texttt{libras} & libra & £ & \emph{«libra / libras»} \\
%   \texttt{¥} & \texttt{yen}, \texttt{yenes} & yen & ¥ & \emph{«yen / yenes»} \\
%   \texttt{yuan} & \texttt{yuanes} & yuan & ¥ & \emph{«yuan / yuanes»} \\
%   \texttt{₩} & \texttt{won}, \texttt{wons} & won & \texttt{₩} & \emph{«won / wons»} \\
%   \texttt{₪} & \texttt{shekel}, \texttt{shekels}, \texttt{sequel}, \texttt{sequeles} & séquel & ₪ & \emph{«séquel / séqueles»} \\
%   \texttt{₹} & \texttt{rupia}, \texttt{rupias} & rupia & ₹ & \emph{«rupia / rupias»} \\
%   \texttt{₽} & \texttt{rublo}, \texttt{rublos} & rublo & \texttt{₽} & \emph{«rublo / rublos»} \\
%   \texttt{₺} & \texttt{lira}, \texttt{liras} & lira & \texttt{₺} & \emph{«lira / liras»} \\
%   \texttt{₴} & \texttt{grivna}, \texttt{grivnas} & grivna & \texttt{₴} & \emph{«grivna / grivnas»} \\
%   \texttt{¢} & \texttt{centavo}, \texttt{centavos} & centavo & ¢ & \emph{«centavo / centavos»} \\
%   \midrule
%   \multicolumn{5}{c}{\textbf{Divisas en formato ISO (Mayúsculas)}} \\
%   \midrule
%   \texttt{CLP} & --- & peso chileno & \texttt{CLP} & \emph{«peso chileno / pesos chilenos»} \\
%   \texttt{MXN} & --- & peso mexicano & \texttt{MXN} & \emph{«peso mexicano / pesos mexicanos»} \\
%   \texttt{USD} & --- & dólar & \texttt{USD} & \emph{«dólar estadounidense / dólares estadounidenses»} \\
%   \texttt{EUR} & --- & euro & \texttt{EUR} & \emph{«euro / euros»} \\
%   \texttt{GBP} & --- & libra & \texttt{GBP} & \emph{«libra / libras»} \\
%   \texttt{JPY} & --- & yen & \texttt{JPY} & \emph{«yen / yenes»} \\
%   \texttt{CNY} & --- & yuan & \texttt{CNY} & \emph{«yuan / yuanes»} \\
%   \texttt{KRW} & --- & won & \texttt{KRW} & \emph{«won / wons»} \\
%   \texttt{ILS} & --- & séquel & \texttt{ILS} & \emph{«séquel / séqueles»} \\
%   \texttt{INR} & --- & rupia & \texttt{INR} & \emph{«rupia / rupias»} \\
%   \texttt{RUB} & --- & rublo & \texttt{RUB} & \emph{«rublo / rublos»} \\
%   \texttt{TRY} & --- & lira & \texttt{TRY} & \emph{«lira / liras»} \\
%   \texttt{UAH} & --- & grivna & \texttt{UAH} & \emph{«grivna / grivnas»} \\
%   \midrule
%   \multicolumn{5}{c}{\textbf{Códigos ISO en minúsculas}} \\
%   \midrule
%   \texttt{clp} & --- & peso chileno & \$ & \emph{«peso chileno / pesos chilenos»} \\
%   \texttt{mxn} & --- & peso mexicano & \$ & \emph{«peso mexicano / pesos mexicanos»} \\
%   \texttt{usd} & --- & dólar & \$ & \emph{«dólar estadounidense / dólares estadounidenses»} \\
%   \texttt{eur} & --- & euro & € & \emph{«euro / euros»} \\
%   \texttt{gbp} & --- & libra & £ & \emph{«libra / libras»} \\
%   \texttt{jpy} & --- & yen & ¥ & \emph{«yen / yenes»} \\
%   \texttt{cny} & --- & yuan & ¥ & \emph{«yuan / yuanes»} \\
%   \texttt{krw} & --- & won & \texttt{₩} & \emph{«won / wons»} \\
%   \texttt{ils} & --- & séquel & ₪ & \emph{«séquel / séqueles»} \\
%   \texttt{inr} & --- & rupia & ₹ & \emph{«rupia / rupias»} \\
%   \texttt{rub} & --- & rublo & \texttt{₽} & \emph{«rublo / rublos»} \\
%   \texttt{try} & --- & lira & \texttt{₺} & \emph{«lira / liras»} \\
%   \texttt{uah} & --- & grivna & \texttt{₴} & \emph{«grivna / grivnas»} \\
% \end{longtable}
% \endgroup
%
% \subsection{El comando \cs[no-index]{spshort}}\label{subsec:spshort}
%
% \begin{function}{\spshort}
%   \begin{syntax}
%     |\spshort|\mymarg{abreviaturas \textnormal{\textcolor{lightgray}{\textbar}} acrónimos \textnormal{\textcolor{lightgray}{\textbar}} números ordinales}
%     |\spshort|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{abreviaturas \textnormal{\textcolor{lightgray}{\textbar}} acrónimos \textnormal{\textcolor{lightgray}{\textbar}} números ordinales}
%     |\spshort|\myoarg{read-arg\textnormal{\textcolor{gray}{=}}\mymarg{verbalización \textnormal{NVDA}}}\mymarg{comandos \textnormal{\hologo{LaTeX}}}
%   \end{syntax}
%
%   El comando \ics*{spshort} trabaja \emph{solo en modo texto} y recibe
%   \emph{un argumento obligatorio} del tipo \mymarg{abreviaturas}, \mymarg{acrónimos} o \mymarg{números ordinales}
%   descritos en~\S\ref{subsubsec:arg-spshort}.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
% y se ejecuta |\spshort{dr.a}| se imprimirá `Dr.\textsuperscript{a}' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«doctora»};
% si se ejecuta |\spshort{onu}| se imprimirá `ONU' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«organización de las naciones unidas»}
% y si se ejecuta |\spshort{mineduc}| se imprimirá `Mineduc' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«ministerio de educación»}.
%
% Para la escritura de \mymarg{números ordinales} se puede ejecutar
% |\spshort{1.a}| que imprimirá `{\addfontfeatures{Numbers=Lining}1.\textsuperscript{a}}'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«primera»};
% si se ejecuta |\spshort{1.o}| imprimirá `{\addfontfeatures{Numbers=Lining}1.\textsuperscript{o}}'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«primero»}.
%
% Para crear una nueva \mymarg{abreviatura}, \mymarg{acrónimo} o \mymarg{número ordinal}
% que no se encuentre registrada por \mypkg*[no-index]{spintent} dentro del módulo Lua \myluamod{}
% descritas en~\S\ref{subsubsec:arg-spshort} se debe utilizar la llave
% \mykey{read-arg} y definir esta dentro del \emph{argumento obligatorio}
% que puede contener \mymarg{comandos \hologo{LaTeX}} \emph{en modo texto}
% los cuales se imprimirán en salida \texttt{PDF}.
%
% \begin{important}*
%   Para la escritura de \mymarg{números ordinales} se debe tener cuidado
%   y no confundir la \emph{«letra o volada»} `\textsuperscript{o}' con
%   el símbolo de grado `°'. Si la fuente tipográfica del \emph{modo texto}
%   está utilizando |Numbers=OldStyle|, los números pueden quedar muy separados
%   de la \emph{«letra volada»}, la solución para esto es agregar |\addfontfeatures{Numbers=Lining}|
%   dentro de un grupo junto al comando: |{\addfontfeatures{Numbers=Lining}\spshort{1.o}}|.
% \end{important}
%
% \begin{important}*
%   El comando |\spshort| está pensado como un reemplazo para \emph{shorthands}
%   y el comando |\sptext| provistos por el paquete \mypkg{babel} en español
%   compatible con \emph{tagged} \texttt{PDF}. Es de las pocas utilidades que provee
%   el paquete \mypkg*[no-index]{spintent} que trabajan \emph{solo en modo texto}. Internamente
%   utiliza la llave \mykey{E} provista por el paquete
%   \mypkg{tagpdf}\cite{tagpdf} para el \emph{expansion text}.
% \end{important}
%
% \subsubsection{Llaves para \cs[no-index]{spshort}}
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spshort]
%
% Establece la \emph{verbalización del argumento} \mymarg{comandos \textnormal{\hologo{LaTeX}}}
% de la nueva \mymarg{abreviatura}, \mymarg{acrónimo} o \mymarg{número ordinal} para \mynvmath{NVDA/MathCAT}.
% El valor de esta llave \emph{siempre} debe pasarse entre `|{ }|'. Por ejemplo:~%
% |\spshort[read-arg={octavo}]{8.\textsuperscript{\emph{avo}}}|
% creará una nueva \mymarg{abreviatura} que imprimirá `8.\textsuperscript{\emph{avo}}'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«octavo»}.
%
% \keyexampcmd{small-caps}{true \textnormal{\textcolor{lightgray}{\textbar}} false}{false}[spshort]
%
% Establece si se usarán \emph{versalitas} para imprimir \mymarg{acrónimos}
% y para la \emph{«letra volada»} en \mymarg{abreviaturas} y \mymarg{números ordinales}
% respetando las reglas de la \textsc{rae} para el uso de estas. Si se establece en |true|,
% las abreviaturas con mayúscula como \texttt{EE.~UU.} y los acrónimos
% de cinco o más letras como \texttt{Unicef} no se verán afectados; si se
% establece en |false|, no se aplicarán \emph{versalitas}. Por ejemplo:~%
% |\spshort[small-caps=true]{dr.a}| imprimirá `Dr.\textsuperscript{\textsc{a}}'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«doctora»}.
%
% \subsubsection{Argumentos para \cs[no-index]{spshort}}\label{subsubsec:arg-spshort}
%
% La tabla~\ref{tab:spshort-abreviaturas} resume las abreviaturas,
% expresiones latinas y títulos registrados en el módulo Lua \myluamod{}.
%
% \begin{longtable}{@{\extracolsep{\fill}}l p{3.5cm} c p{5cm}@{}}
% \caption{Abreviaturas soportadas} \label{tab:spshort-abreviaturas} \\
% \toprule
% \textbf{Entrada} & \textbf{Alias / Variantes} & \textbf{Salida PDF} & \multicolumn{1}{c}{\textbf{Lectura NVDA}} \\
% \midrule
% \endfirsthead
% \caption[]{\emph{(Continuación: Abreviaturas soportadas)}} \\
% \toprule
% \textbf{Entrada} & \textbf{Alias / Variantes} & \textbf{Salida PDF} & \multicolumn{1}{c}{\textbf{Lectura NVDA}} \\
% \midrule
% \endhead
% \midrule
% \multicolumn{4}{r}{\emph{Continúa en la siguiente página...}} \\
% \endfoot
% \bottomrule
% \endlastfoot
% \multicolumn{4}{c}{\textbf{Abreviaturas con letras voladas}} \\
% \midrule
% \texttt{dr.a} & \texttt{dr.ª} & Dr.$^{\text{a}}$ & \emph{«doctora»} \\
% \texttt{dr.as} & --- & Dr.$^{\text{as}}$ & \emph{«doctoras»} \\
% \texttt{prof.a} & --- & Prof.$^{\text{a}}$ & \emph{«profesora»} \\
% \texttt{d.a} & \texttt{d.ª} & D.$^{\text{a}}$ & \emph{«doña»} \\
% \texttt{m.a} & --- & M.$^{\text{a}}$ & \emph{«maría»} \\
% \texttt{n.o} & \texttt{n.º} & N.$^{\text{o}}$ & \emph{«número»} \\
% \texttt{n.os} & --- & N.$^{\text{os}}$ & \emph{«números»} \\
% \texttt{c.ia} & --- & C.$^{\text{ia}}$ & \emph{«compañía»} \\
% \midrule
% \multicolumn{4}{c}{\textbf{Abreviaturas comunes y expresiones latinas}} \\
% \midrule
% \texttt{pág.} & \texttt{pag.} & pág. & \emph{«página»} \\
% \texttt{vol.} & --- & vol. & \emph{«volumen»} \\
% \texttt{etc.} & --- & etc. & \emph{«etcétera»} \\
% \texttt{et al.} & \texttt{et. al.} & et~al. & \emph{«y otros»} \\
% \texttt{ibíd.} & \texttt{ibid.} & ibíd. & \emph{«ibídem»} \\
% \texttt{op. cit.} & \texttt{op.cit.} & op.~cit. & \emph{«obra citada»} \\
% \texttt{loc. cit.} & \texttt{loc.cit.} & loc.~cit. & \emph{«lugar citado»} \\
% \texttt{v. gr.} & \texttt{v.gr.} & v.~gr. & \emph{«verbigracia»} \\
% \texttt{i. e.} & \texttt{i.e.} & i.~e. & \emph{«esto es»} \\
% \texttt{e. g.} & \texttt{e.g.} & e.~g. & \emph{«por ejemplo»} \\
% \texttt{p. ej.} & \texttt{p.ej.} & p.~ej. & \emph{«por ejemplo»} \\
% \midrule
% \multicolumn{4}{c}{\textbf{Títulos, profesiones y cargos}} \\
% \midrule
% \texttt{sr.} & --- & Sr. & \emph{«señor»} \\
% \texttt{sra.} & --- & Sra. & \emph{«señora»} \\
% \texttt{srta.} & --- & Srta. & \emph{«señorita»} \\
% \texttt{dr.} & --- & Dr. & \emph{«doctor»} \\
% \texttt{dra.} & --- & Dra. & \emph{«doctora»} \\
% \texttt{dres.} & --- & Dres. & \emph{«doctores»} \\
% \texttt{dras.} & --- & Dras. & \emph{«doctoras»} \\
% \texttt{prof.} & --- & Prof. & \emph{«profesor»} \\
% \texttt{profa.} & --- & Profa. & \emph{«profesora»} \\
% \texttt{profs.} & --- & Profs. & \emph{«profesores»} \\
% \texttt{ing.} & --- & Ing. & \emph{«ingeniero»} \\
% \texttt{ings.} & --- & Ings. & \emph{«ingenieros»} \\
% \texttt{lic.} & --- & Lic. & \emph{«licenciado»} \\
% \texttt{v. b.} & \texttt{v.b.} & V.~B. & \emph{«visto bueno»} \\
% \midrule
% \multicolumn{4}{c}{\textbf{Abreviaturas compuestas e institucionales}} \\
% \midrule
% \texttt{s. a.} & \texttt{s.a.} & S.~A. & \emph{«sociedad anónima»} \\
% \texttt{ee. uu.} & \texttt{ee.uu.} & EE.~UU. & \emph{«estados unidos»} \\
% \texttt{dd. hh.} & \texttt{dd.hh.} & DD.~HH. & \emph{«derechos humanos»} \\
% \texttt{jj. oo.} & \texttt{jj.oo.} & JJ.~OO. & \emph{«juegos olímpicos»} \\
% \texttt{ff. aa.} & \texttt{ff.aa.} & FF.~AA. & \emph{«fuerzas armadas»} \\
% \texttt{rr. ee.} & \texttt{rr.ee.} & RR.~EE. & \emph{«relaciones exteriores»} \\
% \texttt{cc. aa.} & \texttt{cc.aa.} & CC.~AA. & \emph{«comunidades autónomas»} \\
% \texttt{p. d.} & \texttt{p.d.} & P.~D. & \emph{«posdata»} \\
% \texttt{a. c.} & \texttt{a.c.} & a.~C. & \emph{«antes de Cristo»} \\
% \texttt{d. c.} & \texttt{d.c.} & d.~C. & \emph{«después de Cristo»} \\
% \texttt{d. n. i.} & \texttt{d.n.i.} & D.~N.~I. & \emph{«documento nacional de identidad»} \\
% \texttt{r. u. t.} & \texttt{r.u.t.} & R.~U.~T. & \emph{«rol único tributario»} \\
% \texttt{r. u. n.} & \texttt{r.u.n.} & R.~U.~N. & \emph{«rol único nacional»} \\
% \end{longtable}
%
% La tabla~\ref{tab:spshort-siglas} resume las siglas y acrónimos registrados en el módulo Lua \myluamod{}.
%
% \begin{longtable}{@{\extracolsep{\fill}}l p{3.5cm} c p{5cm}@{}}
% \caption{Siglas y acrónimos soportados} \label{tab:spshort-siglas} \\
% \toprule
% \textbf{Entrada} & \textbf{Alias / Variantes} & \textbf{Salida PDF} & \multicolumn{1}{c}{\textbf{Lectura NVDA}} \\
% \midrule
% \endfirsthead
% \caption[]{\emph{(Continuación: Siglas y acrónimos)}} \\
% \toprule
% \textbf{Entrada} & \textbf{Alias / Variantes} & \textbf{Salida PDF} & \multicolumn{1}{c}{\textbf{Lectura NVDA}} \\
% \midrule
% \endhead
% \midrule
% \multicolumn{4}{r}{\emph{Continúa en la siguiente página...}} \\
% \endfoot
% \bottomrule
% \endlastfoot
% \multicolumn{4}{c}{\textbf{Siglas (Deletreo o lectura expandida)}} \\
% \midrule
% \texttt{dni} & --- & DNI & \emph{«documento nacional de identidad»} \\
% \texttt{rut} & --- & RUT & \emph{«rol único tributario»} \\
% \texttt{run} & --- & RUN & \emph{«rol único nacional»} \\
% \texttt{onu} & --- & ONU & \emph{«organización de las naciones unidas»} \\
% \texttt{rae} & --- & RAE & \emph{«real academia española»} \\
% \texttt{ong} & --- & ONG & \emph{«organización no gubernamental»} \\
% \texttt{ongs} & --- & ONGs & \emph{«organizaciones no gubernamentales»} \\
% \texttt{urss} & --- & URSS & \emph{«unión de repúblicas socialistas soviéticas»} \\
% \texttt{oea} & --- & OEA & \emph{«organización de los estados americanos»} \\
% \texttt{oms} & --- & OMS & \emph{«organización mundial de la salud»} \\
% \texttt{fmi} & --- & FMI & \emph{«fondo monetario internacional»} \\
% \texttt{bid} & --- & BID & \emph{«banco interamericano de desarrollo»} \\
% \texttt{otan} & --- & OTAN & \emph{«organización del tratado del atlántico norte»} \\
% \texttt{ue} & --- & UE & \emph{«unión europea»} \\
% \texttt{ru} & --- & RU & \emph{«reino unido»} \\
% \midrule
% \multicolumn{4}{c}{\textbf{Acrónimos silábicos}} \\
% \midrule
% \texttt{unicef} & --- & Unicef & \emph{«fondo de las naciones unidas para la infancia»} \\
% \texttt{unesco} & --- & Unesco & \emph{«organización de las naciones unidas para la educación, la ciencia y la cultura»} \\
% \texttt{mercosur} & --- & Mercosur & \emph{«mercado común del sur»} \\
% \texttt{cepal} & --- & Cepal & \emph{«comisión económica para américa latina»} \\
% \texttt{mineduc} & --- & Mineduc & \emph{«ministerio de educación»} \\
% \texttt{conicet} & --- & Conicet & \emph{«consejo nacional de investigaciones»} \\
% \end{longtable}
%
% Según la \textsc{rae}, las siglas no cambian en plural: se escribe «las ONG».
% La entrada \texttt{ongs} imprime «ONGs» a propósito, para que
% \mynvmath{NVDA/MathCAT} lea el plural \emph{«organizaciones no
% gubernamentales»}; la entrada \texttt{ong} imprime «ONG» y lee el singular.
%
% La tabla~\ref{tab:spshort-ordinales} resume ejemplos de números ordinales
% registrados en el módulo Lua \myluamod{}.
%
% \begin{longtable}{@{\extracolsep{\fill}}l p{3.5cm} c p{5cm}@{}}
% \caption{Números ordinales soportados} \label{tab:spshort-ordinales} \\
% \toprule
% \textbf{Entrada} & \textbf{Alias / Variantes} & \textbf{Salida PDF} & \multicolumn{1}{c}{\textbf{Lectura NVDA}} \\
% \midrule
% \endfirsthead
% \caption[]{\emph{(Continuación: Números ordinales)}} \\
% \toprule
% \textbf{Entrada} & \textbf{Alias / Variantes} & \textbf{Salida PDF} & \multicolumn{1}{c}{\textbf{Lectura NVDA}} \\
% \midrule
% \endhead
% \midrule
% \multicolumn{4}{r}{\emph{Continúa en la siguiente página...}} \\
% \endfoot
% \bottomrule
% \endlastfoot
% \multicolumn{4}{c}{\textbf{Sufijos para ordinales (Ejemplos)}} \\
% \midrule
% \texttt{1.a} & \texttt{1.ª} & 1.$^{\text{a}}$ & \emph{«primera»} \\
% \texttt{2.o} & \texttt{2.º} & 2.$^{\text{o}}$ & \emph{«segundo»} \\
% \texttt{3.er} & --- & 3.$^{\text{er}}$ & \emph{«tercer»} \\
% \texttt{4.os} & --- & 4.$^{\text{os}}$ & \emph{«cuartos»} \\
% \texttt{5.as} & --- & 5.$^{\text{as}}$ & \emph{«quintas»} \\
% \end{longtable}
%
% \begin{important}*
%   Los números ordinales se verbalizan con palabras desde el |1| hasta
%   el |99|. Desde el |100| el ordinal no se convierte en palabras y se
%   entrega a \mynvmath{NVDA/MathCAT} tal como se imprime, por ejemplo
%   |100.o| como \emph{«100º»}.
% \end{important}
%
% \section{Utilidades generales}\label{sec:utilidades}
%
% Además de los \emph{«cuatro grandes»} y sus asociados, el paquete \mypkg*[no-index]{spintent}
% provee tres utilidades de uso general: |\spsiglo| para la escritura y
% lectura de siglos, |\sptime| para la escritura y lectura de tiempo y
% |\spdate| para la escritura y lectura de fechas.
%
% El comando |\spsiglo| trabaja en \emph{modo texto} y en \emph{modo
% matemático}; |\sptime| y |\spdate| solo en \emph{modo matemático}. Los tres
% reciben un \emph{argumento obligatorio}, y un argumento que no cumple alguna
% de las formas descritas devuelve un \enquote{error} y se detiene la
% compilación del documento.
%
% \subsection*{El comando \cs[no-index]{spsiglo}}\label{subsec:spsiglo}
%
% \begin{function}{\spsiglo}
%   \begin{syntax}
%     |\spsiglo|\mymarg{siglo} \qquad\qquad |\spsiglo|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{siglo}
%   \end{syntax}
%   El comando \ics*{spsiglo} trabaja en \emph{modo texto} y en \emph{modo matemático},
%   recibe el \emph{argumento obligatorio} \mymarg{siglo}, el cual puede
%   ser un \emph{número arábigo} `|21|' o un \emph{número romano} `|XXI|'
%   mayor que `|0|' y menor que `|4000|'.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
% y se ejecuta |\spsiglo{21}| o |\spsiglo{XXI}| se imprimirá `\textsc{xxi}' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«veintiuno»}.
%
% \begin{important}*
%  El \emph{argumento obligatorio} \mymarg{siglo} \emph{«siempre»} es
%  convertido a \emph{número romano en versalitas} y es verbalizado por
%  \mynvmath{NVDA/MathCAT} como \emph{número arábigo}. Esta es la única utilidad
%  que provee el paquete \mypkg*[no-index]{spintent} que trabaja en \emph{modo texto}
%  y en \emph{modo matemático}. Internamente utiliza la llave \mykey{alt}
%  provista por el paquete \mypkg{tagpdf} en \emph{modo texto}
%  y usa |\MathMLintent| para el \emph{modo matemático}.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spsiglo}}
%
% \keyexampcmd{print-era}{ac \textnormal{\textcolor{lightgray}{\textbar}} dc \textnormal{\textcolor{lightgray}{\textbar}} none}{none}[spsiglo]
%
% Establece si se \emph{imprime} o no la abreviatura después de imprimir el argumento \mymarg{siglo}.
% Si se establece en |ac|, |\spsiglo[print-era=ac]{XXI}| imprimirá `\textsc{xxi} a.~C.'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«veintiuno antes de cristo»};
% si se establece en |dc|, |\spsiglo[print-era=dc]{XXI}| imprimirá `\textsc{xxi} d.~C.'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«veintiuno después de cristo»};
% si se establece en |none|, |\spsiglo[print-era=none]{XXI}| imprimirá `\textsc{xxi}'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«veintiuno»}.
%
% \subsection*{El comando \cs[no-index]{sptime}}\label{subsec:sptime}
%
% \begin{function}{\sptime}
%   \begin{syntax}
%     |\sptime|\mymarg{horas \textnormal{\textcolor{red}{:}} minutos} \qquad\qquad |\sptime|\mymarg{horas \textnormal{\textcolor{red}{:}} minutos \textnormal{\textcolor{red}{:}} segundos}
%   \end{syntax}
%   El comando \ics*{sptime} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento obligatorio} \mymarg{horas \textnormal{\textcolor{red}{:}} minutos}
%   o \mymarg{horas \textnormal{\textcolor{red}{:}} minutos \textnormal{\textcolor{red}{:}} segundos}.
% \end{function}
%
% Por ejemplo:~|$\sptime{23:45}$| imprimirá en \emph{«modo texto»} la
% salida \texttt{PDF} `23:45' y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«veintitrés cuarenta y cinco minutos»}.
%
% \subsection*{El comando \cs[no-index]{spdate}}\label{subsec:spdate}
%
% \begin{function}{\spdate}
%   \begin{syntax}
%     |\spdate|\mymarg{\textnormal{DD\textcolor{red}{/}MM\textcolor{red}{/}YYYY}} \hfill |\spdate|\mymarg{\textnormal{YYYY\textcolor{red}{/}MM\textcolor{red}{/}DD}} \hfill |\spdate|\mymarg{\textnormal{YYYY\textcolor{red}{-}MM\textcolor{red}{-}DD}} \hfill |\spdate|\mymarg{\textnormal{DD\textcolor{red}{-}MM\textcolor{red}{-}YYYY}}
%   \end{syntax}
%   El comando \ics*{spdate} trabaja en \emph{modo matemático}
%   y recibe el \emph{argumento} obligatorio
%   \mymarg{\textnormal{DD\textcolor{red}{/}MM\textcolor{red}{/}YYYY}},
%   \mymarg{\textnormal{DD\textcolor{red}{-}MM\textcolor{red}{-}YYYY}} o
%   \mymarg{\textnormal{YYYY\textcolor{red}{-}MM\textcolor{red}{-}DD}}.
% \end{function}
%
% Por ejemplo:~|$\spdate{1981-01-02}$| y |$\spdate{02/01/1981}$|
% imprimirán en \emph{«modo texto»} la salida \texttt{PDF} `1981-01-02'
% y `02/01/1981' respectivamente y en ambos casos, \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«dos de enero de mil novecientos ochenta y uno»}.
%
% \begin{important}*
%   Si se pasa el argumento
%   \mymarg{\textnormal{YYYY\textcolor{red}{/}MM\textcolor{red}{/}DD}},
%   internamente se convertirá en
%   \mymarg{\textnormal{DD\textcolor{red}{/}MM\textcolor{red}{/}YYYY}}
%   y se imprimirá de esa forma por compatibilidad con \mynvmath{NVDA/MathCAT}.
% \end{important}
%
% \section{Utilidades para símbolos}\label{sec:simbolos}
%
% El paquete \mypkg*[no-index]{spintent} provee cinco comandos |\spnewread|,
% |\spread|, |\spusep|, |\spdsym| y |\spmsym| para trabajar con \emph{«símbolos matemáticos»} descritos
% en esta sección. Están pensados desde el punto de vista docente, para
% situaciones donde la lectura de un \emph{«símbolo»}
% es relevante.
%
% El comando |\spnewread| registra la verbalización de un símbolo y trabaja en
% \emph{modo texto}; |\spread| usa un símbolo registrado, |\spusep| escribe
% paréntesis, corchetes y llaves sueltos, y |\spdsym| y |\spmsym| establecen
% cómo se imprime y se verbaliza el símbolo de división y el de multiplicación.
% Estos cuatro trabajan en \emph{modo matemático}.
%
% \begin{important}*
%  A diferencia de las otras utilidades implementadas por
%  \mypkg*[no-index]{spintent}, que analizan y dan formato a la salida, estos no
%  procesan los \emph{«símbolos matemáticos»}, solo afectan la \emph{forma}
%  semántica en que serán verbalizados por \mynvmath{NVDA/MathCAT}.
% \end{important}
%
% \subsection*{El comando \cs[no-index]{spnewread}}\label{subsec:spnewread}
%
% \begin{function}{\spnewread}
%   \begin{syntax}
%     |\spnewread|\mymarg{símbolo}\mymarg{verbalización \textnormal{NVDA}}
%   \end{syntax}
%   El comando \ics*{spnewread} trabaja en \emph{modo texto} y recibe
%   dos \emph{argumentos obligatorios}: \mymarg{símbolo} y \mymarg{verbalización \textnormal{NVDA}},
%   los cuales se almacenan de forma interna para ser utilizados como
%   \mymarg{argumento} en |\spread|. Solo se establece como \mynvmath{NVDA/MathCAT}
%   verbalizará el \mymarg{símbolo} al utilizarlo con |\spread| sin modificar la impresión de este.
% \end{function}
%
% El argumento \mymarg{símbolo} no debe estar registrado. Si ya está registrado
% se devuelve un \enquote{error} y se detiene la compilación del
% documento. La declaración es «global» y se almacena en el módulo \myluamod{}.
% Por ejemplo:~|\spnewread{\cong}{es-congruente-con}| registrará
% `|\cong|' y cuando se utilice |$\spread{\cong}$| se imprimirá
% `$\mathcolor{red}{\cong}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«es congruente con»}.
%
% \begin{important}*
%   El \mymarg{símbolo} debe ser un único comando, incluso uno
%   definido por el usuario. Por ejemplo:~|\newcommand{\xcuad}{x^2}|,
%   |\spnewread{\xcuad}{x-al-cuadrado}| registrará `|\xcuad|' y cuando se
%   utilice |$\spread{\xcuad}$| se imprimirá `$x^2$' en la salida
%   \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
%   \emph{«x al cuadrado»}.
% \end{important}
%
% \begin{important}*
%   Los espacios y los guiones bajos de \mymarg{verbalización
%   \textnormal{NVDA}} se convierten en guiones `|-|', por lo que
%   |{es congruente con}| equivale a |{es-congruente-con}|. Los guiones
%   repetidos se reducen a uno y se eliminan los del inicio y del final.
%   Si el resultado queda vacío se devuelve un \enquote{error} y se
%   detiene la compilación del documento.
% \end{important}
%
% \subsection*{El comando \cs[no-index]{spread}}\label{subsec:spread}
%
% \begin{function}{\spread}
%   \begin{syntax}
%     |\spread|\mymarg{símbolo} \qquad\qquad |\spread|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{símbolo}
%     |\spread|\myoarg{read-arg\textnormal{\textcolor{gray}{=}}\mymarg{verbalización \textnormal{NVDA}}}\mymarg{comandos \textnormal{\hologo{LaTeX}}}
%   \end{syntax}
%   El comando \ics*{spread} trabaja en \emph{modo matemático} y
%   recibe el \emph{argumento obligatorio} de la forma \mymarg{símbolo} previamente
%   registrado por el comando |\spnewread| o \mymarg{comandos \textnormal{\hologo{LaTeX}}}
%   para ser usado junto a la llave \mykey{read-arg}.
% \end{function}
%
% Si el argumento es \mymarg{símbolo}, |$\spread{\cong}$| imprimirá
% `$\mathcolor{red}{\cong}$' en la salida \texttt{PDF} y el argumento \mymarg{verbalización \textnormal{NVDA}}
% establecido al registrar \mymarg{símbolo} con |\spnewread| se verbalizará
% en \mynvmath{NVDA/MathCAT}.
%
% Si el argumento es \mymarg{comandos \textnormal{\hologo{LaTeX}}} y no
% se establece la llave \mykey{read-arg} se devuelve un \enquote{error}
% y se detiene la compilación del documento.
%
% \subsubsection*{Llaves para \cs[no-index]{spread}}
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spread]
%
% Establece la \emph{verbalización} del argumento \mymarg{comandos \textnormal{\hologo{LaTeX}}}.
% El valor de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'
% y sobrescribe la verbalización del \mymarg{símbolo} si está registrado
% previamente. Por ejemplo:~|$\spread[read-arg={es-paralela-a}]{\parallel}$|
% imprimirá `$\mathcolor{red}{\parallel}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«es paralela a»}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spread]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del argumento \mymarg{símbolo} o \mymarg{comandos \textnormal{\hologo{LaTeX}}}
% pasado al comando sin modificar la impresión de este. Si
% se establece en |true|, \mynvmath{NVDA/MathCAT} NO verbalizará la
% salida del comando; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% sí verbalizará la salida del comando.
%
% \begin{important}*
%  Al utilizar \mykey{silent} no es necesario que el argumento
%  \mymarg{símbolo} esté registrado o usar la llave \mykey{read-arg} con
%  \mymarg{comandos \textnormal{\hologo{LaTeX}}}. Dependiendo de lo que
%  contenga el \mymarg{argumento}, puede que \mykey{silent} no sea suficiente,
%  en ese caso utilice |read-arg={_-}|.
% \end{important}
%
% \subsection*{El comando \cs[no-index]{spusep}}\label{subsec:spusep}
%
% \begin{function}{\spusep}
%   \begin{syntax}
%     |\spusep|\mymarg{\normalfont\ttfamily\textbackslash\{\textcolor{lightgray}{\textbar}[\textcolor{lightgray}{\textbar}(\textcolor{lightgray}{\textbar})\textcolor{lightgray}{\textbar}]\textcolor{lightgray}{\textbar}\textbackslash\}} \qquad\qquad  |\spusep|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{\normalfont\ttfamily\textbackslash\{\textcolor{lightgray}{\textbar}[\textcolor{lightgray}{\textbar}(\textcolor{lightgray}{\textbar})\textcolor{lightgray}{\textbar}]\textcolor{lightgray}{\textbar}\textbackslash\}}
%   \end{syntax}
%   El comando \ics*{spusep} trabaja en \emph{modo matemático} y recibe el
%   \emph{argumento obligatorio} \mymarg{\normalfont\ttfamily\textbackslash\{\textcolor{lightgray}{\textbar}[\textcolor{lightgray}{\textbar}(\textcolor{lightgray}{\textbar})\textcolor{lightgray}{\textbar}]\textcolor{lightgray}{\textbar}\textbackslash\}},
%   el cual debe ser \emph{«solo uno»} de los paréntesis soportados: `\texttt{\textcolor{keyname}{(}}',
%   `\texttt{\textcolor{keyname}{)}}', `\texttt{\textcolor{keyname}{[}}', `\texttt{\textcolor{keyname}{]}}',
%   `\texttt{\textcolor{keyname}{\textbackslash\{}}' o `\texttt{\textcolor{keyname}{\textbackslash\}}}'.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val}, \myhlc|$\spusep{(}$|
% imprimirá `$\mathcolor{red}{(}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«abrir paréntesis»} acorde a su propia heurística.
%
% \begin{important}*
%   El motivo de existencia de |\spusep| es que los comandos |\left|,
%   |\right|, |\Uleft| y |\Uright| provistos por \hologo{LuaTeX} crean
%   un \emph{grupo} implícito que dificulta el código para \emph{tagged}
%   \texttt{PDF} cuando se necesita interrumpir algo entre ellos, por
%   ejemplo al construir expresiones complejas con |\spnZ|, |\spmQ| o
%   |\spdiv|. El comando |\spusep| imprime el \emph{delimitador} sin crear
%   ningún grupo.
% \end{important}
%
% \begin{important}*
%   Hay un segundo motivo para usar |\spusep|. Varios comandos aceptan un
%   \emph{argumento opcional} entre corchetes, \myoarg{key \textnormal{\textcolor{gray}{=}} val},
%   o delimitado por paréntesis, \myparg{número\textnormal{\textcolor{gray}{,}} número\textnormal{\textcolor{gray}{,}} \dots},
%   o ambos, escrito justo después del nombre del comando. Si a continuación de
%   un comando necesita un `\texttt{\textcolor{keyname}{[}}' o un `\texttt{\textcolor{keyname}{(}}'
%   explícito que no sea ese argumento, escríbalo con |\spusep|; de lo contrario,
%   \hologo{LaTeX} lo interpretará como el comienzo del argumento opcional. Por
%   ejemplo, |$\spmedia\spusep{[}$| imprime el símbolo de |\spmedia| seguido de
%   `$\mathcolor{red}{[}$', mientras que |\spmedia[| haría que \hologo{LaTeX} lea lo que sigue como
%   llaves \myoarg{key \textnormal{\textcolor{gray}{=}} val}.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spusep}}
%
% \keyexampcmd{size}{none \textnormal{\textcolor{lightgray}{\textbar}}
%   big \textnormal{\textcolor{lightgray}{\textbar}}
%   Big \textnormal{\textcolor{lightgray}{\textbar}}
%   bigg \textnormal{\textcolor{lightgray}{\textbar}}
%   Bigg}{none}[spusep]
%
% Establece el \emph{tamaño del paréntesis} pasado como \mymarg{argumento}
% que se imprimirá en la salida \texttt{PDF} al ejecutar el comando sin modificar
% la verbalización de este. Si se establece en \mykey{none}, se imprime el \mymarg{argumento}
% en el tamaño por defecto del modo matemático en curso; si se establece en \mykey{big},
% se usa |\bigl|/|\bigr| antes del \mymarg{argumento}; si se establece en \mykey{Big},
% se usa |\Bigl|/|\Bigr| antes del \mymarg{argumento}; si se establece en
% \mykey{bigg}, se usa |\biggl|/|\biggr| antes del \mymarg{argumento}; y
% si se establece en \mykey{Bigg}, se usa |\Biggl|/|\Biggr| antes del \mymarg{argumento}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spusep]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} del \mymarg{argumento} pasado al comando sin
% modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se
% establece en |false|, sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spdsym}}\label{subsec:spdsym}
%
% \begin{function}{\spdsym}
%   \begin{syntax}
%    |\spdsym| \qquad\qquad |\spdsym|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   \end{syntax}
%   El comando \ics*{spdsym} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   mediante el cual se establece la \emph{«forma»} en la que se imprimirá el
%   \emph{símbolo de división} y cómo se verbalizará en \mynvmath{NVDA/MathCAT}.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spdsym$| imprimirá `$\mathcolor{red}{:}$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«dividido»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spdsym}}\label{subsubsec:key-spdsym}
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spdsym]
%
% Establece el \emph{prefijo} que será \emph{verbalizado} por
% \mynvmath{NVDA/MathCAT} \emph{«antes»} de \emph{verbalizar} el \mymarg{valor} establecido
% por la llave \mykey{read-sym}. El valor de esta llave \emph{siempre}
% debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{suffix}{sufijo}{no usado}[spdsym]
%
% Establece el \emph{sufijo} que será \emph{verbalizado} por
% \mynvmath{NVDA/MathCAT} \emph{«después»} de \emph{verbalizar} el \mymarg{valor} establecido
% por la llave \mykey{read-sym}. El valor de esta llave \emph{siempre}
% debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{set-sym}{old-style \textnormal{\textcolor{lightgray}{\textbar}} two-dots \textnormal{\textcolor{lightgray}{\textbar}} slash}{two-dots}[spdsym]
%
% Establece el \emph{símbolo de división} que se imprimirá en la salida
% \texttt{PDF}. Si se establece en |old-style|, imprimirá
% `$\mathcolor{red}{\div}$'; si se establece en |two-dots|, imprimirá
% `$\mathcolor{red}{:}$' y si se establece en |slash|, imprimirá
% `$\mathcolor{red}{/}$'.
%
% \keyexampcmd{read-sym}{dividido \textnormal{\textcolor{lightgray}{\textbar}} dividido-por \textnormal{\textcolor{lightgray}{\textbar}} dividido-entre}{dividido}[spdsym]
%
% Establece la \emph{forma} con la cual \mynvmath{NVDA/MathCAT} verbalizará el
% \emph{símbolo de división}. Si se establece en |dividido|, \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«dividido»}; si se establece en |dividido-por|, \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«dividido por»} y si se establece en |dividido-entre|,
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«dividido entre»}.
%
% \subsection*{El comando \cs[no-index]{spmsym}}\label{subsec:spmsym}
%
% \begin{function}{\spmsym}
%   \begin{syntax}
%     |\spmsym| \qquad\qquad |\spmsym|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   \end{syntax}
%   El comando \ics*{spmsym} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   mediante el cual se establece la \emph{«forma»} en la que se imprimirá el
%   \emph{símbolo de multiplicación} y cómo se verbalizará en \mynvmath{NVDA/MathCAT}.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spmsym$| imprimirá `$\mathcolor{red}{\cdot}$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«por»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spmsym}}\label{subsubsec:key-spmsym}
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spmsym]
%
% Establece el \emph{prefijo} que será \emph{verbalizado} por \mynvmath{NVDA/MathCAT}
% \emph{«antes»} de \emph{verbalizar} el \mymarg{valor} establecido por
% la llave \mykey{read-sym}. El valor de esta llave \emph{siempre}
% debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{suffix}{sufijo}{no usado}[spmsym]
%
% Establece el \emph{sufijo} que será \emph{verbalizado} por \mynvmath{NVDA/MathCAT}
% \emph{«después»} de \emph{verbalizar} el \mymarg{valor} establecido por
% la llave \mykey{read-sym}. El valor de esta llave \emph{siempre}
% debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{set-sym}{cross \textnormal{\textcolor{lightgray}{\textbar}} dot \textnormal{\textcolor{lightgray}{\textbar}} asterisk}{dot}[spmsym]
%
% Establece el \emph{símbolo de multiplicación} que se imprimirá en la salida
% \texttt{PDF}. Si se establece en |cross|, imprimirá
% `$\mathcolor{red}{\times}$'; si se establece en |dot|, imprimirá
% `$\mathcolor{red}{\cdot}$' y si se establece en |asterisk|, imprimirá
% `$\mathcolor{red}{\ast}$'.
%
% \keyexampcmd{read-sym}{por \textnormal{\textcolor{lightgray}{\textbar}} multiplicado-por \textnormal{\textcolor{lightgray}{\textbar}} veces}{por}[spmsym]
%
% Establece la \emph{forma} con la cual \mynvmath{NVDA/MathCAT}
% verbalizará el \emph{símbolo de multiplicación}. Si se establece en |por|,
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«por»}; si se establece en
% |multiplicado-por|, \mynvmath{NVDA/MathCAT} verbalizará \emph{«multiplicado por»}
% y si se establece en |veces|, \mynvmath{NVDA/MathCAT} verbalizará \emph{«veces»}.
%
% \keyexampcmd{print-sym}{true \textnormal{\textcolor{lightgray}{\textbar}} false}{true}[spmsym]
%
% Establece si se \emph{imprime} el \emph{símbolo de multiplicación}.
% Si se establece en |false|, \mynvmath{NVDA/MathCAT}
% verbalizará el \emph{símbolo de multiplicación} acorde al valor establecido
% por la llave \mykey{read-sym}, pero no se imprimirá este en
% la salida \texttt{PDF}.
%
% \section{Utilidades para números}\label{sec:numeros}
%
% El paquete \mypkg*[no-index]{spintent} provee diez comandos para trabajar con
% números, descritos en esta sección: |\spdiv| y |\spmul| para la división y
% la multiplicación, |\spnD| y |\spnM| para los divisores y los múltiplos de un
% número, |\spmcd| y |\spmcm| para el máximo común divisor y el mínimo común
% múltiplo, |\spnZ| para los números enteros, |\spmQ| para los números mixtos
% y |\sprazon| y |\spProp| para las razones y las proporciones. Todos trabajan
% en \emph{modo matemático}.
%
% Los comandos |\spnD|, |\spnM|, |\spmcd| y |\spmcm| reciben un \emph{argumento
% opcional delimitado por paréntesis}; los demás reciben \emph{argumentos
% obligatorios}. Los argumentos numéricos de varios de ellos se procesan
% internamente bajo |\spnum|, de modo que siguen sus reglas de escritura
% (\S\ref{subsec:spnum}).
%
% \subsection*{El comando \cs[no-index]{spdiv}}\label{subsec:spdiv}
%
% \begin{function}{\spdiv}
%   \begin{syntax}
%     |\spdiv|\mymarg{dividendo}\mymarg{divisor} \qquad\qquad |\spdiv|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{dividendo}\mymarg{divisor}
%   \end{syntax}
%   El comando \ics*{spdiv} trabaja en \emph{modo matemático} y recibe
%   dos \emph{argumentos obligatorios} \mymarg{dividendo} y \mymarg{divisor}.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spdiv{20}{26}$| imprimirá `$20:26$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«veinte dividido veintiséis»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spdiv}}
%
% El comando |\spdiv| posee las \mymeta{meta-keys} \mykey[spdiv]{cifras-sep},
% \mykey[spdiv]{read-sign}, \mykey[spdiv]{read-period-sep} y
% \mykey[spdiv]{notation-sym} pasadas al comando |\spnum| (\S\ref{subsubsec:key-spnum})
% junto a las \mymeta{meta-keys} \mykey[spdiv]{set-sym} y \mykey[spdiv]{read-sym}
% pasadas al comando |\spdsym| (v.\,pág.\,\pageref{subsubsec:key-spdsym}).
%
% \keyexampcmd{wrap-parens}{true \textnormal{\textcolor{lightgray}{\textbar}} false}{false}[spdiv]
%
% Establece si se \emph{imprimen} paréntesis redondos `|(|\dots|)|'
% alrededor de los \emph{argumentos} \mymarg{dividendo} y \mymarg{divisor}
% pasados a |\spdiv|. Si se establece en |true|, se imprimirán paréntesis
% redondos alrededor de ambos \emph{argumentos}; si se establece en |false|,
% no se imprimirán los paréntesis.
%
% \keyexampcmd{read-parens}{true \textnormal{\textcolor{lightgray}{\textbar}} false}{true}[spdiv]
%
% Establece si \mynvmath{NVDA/MathCAT} \emph{verbalizará} los paréntesis
% redondos `|(|\dots|)|' alrededor de los \emph{argumentos} \mymarg{dividendo}
% y \mymarg{divisor} pasados a |\spdiv|. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} verbalizará la lectura de los paréntesis;
% si se establece en |false|, \mynvmath{NVDA/MathCAT} no los verbalizará.
%
% \subsection*{El comando \cs[no-index]{spmul}}\label{subsec:spmul}
%
% \begin{function}{\spmul}
%   \begin{syntax}
%     |\spmul|\mymarg{factor}\mymarg{factor} \qquad\qquad |\spmul|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{factor}\mymarg{factor}
%   \end{syntax}
%   El comando \ics*{spmul} trabaja en \emph{modo matemático} y recibe
%   dos \emph{argumentos obligatorios} \mymarg{factor} y \mymarg{factor}.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spmul{20}{26}$| imprimirá `$20 \cdot 26$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«veinte por veintiséis»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spmul}}
%
% El comando |\spmul| posee las \mymeta{meta-keys} \mykey[spmul]{cifras-sep},
% \mykey[spmul]{read-sign}, \mykey[spmul]{read-period-sep} y
% \mykey[spmul]{notation-sym} pasadas al comando |\spnum| (\S\ref{subsubsec:key-spnum})
% junto a las \mymeta{meta-keys} \mykey[spmul]{set-sym} y \mykey[spmul]{read-sym}
% pasadas al comando |\spmsym| (v.\,pág.\,\pageref{subsubsec:key-spmsym}).
%
% \keyexampcmd{wrap-parens}{true \textnormal{\textcolor{lightgray}{\textbar}} false}{false}[spmul]
%
% Establece si se \emph{imprimen} paréntesis redondos `|(|\dots|)|'
% alrededor de los \emph{argumentos} \mymarg{factor} y \mymarg{factor}
% pasados a |\spmul|. Si se establece en |true|, se imprimirán paréntesis
% redondos alrededor de ambos \emph{argumentos}; si se establece en |false|,
% no se imprimirán los paréntesis.
%
% \keyexampcmd{read-parens}{true \textnormal{\textcolor{lightgray}{\textbar}} false}{true}[spmul]
%
% Establece si \mynvmath{NVDA/MathCAT} \emph{verbalizará} los paréntesis
% redondos `|(|\dots|)|' alrededor de los \emph{argumentos} \mymarg{factor}
% y \mymarg{factor} pasados a |\spmul|. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} verbalizará la lectura de los paréntesis;
% si se establece en |false|, \mynvmath{NVDA/MathCAT} no los verbalizará.
%
% \subsection*{El comando \cs[no-index]{spnD}}\label{subsec:spnD}
%
% \begin{function}{\spnD}
%   \begin{syntax}
%     |\spnD| \hfill |\spnD|\myparg{número} \hfill |\spnD|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{número} \hfill |\spnD|\myparg{letra} \hfill |\spnD|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{letra}
%   \end{syntax}
%   El comando \ics*{spnD} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento opcional delimitado por paréntesis} de la forma
%   \myparg{número} que se procesa internamente por |\spnum| o \myparg{letra}
%   que se ejecuta de manera directa.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
% ni el \emph{argumento opcional delimitado por paréntesis}, |$\spnD$|
% imprimirá `$\mathrm{D}(n)$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«divisores de ene»}.
%
% Si se usa el argumento \myparg{número}, |$\spnD(6)$| imprimirá
% `$\mathrm{D}(6)$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«divisores de seis»}.
%
% Si se usa el argumento \myparg{letra}, |$\spnD(m)$| imprimirá
% `$\mathrm{D}(m)$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«divisores de eme»}.
%
% \begin{important}*
%   El número |0| no es válido: |$\spnD(0)$| devuelve un
%   \enquote{error} y se detiene la compilación del documento.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spnD}}
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spnD]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el concepto \emph{«divisores de»} al ejecutar el comando sin modificar
% la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«divisores de»} y el \myparg{argumento};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} solo verbalizará el
% \myparg{argumento}.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spnD]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el concepto \emph{«divisores de»}. El valor
% de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{result}{true \textnormal{%
%   \textcolor{lightgray}{\textbar}} false}{false}[spnD]
%
% Establece si se \emph{imprime} el resultado de calcular los \emph{divisores}
% del argumento \myparg{número}. Si se establece en |true|, |$\spnD[result=true](6)$|
% imprimirá `$\mathrm{D}(6) = \{1, 2, 3, 6\}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«divisores de seis son uno,
% dos, tres y seis»}; si se establece en |false|, no se imprimirá ni verbalizará
% el resultado de calcular los \emph{divisores}.
%
% \keyexampcmd{result-num}{número natural}{todos}[spnD]
%
% Establece la \emph{cantidad} de divisores que se mostrarán en la salida
% \texttt{PDF} al establecer \mykey{result}|=true|. Si se establece en |2|, solo
% se imprimirán dos valores y luego se añadirá `\dots' en la salida
% \texttt{PDF}; si no se establece, se imprimirán todos los divisores,
% porque el conjunto es finito.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spnD]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del concepto \emph{«divisores de»} y del \myparg{argumento} pasado al comando
% sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se establece
% en |false|, \mynvmath{NVDA/MathCAT} sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spnM}}\label{subsec:spnM}
%
% \begin{function}{\spnM}
%   \begin{syntax}
%     |\spnM| \hfill |\spnM|\myparg{número} \hfill |\spnM|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{número} \hfill |\spnM|\myparg{letra} \hfill |\spnM|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{letra}
%   \end{syntax}
%   El comando \ics*{spnM} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento opcional delimitado por paréntesis} de la forma
%   \myparg{número} que se procesa internamente por |\spnum| o \myparg{letra}
%   que se ejecuta de manera directa.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
% ni el \emph{argumento opcional delimitado por paréntesis}, |$\spnM$|
% imprimirá `$\mathrm{M}(n)$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«múltiplos de ene»}.
%
% Si se usa el argumento \myparg{número}, |$\spnM(6)$| imprimirá
% `$\mathrm{M}(6)$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«múltiplos de seis»}.
%
% Si se usa el argumento \myparg{letra}, |$\spnM(m)$| imprimirá
% `$\mathrm{M}(m)$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«múltiplos de eme»}.
%
% \begin{important}*
%   El número |0| no es válido: |$\spnM(0)$| devuelve un
%   \enquote{error} y se detiene la compilación del documento.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spnM}}
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spnM]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el concepto \emph{«múltiplos de»} al ejecutar el comando sin modificar
% la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«múltiplos de»} y el \myparg{argumento};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} solo verbalizará el
% \myparg{argumento}.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spnM]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el concepto \emph{«múltiplos de»}. El valor
% de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{result}{true \textnormal{%
%   \textcolor{lightgray}{\textbar}} false}{false}[spnM]
%
% Establece si se \emph{imprime} el resultado de calcular los \emph{múltiplos}
% del argumento \myparg{número}. Si se establece en |true|, |$\spnM[result=true](6)$|
% imprimirá `$\mathrm{M}(6) = \{6, 12, 18, 24, 30, \dots\}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«múltiplos de seis son seis,
% doce, dieciocho, veinticuatro, treinta puntos suspensivos»}; si se
% establece en |false|, no se imprimirá ni verbalizará el resultado de
% calcular los \emph{múltiplos}.
%
% \keyexampcmd{result-num}{número natural}{5}[spnM]
%
% Establece la \emph{cantidad} de números que se mostrarán en la salida
% \texttt{PDF} al establecer \mykey{result}|=true|. Si se establece en |2|, solo
% se imprimirán dos valores en la salida \texttt{PDF}; si no se establece,
% se imprimirán como máximo |5| valores y luego se añadirá `\dots' en la
% salida \texttt{PDF}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spnM]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del concepto \emph{«múltiplos de»} y del \myparg{argumento} pasado al comando
% sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se establece
% en |false|, \mynvmath{NVDA/MathCAT} sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spmcd}}\label{subsec:spmcd}
%
% \begin{function}{\spmcd}
%   \begin{syntax}
%     |\spmcd| \hfill |\spmcd|\myparg{número\textnormal{\textcolor{gray}{,}} número\textnormal{\textcolor{gray}{,}} \dots} \hfill |\spmcd|\myparg{letra\textnormal{\textcolor{gray}{,}} letra\textnormal{\textcolor{gray}{,}} \dots} \hfill |\spmcd|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{número\textnormal{\textcolor{gray}{,}} número\textnormal{\textcolor{gray}{,}} \dots}
%   \end{syntax}
%   El comando \ics*{spmcd} trabaja en \emph{modo matemático} y recibe el
%   \emph{argumento opcional delimitado por paréntesis} de la forma
%   \myparg{número\textnormal{\textcolor{gray}{,}} número\textnormal{\textcolor{gray}{,}} \dots}
%   que se procesa internamente por |\spnum| o \myparg{letra\textnormal{\textcolor{gray}{,}} letra\textnormal{\textcolor{gray}{,}} \dots}
%   que se ejecuta de manera directa.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
% ni el \emph{argumento opcional delimitado por paréntesis}, |$\spmcd$|
% imprimirá `$\mathrm{mcd}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«máximo común divisor»}.
%
% Si se usa el argumento \myparg{número\textnormal{\textcolor{gray}{,}} número\textnormal{\textcolor{gray}{,}} \dots},
% |$\spmcd(2, 3, 6)$| imprimirá `$\mathrm{mcd}(2, 3, 6)$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«máximo común
% divisor entre dos, tres y seis»}.
%
% Si se usa el argumento \myparg{letra\textnormal{\textcolor{gray}{,}} letra\textnormal{\textcolor{gray}{,}} \dots},
% |$\spmcd(m, n)$| imprimirá `$\mathrm{mcd}(m, n)$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«máximo común
% divisor entre eme y ene»}.
%
% \begin{important}*
%  Los argumentos \myparg{número\textnormal{\textcolor{gray}{,}} número\textnormal{\textcolor{gray}{,}} \dots}
%  o \myparg{letra\textnormal{\textcolor{gray}{,}} letra\textnormal{\textcolor{gray}{,}} \dots}
%  requieren al menos dos elementos.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spmcd}}
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spmcd]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el concepto establecido por la llave \mykey{read-sty} al ejecutar el comando
% sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«máximo común divisor \dots»}
% o \emph{«m c d \dots»} y el \myparg{argumento}; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% solo verbalizará el \myparg{argumento}.
%
% \keyexampcmd{read-sty}{terse \textnormal{%
%   \textcolor{lightgray}{\textbar}} clear}{clear}[spmcd]
%
% Establece el \emph{concepto} que verbalizará \mynvmath{NVDA/MathCAT}
% al ejecutar el comando sin modificar la impresión de este. Si se
% establece en |terse|, \mynvmath{NVDA/MathCAT} verbalizará \emph{«m c d»};
% si se establece en |clear|, \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«máximo común divisor»}.
%
% \keyexampcmd{upper-case}{true \textnormal{%
%   \textcolor{lightgray}{\textbar}} false}{false}[spmcd]
%
% Establece si se usarán \emph{mayúsculas} en la salida \texttt{PDF} al
% ejecutar el comando sin modificar la verbalización de este. Si se establece en |true|, se
% imprimirá `$\mathrm{MCD}$' en la salida \texttt{PDF}; si se establece en
% |false|, se imprimirá `$\mathrm{mcd}$' en la salida \texttt{PDF}.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spmcd]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el concepto establecido por la llave \mykey{read-sty}.
% El valor de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{result}{true \textnormal{%
%   \textcolor{lightgray}{\textbar}} false}{false}[spmcd]
%
% Establece si se \emph{imprime} el resultado de calcular el \emph{«máximo
% común divisor»} del argumento \myparg{número\textnormal{\textcolor{gray}{,}} número\textnormal{\textcolor{gray}{,}} \dots}.
% Si se establece en |true|: |$\spmcd[result=true](2, 3, 6)$| imprimirá
% `$\mathrm{mcd}(2, 3, 6) = 1$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«máximo común divisor entre
% dos, tres y seis es igual a uno»}; si se establece en |false| no se
% imprimirá ni verbalizará el resultado de calcular el
% \emph{«máximo común divisor»}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spmcd]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del concepto \emph{«máximo común divisor \dots»} y del \myparg{argumento}
% pasado al comando sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se establece
% en |false|, \mynvmath{NVDA/MathCAT} sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spmcm}}\label{subsec:spmcm}
%
% \begin{function}{\spmcm}
%   \begin{syntax}
%     |\spmcm| \hfill |\spmcm|\myparg{número\textnormal{\textcolor{gray}{,}} número\textnormal{\textcolor{gray}{,}} \dots} \hfill |\spmcm|\myparg{letra\textnormal{\textcolor{gray}{,}} letra\textnormal{\textcolor{gray}{,}} \dots} \hfill |\spmcm|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{número\textnormal{\textcolor{gray}{,}} número\textnormal{\textcolor{gray}{,}} \dots}
%   \end{syntax}
%   El comando \ics*{spmcm} trabaja en \emph{modo matemático} y recibe el
%   \emph{argumento opcional delimitado por paréntesis} de la forma
%   \myparg{número\textnormal{\textcolor{gray}{,}} número\textnormal{\textcolor{gray}{,}} \dots}
%   que se procesa internamente por |\spnum| o \myparg{letra\textnormal{\textcolor{gray}{,}} letra\textnormal{\textcolor{gray}{,}} \dots}
%   que se ejecuta de manera directa.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
% ni el \emph{argumento opcional delimitado por paréntesis}, |$\spmcm$|
% imprimirá `$\mathrm{mcm}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«mínimo común múltiplo»}.
%
% Si se usa el argumento \myparg{número\textnormal{\textcolor{gray}{,}} número\textnormal{\textcolor{gray}{,}} \dots},
% |$\spmcm(2, 3, 6)$| imprimirá `$\mathrm{mcm}(2, 3, 6)$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«mínimo común
% múltiplo entre dos, tres y seis»}.
%
% Si se usa el argumento \myparg{letra\textnormal{\textcolor{gray}{,}} letra\textnormal{\textcolor{gray}{,}} \dots},
% |$\spmcm(m, n)$| imprimirá `$\mathrm{mcm}(m, n)$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«mínimo común
% múltiplo entre eme y ene»}.
%
% \begin{important}*
%  Los argumentos \myparg{número\textnormal{\textcolor{gray}{,}} número\textnormal{\textcolor{gray}{,}} \dots}
%  o \myparg{letra\textnormal{\textcolor{gray}{,}} letra\textnormal{\textcolor{gray}{,}} \dots}
%  requieren al menos dos elementos.
% \end{important}
% \subsubsection*{Llaves para \cs[no-index]{spmcm}}
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spmcm]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el concepto establecido por la llave \mykey{read-sty} al ejecutar el comando
% sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«mínimo común múltiplo \dots»}
% o \emph{«m c m \dots»} y el \myparg{argumento}; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% solo verbalizará el \myparg{argumento}.
%
% \keyexampcmd{read-sty}{terse \textnormal{%
%   \textcolor{lightgray}{\textbar}} clear}{clear}[spmcm]
%
% Establece el \emph{concepto} que verbalizará \mynvmath{NVDA/MathCAT}
% al ejecutar el comando sin modificar la impresión de este. Si se
% establece en |terse|, \mynvmath{NVDA/MathCAT} verbalizará \emph{«m c m»};
% si se establece en |clear|, \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«mínimo común múltiplo»}.
%
% \keyexampcmd{upper-case}{true \textnormal{%
%   \textcolor{lightgray}{\textbar}} false}{false}[spmcm]
%
% Establece si se usarán \emph{mayúsculas} en la salida \texttt{PDF} al
% ejecutar el comando sin modificar la verbalización de este. Si se establece en |true|, se
% imprimirá `$\mathrm{MCM}$' en la salida \texttt{PDF}; si se establece en
% |false|, se imprimirá `$\mathrm{mcm}$' en la salida \texttt{PDF}.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spmcm]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el concepto establecido por la llave \mykey{read-sty}.
% El valor de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{result}{true \textnormal{%
%   \textcolor{lightgray}{\textbar}} false}{false}[spmcm]
%
% Establece si se \emph{imprime} el resultado de calcular el \emph{«mínimo
% común múltiplo»} del argumento \myparg{número\textnormal{\textcolor{gray}{,}} número\textnormal{\textcolor{gray}{,}} \dots}.
% Si se establece en |true|: |$\spmcm[result=true](2, 3, 6)$| imprimirá
% `$\mathrm{mcm}(2, 3, 6) = 6$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«mínimo común múltiplo entre
% dos, tres y seis es igual a seis»}; si se establece en |false| no se
% imprimirá ni verbalizará el resultado de calcular el
% \emph{«mínimo común múltiplo»}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spmcm]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del concepto \emph{«mínimo común múltiplo \dots»} y del \myparg{argumento}
% pasado al comando sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se establece
% en |false|, \mynvmath{NVDA/MathCAT} sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spnZ}}\label{subsec:spnZ}
%
% \begin{function}{\spnZ}
%   \begin{syntax}
%     |\spnZ|\mymarg{número entero} \qquad\qquad |\spnZ|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{número entero}
%   \end{syntax}
%   El comando \ics*{spnZ} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento} obligatorio \mymarg{número entero}, el cual se procesa
%   internamente bajo el comando |\spnum|.
%   Este comando está dedicado exclusivamente al manejo de \emph{«números enteros»},
%   y añade algunas utilidades prácticas para la demarcación e
%   interacción con \mynvmath{NVDA/MathCAT}.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spnZ{23}$| imprimirá `$23$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«veintitrés»}; |$\spnZ{-23}$|
% imprimirá `$-23$' y verbalizará \emph{«menos veintitrés»}. El argumento
% debe ser un entero: con un decimal o una unidad, el comando devuelve un
% \enquote{error} y se detiene la compilación del documento.
%
% \subsubsection*{Llaves para \cs[no-index]{spnZ}}
%
% El comando |\spnZ| posee las \mymeta{meta-keys} \mykey[spnZ]{cifras-sep}
% y \mykey[spnZ]{read-sign} pasadas al comando |\spnum| (\S\ref{subsubsec:key-spnum}).
%
% \keyexampcmd{wrap-parens}{true \textnormal{%
%   \textcolor{lightgray}{\textbar}} false}{false}[spnZ]
%
% Establece si se \emph{imprimen} paréntesis redondos `|(|\dots|)|'
% alrededor del argumento \mymarg{número entero} pasado a |\spnZ|. Si se
% establece en |true|, se imprimirán paréntesis redondos alrededor del
% \mymarg{número entero}; si se establece en |false|, no se imprimirán
% los paréntesis. Por ejemplo:~|$\spnZ[wrap-parens=true]{-23}$| imprimirá
% `$(-23)$' en la salida \texttt{PDF}.
%
% \keyexampcmd{read-parens}{true \textnormal{%
%   \textcolor{lightgray}{\textbar}} false}{true}[spnZ]
%
% Establece si \mynvmath{NVDA/MathCAT} \emph{verbalizará} los paréntesis redondos
% `|(|\dots|)|' alrededor del \emph{argumento} \mymarg{número entero} pasado a
% |\spnZ|. Si se establece en |true|, \mynvmath{NVDA/MathCAT} verbalizará la lectura
% de los paréntesis `|(|\dots|)|'; si se establece en |false|, \mynvmath{NVDA/MathCAT}\newline
% no los verbalizará:~|$\spnZ[wrap-parens=true,read-parens=false]{-23}$|
% verbalizará \emph{«menos veintitrés»}.
%
% \subsection*{El comando \cs[no-index]{spmQ}}\label{subsec:spmQ}
%
% \begin{function}{\spmQ}
%   \begin{syntax}
%     |\spmQ|\mymarg{entero}\mymarg{numerador}\mymarg{denominador} \hfill |\spmQ|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{entero}\mymarg{numerador}\mymarg{denominador}
%   \end{syntax}
%   El comando \ics*{spmQ} trabaja en \emph{modo matemático} y recibe
%   \emph{tres argumentos obligatorios}: \mymarg{número entero},
%   \mymarg{numerador} y \mymarg{denominador}, los procesa internamente
%   y devuelve un \emph{«número mixto»}. Los tres deben ser \emph{enteros};
%   el \mymarg{numerador} y el \mymarg{denominador} no pueden llevar signo
%   y el \mymarg{denominador} no puede ser cero. Cualquier otro valor
%   devuelve un \enquote{error} y se detiene la compilación del documento.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spmQ{2}{5}{7}$| imprimirá `$\scalerel{2}{\frac{5}{7}}$'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«dos y cinco séptimos»}.
%
% \begin{important}*
%   En el caso de las fracciones, y en especial con los \emph{«números
%   mixtos»}, no se pueden manejar muchas cosas desde el lado de
%   \mypkg*[no-index]{spintent}. Por ejemplo, la entrada |$3\frac{5}{7}$|
%   con \emph{tagged} \texttt{PDF} se verbaliza por \mynvmath{NVDA/MathCAT}
%   como \emph{«tres más cinco séptimos»}, lo cual no está mal, pero
%   pedagógicamente hablando no es del todo correcto.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spmQ}}
%
% \keyexampcmd{join-word}{entero \textnormal{\textcolor{lightgray}{\textbar}}
%   enteros \textnormal{\textcolor{lightgray}{\textbar}} y}{y}[spmQ]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbaliza la
% conexión entre el entero y la fracción que forman el \emph{número mixto}.
% Si se establece en |entero|, |$\spmQ[join-word=entero]{1}{5}{7}$|
% imprimirá `$\scalerel{1}{\frac{5}{7}}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«un entero cinco séptimos»};
% si se establece en |enteros|, |$\spmQ[join-word=enteros]{2}{5}{7}$|
% imprimirá `$\scalerel{2}{\frac{5}{7}}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«dos enteros cinco séptimos»};
% si se establece en |y|, |$\spmQ[join-word=y]{2}{5}{7}$|
% imprimirá `$\scalerel{2}{\frac{5}{7}}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«dos y cinco séptimos»}.
%
% \keyexampcmd{use-spnZ}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spmQ]
%
% Establece si el \mymarg{numerador} y el \mymarg{denominador} que forman
% la fracción del \emph{número mixto} se procesan internamente bajo el
% comando |\spnZ|, añadiendo así \emph{semántica de entero} en la
% estructura \mynvmath{MathML} de la salida \texttt{PDF}. Si se establece
% en |true|, \mymarg{numerador} y \mymarg{denominador} se pasarán a |\spnZ|
% y, por ejemplo, |$\spmQ{2}{5}{7}$| se verbalizará
% \emph{«dos y cinco séptimos»}; si se establece en |false|, los
% argumentos se tipografían directamente sin procesamiento semántico
% adicional y \mynvmath{NVDA/MathCAT} aplica sus propias reglas a la fracción:
% |$\spmQ[use-spnZ=false]{2}{5}{7}$| también se verbalizará
% \emph{«dos y cinco séptimos»}, de modo que la diferencia está solo en la
% estructura \mynvmath{MathML} de la salida \texttt{PDF}.
%
% \keyexampcmd{use-dfrac}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spmQ]
%
% Establece si la fracción del \emph{número mixto} se imprime usando
% |\dfrac| en lugar de |\frac|. Si se establece en |true|,
% |$\spmQ[use-dfrac=true]{2}{5}{7}$| imprimirá la fracción en tamaño
% \emph{display} `$\scalerel{2}{\dfrac{5}{7}}$' en la salida \texttt{PDF};
% si se establece en |false|, la fracción se imprime con |\frac| siguiendo
% el tamaño matemático del contexto en curso.
%
% \keyexampcmd{wrap-parens}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spmQ]
%
% Establece si se \emph{imprimen} paréntesis redondos `|(|\dots|)|'
% alrededor del \emph{número mixto} completo devuelto por |\spmQ|. Si se
% establece en |true|, se imprimirán paréntesis redondos cuyo tamaño se
% ajusta automáticamente al estilo matemático en curso; si se establece en
% |false|, no se imprimirán los paréntesis.
%
% \keyexampcmd{read-parens}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spmQ]
%
% Establece si \mynvmath{NVDA/MathCAT} \emph{verbalizará} los paréntesis
% redondos `|(|\dots|)|' alrededor del \emph{número mixto} cuando la llave
% \mykey{wrap-parens} está activa. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} verbalizará la lectura de los paréntesis; si se
% establece en |false|, \mynvmath{NVDA/MathCAT} no los verbalizará.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spmQ]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} del \emph{número mixto} sin modificar la
% impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT} NO
% verbalizará la salida del comando; si se establece en |false|, sí
% verbalizará la salida del comando. Un argumento inválido devuelve siempre un
% \enquote{error} y se detiene la compilación del documento,
% también con \mykey{silent}|=true|.
%
% \subsection*{El comando \cs[no-index]{sprazon}}\label{subsec:sprazon}
%
% \begin{function}{\sprazon}
%   \begin{syntax}
%     |\sprazon|\mymarg{antecedente}\mymarg{consecuente} \quad\quad |\sprazon|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{antecedente}\mymarg{consecuente}
%   \end{syntax}
%   El comando \ics*{sprazon} trabaja en \emph{modo matemático} y recibe
%   \emph{dos argumentos obligatorios}: \mymarg{antecedente} y
%   \mymarg{consecuente}, los tipografía como una fracción y devuelve una
%   \emph{«razón»}.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\sprazon{3}{4}$| imprimirá `$\frac{3}{4}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«tres es a cuatro»}.
%
% \begin{important}*
%   El \mymarg{antecedente} y el \mymarg{consecuente} se tipografían
%   directamente, sin procesamiento semántico adicional.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{sprazon}}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[sprazon]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} de la \emph{razón} pasada al comando sin
% modificar la impresión de esta. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se
% establece en |false|, sí verbalizará la salida del comando.
%
% \keyexampcmd{use-dfrac}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[sprazon]
%
% Establece si la fracción de la \emph{razón} se imprime usando |\dfrac| en
% lugar de |\frac|. Si se establece en |true|,
% |$\sprazon[use-dfrac=true]{3}{4}$| imprimirá la fracción en tamaño
% \emph{display} `$\dfrac{3}{4}$' en la salida \texttt{PDF}; si se establece
% en |false|, la fracción se imprime con |\frac| siguiendo el tamaño
% matemático del contexto en curso.
%
% \keyexampcmd{inline}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[sprazon]
%
% Establece si la \emph{razón} se imprime en línea con dos puntos `|:|' en
% lugar de una fracción. Si se establece en |true|,
% |$\sprazon[inline=true]{3}{4}$| imprimirá `$3:4$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«tres es a cuatro»}; si se establece en |false|, la razón se
% imprime como una fracción.
%
% \begin{important}*
%   La llave \mykey{use-dfrac} no tiene efecto cuando la llave
%   \mykey{inline} se establece en |true|, porque en ese caso no se
%   imprime una fracción.
% \end{important}
%
% \subsection*{El comando \cs[no-index]{spProp}}\label{subsec:spProp}
%
% \begin{function}{\spProp}
%   \begin{syntax}
%     |\spProp|\mymarg{a}\mymarg{b}\mymarg{c}\mymarg{d} \quad\quad |\spProp|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{a}\mymarg{b}\mymarg{c}\mymarg{d}
%   \end{syntax}
%   El comando \ics*{spProp} trabaja en \emph{modo matemático} y recibe
%   \emph{cuatro argumentos obligatorios}: \mymarg{a} y \mymarg{b} forman
%   la primera \emph{razón}, \mymarg{c} y \mymarg{d} forman la segunda, y
%   devuelve una \emph{«proporción»} con las dos razones unidas por el signo
%   igual.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spProp{3}{4}{6}{8}$| imprimirá
% `$\frac{3}{4}=\frac{6}{8}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«tres es a cuatro como seis es a ocho»}.
%
% \begin{important}*
%   Los cuatro argumentos se tipografían directamente, sin procesamiento
%   semántico adicional.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spProp}}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spProp]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} de la \emph{proporción} pasada al comando sin
% modificar la impresión de esta. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se
% establece en |false|, sí verbalizará la salida del comando.
%
% \keyexampcmd{use-dfrac}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spProp]
%
% Establece si las fracciones de la \emph{proporción} se imprimen usando
% |\dfrac| en lugar de |\frac|. Si se establece en |true|,
% |$\spProp[use-dfrac=true]{3}{4}{6}{8}$| imprimirá las fracciones en tamaño
% \emph{display} `$\dfrac{3}{4}=\dfrac{6}{8}$' en la salida \texttt{PDF}; si se
% establece en |false|, las fracciones se imprimen con |\frac| siguiendo el
% tamaño matemático del contexto en curso.
%
% \keyexampcmd{inline}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spProp]
%
% Establece si las \emph{razones} de la \emph{proporción} se imprimen en línea
% con dos puntos `|:|' en lugar de fracciones. Si se establece en |true|,
% |$\spProp[inline=true]{3}{4}{6}{8}$| imprimirá `$3:4=6:8$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«tres es a cuatro como seis es a ocho»}; si se establece en |false|,
% las razones se imprimen como fracciones.
%
% \begin{important}*
%   La llave \mykey{use-dfrac} no tiene efecto cuando la llave
%   \mykey{inline} se establece en |true|, porque en ese caso no se
%   imprimen fracciones.
% \end{important}
%
% \section{Utilidades para geometría}\label{sec:geometria}
%
% El paquete \mypkg*[no-index]{spintent} provee comandos para trabajar
% con \emph{geometría} descritos en esta sección. Todos los comandos
% trabajan en \emph{modo matemático} y reciben como \mymarg{argumento}
% base una \emph{única} letra, mayúscula en el caso de puntos, con un
% modificador del tipo subíndice/prima, un número o comando.
%
% Algunos comandos reciben \mymarg{argumento} único, como una \enquote{\emph{letra}},
% o más de uno dentro del mismo, donde opcionalmente se pueden separar
% por comas `|,|'.
%
% \begin{important}*
%   Las primas se deben pasar de forma directa `|'|' y los subíndices siempre
%   deben escribirse usando llaves `|A_{...}|' y nunca sin estas `|A_n|'
%   que funciona solo por coincidencia.
% \end{important}
%
% \subsection*{El comando \cs[no-index]{spcoord}}\label{subsec:spcoord}
%
% \begin{function}{\spcoord}
%   \begin{syntax}
%     \mycolrow{17.9em}|\spcoord|\mymarg{abscisa\textnormal{\textcolor{red}{,}} ordenada}\mycolsep |\spcoord|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{abscisa\textnormal{\textcolor{red}{,}} ordenada}
%     \mycolrow{17.9em}|\spcoord|\mymarg{abscisa\textnormal{\textcolor{red}{;}} ordenada}\mycolsep |\spcoord|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{abscisa\textnormal{\textcolor{red}{;}} ordenada}
%   \end{syntax}
%   El comando \ics*{spcoord} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento obligatorio} \mymarg{abscisa\textnormal{\textcolor{red}{,}} ordenada}
%   o \mymarg{abscisa\textnormal{\textcolor{red}{;}} ordenada}, el cual
%   se procesa internamente bajo el comando |\spnum| cuando es numérico
%   no fraccionario.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spcoord{-3, 4}$| imprimirá `$(-3, 4)$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«menos tres coma cuatro»};
% |$\spcoord{x_{1},y_{1}}$| imprimirá `$(x_{1}, y_{1})$' y verbalizará
% \emph{«x subíndice uno coma y subíndice uno»}.
% La lectura del subíndice la decide \mynvmath{NVDA/MathCAT}, porque
% |\spcoord| no tiene la llave \mykey{read-cmd}.
%
% \begin{important}*
% El comando |\spcoord| solo acepta \emph{un separador} `\textcolor{red}{,}'
% o `\textcolor{red}{;}' entre la abscisa y la ordenada,\newline fuera de llaves:~%
% |$\spcoord{a_{1,2}, b}$| es válido porque la coma de las llaves no
% separa. Con `\textcolor{red}{;}' las comas son decimales, por ejemplo
% |$\spcoord{1,5; 2,5}$|.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spcoord}}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spcoord]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del \mymarg{argumento} pasado al comando sin modificar la impresión de este.
% Si se establece en |true|, \mynvmath{NVDA/MathCAT} NO verbalizará la salida
% del comando; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spPoint}}\label{subsec:spPoint}
%
% \begin{function}{\spPoint}
%   \begin{syntax}
%     \mycolrow{22.9em}|\spPoint|\mymarg{punto}\mycolsep |\spPoint|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto}
%     \mycolrow{22.9em}|\spPoint|\mymarg{punto}\myparg{abscisa\textnormal{\textcolor{red}{,}} ordenada}\mycolsep |\spPoint|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto}\myparg{abscisa\textnormal{\textcolor{red}{,}} ordenada}
%     \mycolrow{22.9em}|\spPoint|\mymarg{punto}\myparg{abscisa\textnormal{\textcolor{red}{;}} ordenada}\mycolsep |\spPoint|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto}\myparg{abscisa\textnormal{\textcolor{red}{;}} ordenada}
%   \end{syntax}
%   El comando \ics*{spPoint} trabaja en \emph{modo matemático} y
%   recibe el \emph{argumento obligatorio} \mymarg{punto}, el cual es una
%   \emph{única letra mayúscula} con prima `|'|' o subíndice
%   `|_{...}|' y el \emph{argumento opcional delimitado por paréntesis}
%   \myparg{abscisa\textnormal{\textcolor{red}{,}} ordenada}, el cual se
%   procesa internamente bajo el comando |\spcoord|.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spPoint{A}$| imprimirá `$A$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«punto A»};
% |$\spPoint{A_{1}}$| imprimirá `$A_1$' y verbalizará
% \emph{«punto A sub uno»}.
%
% Si se proporciona el \emph{argumento opcional delimitado por paréntesis},
% |$\spPoint{A}(-3,4)$| imprimirá `$A(-3,4)$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«punto A menos tres coma cuatro»};
% |$\spPoint{A_{1}}(-3,4)$| imprimirá `$A_{1}(-3,4)$' y verbalizará
% \emph{«punto A sub uno menos tres coma cuatro»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spPoint}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spPoint]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará
% el argumento \mymarg{punto}. Si se establece en |school|,
% \mynvmath{NVDA/MathCAT} verbalizará el argumento \mymarg{punto} sin el prefijo
% «mayúscula»: |$\spPoint{A}$| se verbaliza \emph{«punto A»}; si se establece
% en |mathcat|, \mynvmath{NVDA/MathCAT} aplica sus propias reglas, que pueden
% incluir el prefijo «mayúscula» dependiendo de la configuración del
% usuario.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spPoint]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el concepto \emph{«punto»}. El valor de esta llave
% \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spPoint]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el concepto \emph{«punto»} al ejecutar el comando sin modificar la impresión
% de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT} verbalizará
% el concepto y el \mymarg{argumento}; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% solo verbalizará el \mymarg{argumento} acorde al valor establecido por
% la llave \mykey{read-cmd}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spPoint]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del concepto \emph{«punto»} y del \mymarg{argumento} pasado al comando
% sin modificar la impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% NO verbalizará la salida del comando; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spvec}}\label{subsec:spvec}
%
% \begin{function}{\spvec}
%   \begin{syntax}
%     \mycolrow{27.3em}|\spvec|\mymarg{letra}\mycolsep |\spvec|\mymarg{punto\textnormal{\textcolor{gray}{,}} punto}
%     \mycolrow{27.3em}|\spvec|\mymarg{letra}\myparg{abscisa\textnormal{\textcolor{red}{,}} ordenada}\mycolsep |\spvec|\mymarg{punto\textnormal{\textcolor{gray}{,}} punto}\myparg{abscisa\textnormal{\textcolor{red}{,}} ordenada}
%     \mycolrow{27.3em}|\spvec|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{letra}\myparg{abscisa\textnormal{\textcolor{red}{,}} ordenada}\mycolsep |\spvec|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto\textnormal{\textcolor{gray}{,}} punto}\myparg{abscisa\textnormal{\textcolor{red}{,}} ordenada}
%   \end{syntax}
%   El comando \ics*{spvec} trabaja en \emph{modo matemático} y recibe un
%   \emph{argumento obligatorio} de la forma \mymarg{letra} minúscula
%   o \mymarg{punto\textnormal{\textcolor{gray}{,}} punto} junto al
%   \emph{argumento opcional delimitado por paréntesis} \myparg{abscisa\textnormal{\textcolor{red}{,}} ordenada}, el cual se
%   procesa internamente bajo el comando |\spcoord|.
% \end{function}
%
% Si el \emph{argumento} es de la forma \mymarg{punto\textnormal{\textcolor{gray}{,}} punto}
% con prima(s) `|'|' o subíndice(s) `|_{...}|' opcionales se aplica
% |\overrightarrow| en la impresión; si el \emph{argumento} es de
% la forma \mymarg{letra} minúscula con subíndice `|_{...}|' opcional
% se aplica |\vec| en la impresión.
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spvec{a}$| imprimirá `$\vec{a}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«vector a»};
% |$\spvec{AB}$| imprimirá `$\overrightarrow{AB}$' y verbalizará
% \emph{«vector A B»}.
%
% Si se proporciona el \emph{argumento opcional delimitado por paréntesis},
% |$\spvec{a}(-3,4)$| imprimirá `$\vec{a}=(-3,4)$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«vector a es igual a menos tres coma cuatro»};
% |$\spvec{AB}(-3,4)$| imprimirá `$\overrightarrow{AB}=(-3,4)$' y verbalizará
% \emph{«vector A B es igual a menos tres coma cuatro»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spvec}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spvec]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará el
% argumento \mymarg{punto\textnormal{\textcolor{gray}{,}} punto}. Si se
% establece en |school|, \mynvmath{NVDA/MathCAT} verbaliza las letras del
% argumento sin el prefijo «mayúscula»: |$\spvec{AB}$| se verbaliza
% \emph{«vector A B»}; si se establece en |mathcat|, \mynvmath{NVDA/MathCAT}
% aplica sus propias reglas, que pueden incluir el prefijo «mayúscula»
% dependiendo de la configuración del usuario.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spvec]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el concepto \emph{«vector»}. El valor de esta
% llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{autofit}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spvec]
%
% Establece si se \emph{utiliza} |\vphantom{b}| al ejecutar el comando
% cuando se utiliza el argumento \mymarg{letra}. Si se establece en |true|,
% se aplicará |\vphantom{b}| al inicio del argumento \mymarg{letra}; si
% se establece en |false|, no se aplicará |\vphantom{b}|.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spvec]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el concepto \emph{«vector»} al ejecutar el comando sin modificar la impresión
% de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT} verbalizará
% el concepto y el \mymarg{argumento}; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% solo verbalizará el \mymarg{argumento} acorde al valor establecido por
% la llave \mykey{read-cmd}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spvec]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del concepto \emph{«vector»} y del \mymarg{argumento} pasado al comando
% sin modificar la impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% NO verbalizará la salida del comando; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{sprecta}}\label{subsec:sprecta}
%
% \begin{function}{\sprecta}
%   \begin{syntax}
%     \mycolrow{14.9em}|\sprecta|\mymarg{punto\textnormal{\textcolor{gray}{,}} punto}\mycolsep |\sprecta|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto\textnormal{\textcolor{gray}{,}} punto}
%     \mycolrow{14.9em}|\sprecta|\mymarg{letra}\mycolsep |\sprecta|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{letra}
%   \end{syntax}
%   El comando \ics*{sprecta} trabaja en \emph{modo matemático} y
%   recibe un \emph{argumento obligatorio} de la forma \mymarg{letra} o
%   \mymarg{punto\textnormal{\textcolor{gray}{,}} punto} con prima(s) `|'|'
%   o subíndice(s) `|_{...}|' opcionales. Si el \emph{argumento} es de
%   la forma \mymarg{punto\textnormal{\textcolor{gray}{,}} punto}
%   se aplica |\overleftrightarrow| en la impresión; si el \emph{argumento}
%   es de la forma \mymarg{letra}, mayúscula o minúscula, se imprimirá solo
%   la \mymarg{letra} sin aplicar |\overleftrightarrow|.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val}, |$\sprecta{AB}$|
% imprimirá `$\overleftrightarrow{AB}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«recta A B»}; |$\sprecta{m}$|
% imprimirá `$m$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«recta m»}.
%
% \subsubsection*{Llaves para \cs[no-index]{sprecta}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[sprecta]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará el
% argumento \mymarg{punto\textnormal{\textcolor{gray}{,}} punto} y el argumento
% \mymarg{letra} cuando está en mayúscula. Si se establece en |school|, \mynvmath{NVDA/MathCAT}
% verbaliza las letras del argumento sin el prefijo «mayúscula»: |$\sprecta{AB}$|
% se verbaliza \emph{«recta A B»}; si se establece en |mathcat|, \mynvmath{NVDA/MathCAT}
% aplica sus propias reglas, que pueden incluir el prefijo «mayúscula»
% dependiendo de la configuración del usuario.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[sprecta]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el concepto \emph{«recta»}. El valor de esta
% llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[sprecta]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el concepto \emph{«recta»} al ejecutar el comando sin modificar la impresión
% de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT} verbalizará
% el concepto y el \mymarg{argumento}; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% solo verbalizará el \mymarg{argumento} acorde al valor establecido por
% la llave \mykey{read-cmd}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[sprecta]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del concepto \emph{«recta»} y del \mymarg{argumento} pasado al comando
% sin modificar la impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% NO verbalizará la salida del comando; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{sprayo}}\label{subsec:sprayo}
%
% \begin{function}{\sprayo}
%   \begin{syntax}
%     |\sprayo|\mymarg{punto\textnormal{\textcolor{gray}{,}} punto} \quad\quad |\sprayo|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto\textnormal{\textcolor{gray}{,}} punto}
%   \end{syntax}
%   El comando \ics*{sprayo} trabaja en \emph{modo matemático} y
%   recibe el \emph{argumento obligatorio} \mymarg{punto\textnormal{\textcolor{gray}{,}} punto}
%   con prima(s) `|'|' o subíndice(s) `|_{...}|' opcionales. Representa
%   un \emph{rayo} o \emph{semirrecta} con origen en el primer
%   punto y dirección hacia el segundo, aplicando |\overrightarrow| en la impresión.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\sprayo{AB}$| imprimirá `$\overrightarrow{AB}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«rayo A B»}.
%
% \subsubsection*{Llaves para \cs[no-index]{sprayo}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[sprayo]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará
% el argumento \mymarg{punto\textnormal{\textcolor{gray}{,}} punto}. Si
% se establece en |school|, \mynvmath{NVDA/MathCAT} verbaliza las letras
% del argumento sin el prefijo «mayúscula»: |$\sprayo{AB}$| se verbaliza
% \emph{«rayo A B»}; si se establece en |mathcat|, \mynvmath{NVDA/MathCAT}
% aplica sus propias reglas, que pueden incluir el prefijo «mayúscula»
% dependiendo de la configuración del usuario.
%
% \keyexampcmd{read-sty}{rayo \textnormal{\textcolor{lightgray}{\textbar}}
%   semirrecta}{rayo}[sprayo]
%
% Establece el \emph{concepto} que verbalizará \mynvmath{NVDA/MathCAT}
% al ejecutar el comando sin modificar la impresión de este. Si se utiliza
% |$\sprayo[read-sty=rayo]{AB}$|, \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«rayo A B»}; si se utiliza |$\sprayo[read-sty=semirrecta]{AB}$|,
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«semirrecta A B»}.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[sprayo]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el \emph{«concepto»} establecido por la llave
% \mykey{read-sty}. El valor de esta llave \emph{siempre} debe pasarse entre
% llaves `|{ }|'.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[sprayo]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el \emph{«concepto»} establecido por la llave \mykey{read-sty} al ejecutar el
% comando sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} verbalizará el concepto y el \mymarg{argumento};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} solo verbalizará
% el \mymarg{argumento} acorde al valor establecido por la llave \mykey{read-cmd}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[sprayo]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del \emph{«concepto»} establecido por la llave \mykey{read-sty} y del
% \mymarg{argumento} pasado al comando sin modificar la impresión de este.
% Si se establece en |true|, \mynvmath{NVDA/MathCAT} NO verbalizará la salida
% del comando; si se establece en |false|, \mynvmath{NVDA/MathCAT} sí
% verbalizará la salida del comando.
%
% \begin{important}*
%   La impresión `$\overrightarrow{AB}$' en la salida \texttt{PDF}
%   es idéntica en ambos casos. La elección depende de la terminología
%   del libro de texto o del currículo nacional: en Chile y España se
%   prefiere «semirrecta»; en México y otros países de América Latina se
%   usa «rayo».
% \end{important}
%
% \subsection*{El comando \cs[no-index]{spside}}\label{subsec:spside}
%
% \begin{function}{\spside}
%   \begin{syntax}
%     |\spside|\mymarg{punto\textnormal{\textcolor{gray}{,}} punto} \quad\quad |\spside|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto\textnormal{\textcolor{gray}{,}} punto}
%   \end{syntax}
%
%   El comando \ics*{spside} trabaja en \emph{modo matemático} y
%   recibe el \emph{argumento obligatorio} \mymarg{punto\textnormal{\textcolor{gray}{,}} punto}
%   con prima(s) `|'|' o subíndice(s) `|_{...}|' opcionales. Representa
%   un \emph{lado}, \emph{segmento} o \emph{trazo} determinado por dos puntos,
%   aplicando |\overline| en la impresión.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spside{AB}$| imprimirá `$\overline{AB}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«lado A B»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spside}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spside]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará el
% argumento \mymarg{punto\textnormal{\textcolor{gray}{,}} punto}. Si
% se establece en |school|, \mynvmath{NVDA/MathCAT} verbaliza las letras
% del argumento sin el prefijo «mayúscula»: |$\spside{AB}$|
% se verbaliza \emph{«lado A B»}; si se establece en |mathcat|, \mynvmath{NVDA/MathCAT}
% aplica sus propias reglas, que pueden incluir el prefijo «mayúscula»
% dependiendo de la configuración del usuario.
%
% \keyexampcmd{read-sty}{lado \textnormal{\textcolor{lightgray}{\textbar}}
%   segmento \textnormal{\textcolor{lightgray}{\textbar}}
%   trazo}{lado}[spside]
%
% Establece el \emph{concepto} que verbalizará \mynvmath{NVDA/MathCAT}
% al ejecutar el comando sin modificar la impresión de este. Si se utiliza
% |$\spside[read-sty=lado]{AB}$|, \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«lado A B»}; si se utiliza |$\spside[read-sty=segmento]{AB}$|,
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«segmento A B»} y si se utiliza
% |$\spside[read-sty=trazo]{AB}$|, \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«trazo A B»}.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spside]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el \emph{«concepto»} establecido por la llave
% \mykey{read-sty}. El valor de esta llave \emph{siempre} debe pasarse
% entre llaves `|{ }|'.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spside]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el \emph{«concepto»} establecido por la llave \mykey{read-sty} al ejecutar el
% comando sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} verbalizará el concepto y el \mymarg{argumento};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} solo verbalizará
% el \mymarg{argumento} acorde al valor establecido por la llave \mykey{read-cmd}.
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spside]
%
% Sobrescribe el \emph{concepto} establecido por la llave \mykey{read-sty}
% que será verbalizado por \mynvmath{NVDA/MathCAT} al ejecutar el comando
% sin modificar la impresión de este. El valor de esta llave \emph{siempre}
% debe pasarse entre llaves `|{ }|'. Por ejemplo:~|$\spside[read-arg={hipotenusa}]{AB}$|
% imprimirá `$\overline{AB}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«hipotenusa A B»}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spside]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del \emph{«concepto»} establecido por la llave \mykey{read-sty} y del
% \mymarg{argumento} pasado al comando sin modificar la impresión de este.
% Si se establece en |true|, \mynvmath{NVDA/MathCAT} NO verbalizará la salida
% del comando; si se establece en |false|, \mynvmath{NVDA/MathCAT} sí
% verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spmside}}\label{subsec:spmside}
%
% \begin{function}{\spmside}
%   \begin{syntax}
%     |\spmside|\mymarg{punto\textnormal{\textcolor{gray}{,}} punto} \quad\quad |\spmside|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto\textnormal{\textcolor{gray}{,}} punto}
%   \end{syntax}
%
%   El comando \ics*{spmside} trabaja en \emph{modo matemático} y
%   recibe el \emph{argumento obligatorio} \mymarg{punto\textnormal{\textcolor{gray}{,}} punto}
%   con prima(s) `|'|' o subíndice(s) `|_{...}|' opcionales. Representa
%   la \emph{medida} del \emph{lado}, \emph{segmento} o \emph{trazo}
%   determinado por dos puntos. La distinción entre |\spside| y |\spmside| es
%   semántica: |\spside| representa el \emph{objeto geométrico}
%   `$\overline{AB}$'; |\spmside| representa su \emph{medida}, que es un
%   número real positivo.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spmside{AB}$| imprimirá `$AB$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«medida del lado A B»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spmside}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spmside]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará el
% argumento \mymarg{punto\textnormal{\textcolor{gray}{,}} punto}. Si
% se establece en |school|, \mynvmath{NVDA/MathCAT} verbaliza las letras
% del argumento sin el prefijo «mayúscula»: |$\spmside{AB}$|
% se verbaliza \emph{«medida del lado A B»}; si se establece en |mathcat|,
% \mynvmath{NVDA/MathCAT} aplica sus propias reglas, que pueden incluir
% el prefijo «mayúscula» dependiendo de la configuración del usuario.
%
% \keyexampcmd{read-sty}{lado \textnormal{\textcolor{lightgray}{\textbar}}
%   segmento \textnormal{\textcolor{lightgray}{\textbar}}
%   trazo}{lado}[spmside]
%
% Establece el \emph{concepto} que verbalizará \mynvmath{NVDA/MathCAT}
% después de verbalizar el término \emph{«medida del»} al ejecutar el comando sin
% modificar la impresión de este. Si se utiliza |$\spmside[read-sty=lado]{AB}$|,
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«medida del lado A B»}; si se
% utiliza |$\spmside[read-sty=segmento]{AB}$|, \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«medida del segmento A B»} y si se utiliza
% |$\spmside[read-sty=trazo]{AB}$|, \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«medida del trazo A B»}.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spmside]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el término \emph{«medida del»} seguido por
% el \emph{«concepto»} establecido por la llave \mykey{read-sty}. El valor de esta llave
% \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spmside]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el término \emph{«medida del»} y el \emph{«concepto»} establecido por
% la llave \mykey{read-sty} al ejecutar el comando sin modificar la impresión
% de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT} verbalizará
% el término \emph{«medida del»}, el concepto y el \mymarg{argumento};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} solo verbalizará
% el \mymarg{argumento} acorde al valor establecido por la llave \mykey{read-cmd}.
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spmside]
%
% Sobrescribe el \emph{concepto} establecido por la llave \mykey{read-sty}
% que será verbalizado por \mynvmath{NVDA/MathCAT} al ejecutar el comando
% sin modificar la impresión de este. El valor de esta llave \emph{siempre}
% debe pasarse entre llaves `|{ }|' y se debe usar en conjunto con la llave
% \mykey{concept}.
%
% Por ejemplo:~|$\spmside[concept=false,read-arg={medida de la cuerda}]{AB}$|
% imprimirá `$AB$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«medida de la cuerda A B»}.
%
% \keyexampcmd{print-sym}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spmside]
%
% Establece \emph{si se imprime} la notación visual `$\mathrm{m}$' antes
% de aplicar |\overline| al \mymarg{argumento}. Si se establece en |true|,
% |$\spmside[print-sym=true]{AB}$| imprimirá `$\operatorname{m}\overline{AB}$'
% en la salida \texttt{PDF}; si se establece en |false| imprimirá `$AB$'
% en la salida \texttt{PDF}. En ambos casos \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«medida del lado A B»}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spmside]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del término \emph{«medida del»}, del \emph{«concepto»} establecido por
% la llave \mykey{read-sty} y del \mymarg{argumento} pasado al comando
% sin modificar la impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% NO verbalizará la salida del comando; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spangle}}\label{subsec:spangle}
%
% \begin{function}{\spangle}
%   \begin{syntax}
%     \mycolrow{32.5em}|\spangle|\mymarg{punto \textnormal{\textcolor{lightgray}{\textbar}} letra griega \textnormal{\textcolor{lightgray}{\textbar}} número}\mycolsep |\spangle|\mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}
%     \mycolrow{32.5em}|\spangle|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto \textnormal{\textcolor{lightgray}{\textbar}} letra griega \textnormal{\textcolor{lightgray}{\textbar}} número}\mycolsep |\spangle|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}
%   \end{syntax}
%   El comando \ics*{spangle} trabaja en \emph{modo matemático} y
%   recibe un \emph{argumento obligatorio} de la forma \mymarg{punto \textnormal{\textcolor{lightgray}{\textbar}} letra griega \textnormal{\textcolor{lightgray}{\textbar}} número}
%   o \mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}
%   aplicando |\angle| o |\rightanglesqr| en la impresión. Solo los argumentos
%   de la forma \mymarg{punto} o \mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}
%   admiten prima(s) `|'|' o subíndice(s) `|_{...}|' opcionales.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val}, |$\spangle{A}$|
% imprimirá `$\angle A$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«ángulo en A»}; |$\spangle{\alpha}$| imprimirá
% `$\angle\alpha$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«ángulo alfa»}; |$\spangle{1}$| imprimirá
% `$\angle 1$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«ángulo uno»} y |$\spangle{AOB}$| imprimirá
% `$\angle AOB$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«ángulo A O B»}.
%
% \begin{important}*
% Si no se tiene instalada la fuente matemática \texttt{NewCMMath-Regular.otf},
% parte del paquete \mypkg[no-index]{newcomputermodern}\cite{newcomputermodern}, en el sistema o
% no se provee una fuente matemática con soporte para |\rightanglesqr|,
% se usará |\rightangle| en su reemplazo.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spangle}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spangle]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará el
% argumento \mymarg{punto} o \mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}.
% Si se establece en |school|, \mynvmath{NVDA/MathCAT} verbaliza las letras
% del argumento sin el prefijo «mayúscula»: |$\spangle{AOB}$|
% se verbaliza \emph{«ángulo A O B»}; si se establece en |mathcat|,
% \mynvmath{NVDA/MathCAT} aplica sus propias reglas, que pueden incluir
% el prefijo «mayúscula» dependiendo de la configuración del usuario.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spangle]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el concepto \emph{«ángulo»}. El valor de esta
% llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{recto}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spangle]
%
% Establece si se \emph{usará} |\rightanglesqr| en la impresión añadiendo
% el término \emph{«recto»} a la verbalización en \mynvmath{NVDA/MathCAT}.
% Si se establece en |true|, |$\spangle[recto=true]{A}$| imprimirá `$\rightanglesqr A$'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«ángulo recto en A»},
% |$\spangle[recto=true]{AOB}$| imprimirá `$\rightanglesqr AOB$' en la
% salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«ángulo recto A O B»};
% si se establece en |false|, imprimirá usando |\angle| en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} omitirá el término \emph{«recto»}.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spangle]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el concepto \emph{«ángulo»} al ejecutar el comando sin modificar la impresión
% de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT} verbalizará
% el concepto \emph{«ángulo»} y el \mymarg{argumento}; si se establece en |false|,
% \mynvmath{NVDA/MathCAT} solo verbalizará el \mymarg{argumento} acorde
% al valor establecido por la llave \mykey{read-cmd}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spangle]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del concepto \emph{«ángulo»} y del \mymarg{argumento} pasado al comando
% sin modificar la impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% NO verbalizará la salida del comando; si se establece en |false|, \mynvmath{NVDA/MathCAT} sí
% verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spmangle}}\label{subsec:spmangle}
%
% \begin{function}{\spmangle}
%   \begin{syntax}
%     \mycolrow{26.4em}|\spmangle|\mymarg{punto \textnormal{\textcolor{lightgray}{\textbar}} letra griega \textnormal{\textcolor{lightgray}{\textbar}} número}\mycolsep |\spmangle|\mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}
%     |\spmangle|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto \textnormal{\textcolor{lightgray}{\textbar}} letra griega \textnormal{\textcolor{lightgray}{\textbar}} número}
%     |\spmangle|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}
%   \end{syntax}
%   El comando \ics*{spmangle} trabaja en \emph{modo matemático} y
%   recibe un \emph{argumento obligatorio} de la forma \mymarg{punto \textnormal{\textcolor{lightgray}{\textbar}} letra griega \textnormal{\textcolor{lightgray}{\textbar}} número}
%   o \mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}
%   aplicando |\measuredangle| o |\measuredrightangle| en la impresión.
%   Solo los argumentos de la forma \mymarg{punto} o \mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}
%   admiten prima(s) `|'|' o subíndice(s) `|_{...}|' opcionales.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val}, |$\spmangle{A}$|
% imprimirá `$\measuredangle A$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«medida del ángulo en A»}; |$\spmangle{\alpha}$| imprimirá
% `$\measuredangle\alpha$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«medida del ángulo alfa»}; |$\spmangle{1}$| imprimirá
% `$\measuredangle 1$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«medida del ángulo uno»} y |$\spmangle{AOB}$| imprimirá
% `$\measuredangle AOB$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«medida del ángulo A O B»}.
%
% \begin{important}*
% La distinción entre |\spangle| y |\spmangle| es semántica: |\spangle|
% representa el \emph{objeto geométrico} `$\angle A O B$'; |\spmangle|
% representa su \emph{medida}, que es un valor en grados.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spmangle}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spmangle]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará el
% argumento \mymarg{punto} o \mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}.
% Si se establece en |school|, \mynvmath{NVDA/MathCAT} verbaliza las letras
% del argumento sin el prefijo «mayúscula»: |$\spmangle{AOB}$|
% se verbaliza \emph{«medida del ángulo A O B»}; si se establece en |mathcat|,
% \mynvmath{NVDA/MathCAT} aplica sus propias reglas, que pueden incluir
% el prefijo «mayúscula» dependiendo de la configuración del usuario.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spmangle]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el término \emph{«medida del»} seguido por el
% concepto \emph{«ángulo»}. El valor de esta llave \emph{siempre} debe
% pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{recto}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spmangle]
%
% Establece si se \emph{usará} |\measuredrightangle| en la impresión añadiendo
% el término \emph{«recto»} a la verbalización en \mynvmath{NVDA/MathCAT}.
% Si se establece en |true|, |$\spmangle[recto=true]{A}$| imprimirá
% `$\measuredrightangle A$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«medida del ángulo recto en A»}, |$\spmangle[recto=true]{AOB}$|
% imprimirá `$\measuredrightangle AOB$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«medida del ángulo recto A O B»}; si se establece en |false|,
% imprimirá usando |\measuredangle| en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% omitirá el término \emph{«recto»}.
%
% \keyexampcmd{print-sym}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spmangle]
%
% Establece \emph{si se imprime} la notación visual `$\mathrm{m}$' antes
% del argumento pasado al comando sin modificar la verbalización de este.
% Si se establece en |true|, |$\spmangle[print-sym=true]{AOB}$| imprimirá
% `$\operatorname{m}\angle AOB$' en la salida \texttt{PDF}; si se establece
% en |false| imprimirá `$\measuredangle AOB$' en la salida \texttt{PDF}.
% En ambos casos \mynvmath{NVDA/MathCAT} verbalizará \emph{«medida del ángulo A O B»}.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spmangle]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el término \emph{«medida del»} seguido por el concepto \emph{«ángulo»} al
% ejecutar el comando sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«medida del ángulo»} y el \mymarg{argumento};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} solo verbalizará el
% \mymarg{argumento} acorde al valor establecido por la llave \mykey{read-cmd}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spmangle]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del término \emph{«medida del»}, del concepto \emph{«ángulo»} y del \mymarg{argumento}
% pasado al comando sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se establece
% en |false|, \mynvmath{NVDA/MathCAT} sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spang}}\label{subsec:spang}
%
% \begin{function}{\spang}
%   \begin{syntax}
%     |\spang|\mymarg{grados} \hfill |\spang|\mymarg{grados \textnormal{\textcolor{red}{:}} minutos} \hfill |\spang|\mymarg{grados \textnormal{\textcolor{red}{:}} minutos \textnormal{\textcolor{red}{:}} segundos}
%   \end{syntax}
%   El comando \ics*{spang} trabaja en \emph{modo matemático} y recibe el
%   \emph{argumento obligatorio} \mymarg{grados}, \mymarg{grados \textnormal{\textcolor{red}{:}} minutos}
%   o \mymarg{grados \textnormal{\textcolor{red}{:}} minutos
%   \textnormal{\textcolor{red}{:}} segundos}. Los procesa internamente bajo
%   los comandos |\spnum| y |\spunit| para imprimir y verbalizar.
% \end{function}
%
% Por ejemplo:~|$\spang{45:30:15}$| imprimirá `$45°30′15″$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«cuarenta y cinco grados
% treinta minutos quince segundos»}.
%
% \subsection*{El comando \cs[no-index]{sparc}}\label{subsec:sparc}
%
% \begin{function}{\sparc}
%   \begin{syntax}
%     \mycolrow{21.1em}|\sparc|\mymarg{punto\textnormal{\textcolor{gray}{,}} punto}\mycolsep |\sparc|\mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}
%     \mycolrow{21.1em}|\sparc|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto\textnormal{\textcolor{gray}{,}} punto}\mycolsep |\sparc|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}
%   \end{syntax}
%   El comando \ics*{sparc} trabaja en \emph{modo matemático} y recibe un
%   \emph{argumento obligatorio} de la forma \mymarg{punto\textnormal{\textcolor{gray}{,}} punto}
%   o \mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}
%   con prima(s) `|'|' o subíndice(s) `|_{...}|' opcionales aplicando
%   |\widearc| en la impresión.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\sparc{AB}$| imprimirá `$\widearc{AB}$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«arco A B»}.
%
% \begin{important}*
% El acento |\widearc| es proporcionado por algunos paquetes como
% \mypkg[no-index]{fourier-otf} o \mypkg[no-index]{kpfonts-otf}, en caso
% de no estar disponible se utilizará una versión interna para este.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{sparc}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[sparc]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará el
% argumento \mymarg{punto\textnormal{\textcolor{gray}{,}} punto} o
% \mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}.
% Si se establece en |school|, \mynvmath{NVDA/MathCAT} verbaliza las letras
% del argumento sin el prefijo «mayúscula»: |$\sparc{AB}$|
% se verbaliza \emph{«arco A B»}; si se establece en |mathcat|,
% \mynvmath{NVDA/MathCAT} aplica sus propias reglas, que pueden incluir
% el prefijo «mayúscula» dependiendo de la configuración del usuario.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[sparc]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el concepto \emph{«arco»}. El valor de esta
% llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[sparc]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el concepto \emph{«arco»} al ejecutar el comando sin modificar la impresión
% de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«arco»} y el \mymarg{argumento}; si se establece en |false|,
% \mynvmath{NVDA/MathCAT} solo verbalizará el \mymarg{argumento} acorde
% al valor establecido por la llave \mykey{read-cmd}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[sparc]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del concepto \emph{«arco»} y del \mymarg{argumento} pasado al comando
% sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se establece
% en |false|, \mynvmath{NVDA/MathCAT} sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spmarc}}\label{subsec:spmarc}
%
% \begin{function}{\spmarc}
%   \begin{syntax}
%     \mycolrow{21.7em}|\spmarc|\mymarg{punto\textnormal{\textcolor{gray}{,}} punto}\mycolsep |\spmarc|\mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}
%     \mycolrow{21.7em}|\spmarc|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto\textnormal{\textcolor{gray}{,}} punto}\mycolsep |\spmarc|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}
%   \end{syntax}
%   El comando \ics*{spmarc} trabaja en \emph{modo matemático} y
%   recibe un \emph{argumento obligatorio} de la forma \mymarg{punto\textnormal{\textcolor{gray}{,}} punto}
%   o \mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}
%   con prima(s) `|'|' o subíndice(s) `|_{...}|' opcionales colocando
%   `$\operatorname{m}$' antes de aplicar |\widearc| en la impresión.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spmarc{AB}$| imprimirá `$\operatorname{m}\widearc{AB}$' en la
% salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«medida del arco A B»}.
%
% \begin{important}*
% La distinción entre |\sparc| y |\spmarc| es semántica:
% |\sparc| representa el \emph{objeto geométrico} `$\widearc{AB}$';
% |\spmarc| representa su \emph{medida}, que es un valor en grados.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spmarc}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spmarc]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará el
% argumento \mymarg{punto\textnormal{\textcolor{gray}{,}} punto} o
% \mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto}.
% Si se establece en |school|, \mynvmath{NVDA/MathCAT} verbaliza las letras
% del argumento sin el prefijo «mayúscula»: |$\spmarc{AB}$|
% se verbaliza \emph{«medida del arco A B»}; si se establece en |mathcat|,
% \mynvmath{NVDA/MathCAT} aplica sus propias reglas, que pueden incluir
% el prefijo «mayúscula» dependiendo de la configuración del usuario.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spmarc]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el término \emph{«medida del»} seguido por
% el concepto \emph{«arco»}. El valor de esta llave \emph{siempre} debe
% pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spmarc]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el término \emph{«medida del»} y el concepto \emph{«arco»} al ejecutar
% el comando sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«medida del arco»} y el \mymarg{argumento};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} solo verbalizará el
% \mymarg{argumento} acorde al valor establecido por la llave \mykey{read-cmd}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spmarc]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del término \emph{«medida del»}, el concepto \emph{«arco»} y el \mymarg{argumento}
% pasado al comando sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se establece
% en |false|, \mynvmath{NVDA/MathCAT} sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spPoly}}\label{subsec:spPoly}
%
% \begin{function}{\spPoly}
%   \begin{syntax}
%     |\spPoly|\mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots} \quad\quad |\spPoly|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}
%   \end{syntax}
%   El comando \ics*{spPoly} trabaja en \emph{modo matemático} y
%   recibe el \emph{argumento obligatorio} \mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots},
%   una lista de al menos tres \mymarg{puntos} con prima(s) `|'|' o
%   subíndice(s) `|_{...}|' opcionales. El \emph{símbolo} usado en la
%   impresión se maneja por medio de la llave \mykey{print-sym} y la
%   verbalización del concepto asociado se determina acorde al número
%   de puntos en el argumento y la llave \mykey{concept}.
% \end{function}
%
% Si son tres puntos se aplica |\triangle| y añade el concepto \emph{«triángulo»}
% a la verbalización; si son cuatro puntos se aplica |\mdlgwhtsquare|
% y añade el concepto \emph{«cuadrado»} a la verbalización; si son cinco
% o más puntos no se aplica ningún símbolo y se añade a la verbalización
% el nombre del polígono: \emph{«pentágono»}, \emph{«hexágono»},
% \emph{«heptágono»} u \emph{«octágono»} de cinco a ocho puntos, y
% \emph{«polígono»} desde nueve puntos.
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spPoly{ABC}$| imprimirá `$\triangle ABC$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«triángulo A B C»}; |$\spPoly{ABCD}$| imprimirá
% `$\mdlgwhtsquare ABCD$' y verbalizará \emph{«cuadrado A B C D»};
% |$\spPoly{ABCDE}$| imprimirá `$ABCDE$' y verbalizará
% \emph{«pentágono A B C D E»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spPoly}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spPoly]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará el
% argumento \mymarg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}.
% Si se establece en |school|, \mynvmath{NVDA/MathCAT} verbaliza las letras
% del argumento sin el prefijo «mayúscula»: |$\spPoly{ABC}$|
% se verbaliza \emph{«triángulo A B C»}; si se establece en |mathcat|,
% \mynvmath{NVDA/MathCAT} aplica sus propias reglas, que pueden incluir
% el prefijo «mayúscula» dependiendo de la configuración del usuario.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spPoly]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el \mymarg{argumento} o el concepto \emph{«triángulo»},
% \emph{«cuadrado»}, \emph{«pentágono»}, \emph{«hexágono»}, \emph{«heptágono»},
% \emph{«octágono»} o \emph{«polígono»} establecido por la llave \mykey{concept}.
% El valor de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spPoly]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el concepto, que es \emph{«triángulo»}, \emph{«cuadrado»}, \emph{«pentágono»},
% \emph{«hexágono»}, \emph{«heptágono»}, \emph{«octágono»} o
% \emph{«polígono»}, acorde al número de puntos en el \mymarg{argumento} al
% ejecutar el comando sin modificar la impresión de este. Si se establece en
% |true|, \mynvmath{NVDA/MathCAT} verbalizará el concepto y el
% \mymarg{argumento};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} solo verbalizará el
% \mymarg{argumento} acorde al valor establecido por la llave \mykey{read-cmd}.
%
% \keyexampcmd{print-sym}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spPoly]
%
% Establece si se \emph{usará} |\triangle| o |\mdlgwhtsquare| en la impresión
% sin modificar la verbalización de este. Si se establece en |true|,
% |$\spPoly[print-sym=true]{ABC}$| imprimirá `$\triangle ABC$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«triángulo A B C»};
% |$\spPoly[print-sym=true]{ABCD}$| imprimirá `$\mdlgwhtsquare ABCD$' en
% la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«cuadrado A B C D»};
% si se establece en |false|, solo se imprimirá el \mymarg{argumento} en
% la salida \texttt{PDF} y la verbalización será la misma.
%
% \begin{important}*
%   Esta llave no tiene efecto con cinco o más puntos, porque no hay un
%   símbolo definido para ellos.
% \end{important}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spPoly]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del concepto establecido por la llave \mykey{concept} y del \mymarg{argumento}
% pasado al comando sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se establece
% en |false|, \mynvmath{NVDA/MathCAT} sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spAfig}}\label{subsec:spAfig}
%
% \begin{function}{\spAfig}
%   \begin{syntax}
%     \mycolrow{24.9em}|\spAfig|\mycolsep |\spAfig|\myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}
%     \mycolrow{24.9em}|\spAfig|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mycolsep |\spAfig|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}
%     \mycolrow{24.9em}|\spAfig|\myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}\mycolsep |\spAfig|\myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{T}}
%     \mycolrow{24.9em}|\spAfig|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}\mycolsep |\spAfig|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{T}}
%   \end{syntax}
%   El comando \ics*{spAfig} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento opcional delimitado por paréntesis} de la forma
%   \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n},
%   \myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}
%   o \myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{T}}
%   aplicando |\mathcal{A}| a la impresión. El \emph{símbolo} usado en
%   la impresión se maneja por medio de la llave \mykey{print-sym} y la
%   verbalización del concepto asociado se determina acorde al \myparg{argumento}
%   y la llave \mykey{concept}.
% \end{function}
%
% Si se proporciona el \emph{argumento} \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n},
% |$\spAfig(1)$| imprimirá `$\mathcal{A}_{1}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«área uno»};
% |$\spAfig(n)$| imprimirá `$\mathcal{A}_{n}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«área ene»}.
%
% Si se proporciona el \emph{argumento} \myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots},
% |$\spAfig(ABC)$| imprimirá `$\mathcal{A}_{\triangle ABC}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«área del triángulo A B C»};
% |$\spAfig(ABCD)$| imprimirá `$\mathcal{A}_{\mdlgwhtsquare ABCD}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«área del cuadrado A B C D»};
% |$\spAfig(ABCDE)$| imprimirá `$\mathcal{A}_{ABCDE}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«área del pentágono A B C D E»}.
% Con cinco o más puntos no se imprime ningún símbolo y se añade a la
% verbalización el nombre del polígono: \emph{«pentágono»}, \emph{«hexágono»},
% \emph{«heptágono»} u \emph{«octágono»} de cinco a ocho puntos, y
% \emph{«polígono»} desde nueve puntos.
%
% Si se proporciona el \emph{argumento} \myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{T}},
% |$\spAfig(F)$| imprimirá `$\mathcal{A}_{F}$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«área de la figura»};
% |$\spAfig(F_{1})$| imprimirá `$\mathcal{A}_{F_{1}}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«área de la figura uno»}.
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
% ni el \emph{argumento opcional delimitado por paréntesis}, |$\spAfig$| imprimirá
% `$\mathcal{A}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«área»}.
%
% \begin{important}*
%   El \emph{argumento} \myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}
%   acepta prima(s) `|'|' o subíndice(s) `|_{...}|'. El \emph{argumento}
%   \myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{T}}
%   solo acepta subíndices `|_{...}|' y tiene una impresión y verbalización
%   fija para cada letra: |F| \emph{«figura»}, |P| \emph{«polígono»},
%   |B| \emph{«basal»}, |L| \emph{«lateral»} y |T| \emph{«total»}.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spAfig}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spAfig]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará el
% argumento. Si se establece en |school|, \mynvmath{NVDA/MathCAT} verbaliza las letras
% del argumento sin el prefijo «mayúscula»: |$\spAfig(ABC)$|
% se verbaliza \emph{«área del triángulo A B C»}; si se establece en |mathcat|,
% \mynvmath{NVDA/MathCAT} aplica sus propias reglas, que pueden incluir
% el prefijo «mayúscula» dependiendo de la configuración del usuario.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spAfig]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el término \emph{«área del»} y el concepto
% \emph{«nombre de la figura»} acorde al \myparg{argumento} pasado al comando.
% El valor de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spAfig]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el término \emph{«área del»} y el concepto \emph{«nombre de la figura»}
% acorde al \myparg{argumento} pasado al comando sin modificar
% la impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% verbalizará el término \emph{«área del»}, el concepto \emph{«nombre de la figura»}
% y el \myparg{argumento}; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% solo verbalizará el \myparg{argumento} acorde al valor establecido por
% la llave \mykey{read-cmd}.
%
% \begin{important}*
%   Utilizar \mykey{concept}|=false| sin el \myparg{argumento} es equivalente a usar
%   \mykey{silent}|=true|.
% \end{important}
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spAfig]
%
% Sobrescribe el \emph{concepto} \emph{«nombre de la figura»} que verbalizará
% \mynvmath{NVDA/MathCAT} al ejecutar el comando sin modificar la impresión
% de este, cuando se utiliza el argumento
% \myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}.
% Con \mykey{concept}|=true| se mantiene el término
% \emph{«área del»} antes del valor; con \mykey{concept}|=false| se verbaliza solo el valor
% de esta llave. El valor de esta llave \emph{siempre} debe pasarse entre
% llaves `|{ }|' y se debe usar en conjunto con la llave
% \mykey{concept}. Por ejemplo:~|$\spAfig[read-arg={trapecio}](ABCD)$| imprimirá
% `$\mathcal{A}_{\mdlgwhtsquare ABCD}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«área del trapecio A B C D»}.
%
% \begin{important}*
%   Esta llave no tiene efecto y emite un aviso con los argumentos
%   \myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{T}} y \myparg{1
%   \textnormal{\textcolor{lightgray}{\textbar}} 2
%   \textnormal{\textcolor{lightgray}{\textbar}} 3
%   \textnormal{\textcolor{lightgray}{\textbar}} \ldots
%   \textnormal{\textcolor{lightgray}{\textbar}} n}.
% \end{important}
%
% \keyexampcmd*{concept\textcolor{MediumOrchid}{*}}{valor prohibido}{no usado}[spAfig]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del concepto \emph{«nombre de la figura»} acorde al \myparg{argumento} pasado
% al comando sin modificar la impresión de este. Si se establece, \mynvmath{NVDA/MathCAT}
% verbalizará el término \emph{«área»} y el \myparg{argumento} acorde al
% valor establecido por la llave \mykey{read-cmd}.
%
% \keyexampcmd{read-sub}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spAfig]
%
% Establece si se \emph{antepone} la palabra «sub» al verbalizar los argumentos
% \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
% y \myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{T}}
% para \mynvmath{NVDA/MathCAT}. Si se utiliza \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
% y se establece en |true|, |$\spAfig[read-sub=true](1)$| imprimirá
% `$\mathcal{A}_{1}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«área sub uno»}; si se establece en |false|, la impresión
% será la misma y se verbaliza \emph{«área uno»}.
%
% Si se utiliza \myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{T}}
% y se establece en |true|, |$\spAfig[read-sub=true](F_{1})$| imprimirá
% `$\mathcal{A}_{F_{1}}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«área de la figura sub uno»}; si se establece en |false|,
% la impresión será la misma y se verbaliza \emph{«área de la figura uno»}.
%
% \keyexampcmd{print-sym}{auto \textnormal{\textcolor{lightgray}{\textbar}}
%   triangle \textnormal{\textcolor{lightgray}{\textbar}}
%   square \textnormal{\textcolor{lightgray}{\textbar}}
%   none}{auto}[spAfig]
%
% Establece si se \emph{usará} |\triangle| o |\mdlgwhtsquare| en la impresión
% para los argumentos \myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}
% y \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
% pasado al comando sin modificar la verbalización de este. Si se utiliza
% \myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}
% se infiere el \emph{símbolo} acorde al número de puntos; si se establece en
% \mykey{auto} se imprime y si se establece en \mykey{none} se omite:
% |$\spAfig[print-sym=auto](ABC)$| y |$\spAfig[print-sym=none](ABC)$| imprimirán
% `$\mathcal{A}_{\triangle ABC}$' y `$\mathcal{A}_{ABC}$' en la salida
% \texttt{PDF} y en ambos casos \mynvmath{NVDA/MathCAT} verbalizará \emph{«área del triángulo A B C»}.
%
% Si se utiliza \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
% los valores \mykey{auto} y \mykey{none} quedan sin efecto; si se establece en \mykey{triangle}
% o \mykey{square} anida el argumento bajo el \emph{símbolo}:
% \lstinline[language=spintent-doc,emph={[4]{triangle}},emphstyle={[4]\color{keyname}}]|$\spAfig[print-sym=triangle](1)$|
% imprimirá `$\mathcal{A}_{\triangle_{1}}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«área del triángulo uno»};
% \lstinline[language=spintent-doc,emph={[4]{square}},emphstyle={[4]\color{keyname}}]|$\spAfig[print-sym=square](1)$|
% imprimirá `$\mathcal{A}_{\mdlgwhtsquare_{1}}$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«área del cuadrado uno»}.
%
% \begin{important}*
%   Esta llave no se puede utilizar con el argumento
%   \myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{T}}.
% \end{important}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spAfig]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del término \emph{«área del»}, el concepto \emph{«nombre de la figura»}
% y el \mymarg{argumento} pasado al comando sin modificar la impresión de este.
% Si se establece en |true|, \mynvmath{NVDA/MathCAT} NO verbalizará la salida
% del comando; si se establece en |false|, \mynvmath{NVDA/MathCAT} sí verbalizará
% la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spPfig}}\label{subsec:spPfig}
%
% \begin{function}{\spPfig}
%   \begin{syntax}
%     \mycolrow{24.9em}|\spPfig|\mycolsep |\spPfig|\myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}
%     \mycolrow{24.9em}|\spPfig|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mycolsep |\spPfig|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}
%     \mycolrow{24.9em}|\spPfig|\myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}\mycolsep |\spPfig|\myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{T}}
%     \mycolrow{24.9em}|\spPfig|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}\mycolsep |\spPfig|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{T}}
%   \end{syntax}
%   El comando \ics*{spPfig} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento opcional delimitado por paréntesis} de la forma
%   \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n},
%   \myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}
%   o \myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{T}}
%   aplicando |\mathcal{P}| a la impresión. El \emph{símbolo} usado en
%   la impresión se maneja por medio de la llave \mykey{print-sym} y la
%   verbalización del concepto asociado se determina acorde al \myparg{argumento}
%   y la llave \mykey{concept}.
% \end{function}
%
% Si se proporciona el \emph{argumento} \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n},
% |$\spPfig(1)$| imprimirá `$\mathcal{P}_{1}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«perímetro uno»};
% |$\spPfig(n)$| imprimirá `$\mathcal{P}_{n}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«perímetro ene»}.
%
% Si se proporciona el \emph{argumento} \myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots},
% |$\spPfig(ABC)$| imprimirá `$\mathcal{P}_{\triangle ABC}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«perímetro del triángulo A B C»};
% |$\spPfig(ABCD)$| imprimirá `$\mathcal{P}_{\mdlgwhtsquare ABCD}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«perímetro del cuadrado A B C D»};
% |$\spPfig(ABCDE)$| imprimirá `$\mathcal{P}_{ABCDE}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«perímetro del pentágono A B C D E»}.
% Con cinco o más puntos no se imprime ningún símbolo y se añade a la
% verbalización el nombre del polígono: \emph{«pentágono»}, \emph{«hexágono»},
% \emph{«heptágono»} u \emph{«octágono»} de cinco a ocho puntos, y
% \emph{«polígono»} desde nueve puntos.
%
% Si se proporciona el \emph{argumento} \myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{T}},
% |$\spPfig(F)$| imprimirá `$\mathcal{P}_{F}$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«perímetro de la figura»};
% |$\spPfig(F_{1})$| imprimirá `$\mathcal{P}_{F_{1}}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«perímetro de la figura uno»}.
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
% ni el \emph{argumento opcional delimitado por paréntesis}, |$\spPfig$| imprimirá
% `$\mathcal{P}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«perímetro»}.
%
% \begin{important}*
%   El \emph{argumento} \myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}
%   acepta prima(s) `|'|' o subíndice(s) `|_{...}|'. El \emph{argumento}
%   \myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{T}}
%   solo acepta subíndices `|_{...}|' y tiene una impresión y verbalización
%   fija para cada letra: |F| \emph{«figura»}, |P| \emph{«polígono»},
%   |B| \emph{«basal»}, |L| \emph{«lateral»} y |T| \emph{«total»}.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spPfig}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spPfig]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará el
% argumento. Si se establece en |school|, \mynvmath{NVDA/MathCAT} verbaliza las letras
% del argumento sin el prefijo «mayúscula»: |$\spPfig(ABC)$|
% se verbaliza \emph{«perímetro del triángulo A B C»}; si se establece en |mathcat|,
% \mynvmath{NVDA/MathCAT} aplica sus propias reglas, que pueden incluir
% el prefijo «mayúscula» dependiendo de la configuración del usuario.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spPfig]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el término \emph{«perímetro del»} y el concepto
% \emph{«nombre de la figura»} acorde al \myparg{argumento} pasado al comando.
% El valor de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spPfig]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el término \emph{«perímetro del»} y el concepto \emph{«nombre de la figura»}
% acorde al \myparg{argumento} pasado al comando sin modificar
% la impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% verbalizará el término \emph{«perímetro del»}, el concepto \emph{«nombre de la figura»}
% y el \myparg{argumento}; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% solo verbalizará el \myparg{argumento} acorde al valor establecido por
% la llave \mykey{read-cmd}.
%
% \begin{important}*
%   Utilizar \mykey{concept}|=false| sin el \myparg{argumento} es equivalente a usar
%   \mykey{silent}|=true|.
% \end{important}
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spPfig]
%
% Sobrescribe el \emph{concepto} \emph{«nombre de la figura»} que verbalizará
% \mynvmath{NVDA/MathCAT} al ejecutar el comando sin modificar la impresión
% de este, cuando se utiliza el argumento
% \myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}.
% Con \mykey{concept}|=true| se mantiene el término
% \emph{«perímetro del»} antes del valor; con \mykey{concept}|=false| se verbaliza solo el valor
% de esta llave. El valor de esta llave \emph{siempre} debe pasarse entre
% llaves `|{ }|' y se debe usar en conjunto con la llave
% \mykey{concept}. Por ejemplo:~|$\spPfig[read-arg={trapecio}](ABCD)$| imprimirá
% `$\mathcal{P}_{\mdlgwhtsquare ABCD}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«perímetro del trapecio A B C D»}.
%
% \begin{important}*
%   Esta llave no tiene efecto y emite un aviso con los argumentos
%   \myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{T}} y \myparg{1
%   \textnormal{\textcolor{lightgray}{\textbar}} 2
%   \textnormal{\textcolor{lightgray}{\textbar}} 3
%   \textnormal{\textcolor{lightgray}{\textbar}} \ldots
%   \textnormal{\textcolor{lightgray}{\textbar}} n}.
% \end{important}
%
% \keyexampcmd*{concept\textcolor{MediumOrchid}{*}}{valor prohibido}{no usado}[spPfig]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del concepto \emph{«nombre de la figura»} acorde al \myparg{argumento} pasado
% al comando sin modificar la impresión de este. Si se establece, \mynvmath{NVDA/MathCAT}
% verbalizará el término \emph{«perímetro»} y el \myparg{argumento} acorde al
% valor establecido por la llave \mykey{read-cmd}.
%
% \keyexampcmd{read-sub}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spPfig]
%
% Establece si se \emph{antepone} la palabra «sub» al verbalizar los argumentos
% \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
% y \myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{T}}
% para \mynvmath{NVDA/MathCAT}. Si se utiliza \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
% y se establece en |true|, |$\spPfig[read-sub=true](1)$| imprimirá
% `$\mathcal{P}_{1}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«perímetro sub uno»}; si se establece en |false|, la impresión
% será la misma y se verbaliza \emph{«perímetro uno»}.
%
% Si se utiliza \myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}} \textnormal{T}}
% y se establece en |true|, |$\spPfig[read-sub=true](F_{1})$| imprimirá
% `$\mathcal{P}_{F_{1}}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«perímetro de la figura sub uno»}; si se establece en |false|,
% la impresión será la misma y se verbaliza \emph{«perímetro de la figura uno»}.
%
% \keyexampcmd{print-sym}{auto \textnormal{\textcolor{lightgray}{\textbar}}
%   triangle \textnormal{\textcolor{lightgray}{\textbar}}
%   square \textnormal{\textcolor{lightgray}{\textbar}}
%   none}{auto}[spPfig]
%
% Establece si se \emph{usará} |\triangle| o |\mdlgwhtsquare| en la impresión
% para los argumentos \myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}
% y \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
% pasado al comando sin modificar la verbalización de este. Si se utiliza
% \myparg{punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} punto\textnormal{\textcolor{gray}{,}} \ldots}
% se infiere el \emph{símbolo} acorde al número de puntos; si se establece en
% \mykey{auto} se imprime y si se establece en \mykey{none} se omite:
% |$\spPfig[print-sym=auto](ABC)$| y |$\spPfig[print-sym=none](ABC)$| imprimirán
% `$\mathcal{P}_{\triangle ABC}$' y `$\mathcal{P}_{ABC}$' en la salida
% \texttt{PDF} y en ambos casos \mynvmath{NVDA/MathCAT} verbalizará \emph{«perímetro del triángulo A B C»}.
%
% Si se utiliza \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
% los valores \mykey{auto} y \mykey{none} quedan sin efecto; si se establece en \mykey{triangle}
% o \mykey{square} anida el argumento bajo el \emph{símbolo}:
% \lstinline[language=spintent-doc,emph={[4]{triangle}},emphstyle={[4]\color{keyname}}]|$\spPfig[print-sym=triangle](1)$|
% imprimirá `$\mathcal{P}_{\triangle_{1}}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«perímetro del triángulo uno»};
% \lstinline[language=spintent-doc,emph={[4]{square}},emphstyle={[4]\color{keyname}}]|$\spPfig[print-sym=square](1)$|
% imprimirá `$\mathcal{P}_{\mdlgwhtsquare_{1}}$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«perímetro del cuadrado uno»}.
%
% \begin{important}*
%   Esta llave no se puede utilizar con el argumento
%   \myparg{\textnormal{F} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{P} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{B} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{L} \textnormal{\textcolor{lightgray}{\textbar}}
%   \textnormal{T}}.
% \end{important}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spPfig]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del término \emph{«perímetro del»}, el concepto \emph{«nombre de la figura»}
% y el \mymarg{argumento} pasado al comando sin modificar la impresión de este.
% Si se establece en |true|, \mynvmath{NVDA/MathCAT} NO verbalizará la salida
% del comando; si se establece en |false|, \mynvmath{NVDA/MathCAT} sí verbalizará
% la salida del comando.
%
% \subsection*{Los comandos \cs[no-index]{sprad} y \cs[no-index]{spdiam}}\label{subsec:sprad}
%
% \begin{function}{\sprad, \spdiam}
%   \begin{syntax}
%     |\sprad|  \quad\quad |\sprad|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%     |\spdiam| \quad\quad |\spdiam|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   \end{syntax}
%   Los comandos \ics*{sprad} y \ics*{spdiam} trabajan en \emph{modo
%   matemático} y reciben el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   mediante el cual se establece la \emph{«forma»} en la que se imprimirán
%   en la salida \texttt{PDF} las letras `$r$' y `$d$' y cómo se verbalizarán
%   en \mynvmath{NVDA/MathCAT}.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\sprad$| imprimirá `$r$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«radio»}; |$\spdiam$| imprimirá `$d$' en la
% salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«diámetro»}.
%
% \subsubsection*{Llaves para \cs[no-index]{sprad} y \cs[no-index]{spdiam}}
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[sprad,spdiam]
%
% Establece si se \emph{verbalizan} en \mynvmath{NVDA/MathCAT}
% los conceptos \emph{«radio»} o \emph{«diámetro»} al ejecutar el comando
% sin modificar la impresión de este. Si se establece en |true|, |$\sprad$| y |$\spdiam$|
% se verbalizan como \emph{«radio»} y \emph{«diámetro»} en \mynvmath{NVDA/MathCAT};
% si se establece en |false|, se verbalizarán las letras `$r$', `$R$', `$d$' o `$D$'
% acorde al valor establecido por la llave \mykey{read-cmd}.
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[sprad,spdiam]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará las
% letras `$R$' o `$D$' cuando \mykey{concept}|=false| y \mykey{upper-case}|=true|. Si se
% establece en |school|, \mynvmath{NVDA/MathCAT} verbalizará las letras `$R$'
% o `$D$' sin el prefijo «mayúscula»; si se establece en |mathcat|,
% \mynvmath{NVDA/MathCAT} aplica sus propias reglas, que pueden incluir
% el prefijo «mayúscula» dependiendo de la configuración del usuario.
%
% \keyexampcmd{upper-case}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[sprad,spdiam]
%
% Establece si se \emph{imprime} usando mayúsculas. Si se establece en |true|,
% |$\sprad[upper-case=true]$| y |$\spdiam[upper-case=true]$| imprimirán `$R$' y `$D$'
% en la salida \texttt{PDF}; si se establece en |false| se imprimirá usando
% minúsculas.
%
% \begin{important}*
%   La verbalización al establecer esta llave es controlada por la llave \mykey{read-cmd}.
% \end{important}
%
% \keyexampcmd{print-sym}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spdiam]
%
% Establece si se \emph{usará} |\diameter| antes de la letra `$d$' o `$D$'
% en la impresión del comando |\spdiam|. Si se establece en |true|,
% |$\spdiam[print-sym=true]$| imprimirá `$\diameter d$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«diámetro»}.
%
% \begin{important}*
%   La llave \mykey{print-sym} no tiene ningún efecto en |\sprad|.
% \end{important}
%
% \keyexampcmd{sub-index}{subíndice}{no usado}[sprad,spdiam]
%
% Establece un \emph{subíndice} sobre las letras `$r$', `$R$', `$d$' o `$D$'.
% El valor de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
% Por ejemplo:~|$\sprad[sub-index={1}]$| imprimirá `$r_{1}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«radio uno»}.
%
% \keyexampcmd{read-sub}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[sprad,spdiam]
%
% Establece si se \emph{antepone} la palabra \emph{«sub»} al verbalizar
% el subíndice establecido por la llave \mykey{sub-index} al ejecutar el
% comando sin modificar la impresión de este. Si se establece en |true|,\newline
% |$\sprad[sub-index={1},read-sub=true]$| se verbalizará \emph{«radio sub uno»}
% en \mynvmath{NVDA/MathCAT}; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«radio uno»}.
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[sprad,spdiam]
%
% Sobrescribe el \emph{concepto} establecido por la llave \mykey{concept} que
% será verbalizado por \mynvmath{NVDA/MathCAT} al ejecutar el comando sin
% modificar la impresión de este. El valor de esta llave \emph{siempre}
% debe pasarse entre llaves `|{ }|' y se debe usar en conjunto
% con la llave \mykey{concept}.
%
% Por ejemplo:~|$\sprad[read-arg={medida exacta}]$| imprimirá `$r$' en
% la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«radio medida exacta»}; |$\sprad[concept=false,read-arg={medida exacta}]$|
% imprimirá `$r$' y \mynvmath{NVDA/MathCAT} verbalizará \emph{«medida
% exacta»}.
%
% \begin{important}*
%  Las llaves \mykey{sub-index} y \mykey{read-sub} no tienen efecto al usar \mykey{read-arg}.
% \end{important}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[sprad,spdiam]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del \emph{concepto} establecido por la llave \mykey{concept} y de las
% letras `$r$', `$R$', `$d$' o `$D$' al ejecutar el comando sin modificar
% la impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% NO verbalizará la salida del comando; si se establece en |false|,
% \mynvmath{NVDA/MathCAT} sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spcirc}}\label{subsec:spcirc}
%
% \begin{function}{\spcirc}
%   \begin{syntax}
%     \mycolrow{14.9em}|\spcirc|\mycolsep |\spcirc|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%     \mycolrow{14.9em}|\spcirc|\myparg{centro}\mycolsep |\spcirc|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{centro}
%     \mycolrow{14.9em}|\spcirc|\myparg{centro\textnormal{\textcolor{red}{,}} radio}\mycolsep |\spcirc|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{centro\textnormal{\textcolor{red}{,}} radio}
%   \end{syntax}
%   El comando \ics*{spcirc} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento opcional delimitado por paréntesis} de la forma
%   \myparg{centro} o \myparg{centro\textnormal{\textcolor{red}{,}} radio},
%   donde \mymeta{radio} es opcional y debe pasarse junto al argumento
%   \myparg{centro} separado por una coma `\textcolor{red}{,}' obligatoria.
% \end{function}
%
% La forma en la que se procesa el \mymeta{radio} junto a su impresión y
% verbalización se establece de manera interna: si es un \mymeta{número}
% entero o decimal se usa |\spnum|; si es una \mymeta{medida} se usa
% |\spunit|; si es un segmento \mymeta{punto punto} se usa |\spside| o
% |\spmside| acorde al valor establecido por la llave
% \mykey{radio-sty}; y si es una letra se imprime tal cual. Cualquier otra
% forma devuelve un \enquote{error} y se detiene la compilación del
% documento.
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
% ni el \emph{argumento opcional delimitado por paréntesis}, |$\spcirc$|
% imprimirá `$\mathcolor{red}{\bigodot}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«circunferencia»}.
%
% Si se proporciona el \emph{argumento} \myparg{centro}, |$\spcirc(O)$|
% imprimirá `$\bigodot(O)$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«circunferencia de centro en O»}.
%
% Si se proporciona el \emph{argumento} \myparg{centro\textnormal{\textcolor{red}{,}} radio},
% |$\spcirc(O,3)$| imprimirá `$\bigodot(O,3)$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«circunferencia de centro
% en O y radio tres»}.
%
% \begin{important}*
%   El argumento \myparg{centro} y el argumento \mymeta{radio} cuando tiene
%   la forma \mymeta{punto punto} aceptan prima `|'|' o subíndice `|_{...}|'.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spcirc}}
%
% El comando |\spcirc| posee las \mymeta{meta-keys} \mykey[spcirc]{spside}
% pasada al comando |\spside| (v.\,pág.\,\pageref{subsec:spside}) y \mykey[spcirc]{spmside}
% pasada al comando |\spmside| (v.\,pág.\,\pageref{subsec:spmside}).
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spcirc]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará
% el argumento \myparg{centro}. Si se establece en |school|, \mynvmath{NVDA/MathCAT}
% verbaliza el argumento \myparg{centro} sin el prefijo «mayúscula»;
% si se establece en |mathcat|, \mynvmath{NVDA/MathCAT} aplica sus propias
% reglas, que pueden incluir el prefijo «mayúscula» dependiendo de la
% configuración del usuario.
%
% \begin{important}*
%   Esta llave tiene efecto sobre el argumento \mymeta{radio} cuando tiene
%   la forma \mymeta{punto punto}.
% \end{important}
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spcirc]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el \emph{«concepto»} establecido por la
% llave \mykey{read-sty}. El valor de esta llave \emph{siempre} debe
% pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{print-sym}{usual \textnormal{\textcolor{lightgray}{\textbar}}
%   odot}{odot}[spcirc]
%
% Establece si se \emph{usará} |\bigodot| o la letra `$C$' en la impresión
% del comando sin modificar la verbalización de este. Si se establece en
% \mykey{usual}, |$\spcirc[print-sym=|\mykey{usual}|](O)$| imprimirá `$C(O)$'
% en la salida \texttt{PDF}; si se establece en \mykey{odot},
% \lstinline[language=spintent-doc,emph={[4]{odot}},emphstyle={[4]\color{keyname}}]|$\spcirc[print-sym=odot](O)$|
% imprimirá `$\bigodot(O)$' en la salida \texttt{PDF}.
%
% \keyexampcmd{read-sty}{circun \textnormal{\textcolor{lightgray}{\textbar}}
%   circulo}{circun}[spcirc]
%
% Establece el \emph{concepto} que verbalizará \mynvmath{NVDA/MathCAT}
% al ejecutar el comando sin modificar la impresión de este. Si se
% establece en \mykey{circun}, |$\spcirc[read-sty=circun](O)$| se verbalizará
% \emph{«circunferencia de centro en O»} en \mynvmath{NVDA/MathCAT}; si
% se establece en \mykey{circulo}, |$\spcirc[read-sty=circulo](O)$| se verbalizará
% \emph{«círculo de centro en O»} en \mynvmath{NVDA/MathCAT}.
%
% \keyexampcmd{diameter}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spcirc]
%
% Establece \emph{cómo} se verbaliza el argumento \mymeta{radio} al ejecutar
% el comando sin modificar la impresión de este. Si se establece en |true|,
% |$\spcirc[diameter=true](O, 3)$| se verbalizará \emph{«circunferencia de centro en O y diámetro tres»}
% en \mynvmath{NVDA/MathCAT}; si se establece en |false|, |$\spcirc[diameter=false](O, 3)$|
% se verbalizará \emph{«circunferencia de centro en O y radio tres»} en
% \mynvmath{NVDA/MathCAT}.
%
% \keyexampcmd{radio-sty}{side \textnormal{\textcolor{lightgray}{\textbar}}
%   mside}{mside}[spcirc]
%
% Establece el \emph{comando} que procesa internamente el argumento \mymeta{radio}
% cuando tiene la forma \mymeta{punto punto}. Si se establece en \mykey{side}
% se usará |\spside|\mymarg{punto punto}, |$\spcirc[radio-sty=side](O, AB)$|
% imprimirá `$\bigodot(O, \overline{AB})$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«circunferencia de centro en O y radio lado A B»};
% si se establece en \mykey{mside} se usará |\spmside|\mymarg{punto punto},
% |$\spcirc[radio-sty=mside](O, AB)$| imprimirá `$\bigodot(O, AB)$' en la
% salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«circunferencia
% de centro en O y radio medida del lado A B»}.
%
% \begin{important}*
%   Las \mymeta{meta-keys} \mykey{spside} y \mykey{spmside}
%   permiten modificar la impresión y verbalización del argumento \mymeta{radio}
%   cuando tiene la forma \mymeta{punto punto}.
% \end{important}
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spcirc]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el \emph{«concepto»} establecido por la llave \mykey{read-sty} al
% ejecutar el comando sin modificar la impresión de este. Si se
% establece en |true|, \mynvmath{NVDA/MathCAT} verbalizará el \emph{«concepto»} y
% el argumento \myparg{centro} o \myparg{centro\textnormal{\textcolor{red}{,}} radio};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} solo verbalizará el
% argumento \myparg{centro} o \myparg{centro\textnormal{\textcolor{red}{,}} radio}
% acorde al valor establecido por la llave \mykey{read-cmd}.
%
% \begin{important}*
%   Utilizar \mykey{concept}|=false| sin el argumento \myparg{centro} es equivalente a
%   usar \mykey{silent}|=true|.
% \end{important}
%
% \keyexampcmd{sub-index}{subíndice}{no usado}[spcirc]
%
% Establece un \emph{subíndice} sobre |\bigodot| o la letra `$C$' establecida
% por la llave \mykey{print-sym}. El valor de esta llave \emph{siempre} debe pasarse
% entre llaves `|{ }|'.\newline Por ejemplo:~|$\spcirc[sub-index={1},radio-sty=mside](O, AB)$|
% imprimirá `$\bigodot_{1}(O, AB)$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«circunferencia uno de centro en O y radio medida del lado A B»}.
%
% \keyexampcmd{read-sub}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spcirc]
%
% Establece si se \emph{antepone} la palabra \emph{«sub»} al verbalizar
% el subíndice establecido por la llave \mykey{sub-index} al ejecutar el comando sin modificar la
% impresión de este. Si se establece en |true|,\newline |$\spcirc[sub-index={1},read-sub=true](O)$|
% se verbalizará \emph{«circunferencia sub uno de centro en O»} en \mynvmath{NVDA/MathCAT};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} verbalizará \emph{«circunferencia uno de centro en O»}.
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spcirc]
%
% Sobrescribe el \emph{concepto} establecido por la llave \mykey{read-sty}
% que será verbalizado por \mynvmath{NVDA/MathCAT} al ejecutar el
% comando sin modificar la impresión de este. El valor de esta llave
% \emph{siempre} debe pasarse entre llaves `|{ }|'. Por ejemplo:~|$\spcirc[read-arg={disco}](O)$|
% imprimirá `$\bigodot(O)$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«disco de centro en O»}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spcirc]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del \emph{«concepto»} establecido por la llave \mykey{read-sty}, del
% argumento \myparg{centro} o \myparg{centro\textnormal{\textcolor{red}{,}} radio}
% pasados al comando sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se establece
% en |false|, \mynvmath{NVDA/MathCAT} sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spAcirc}}\label{subsec:spAcirc}
%
% \begin{function}{\spAcirc}
%   \begin{syntax}
%     \mycolrow{18.2em}|\spAcirc|\mycolsep |\spAcirc|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%     \mycolrow{18.2em}|\spAcirc|\myparg{centro}\mycolsep |\spAcirc|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{centro}
%     \mycolrow{18.2em}|\spAcirc|\myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}\mycolsep |\spAcirc|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
%   \end{syntax}
%   El comando \ics*{spAcirc} trabaja en \emph{modo matemático} y
%   recibe el \emph{argumento opcional delimitado por paréntesis} de la
%   forma \myparg{centro} o \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n},
%   aplicando |\mathcal{A}| a la impresión.
% \end{function}
%
%   Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   ni el \emph{argumento opcional delimitado por paréntesis}, |$\spAcirc$|
%   imprimirá `$\mathcal{A}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
%   verbalizará \emph{«área de la circunferencia»}.
%
%   Si se proporciona el \emph{argumento} \myparg{centro},
%   |$\spAcirc(O)$| imprimirá `$\mathcal{A}_{\bigodot(O)}$' en la
%   salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«área
%   de la circunferencia con centro en O»}.
%
%   Si se proporciona el \emph{argumento} \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n},
%   |$\spAcirc(1)$| imprimirá `$\mathcal{A}_{\bigodot_{1}}$' en la salida
%   \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«área de la circunferencia uno»}.
%
% \begin{important}*
%   El \emph{argumento} \myparg{centro} acepta prima `|'|' o
%   subíndice `|_{...}|' y no acepta un \mymeta{radio}. Una coma `|,|'
%   en el argumento devuelve siempre un \enquote{error} y se detiene la compilación del documento.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spAcirc}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spAcirc]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará
% el argumento \myparg{centro}. Si se establece en |school|, \mynvmath{NVDA/MathCAT}
% verbaliza el argumento \myparg{centro} sin el prefijo «mayúscula»; si se
% establece en |mathcat|, \mynvmath{NVDA/MathCAT} aplica sus propias
% reglas, que pueden incluir el prefijo «mayúscula» dependiendo de la
% configuración del usuario.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spAcirc]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el término \emph{«área de \dots»} seguido por
% el \emph{«concepto»} establecido por la llave \mykey{read-sty}. El
% valor de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{read-sty}{circun \textnormal{\textcolor{lightgray}{\textbar}}
%   circulo}{circun}[spAcirc]
%
% Establece el \emph{concepto} que verbalizará \mynvmath{NVDA/MathCAT}
% después de verbalizar el término \emph{«área de \dots»} al ejecutar el
% comando sin modificar la impresión de este. Si se establece en |circun|,
% |$\spAcirc[read-sty=circun](O)$| se verbalizará \emph{«área de la circunferencia con centro en O»}
% en \mynvmath{NVDA/MathCAT}; si se establece en |circulo|, |$\spAcirc[read-sty=circulo](O)$|
% se verbalizará \emph{«área del círculo con centro en O»} en \mynvmath{NVDA/MathCAT}.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spAcirc]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el término \emph{«área de \dots»} y el \emph{«concepto»} establecido
% por la llave \mykey{read-sty} al ejecutar el comando sin modificar la
% impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% verbalizará el término, el concepto y el argumento \myparg{centro} o
% \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} solo verbalizará el
% argumento \myparg{centro} acorde al valor establecido por la llave \mykey{read-cmd}.
%
% \begin{important}*
%   Utilizar \mykey{concept}|=false| sin el argumento \myparg{centro} o con el
%   argumento \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
%   es equivalente a usar \mykey{silent}|=true|.
% \end{important}
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spAcirc]
%
% Sobrescribe el \emph{concepto} establecido por la llave \mykey{read-sty}
% que será verbalizado por \mynvmath{NVDA/MathCAT} al ejecutar el
% comando sin modificar la impresión de este. El valor de esta llave
% \emph{siempre} debe pasarse entre llaves `|{ }|' y se debe usar en
% conjunto con la llave \mykey{concept}. Por ejemplo:~|$\spAcirc[read-arg={esfera}](O)$|
% se verbalizará \emph{«área de la esfera con centro en O»} en \mynvmath{NVDA/MathCAT};\newline
% |$\spAcirc[concept=false,read-arg={esfera}](O)$| se verbalizará
% \emph{«esfera con centro en O»} en \mynvmath{NVDA/MathCAT}.
%
% \begin{important}*
%   Utilizar esta llave con \mykey{concept}|=false| y el argumento
%   \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
%   es equivalente a usar \mykey{silent}|=true|.
% \end{important}
%
% \keyexampcmd{print-sym}{usual \textnormal{\textcolor{lightgray}{\textbar}}
%   odot}{odot}[spAcirc]
%
% Establece si se \emph{usará} |\bigodot| o la letra `$C$' en la impresión
% del comando sin modificar la verbalización de este. Si se establece en |usual|,
% |$\spAcirc[print-sym=usual](O)$| imprimirá `$\mathcal{A}_{C(O)}$'
% en la salida \texttt{PDF}; si se establece en \mykey{odot}, |$\spAcirc[print-sym=odot](O)$|
% imprimirá `$\mathcal{A}_{\bigodot(O)}$' en la salida \texttt{PDF}.
%
% \keyexampcmd{sub-index}{subíndice}{no usado}[spAcirc]
%
% Establece un \emph{subíndice} sobre |\bigodot| o la letra `$C$' establecida
% por la llave \mykey{print-sym} cuando se utiliza el argumento \myparg{centro}.
% El valor de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
% Por ejemplo:~|$\spAcirc[sub-index={1}](O)$| imprimirá `$\mathcal{A}_{\bigodot_{1}(O)}$'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«área de la circunferencia uno con centro en O»}.
%
% \keyexampcmd{read-sub}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spAcirc]
%
% Establece si se \emph{antepone} la palabra \emph{«sub»} al verbalizar
% el subíndice establecido por la llave \mykey{sub-index} o al usar el
% argumento \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
% al ejecutar el comando sin modificar la impresión de este. Si se establece
% en |true|, |$\spAcirc[read-sub=true](1)$| se verbalizará
% \emph{«área de la circunferencia sub uno»} en \mynvmath{NVDA/MathCAT};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} verbalizará \emph{«área de la circunferencia uno»}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spAcirc]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del término \emph{«área de \dots»}, del concepto establecido por la
% llave \mykey{read-sty} y del argumento \myparg{centro} o \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
% pasado al comando sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se
% establece en |false|, \mynvmath{NVDA/MathCAT} sí verbalizará la
% salida del comando.
%
% \subsection*{El comando \cs[no-index]{spPcirc}}\label{subsec:spPcirc}
%
% \begin{function}{\spPcirc}
%   \begin{syntax}
%     \mycolrow{18.2em}|\spPcirc|\mycolsep |\spPcirc|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%     \mycolrow{18.2em}|\spPcirc|\myparg{centro}\mycolsep |\spPcirc|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{centro}
%     \mycolrow{18.2em}|\spPcirc|\myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}\mycolsep |\spPcirc|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
%   \end{syntax}
%   El comando \ics*{spPcirc} trabaja en \emph{modo matemático} y
%   recibe el \emph{argumento opcional delimitado por paréntesis} de la
%   forma \myparg{centro} o \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n},
%   aplicando |\mathcal{P}| a la impresión.
% \end{function}
%
%   Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   ni el \emph{argumento opcional delimitado por paréntesis}, |$\spPcirc$|
%   imprimirá `$\mathcal{P}$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
%   verbalizará \emph{«perímetro de la circunferencia»}.
%
%   Si se proporciona el \emph{argumento} \myparg{centro},
%   |$\spPcirc(O)$| imprimirá `$\mathcal{P}_{\bigodot(O)}$' en la
%   salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«perímetro
%   de la circunferencia con centro en O»}.
%
%   Si se proporciona el \emph{argumento} \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n},
%   |$\spPcirc(1)$| imprimirá `$\mathcal{P}_{\bigodot_{1}}$' en la salida
%   \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«perímetro de la circunferencia uno»}.
%
% \begin{important}*
%   El \emph{argumento} \myparg{centro} acepta prima `|'|' o
%   subíndice `|_{...}|' y no acepta un \mymeta{radio}. Una coma `|,|'
%   en el argumento devuelve siempre un \enquote{error} y se detiene la compilación del documento.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spPcirc}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spPcirc]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará
% el argumento \myparg{centro}. Si se establece en |school|, \mynvmath{NVDA/MathCAT}
% verbaliza el argumento \myparg{centro} sin el prefijo «mayúscula»; si se
% establece en |mathcat|, \mynvmath{NVDA/MathCAT} aplica sus propias
% reglas, que pueden incluir el prefijo «mayúscula» dependiendo de la
% configuración del usuario.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spPcirc]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el término \emph{«perímetro de \dots»} seguido por
% el \emph{«concepto»} establecido por la llave \mykey{read-sty}. El
% valor de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \keyexampcmd{read-sty}{circun \textnormal{\textcolor{lightgray}{\textbar}}
%   circulo}{circun}[spPcirc]
%
% Establece el \emph{concepto} que verbalizará \mynvmath{NVDA/MathCAT}
% después de verbalizar el término \emph{«perímetro de \dots»} al ejecutar el
% comando sin modificar la impresión de este. Si se establece en |circun|,\newline
% |$\spPcirc[read-sty=circun](O)$| se verbalizará \emph{«perímetro de la circunferencia con centro en O»}
% en \mynvmath{NVDA/MathCAT}; si se establece en |circulo|, |$\spPcirc[read-sty=circulo](O)$|
% se verbalizará \emph{«perímetro del círculo con centro en O»} en \mynvmath{NVDA/MathCAT}.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spPcirc]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el término \emph{«perímetro de \dots»} y el \emph{«concepto»} establecido
% por la llave \mykey{read-sty} al ejecutar el comando sin modificar la
% impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% verbalizará el término, el concepto y el argumento \myparg{centro} o
% \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} solo verbalizará el
% argumento \myparg{centro} acorde al valor establecido por la llave \mykey{read-cmd}.
%
% \begin{important}*
%   Utilizar \mykey{concept}|=false| sin el argumento \myparg{centro} o con el
%   argumento \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
%   es equivalente a usar \mykey{silent}|=true|.
% \end{important}
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spPcirc]
%
% Sobrescribe el \emph{concepto} establecido por la llave \mykey{read-sty}
% que será verbalizado por \mynvmath{NVDA/MathCAT} al ejecutar el
% comando sin modificar la impresión de este. El valor de esta llave
% \emph{siempre} debe pasarse entre llaves `|{ }|' y se debe usar en
% conjunto con la llave \mykey{concept}.
%
% Por ejemplo:~|$\spPcirc[read-sty=circulo,read-arg={disco}](O)$|
% se verbalizará \emph{«perímetro del disco con centro en O»} en \mynvmath{NVDA/MathCAT};
% |$\spPcirc[concept=false,read-arg={disco}](O)$| se verbalizará
% \emph{«disco con centro en O»} en \mynvmath{NVDA/MathCAT}.
%
% \begin{important}*
%   Utilizar esta llave con \mykey{concept}|=false| y el argumento
%   \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
%   es equivalente a usar \mykey{silent}|=true|.
% \end{important}
%
% \keyexampcmd{print-sym}{usual \textnormal{\textcolor{lightgray}{\textbar}}
%   odot}{odot}[spPcirc]
%
% Establece si se \emph{usará} |\bigodot| o la letra `$C$' en la impresión
% del comando sin modificar la verbalización de este. Si se establece en |usual|,
% |$\spPcirc[print-sym=usual](O)$| imprimirá `$\mathcal{P}_{C(O)}$'
% en la salida \texttt{PDF}; si se establece en \mykey{odot}, |$\spPcirc[print-sym=odot](O)$|
% imprimirá `$\mathcal{P}_{\bigodot(O)}$' en la salida \texttt{PDF}.
%
% \keyexampcmd{sub-index}{subíndice}{no usado}[spPcirc]
%
% Establece un \emph{subíndice} sobre |\bigodot| o la letra `$C$' establecida
% por la llave \mykey{print-sym} cuando se utiliza el argumento \myparg{centro}.
% El valor de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
% Por ejemplo:~|$\spPcirc[sub-index={1}](O)$| imprimirá `$\mathcal{P}_{\bigodot_{1}(O)}$'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«perímetro de la circunferencia uno con centro en O»}.
%
% \keyexampcmd{read-sub}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spPcirc]
%
% Establece si se \emph{antepone} la palabra \emph{«sub»} al verbalizar
% el subíndice establecido por la llave \mykey{sub-index} o al usar el
% argumento \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
% al ejecutar el comando sin modificar la impresión de este. Si se establece
% en |true|, |$\spPcirc[read-sub=true](1)$| se verbalizará
% \emph{«perímetro de la circunferencia sub uno»} en \mynvmath{NVDA/MathCAT};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} verbalizará \emph{«perímetro de la circunferencia uno»}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spPcirc]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del término \emph{«perímetro de \dots»}, del concepto establecido por la
% llave \mykey{read-sty} y del argumento \myparg{centro} o \myparg{1 \textnormal{\textcolor{lightgray}{\textbar}} 2 \textnormal{\textcolor{lightgray}{\textbar}} 3 \textnormal{\textcolor{lightgray}{\textbar}} \ldots \textnormal{\textcolor{lightgray}{\textbar}} n}
% pasado al comando sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se
% establece en |false|, \mynvmath{NVDA/MathCAT} sí verbalizará la
% salida del comando.
%
% \subsection*{El comando \cs[no-index]{spVol}}\label{subsec:spVol}
%
% \begin{function}{\spVol}
%   \begin{syntax}
%     |\spVol| \qquad\qquad |\spVol|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   \end{syntax}
%   El comando \ics*{spVol} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   mediante el cual se establece la \emph{«forma»} en la que se verbalizará
%   en \mynvmath{NVDA/MathCAT}.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spVol$| imprimirá `$\mathcal{V}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«volumen»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spVol}}
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spVol]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el concepto \emph{«volumen»} al ejecutar el comando sin modificar la
% impresión de este. Si se establece en |true|, |$\spVol$| se verbaliza
% como \emph{«volumen»} en \mynvmath{NVDA/MathCAT}; si se establece en
% |false|, se verbalizará la letra `$\mathcal{V}$' acorde al valor
% establecido por la llave \mykey{read-cmd}.
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spVol]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará la
% letra `$\mathcal{V}$' cuando \mykey{concept}|=false|. Si se establece en |school|,
% \mynvmath{NVDA/MathCAT} verbalizará únicamente la letra `$V$'; si se
% establece en |mathcat|, \mynvmath{NVDA/MathCAT} aplica sus propias reglas
% sobre `$\mathcal{V}$'.
%
% \keyexampcmd{sub-index}{subíndice}{no usado}[spVol]
%
% Establece un \emph{subíndice} sobre `$\mathcal{V}$'. El valor de esta
% llave \emph{siempre} debe pasarse entre llaves `|{ }|'. Por ejemplo:~%
% |$\spVol[sub-index={1}]$| imprimirá `$\mathcal{V}_{1}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«volumen uno»}.
%
% \keyexampcmd{read-sub}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spVol]
%
% Establece si se \emph{antepone} la palabra \emph{«sub»} al verbalizar
% el subíndice establecido por la llave \mykey{sub-index} al ejecutar el
% comando sin modificar la impresión de este. Si se establece en |true|,\newline
% |$\spVol[sub-index={1},read-sub=true]$| se verbalizará \emph{«volumen sub
% uno»} en \mynvmath{NVDA/MathCAT}; si se establece en |false|,
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«volumen uno»}.
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spVol]
%
% Sobrescribe el \emph{concepto} establecido por la llave \mykey{concept} que
% será verbalizado por \mynvmath{NVDA/MathCAT} al ejecutar el comando sin
% modificar la impresión de este. El valor de esta llave \emph{siempre}
% debe pasarse entre llaves `|{ }|' y se debe usar en conjunto
% con la llave \mykey{concept}.
%
% Por ejemplo:~|$\spVol[read-arg={medida exacta}]$| imprimirá `$\mathcal{V}$'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«volumen medida exacta»};
% |$\spVol[concept=false,read-arg={medida exacta}]$| imprimirá `$\mathcal{V}$'
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«medida exacta»}.
%
% \begin{important}*
%  Las llaves \mykey{sub-index} y \mykey{read-sub} no tienen efecto al usar \mykey{read-arg}.
% \end{important}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spVol]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del \emph{concepto} establecido por la llave \mykey{concept} y de
% `$\mathcal{V}$' al ejecutar el comando sin modificar la impresión de
% este. Si se establece en |true|, \mynvmath{NVDA/MathCAT} NO verbalizará
% la salida del comando; si se establece en |false|, \mynvmath{NVDA/MathCAT}
% sí verbalizará la salida del comando.
%
% \section{Utilidades para álgebra}\label{sec:algebra}
%
% El paquete \mypkg*[no-index]{spintent} provee utilidades para trabajar con
% álgebra, descritas en esta sección: |\spinterval| para los intervalos,
% |\spfun| para las funciones, |\spdom|, |\sprec| y |\spcod| para el dominio, el
% recorrido y el codominio de una función, |\spfuncomp| para la composición de
% funciones y |\spabs| para el valor absoluto. Todos trabajan en \emph{modo
% matemático} y reciben un \emph{argumento obligatorio}.
%
% El nombre de una función sigue la misma regla en |\spfun|, |\spdom|, |\sprec|,
% |\spcod| y |\spfuncomp| (v.\,pág.\,\pageref{subsec:spfun}). Un argumento
% que no cumple la forma documentada devuelve un \enquote{error} y se detiene
% la compilación del documento.
%
% \subsection*{El comando \cs[no-index]{spinterval}}\label{subsec:spinterval}
%
% \begin{function}{\spinterval}
%   \begin{syntax}
%     |\spinterval|\mymarg{intervalo} \qquad\qquad |\spinterval|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{intervalo}
%   \end{syntax}
%   El comando \ics*{spinterval} trabaja en \emph{modo matemático} y recibe
%   el \emph{argumento} obligatorio \mymarg{intervalo} de la forma
%   \mymarg{(a\textnormal{\textcolor{red}{;}} b)}, donde `$a$' y `$b$' son los
%   \emph{extremos} del intervalo separados por un punto y coma
%   `\textcolor{red}{;}' o, si no hay punto y coma, por una única coma
%   `\textcolor{red}{,}'. El delimitador izquierdo puede ser `|(|', `|[|' o
%   `|]|' y el derecho `|)|', `|]|' o `|[|', en cualquier combinación.
%   Los extremos numéricos se procesan internamente bajo el comando
%   |\spnum|; cualquier otro extremo, como una letra, un subíndice, una
%   fracción o una raíz, se imprime tal cual y lo verbaliza
%   \mynvmath{NVDA/MathCAT}.
% \end{function}
%
% Un delimitador `|(|' o `|]|' a la izquierda, y `|)|' o `|[|' a la derecha,
% \emph{excluye} su extremo y el intervalo es \emph{abierto} en él; los demás
% delimitadores lo \emph{incluyen} y el intervalo es \emph{cerrado} en él.
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
% y el intervalo es abierto en sus dos extremos, |$\spinterval{(1,5)}$|
% imprimirá `$(1,5)$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«intervalo abierto de uno a cinco»}; la notación
% chilena |$\spinterval{]1,5[}$| imprimirá `$]1,5[$' y se verbalizará igual.
%
% Si el intervalo es cerrado en sus dos extremos, |$\spinterval{[1,5]}$|
% imprimirá `$[1,5]$' y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«intervalo cerrado de uno a cinco»}.
%
% Si el intervalo es abierto en un extremo y cerrado en el otro,
% |$\spinterval{]1,5]}$| imprimirá `$]1,5]$' y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«intervalo abierto en uno y cerrado en cinco»};
% |$\spinterval{[1,5[}$| imprimirá `$[1,5[$' y verbalizará
% \emph{«intervalo cerrado en uno y abierto en cinco»}.
%
% Si un extremo es infinito, |$\spinterval{]-\infty,3]}$| imprimirá
% `$]-\infty,3]$' y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«intervalo abierto en menos infinito y cerrado en tres»}.
%
% Si los extremos son números decimales con coma, el separador debe ser\newline
% el punto y coma:~|$\spinterval{[3,5;4,2]}$| imprimirá `$[3{,}5;4{,}2]$' y
% \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«intervalo cerrado de tres coma cinco a cuatro coma dos»}.
%
% El comando |\spinterval| devuelve un \enquote{error} y se detiene la
% compilación del documento cuando el \mymarg{intervalo} no comienza con un
% delimitador izquierdo y termina con uno derecho de los indicados, cuando un
% extremo está vacío, cuando no hay separador, cuando hay más de una coma sin
% punto y coma, cuando las llaves no están balanceadas o cuando el infinito
% está mal ubicado o se incluye en el intervalo.
%
% \begin{important}*
%   El argumento \mymarg{intervalo} siempre debe pasarse entre llaves
%   `|{ }|', incluso cuando comienza con un corchete: sin ellas,
%   |$\spinterval[1,5]$| interpreta los corchetes como el argumento opcional
%   \myoarg{key \textnormal{\textcolor{gray}{=}} val}.
% \end{important}
%
% \begin{important}*
%   El infinito `$\mathcolor{red}{\infty}$' nunca se incluye en el intervalo: `$-\infty$' solo
%   puede ir en el extremo izquierdo y `$\mathcolor{red}{\infty}$' en el derecho, ambos junto
%   a un delimitador que los excluya.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spinterval}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spinterval]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará los
% extremos del intervalo. Si se establece en |school|,
% |$\spinterval[read-cmd=school]{]-2,+4[}$| se verbalizará
% \emph{«intervalo abierto de menos dos a más cuatro»}; si se establece en
% |mathcat|, \mynvmath{NVDA/MathCAT} aplica sus propias reglas y\newline
% |$\spinterval[read-cmd=mathcat]{]-2,+4[}$| se verbalizará
% \emph{«intervalo abierto de menos dos a positivo cuatro»}.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spinterval]
%
% Establece el \emph{prefijo} que será verbalizado por \mynvmath{NVDA/MathCAT}
% \emph{antes} de verbalizar el \emph{«concepto»} intervalo. El valor de esta
% llave \emph{siempre} debe pasarse entre llaves `|{ }|'. Por ejemplo:~%
% |$\spinterval[prefix={el}]{(1,5)}$| imprimirá `$(1,5)$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«el intervalo abierto de uno a cinco»}.
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spinterval]
%
% Sobrescribe la verbalización del \emph{intervalo} completo que será
% verbalizada por \mynvmath{NVDA/MathCAT} al ejecutar el comando sin modificar
% la impresión de este. El valor de esta llave \emph{siempre} debe pasarse
% entre llaves `|{ }|'. Por ejemplo:~|$\spinterval[read-arg={rango}]{[1,5]}$|
% imprimirá `$[1,5]$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«rango»}.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spinterval]
%
% Establece si se \emph{verbaliza} en \mynvmath{NVDA/MathCAT}
% el \emph{«concepto»} intervalo y el prefijo establecido por la llave
% \mykey{prefix} al ejecutar el comando sin modificar la impresión de este. Si
% se establece en |true|, \mynvmath{NVDA/MathCAT} verbalizará el
% \emph{«concepto»} y los extremos; si se establece en |false|,
% |$\spinterval[concept=false]{(1,5)}$| se verbalizará
% \emph{«abierto de uno a cinco»}.
%
% \keyexampcmd{size}{auto \textnormal{\textcolor{lightgray}{\textbar}}
%   none \textnormal{\textcolor{lightgray}{\textbar}}
%   big \textnormal{\textcolor{lightgray}{\textbar}}
%   Big \textnormal{\textcolor{lightgray}{\textbar}}
%   bigg \textnormal{\textcolor{lightgray}{\textbar}}
%   Bigg}{auto}[spinterval]
%
% Establece el \emph{tamaño} de los delimitadores del intervalo sin modificar
% la verbalización de este. Si se establece en \mykey{auto}, los delimitadores
% conservan su tamaño normal salvo que algún extremo contenga una fracción
% |\frac| o |\dfrac|, en cualquier posición del extremo: en ese caso se usan
% |\bigl| y |\bigr| en estilo texto, |\Bigl| y |\Bigr| en estilo display, y
% siempre |\Bigl| y |\Bigr| cuando la fracción es |\dfrac|. Si se establece en
% \mykey{none}, los delimitadores nunca cambian de tamaño; si se establece en
% \mykey{big}, \mykey{Big}, \mykey{bigg} o \mykey{Bigg}, se usan siempre
% |\bigl| y |\bigr|, |\Bigl| y |\Bigr|, |\biggl| y |\biggr|, o |\Biggl| y
% |\Biggr|, respectivamente.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spinterval]
%
% Establece si se \emph{silencia} la verbalización en \mynvmath{NVDA/MathCAT}
% del \mymarg{intervalo} pasado al comando sin modificar la impresión de este.
% Si se establece en |true|, \mynvmath{NVDA/MathCAT} NO verbalizará la salida
% del comando; si se establece en |false|, \mynvmath{NVDA/MathCAT} sí
% verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spfun}}\label{subsec:spfun}
%
% \begin{function}{\spfun}
%   \begin{syntax}
%     |\spfun|\mymarg{función} \qquad\qquad |\spfun|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{función}
%   \end{syntax}
%   El comando \ics*{spfun} trabaja en \emph{modo matemático} y recibe el
%   \emph{argumento} obligatorio \mymarg{función} de la forma
%   \mymarg{f(x)}, donde `$f$' es el \emph{nombre} de la función y lo que va
%   entre paréntesis son uno o más \emph{argumentos} separados por coma
%   `\textcolor{red}{,}'. El nombre es una letra, mayúscula o minúscula, con
%   prima(s) `|'|' o subíndice `|_{...}|' opcionales; el subíndice solo admite
%   letras y dígitos. Los argumentos pueden ser letras, números o
%   expresiones.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spfun{f(x)}$| imprimirá `$f(x)$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«f de x»};
% |$\spfun{f_{1}(x)}$| imprimirá `$f_{1}(x)$' y verbalizará
% \emph{«f sub uno de x»}; |$\spfun{f'(x)}$| imprimirá `$f'(x)$' y
% verbalizará \emph{«f prima de x»}; |$\spfun{f''(x)}$| imprimirá
% `$f''(x)$' y verbalizará \emph{«f doble prima de x»}.
%
% Con dos o más argumentos, |$\spfun{f(x,y)}$| imprimirá `$f(x,y)$' y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«f de x coma y»}. Un argumento
% puede ser una expresión: |$\spfun{f(x+1)}$| imprimirá `$f(x+1)$' y
% verbalizará \emph{«f de x más 1»}.
%
% Un argumento puede ser a su vez una llamada, tantas veces como se quiera:
% |$\spfun{f(g(x))}$| imprimirá `$f(g(x))$' y verbalizará
% \emph{«f de g de x»}; |$\spfun{f(f^{-1}(y))}$| imprimirá `$f(f^{-1}(y))$' y
% verbalizará \emph{«f de f inversa de y»}; |$\spfun{f(g(x),h(y))}$| imprimirá
% `$f(g(x),h(y))$'. Una llamada anidada se valida igual que la exterior. Si
% el argumento es otra cosa, como |g(x)+1|, se imprime y se verbaliza con
% normalidad pero sin anotar la llamada que contiene. Las llaves
% \mykey{read-cmd} y \mykey{silent} valen para todos los niveles;
% \mykey{concept} y \mykey{prefix} solo para la llamada exterior.
%
% Las comas que quedan dentro de llaves, de paréntesis o de corchetes no
% separan argumentos. Por eso, un número decimal con coma debe escribirse
% entre llaves: |$\spfun{f(1{,}5)}$| tiene un solo argumento.
%
% El comando |\spfun| devuelve un \enquote{error} y se detiene la compilación
% del documento cuando el \mymarg{función} no comienza con un nombre válido,
% cuando el subíndice contiene algo distinto de letras o dígitos, cuando el
% nombre no va seguido de paréntesis, cuando los paréntesis, las llaves o los
% corchetes no están balanceados, cuando un argumento está vacío o cuando hay
% algo después del paréntesis que cierra. Lo mismo vale para una llamada
% anidada en un argumento.
%
% \subsubsection*{Llaves para \cs[no-index]{spfun}}
%
% \keyexampcmd{read-cmd}{school \textnormal{\textcolor{lightgray}{\textbar}}
%   mathcat}{school}[spfun]
%
% Establece la \emph{forma} en la que \mynvmath{NVDA/MathCAT} verbalizará el
% nombre de la función. Si se establece en |school|, \mynvmath{NVDA/MathCAT}
% verbaliza el nombre sin el prefijo «mayúscula»: |$\spfun{F''(x)}$| se
% verbaliza \emph{«F doble prima de x»}; si se establece en |mathcat|,
% \mynvmath{NVDA/MathCAT} aplica sus propias reglas, que pueden incluir el
% prefijo «mayúscula» dependiendo de la configuración del usuario.
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spfun]
%
% Establece el \emph{prefijo} que será verbalizado por
% \mynvmath{NVDA/MathCAT} \emph{antes} de verbalizar el concepto
% \emph{«función»}. El valor de esta llave \emph{siempre} debe pasarse entre
% llaves `|{ }|'. Solo se verbaliza cuando \mykey{concept} es |true|.
%
% \keyexampcmd{concept}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spfun]
%
% Establece si se verbaliza el concepto \emph{«función»} antes de la
% función. Si se establece en |true|, \mynvmath{NVDA/MathCAT} verbalizará el
% concepto, el prefijo establecido por la llave \mykey{prefix} y la función;
% si se establece en |false|, solo verbalizará la función.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spfun]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} de la \mymarg{función} pasada al comando sin
% modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se
% establece en |false|, sí verbalizará la salida del comando. El argumento
% se valida igual en ambos casos.
%
% \subsection*{Los comandos \cs[no-index]{spdom}, \cs[no-index]{sprec} y \cs[no-index]{spcod}}\label{subsec:spdom}
%
% \begin{function}{\spdom, \sprec, \spcod}
%   \begin{syntax}
%     |\spdom|\mymarg{función} \quad\quad |\spdom|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{función}
%     |\sprec|\mymarg{función} \quad\quad |\sprec|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{función}
%     |\spcod|\mymarg{función} \quad\quad |\spcod|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{función}
%   \end{syntax}
%   Los comandos \ics*{spdom}, \ics*{sprec} y \ics*{spcod} trabajan en
%   \emph{modo matemático} y reciben el \emph{argumento} obligatorio
%   \mymarg{función}: el nombre de una función como en |\spfun|, esto es, una letra
%   con prima(s) o subíndice opcionales y, si se quiere, el exponente
%   `|^{-1}|', sin argumentos. Imprimen respectivamente $\operatorname{Dom}$,
%   $\operatorname{Rec}$ y $\operatorname{Cod}$ seguidos de la función entre
%   paréntesis.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val}, |$\spdom{f}$|
% imprimirá `$\operatorname{Dom}(f)$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«dominio de f»};
% |$\sprec{f}$| imprimirá `$\operatorname{Rec}(f)$' y verbalizará
% \emph{«recorrido de f»}; |$\spcod{f}$| imprimirá
% `$\operatorname{Cod}(f)$' y verbalizará \emph{«codominio de f»}.
%
% El nombre de la función admite prima(s) y subíndice, y se verbaliza sin
% anunciar la mayúscula: |$\spdom{f'}$| imprimirá `$\operatorname{Dom}(f')$'
% y verbalizará \emph{«dominio de f prima»}; |$\sprec{f_{1}}$| imprimirá
% `$\operatorname{Rec}(f_{1})$' y verbalizará \emph{«recorrido de f sub
% uno»}; |$\spcod{F''}$| imprimirá `$\operatorname{Cod}(F'')$' y verbalizará
% \emph{«codominio de F doble prima»}.
%
% Con la inversa, |$\spdom{f^{-1}}$| imprimirá
% `$\operatorname{Dom}(f^{-1})$' y verbalizará
% \emph{«dominio de f inversa»};\newline |$\sprec{f^{-1}}$| verbalizará
% \emph{«recorrido de f inversa»}.
%
% Los comandos devuelven un \enquote{error} y se detiene la compilación
% del documento cuando el argumento no es el nombre de una función.
%
% \subsubsection*{Llaves para \cs[no-index]{spdom}, \cs[no-index]{sprec} y \cs[no-index]{spcod}}
%
% \keyexampcmd{prefix}{prefijo}{no usado}[spdom]
%
% Establece el \emph{prefijo} que será verbalizado por
% \mynvmath{NVDA/MathCAT} \emph{antes} de la palabra del concepto. El valor
% de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'. Con
% |$\spdom[prefix={el}]{f}$|, \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«el dominio de f»}; con |$\sprec[prefix={el}]{f}$|, \emph{«el
% recorrido de f»}; con |$\spcod[prefix={el}]{f}$|, \emph{«el codominio de
% f»}. La impresión no cambia.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spdom]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} de la \mymarg{función} pasada al comando sin
% modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando:
% |$\spdom[silent=true]{f}$| imprimirá `$\operatorname{Dom}(f)$' y no se
% verbalizará nada; si se establece en |false|, sí verbalizará la salida del
% comando. El argumento se valida igual en ambos casos.
%
% \subsection*{El comando \cs[no-index]{spfuncomp}}\label{subsec:spfuncomp}
%
% \begin{function}{\spfuncomp}
%   \begin{syntax}
%     |\spfuncomp|\mymarg{composición} \qquad\qquad |\spfuncomp|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{composición}
%   \end{syntax}
%   El comando \ics*{spfuncomp} trabaja en \emph{modo matemático} y recibe el
%   \emph{argumento} obligatorio \mymarg{composición}: dos o más
%   \emph{funciones} separadas por |\circ|, como en \mymarg{f\circ g} o
%   \mymarg{f\circ g\circ h}. Cada función es un nombre como en |\spfun|:
%   una letra, mayúscula o minúscula, con prima(s) o subíndice opcionales, y el
%   exponente `|^{-1}|' de la función inversa. La composición puede ir entre
%   paréntesis, \mymarg{(f\circ g)}, y puede aplicarse a uno o más argumentos
%   entre paréntesis separados por coma, \mymarg{(f\circ g)(x)}. Los
%   argumentos siguen las reglas de |\spfun|: pueden ser letras, números,
%   expresiones o llamadas.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spfuncomp{f\circ g}$| imprimirá `$f\circ g$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«f compuesta con g»};
% |$\spfuncomp{f\circ g\circ h}$| imprimirá `$f\circ g\circ h$' y verbalizará
% \emph{«f compuesta con g compuesta con h»};
% |$\spfuncomp{(f\circ g)(x)}$| imprimirá `$(f\circ g)(x)$' y verbalizará
% \emph{«f compuesta con g de x»}; |$\spfuncomp{(f^{-1}\circ g)(x)}$| imprimirá
% `$(f^{-1}\circ g)(x)$' y verbalizará \emph{«f inversa compuesta con g de x»}.
% Un argumento puede ser una llamada: |$\spfuncomp{(f\circ g)(h(x))}$|
% verbalizará \emph{«f compuesta con g de h de x»}.
%
% El comando |\spfuncomp| devuelve un \enquote{error} y se detiene la
% compilación del documento cuando hay menos de dos funciones, cuando algo
% entre los |\circ| no es el nombre de una función, cuando los paréntesis no
% están balanceados, cuando lo que sigue a la composición no es una lista de
% argumentos entre paréntesis o cuando un argumento está vacío. La forma
% |f\circ g(x)|, sin paréntesis alrededor de la composición, no es válida
% porque es ambigua.
%
% \subsubsection*{Llaves para \cs[no-index]{spfuncomp}}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spfuncomp]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} de la \mymarg{composición} pasada al comando sin
% modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se
% establece en |false|, sí verbalizará la salida del comando. El argumento
% se valida igual en ambos casos.
%
% La composición no admite \mykey{prefix} ni \mykey{read-cmd}: no lleva
% palabra de concepto y su lectura es fija. Los nombres se verbalizan sin
% anunciar la mayúscula.
%
% \subsection*{El comando \cs[no-index]{spabs}}\label{subsec:spabs}
%
% \begin{function}{\spabs}
%   \begin{syntax}
%     |\spabs|\mymarg{x} \qquad\qquad |\spabs|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{x}
%   \end{syntax}
%   El comando \ics*{spabs} trabaja en \emph{modo matemático} y recibe el
%   \emph{argumento obligatorio} \mymarg{x}: cualquier expresión matemática.
%   Imprime el valor absoluto de \mymarg{x} entre las barras |\lvert| y
%   |\rvert|.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spabs{x}$| imprimirá `$\lvert x\rvert$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«valor absoluto de x»};
% |$\spabs{-5}$| imprimirá `$\lvert -5\rvert$' y verbalizará \emph{«valor
% absoluto de menos 5»}; |$\spabs{x-3}$| imprimirá `$\lvert x-3\rvert$' y
% verbalizará \emph{«valor absoluto de x menos 3 fin valor absoluto»}.
%
% Cuando el argumento tiene más de un símbolo, sin contar un signo `|+|' o
% `|-|' inicial, \mynvmath{NVDA/MathCAT} anuncia el final del valor absoluto
% con \emph{«fin valor absoluto»}. Así se sabe dónde termina cuando la
% expresión sigue: |$\spabs{x-3} - 1$| verbalizará \emph{«valor absoluto de x
% menos 3 fin valor absoluto menos 1»}. La llave \mykey{close} controla
% este cierre.
%
% El comando |\spabs| devuelve un \enquote{error} y se detiene la compilación
% del documento cuando el argumento está vacío.
%
% \subsubsection*{Llaves para \cs[no-index]{spabs}}
%
% \keyexampcmd{close}{auto \textnormal{\textcolor{lightgray}{\textbar}}
%   true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{auto}[spabs]
%
% Establece si \mynvmath{NVDA/MathCAT} verbaliza el cierre \emph{«fin valor
% absoluto»} después del argumento. Con |auto| el cierre se verbaliza solo
% cuando \mymarg{x} tiene más de un símbolo, sin contar un signo |+| o |-|
% inicial: |$\spabs{x}$| y |$\spabs{-5}$| no lo llevan y |$\spabs{x-3}$| sí.
% Con |true| siempre se verbaliza: |$\spabs[close=true]{x}$| verbalizará
% \emph{«valor absoluto de x fin valor absoluto»}. Con |false| nunca se
% verbaliza: |$\spabs[close=false]{x-3}$| verbalizará \emph{«valor absoluto de
% x menos 3»}. La llave no tiene efecto con \mykey{silent} ni con
% \mykey{read-arg}.
%
% \keyexampcmd{size}{none \textnormal{\textcolor{lightgray}{\textbar}}
%   big \textnormal{\textcolor{lightgray}{\textbar}} Big
%   \textnormal{\textcolor{lightgray}{\textbar}} bigg
%   \textnormal{\textcolor{lightgray}{\textbar}} Bigg}{none}[spabs]
%
% Establece el \emph{tamaño} de las barras. Con |none| no se cambia el
% tamaño; con |big|, |Big|, |bigg| y |Bigg| se fija con |\bigl|, |\Bigl|,
% |\biggl| y |\Biggl|, y sus pares de cierre. Por ejemplo:~%
% |$\spabs[size=Big]{\dfrac{x}{2}}$| imprime las barras más grandes que el
% texto y no cambia la verbalización.
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spabs]
%
% Sobrescribe lo que verbalizará \mynvmath{NVDA/MathCAT} al ejecutar el
% comando sin modificar la impresión de este. El valor de esta llave
% \emph{siempre} debe pasarse entre llaves `|{ }|'. Por ejemplo:~%
% |$\spabs[read-arg={módulo de x}]{x}$| imprimirá `$\lvert x\rvert$' en la
% salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«módulo de
% x»}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spabs]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} del valor absoluto al ejecutar el comando sin
% modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se
% establece en |false|, sí verbalizará la salida del comando.
%
% \section{Utilidades para estadística}\label{sec:estadistica}
%
% El paquete \mypkg*[no-index]{spintent} provee utilidades para trabajar con
% estadística y probabilidad, descritas en esta sección. Todos los comandos
% trabajan en \emph{modo matemático}.
%
% Los comandos |\spmedia|, |\spmediana|, |\spmoda|, |\spdesv|, |\spvarz|,
% |\sprango|, |\spriq|, |\spmin|, |\spmax| y |\spOmega| escriben un símbolo y no
% reciben argumento obligatorio, solo el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val} que establece cómo se
% verbaliza. Los demás, que son |\spProm|, |\spcuartil|, |\spdecil|, |\spPerc|,
% |\spFrec|, |\spcase|, |\spPval|, |\spProb|, |\spcomp|, |\spcomb| y |\spfact|,
% reciben \emph{argumentos obligatorios}; si no cumplen la forma documentada
% devuelven un \enquote{error} y se detiene la compilación del documento.
%
% \subsection*{El comando \cs[no-index]{spProm}}\label{subsec:spProm}
%
% \begin{function}{\spProm}
%   \begin{syntax}
%     |\spProm|\mymarg{argumento} \qquad\qquad |\spProm|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{argumento}
%   \end{syntax}
%   El comando \ics*{spProm} trabaja en \emph{modo matemático} y recibe el
%   \emph{argumento} obligatorio \mymarg{argumento}: cualquier expresión
%   matemática, como una letra, una letra con subíndice o una expresión.
%   Lo tipografía con una barra encima, la notación usual del
%   \emph{«promedio»}.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spProm{x}$| imprimirá `$\bar{x}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«promedio de x»};
% |$\spProm{x_{1}}$| imprimirá `$\bar{x_{1}}$' y verbalizará
% \emph{«promedio de x subíndice uno»}.
%
% \begin{important}*
%   El \mymarg{argumento} se tipografía directamente, sin procesamiento
%   semántico adicional, y \mynvmath{NVDA/MathCAT} lo verbaliza con sus
%   propias reglas.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spProm}}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spProm]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} del \mymarg{argumento} pasado al comando sin
% modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se
% establece en |false|, sí verbalizará la salida del comando.
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spProm]
%
% Sobrescribe las palabras \emph{«promedio de»} que verbaliza
% \mynvmath{NVDA/MathCAT} antes del \mymarg{argumento}, sin modificar la
% impresión del comando. El valor de esta llave \emph{siempre} debe pasarse
% entre llaves `|{ }|'. Por ejemplo:~|$\spProm[read-arg={media de}]{x}$|
% imprimirá `$\bar{x}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«media de x»}.
%
% \subsection*{Los comandos \cs[no-index]{spmedia}, \cs[no-index]{spmediana}, \cs[no-index]{spmoda}, \cs[no-index]{spdesv} y \cs[no-index]{spvarz}}\label{subsec:spmedia}
%
% \begin{function}{\spmedia, \spmediana, \spmoda, \spdesv, \spvarz}
%   \begin{syntax}
%     |\spmedia|   \quad\quad |\spmedia|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%     |\spmediana| \quad\quad |\spmediana|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%     |\spmoda|    \quad\quad |\spmoda|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%     |\spdesv|    \quad\quad |\spdesv|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%     |\spvarz|    \quad\quad |\spvarz|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   \end{syntax}
%   Los comandos \ics*{spmedia}, \ics*{spmediana}, \ics*{spmoda},
%   \ics*{spdesv} y \ics*{spvarz} trabajan en \emph{modo matemático} y
%   reciben el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   mediante el cual se establece la \emph{«forma»} en la que se
%   verbalizarán en \mynvmath{NVDA/MathCAT} los símbolos de la estadística
%   descriptiva.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}, |$\spmedia$|
% imprimirá `$\mathrm{MA}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«media aritmética»};
% |$\spmediana$| imprimirá `$\mathrm{MD}$' y verbalizará
% \emph{«mediana»}; |$\spmoda$| imprimirá `$\mathrm{MO}$' y verbalizará
% \emph{«moda»}; |$\spdesv$| imprimirá `$s$' y verbalizará
% \emph{«desviación estándar s»}; |$\spvarz$| imprimirá `$s^{2}$' y
% verbalizará \emph{«varianza s al cuadrado»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spmedia}, \cs[no-index]{spmediana}, \cs[no-index]{spmoda}, \cs[no-index]{spdesv} y \cs[no-index]{spvarz}}
%
% \keyexampcmd{sigma}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spdesv,spvarz]
%
% Establece si se \emph{imprime} la letra `$\sigma$' en lugar de la letra
% `$s$'. Si se establece en |true|, |$\spdesv[sigma=true]$| imprimirá
% `$\sigma$' y |$\spvarz[sigma=true]$| imprimirá `$\sigma^{2}$' en la
% salida \texttt{PDF}, y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«desviación estándar sigma»} y \emph{«varianza sigma al cuadrado»}.
% Esta llave solo existe para |\spdesv| y |\spvarz|.
%
% \keyexampcmd{sub-index}{subíndice}{no usado}[spmedia,spmediana,spmoda,spdesv,spvarz]
%
% Establece un \emph{subíndice} sobre el símbolo del comando, que
% \mynvmath{NVDA/MathCAT} verbaliza después del concepto con la palabra
% \emph{«de»}, o \emph{«sub»} en |\spdesv| y |\spvarz| según la llave
% \mykey{read-sub}. El valor de esta llave \emph{siempre} debe pasarse entre
% llaves `|{ }|'. Por ejemplo:~|$\spmedia[sub-index={x}]$| imprimirá
% `$\mathrm{MA}_{x}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«media aritmética de x»};
% |$\spdesv[sub-index={x}]$| imprimirá `$s_{x}$' y verbalizará
% \emph{«desviación estándar s sub x»}; |$\spvarz[sub-index={x}]$| imprimirá
% `$s_{x}^{2}$' y verbalizará \emph{«varianza s sub x al cuadrado»}.
%
% \keyexampcmd{read-sub}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spmedia,spmediana,spmoda,spdesv,spvarz]
%
% Establece si se \emph{usa} la palabra \emph{«sub»} en lugar de
% \emph{«de»} al verbalizar el subíndice establecido por la llave
% \mykey{sub-index}, sin modificar la impresión del comando. Si se
% establece en |true|,\newline |$\spmedia[sub-index={x},read-sub=true]$| se
% verbalizará \emph{«media aritmética sub x»} en \mynvmath{NVDA/MathCAT};
% si se establece en |false|, \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«media aritmética de x»}. El valor por defecto es |false|, salvo en
% |\spdesv| y |\spvarz|, donde es |true| porque estos comandos
% leen su letra y «s sub x» es más claro que «s de x»: en
% \emph{«varianza s de x al cuadrado»}, la «x» podría entenderse como parte
% del exponente. Así, |$\spvarz[sub-index={x},read-sub=false]$| verbalizará
% \emph{«varianza s de x al cuadrado»}.
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spmedia,spmediana,spmoda,spdesv,spvarz]
%
% Sobrescribe el \emph{concepto} que verbalizará \mynvmath{NVDA/MathCAT} al
% ejecutar el comando sin modificar la impresión de este. El valor de esta
% llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% Por ejemplo:~|$\spdesv[read-arg={desviación estándar poblacional}]$|
% imprimirá `$s$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«desviación estándar poblacional»}.
%
% \begin{important}*
%  Las llaves \mykey{sub-index} y \mykey{read-sub} no tienen efecto al usar \mykey{read-arg}.
% \end{important}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spmedia,spmediana,spmoda,spdesv,spvarz]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} del símbolo al ejecutar el comando sin modificar
% la impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% NO verbalizará la salida del comando; si se establece en |false|,
% \mynvmath{NVDA/MathCAT} sí verbalizará la salida del comando.
%
% \subsection*{Los comandos \cs[no-index]{sprango}, \cs[no-index]{spriq}, \cs[no-index]{spmin} y \cs[no-index]{spmax}}\label{subsec:sprango}
%
% \begin{function}{\sprango, \spriq, \spmin, \spmax}
%   \begin{syntax}
%     |\sprango| \quad\quad |\sprango|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%     |\spriq|   \quad\quad |\spriq|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%     |\spmin|   \quad\quad |\spmin|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%     |\spmax|   \quad\quad |\spmax|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   \end{syntax}
%   Los comandos \ics*{sprango}, \ics*{spriq}, \ics*{spmin} y \ics*{spmax}
%   trabajan en \emph{modo matemático} y reciben el \emph{argumento opcional}
%   \myoarg{key \textnormal{\textcolor{gray}{=}} val} mediante el cual se
%   establece la \emph{«forma»} en la que se verbalizarán en
%   \mynvmath{NVDA/MathCAT} estas medidas de la estadística descriptiva.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}, |$\sprango$|
% imprimirá `$R$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«rango»}; |$\spriq$| imprimirá `$\mathrm{RIQ}$' y
% verbalizará \emph{«rango intercuartil»}; |$\spmin$| imprimirá
% `$x_{\min}$' y verbalizará \emph{«mínimo»}; |$\spmax$| imprimirá
% `$x_{\max}$' y verbalizará \emph{«máximo»}.
%
% \subsubsection*{Llaves para \cs[no-index]{sprango}, \cs[no-index]{spriq}, \cs[no-index]{spmin} y \cs[no-index]{spmax}}
%
% Las llaves \mykey{silent}, \mykey{read-arg}, \mykey{sub-index} y
% \mykey{read-sub} de estos comandos funcionan como las de
% |\spmedia| (v.\,pág.\,\pageref{subsec:spmedia}), con una
% excepción: |\spmin| y |\spmax| solo tienen \mykey{silent} y
% \mykey{read-arg}, porque su símbolo ya lleva un subíndice.
%
% \subsection*{Los comandos \cs[no-index]{spcuartil}, \cs[no-index]{spdecil} y \cs[no-index]{spPerc}}\label{subsec:spcuartil}
%
% \begin{function}{\spcuartil, \spdecil, \spPerc}
%   \begin{syntax}
%     |\spcuartil|\mymarg{k} \quad\quad |\spcuartil|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{k}
%     |\spdecil|\mymarg{k}   \quad\quad |\spdecil|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{k}
%     |\spPerc|\mymarg{k}    \quad\quad |\spPerc|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{k}
%   \end{syntax}
%   Los comandos \ics*{spcuartil}, \ics*{spdecil} y \ics*{spPerc} trabajan
%   en \emph{modo matemático} y reciben el \emph{argumento obligatorio}
%   \mymarg{k}: un número entero sin signo, de 1 a 3 en \ics*{spcuartil},
%   de 1 a 9 en \ics*{spdecil} y de 1 a 99 en \ics*{spPerc}. Cualquier otro
%   valor devuelve un \enquote{error} y se detiene la compilación del documento.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spcuartil{1}$| imprimirá `$Q_{1}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«cuartil 1»};
% |$\spdecil{3}$| imprimirá `$D_{3}$' y verbalizará \emph{«decil 3»};
% |$\spPerc{25}$| imprimirá `$P_{25}$' y verbalizará
% \emph{«percentil 25»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spcuartil}, \cs[no-index]{spdecil} y \cs[no-index]{spPerc}}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spcuartil,spdecil,spPerc]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} del símbolo al ejecutar el comando sin modificar
% la impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% NO verbalizará la salida del comando; si se establece en |false|, sí
% verbalizará la salida del comando. Un índice inválido devuelve siempre un
% \enquote{error} y se detiene la compilación del documento,
% también con \mykey{silent}|=true|.
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spcuartil,spdecil,spPerc]
%
% Sobrescribe lo que verbalizará \mynvmath{NVDA/MathCAT} al ejecutar el
% comando sin modificar la impresión de este. El valor de esta llave
% \emph{siempre} debe pasarse entre llaves `|{ }|'.\newline Por ejemplo:~%
% |$\spcuartil[read-arg={primer cuartil}]{1}$| imprimirá `$Q_{1}$' en la
% salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«primer cuartil»}.
%
% \subsection*{El comando \cs[no-index]{spFrec}}\label{subsec:spFrec}
%
% \begin{function}{\spFrec}
%   \begin{syntax}
%     |\spFrec|\mymarg{símbolo} \qquad\qquad |\spFrec|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{símbolo}
%   \end{syntax}
%   El comando \ics*{spFrec} trabaja en \emph{modo matemático} y recibe el
%   \emph{argumento obligatorio} \mymarg{símbolo}: el símbolo de una
%   frecuencia, escrito tal como se imprime. Los símbolos válidos son
%   |f|, |F|, |f_{r}|, |F_{r}|, \myhlc|f_{r}\%| y \myhlc|F_{r}\%|; cualquier otro
%   devuelve un \enquote{error} y se detiene la compilación del documento, salvo
%   que se use la llave
%   \mykey{read-arg}.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spFrec{f}$| imprimirá `$f$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«frecuencia absoluta»};
% |$\spFrec{F}$| imprimirá `$F$' y verbalizará \emph{«frecuencia absoluta
% acumulada»}; |$\spFrec{f_{r}}$| imprimirá `$f_{r}$' y verbalizará
% \emph{«frecuencia relativa»}; |$\spFrec{F_{r}}$| imprimirá `$F_{r}$' y
% verbalizará \emph{«frecuencia relativa acumulada»};
% \myhlc|$\spFrec{f_{r}\%}$| imprimirá `$f_{r}\%$' y verbalizará \emph{«frecuencia
% porcentual»}; \myhlc|$\spFrec{F_{r}\%}$| imprimirá `$F_{r}\%$' y verbalizará
% \emph{«frecuencia porcentual acumulada»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spFrec}}
%
% \keyexampcmd{sub-index}{subíndice}{no usado}[spFrec]
%
% Establece un \emph{subíndice} sobre el símbolo, que
% \mynvmath{NVDA/MathCAT} verbaliza después del nombre de la frecuencia
% con la palabra \emph{«sub»}. Solo es válida con los símbolos |f| y |F|.
% El valor de esta llave \emph{siempre} debe pasarse entre llaves
% `|{ }|'. Por ejemplo:~|$\spFrec[sub-index={i}]{f}$| imprimirá `$f_{i}$' en
% la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«frecuencia absoluta sub i»}.
%
% \keyexampcmd{read-sub}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{true}[spFrec]
%
% Establece si se \emph{usa} la palabra \emph{«sub»} en lugar de
% \emph{«de»} al verbalizar el subíndice establecido por la llave
% \mykey{sub-index}, sin modificar la impresión del comando. Con
% |$\spFrec[sub-index={i},read-sub=false]{f}$|,
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«frecuencia absoluta de i»}.
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spFrec]
%
% Sobrescribe el \emph{nombre de la frecuencia} que verbalizará
% \mynvmath{NVDA/MathCAT} al ejecutar el comando sin modificar la
% impresión de este. Es la única forma de usar un símbolo que no está en
% la tabla anterior: |$\spFrec[read-arg={frecuencia absoluta}]{n_{i}}$|
% imprimirá `$n_{i}$' y verbalizará \emph{«frecuencia absoluta»}. El valor
% de esta llave \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% \begin{important}*
%  Las llaves \mykey{sub-index} y \mykey{read-sub} no tienen efecto al usar \mykey{read-arg}.
% \end{important}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spFrec]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} del símbolo al ejecutar el comando sin modificar
% la impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% NO verbalizará la salida del comando; si se establece en |false|, sí
% verbalizará la salida del comando. Un símbolo inválido devuelve siempre un
% \enquote{error} y se detiene la compilación del documento,
% también con \mykey{silent}|=true|.
%
% \subsection*{El comando \cs[no-index]{spcase}}\label{subsec:spcase}
%
% \begin{function}{\spcase}
%   \begin{syntax}
%     |\spcase|\mymarg{letra} \qquad\qquad |\spcase|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{letra}
%   \end{syntax}
%   El comando \ics*{spcase} trabaja en \emph{modo matemático} y recibe el
%   \emph{argumento obligatorio} \mymarg{letra}: la letra que acompaña a los
%   casos de una probabilidad. Las letras válidas son |f|, |p| y |t|;
%   cualquier otra devuelve un \enquote{error} y se detiene la compilación del
%   documento.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spcase{f}$| imprimirá `$C_{f}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«casos favorables»};
% |$\spcase{p}$| imprimirá `$C_{p}$' y verbalizará \emph{«casos
% posibles»}; |$\spcase{t}$| imprimirá `$C_{t}$' y verbalizará
% \emph{«casos totales»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spcase}}
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spcase]
%
% Sobrescribe el \emph{nombre de los casos} que verbalizará
% \mynvmath{NVDA/MathCAT} al ejecutar el comando sin modificar la
% impresión de este. El valor de esta llave \emph{siempre} debe pasarse
% entre llaves `|{ }|'. Por ejemplo:~%
% |$\spcase[read-arg={casos que cumplen}]{f}$| imprimirá `$C_{f}$' en la
% salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«casos
% que cumplen»}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spcase]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} del símbolo al ejecutar el comando sin modificar
% la impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% NO verbalizará la salida del comando; si se establece en |false|, sí
% verbalizará la salida del comando. Una letra inválida devuelve siempre un
% \enquote{error} y se detiene la compilación del documento,
% también con \mykey{silent}|=true|.
%
% \subsection*{El comando \cs[no-index]{spOmega}}\label{subsec:spOmega}
%
% \begin{function}{\spOmega}
%   \begin{syntax}
%     |\spOmega| \qquad\qquad |\spOmega|\myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   \end{syntax}
%   El comando \ics*{spOmega} trabaja en \emph{modo matemático} y recibe el
%   \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val}
%   mediante el cual se establece la \emph{«forma»} en la que se
%   verbalizará en \mynvmath{NVDA/MathCAT} el espacio muestral.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}, |$\spOmega$|
% imprimirá `$\Omega$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«espacio muestral»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spOmega}}
%
% \keyexampcmd{card}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spOmega]
%
% Establece si se \emph{imprime} el símbolo `$\#$' antes de la letra, para
% representar la cardinalidad del espacio muestral. Si se establece en |true|,
% |$\spOmega[card=true]$| imprimirá `$\#\Omega$' en la salida \texttt{PDF}
% y \mynvmath{NVDA/MathCAT} verbalizará \emph{«cardinalidad del espacio
% muestral»}.
%
% \keyexampcmd{sub-index}{subíndice}{no usado}[spOmega]
%
% Establece un \emph{subíndice} sobre la letra, que
% \mynvmath{NVDA/MathCAT} verbaliza después del nombre con la palabra
% \emph{«sub»}. El valor de esta llave
% \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% Por ejemplo:~|$\spOmega[sub-index={1}]$| imprimirá `$\Omega_{1}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«espacio muestral
% sub 1»}; |$\spOmega[card=true,sub-index={1}]$| imprimirá `$\#\Omega_{1}$'
% y verbalizará \emph{«cardinalidad del espacio muestral sub 1»}.
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spOmega]
%
% Sobrescribe el \emph{nombre} que verbalizará \mynvmath{NVDA/MathCAT} al
% ejecutar el comando sin modificar la impresión de este. El valor de esta
% llave \emph{siempre} debe pasarse entre llaves `|{ }|'.\newline Por ejemplo:~%
% |$\spOmega[read-arg={universo}]$| imprimirá `$\Omega$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«universo»}.
%
% \begin{important}*
%  La llave \mykey{sub-index} no tiene efecto al usar \mykey{read-arg}.
% \end{important}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spOmega]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} del símbolo al ejecutar el comando sin modificar
% la impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT}
% NO verbalizará la salida del comando; si se establece en |false|, sí
% verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spPval}}\label{subsec:spPval}
%
% \begin{function}{\spPval}
%   \begin{syntax}
%     |\spPval|\mymarg{casos}\mymarg{total} \qquad\qquad |\spPval|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{casos}\mymarg{total}
%   \end{syntax}
%   El comando \ics*{spPval} trabaja en \emph{modo matemático} y recibe
%   \emph{dos argumentos obligatorios}: \mymarg{casos} y \mymarg{total}, los
%   tipografía como una fracción y devuelve el \emph{«valor de una
%   probabilidad»}.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional}
% \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spPval{1}{5}$| imprimirá `$\frac{1}{5}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«1 de 5»}.
%
% \begin{important}*
%   Los argumentos \mymarg{casos} y \mymarg{total} se tipografían
%   directamente, sin procesamiento semántico adicional.
% \end{important}
%
% \subsubsection*{Llaves para \cs[no-index]{spPval}}
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spPval]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} del \emph{valor de la probabilidad} pasado al
% comando sin modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se
% establece en |false|, sí verbalizará la salida del comando.
%
% \keyexampcmd{use-dfrac}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spPval]
%
% Establece si la fracción del \emph{valor de la probabilidad} se imprime
% usando |\dfrac| en lugar de |\frac|. Si se establece en |true|,
% |$\spPval[use-dfrac=true]{1}{5}$| imprimirá la fracción en tamaño
% \emph{display} `$\dfrac{1}{5}$' en la salida \texttt{PDF}; si se establece
% en |false|, la fracción se imprime con |\frac| siguiendo el tamaño
% matemático del contexto en curso.
%
% \subsection*{El comando \cs[no-index]{spProb}}\label{subsec:spProb}
%
% \begin{function}{\spProb}
%   \begin{syntax}
%     |\spProb|\mymarg{evento} \qquad\qquad |\spProb|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{evento}
%   \end{syntax}
%   El comando \ics*{spProb} trabaja en \emph{modo matemático} y recibe el
%   \emph{argumento obligatorio} \mymarg{evento}: un evento, o dos eventos
%   unidos por \emph{exactamente uno} de los operadores |\cap|, |\cup|,
%   |\mid| o el carácter `\texttt{\textbar}', escritos tal como se imprimen. El
%   comando imprime la probabilidad entre paréntesis precedida de la letra
%   `$P$'. Cualquier otro argumento, como uno vacío, con llaves o paréntesis sin
%   cerrar, con un evento vacío o con más de un operador a la vez,
%   devuelve un \enquote{error} y se detiene la compilación del documento. Lo
%   que va entre llaves,
%   paréntesis o corchetes no se examina, de modo que
%   |$\spProb{A \cap (B \cup C)}$| es válido.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spProb{A}$| imprimirá `$P(A)$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«probabilidad de A»};
% |$\spProb{A \cap B}$| imprimirá `$P(A \cap B)$' y verbalizará
% \emph{«probabilidad de A y B»}; |$\spProb{A \cup B}$| imprimirá
% `$P(A \cup B)$' y verbalizará \emph{«probabilidad de A o B»};
% |$\spProb{A \mid B}$|, o \myhlc+$\spProb{A | B}$+, imprimirá `$P(A \mid B)$' y
% verbalizará \emph{«probabilidad de A dado B»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spProb}}
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spProb]
%
% Sobrescribe lo que verbalizará \mynvmath{NVDA/MathCAT} al ejecutar el
% comando sin modificar la impresión de este. El valor de esta llave
% \emph{siempre} debe pasarse entre llaves `|{ }|'.
%
% Por ejemplo:~|$\spProb[read-arg={probabilidad del suceso}]{A}$|
% imprimirá `$P(A)$' en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT}
% verbalizará \emph{«probabilidad del suceso»}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spProb]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} de la probabilidad al ejecutar el comando sin
% modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se
% establece en |false|, sí verbalizará la salida del comando. Un argumento
% inválido devuelve siempre un \enquote{error} y se detiene la compilación del
% documento,
% también con \mykey{silent}|=true|.
%
% \begin{important}*
%   Dentro del argumento de |\spProb|, los operadores |\cap|, |\cup| y
%   |\mid| que quedan entre llaves o paréntesis, por ejemplo en
%   |$\spProb{A \cap (B \cup C)}$|, también se verbalizan como «y», «o» y
%   «dado», respectivamente.
% \end{important}
%
% \subsection*{El comando \cs[no-index]{spcomp}}\label{subsec:spcomp}
%
% \begin{function}{\spcomp}
%   \begin{syntax}
%     |\spcomp|\mymarg{evento} \qquad\qquad |\spcomp|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{evento}
%   \end{syntax}
%   El comando \ics*{spcomp} trabaja en \emph{modo matemático} y recibe el
%   \emph{argumento obligatorio} \mymarg{evento}: el evento del que se
%   quiere el complemento, escrito tal como se imprime. El comando imprime el
%   complemento con el exponente `$c$' y se puede usar solo o dentro del
%   argumento de |\spProb|. Si el evento contiene una operación entre
%   conjuntos, que es |\cap|, |\cup|, |\setminus| o |\mid|, se imprime entre
%   paréntesis y se verbaliza entre paréntesis para no confundir el
%   complemento de toda la operación con el de su primer evento.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spcomp{A}$| imprimirá `$A^{c}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«complemento de A»};
% |$\spProb{A \cap \spcomp{B}}$| imprimirá `$P(A \cap B^{c})$' y
% verbalizará \emph{«probabilidad de A y complemento de B»};
% |$\spcomp{A \cup B}$| imprimirá `$(A \cup B)^{c}$' y verbalizará
% \emph{«complemento de se abren paréntesis A o B se cierran
% paréntesis»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spcomp}}
%
% \keyexampcmd{bar}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spcomp]
%
% Establece si el complemento se imprime con una \emph{barra horizontal}
% sobre el evento en lugar del exponente `$c$'. Si se establece en |true|,
% |$\spcomp[bar=true]{A}$| imprimirá `$\overline{A}$' en la salida
% \texttt{PDF}; la verbalización es la misma en ambos casos.
%
% \keyexampcmd{wrap-parens}{auto \textnormal{\textcolor{lightgray}{\textbar}}
%   true \textnormal{\textcolor{lightgray}{\textbar}} false}{auto}[spcomp]
%
% Establece si se \emph{imprimen} paréntesis redondos `|(|\dots|)|' alrededor
% del evento cuando se usa el exponente `$c$'. Esta llave admite
% |true|, |false| y |auto|. Con |auto| se agregan solo si el evento contiene
% |\cap|, |\cup|, |\setminus| o |\mid|; con |true| siempre, y con |false|
% nunca. Con \mykey{bar}|=true| la barra cubre el evento y no se agregan
% paréntesis. Como
% \mynvmath{NVDA/MathCAT} no verbaliza la barra, cuando el evento es compuesto
% y se usa \mykey{bar}|=true| el complemento se verbaliza \emph{«complemento de la
% expresión»}; por ejemplo, |$\spcomp[bar=true]{A \cup B}$| se verbaliza
% \emph{«complemento de la expresión A o B»}, para dejar claro que abarca
% todo el evento.
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spcomp]
%
% Sobrescribe lo que verbalizará \mynvmath{NVDA/MathCAT} al ejecutar el
% comando sin modificar la impresión de este. El valor de esta llave
% \emph{siempre} debe pasarse entre llaves `|{ }|'.\newline Por ejemplo:~%
% |$\spcomp[read-arg={no ocurre A}]{A}$| imprimirá `$A^{c}$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«no ocurre A»}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spcomp]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} del complemento al ejecutar el comando sin
% modificar la impresión de este. Si se establece en |true|,
% \mynvmath{NVDA/MathCAT} NO verbalizará la salida del comando; si se
% establece en |false|, sí verbalizará la salida del comando.
%
% \subsection*{El comando \cs[no-index]{spcomb}}\label{subsec:spcomb}
%
% \begin{function}{\spcomb}
%   \begin{syntax}
%     |\spcomb|\mymarg{n\textnormal{\textcolor{gray}{,}} k} \qquad\qquad |\spcomb|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{n\textnormal{\textcolor{gray}{,}} k}
%   \end{syntax}
%   El comando \ics*{spcomb} trabaja en \emph{modo matemático} y escribe los
%   símbolos de conteo: combinaciones, variaciones, arreglos y permutaciones,
%   con o sin repetición, y las permutaciones circulares. Recibe un único
%   \emph{argumento obligatorio} con elementos separados por comas: \mymarg{n},
%   el total de elementos, y, después de la primera coma, \mymarg{k}, los
%   elementos que se toman. Según el símbolo, lo que sigue a la primera coma
%   puede ser \emph{la lista de repeticiones}, en las permutaciones con repetición,
%   o puede no escribirse, en las permutaciones sin repetición y circulares, que
%   solo llevan \mymarg{n}.
% \end{function}
%
% Los argumentos aceptan números o símbolos; si ambos son números enteros,
% el comando comprueba que $k \leq n$, salvo cuando hay repetición, y que
% las repeticiones no sumen más de $n$.
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spcomb{5,2}$| imprimirá `$\binom{5}{2}$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«5 sobre 2»};
% |$\spcomb[kind=variation]{5,2}$| imprimirá `$V_{2}^{5}$' y verbalizará
% \emph{«variación de 5 elementos tomados de a 2»};
% |$\spcomb[kind=permutation]{n}$| imprimirá `$P_{n}$' y verbalizará
% \emph{«permutación de n elementos»}.
%
% \subsubsection*{Llaves para \cs[no-index]{spcomb}}
%
% \keyexampcmd{kind}{combination \textnormal{\textcolor{lightgray}{\textbar}}
%   variation \textnormal{\textcolor{lightgray}{\textbar}} arrangement
%   \textnormal{\textcolor{lightgray}{\textbar}} permutation}{combination}[spcomb]
%
% Establece el tipo de símbolo: combinaciones, que son $C$ o $\binom{n}{k}$,
% variaciones, que son $V$, arreglos, que son $A$, o permutaciones, que son $P$. Las variaciones y
% los arreglos se escriben distinto pero se verbalizan según su nombre,
% \emph{«variación»} y \emph{«arreglo»}. Con \mykey{kind}|=permutation| el
% \mymarg{k} es opcional: sin él, con |$\spcomb[kind=permutation]{n}$|, se imprime
% $P_{n}$ y se verbaliza \emph{«permutación de n elementos»}, y con él,
% con |$\spcomb[kind=permutation]{n,k}$|, se imprime $P_{k}^{n}$ y se verbaliza
% \emph{«permutación de n elementos tomados de a k»}.
%
% \keyexampcmd{repeat}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spcomb]
%
% Establece si los elementos pueden \emph{repetirse}. Si se establece en
% |true|, se agrega una \emph{R} a la letra y \emph{«con repetición»} a la
% verbalización: |$\spcomb[kind=variation,repeat=true]{5,2}$| imprimirá
% `$VR_{2}^{5}$' y verbalizará
% \emph{«variación con repetición de 5 elementos tomados de a 2»}; si se
% establece en |false|, no se agrega nada. Con \mykey{kind}|=permutation| y
% \mykey{repeat}|=true| lo que sigue a la primera coma es la \emph{lista de repeticiones},
% que también va separada por comas y tiene al menos dos elementos.\newline Por ejemplo:~%
% |$\spcomb[kind=permutation,repeat=true]{10,3,3,4}$|
% imprimirá `$PR_{10}^{3,3,4}$' y verbalizará \emph{«permutación con
% repetición de 10 elementos con repeticiones 3 3 y 4»}.
%
% \keyexampcmd{circular}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spcomb]
%
% Establece si se trata de una \emph{permutación circular}. Si se establece en
% |true|,\newline |$\spcomb[kind=permutation,circular=true]{n}$| imprimirá `$PC_{n}$'
% en la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará
% \emph{«permutación circular de n elementos»}; si se establece en |false|, la
% permutación es la usual. Solo es válida con \mykey{kind}|=permutation| y
% \mykey{repeat}|=false|, y entonces el comando solo recibe \mymarg{n}.
%
% \keyexampcmd{print-sty}{auto \textnormal{\textcolor{lightgray}{\textbar}}
%   binom \textnormal{\textcolor{lightgray}{\textbar}} letter}{auto}[spcomb]
%
% Establece cómo se imprimen las combinaciones sin repetición. Si se establece
% en |auto| o |binom|, se imprime el coeficiente binomial $\binom{n}{k}$ y
% \mynvmath{NVDA/MathCAT} lo verbaliza \emph{«n sobre k»}; si se establece en
% |letter|, |$\spcomb[print-sty=letter]{5,2}$| imprimirá `$C_{2}^{5}$' y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«combinación de 5 elementos
% tomados de a 2»}. El valor |binom| solo es válido para combinaciones sin
% repetición.
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spcomb]
%
% Sobrescribe lo que verbalizará \mynvmath{NVDA/MathCAT} al ejecutar el
% comando sin modificar la impresión de este. El valor de esta llave
% \emph{siempre} debe pasarse entre llaves `|{ }|'.\newline Por ejemplo:~%
% |$\spcomb[read-arg={cinco sobre dos}]{5,2}$| imprimirá `$\binom{5}{2}$' en
% la salida \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«cinco
% sobre dos»}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spcomb]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} del símbolo al ejecutar el comando sin modificar la
% impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT} NO
% verbalizará la salida del comando; si se establece en |false|, sí
% verbalizará la salida del comando.
%
% \begin{important}*
%   Un argumento inválido devuelve siempre un \enquote{error} y se detiene la
%   compilación del documento,
%   también con \mykey{silent}|=true|: argumentos vacíos, una lista de
%   repeticiones con menos de dos elementos o con un elemento vacío, $k > n$
%   sin repetición, repeticiones que suman más de $n$, \mymarg{k}
%   ausente cuando el símbolo lo necesita o presente cuando no lo admite, y
%   combinaciones de llaves que no existen: \mykey{circular} sin \mykey{kind}|=permutation| o
%   con \mykey{repeat}, o \mykey{print-sty}|=binom| fuera de las combinaciones sin repetición.
% \end{important}
%
% \subsection*{El comando \cs[no-index]{spfact}}\label{subsec:spfact}
%
% \begin{function}{\spfact}
%   \begin{syntax}
%     |\spfact|\mymarg{n} \qquad\qquad |\spfact|\myoarg{key \textnormal{\textcolor{gray}{=}} val}\mymarg{n}
%   \end{syntax}
%   El comando \ics*{spfact} trabaja en \emph{modo matemático} y recibe el
%   \emph{argumento obligatorio} \mymarg{n}. Imprime
%   el factorial de \mymarg{n}. Si \mymarg{n} es una expresión, los paréntesis
%   se escriben en el argumento: |$\spfact{(n-1)}$| imprimirá `$(n-1)!$'.
% \end{function}
%
% Si no se proporciona el \emph{argumento opcional} \myoarg{key \textnormal{\textcolor{gray}{=}} val},
% |$\spfact{5}$| imprimirá `$5!$' en la salida \texttt{PDF} y
% \mynvmath{NVDA/MathCAT} verbalizará \emph{«5 factorial»}.
%
% El comando |\spfact| devuelve un \enquote{error} y se detiene la compilación
% del documento cuando el argumento está vacío.
%
% \subsubsection*{Llaves para \cs[no-index]{spfact}}
%
% \keyexampcmd{read-arg}{verbalización \textnormal{NVDA}}{no usado}[spfact]
%
% Sobrescribe lo que verbalizará \mynvmath{NVDA/MathCAT} al ejecutar el
% comando sin modificar la impresión de este. El valor de esta llave
% \emph{siempre} debe pasarse entre llaves `|{ }|'.\newline Por ejemplo:~%
% |$\spfact[read-arg={cinco factorial}]{5}$| imprimirá `$5!$' en la salida
% \texttt{PDF} y \mynvmath{NVDA/MathCAT} verbalizará \emph{«cinco factorial»}.
%
% \keyexampcmd{silent}{true \textnormal{\textcolor{lightgray}{\textbar}}
%   false}{false}[spfact]
%
% Establece si se \emph{silencia} la verbalización en
% \mynvmath{NVDA/MathCAT} del factorial al ejecutar el comando sin modificar
% la impresión de este. Si se establece en |true|, \mynvmath{NVDA/MathCAT} NO
% verbalizará la salida del comando; si se establece en |false|, sí
% verbalizará la salida del comando.
%
% \section{Modo \texttt{mathml-AF} y \texttt{spmmli-tool}}\label{sec:af}
%
% Las matemáticas de un documento \emph{tagged} \texttt{PDF} se configuran con
% la llave |math/setup| de |\DocumentMetadata|. Todos los ejemplos de este
% documento usan \texttt{mathml-SE}, que significa \emph{Structure Elements}: el
% \mynvmath{MathML} de cada fórmula se inserta en el árbol de etiquetas del
% \texttt{PDF}. Existe además el modo \texttt{mathml-AF}, que significa \emph{Associated Files}
% o \emph{«archivos asociados»}, en el que las fórmulas se leen desde un
% archivo externo que contiene un \mynvmath{MathML} por cada fórmula, en
% lugar de generarse al compilar.
%
% \subsection{Cuál usar y cuándo}
%
% Como \texttt{mathml-AF} lee las fórmulas desde un archivo fijo, los cambios
% que haga en las matemáticas \emph{no se reflejan} en él mientras no lo
% genere de nuevo: si agrega, borra o modifica fórmulas con frecuencia, el
% \texttt{PDF} seguirá leyendo el contenido anterior. Por eso se recomienda:
%
% \begin{itemize}[nosep,itemsep=\parskip]
%   \item usar \texttt{mathml-SE} mientras el documento está en desarrollo, y
%   \item usar \texttt{mathml-AF} solo en documentos terminados.
% \end{itemize}
%
% La ventaja de \texttt{mathml-AF} es que el archivo asociado contiene el
% \mynvmath{MathML} de cada fórmula y puede editarse: cambiar su contenido
% cambia la lectura, e incluso se puede escribir \mynvmath{MathML} directamente.
%
% \begin{important}*
%  Editar el archivo asociado es una opción para quien sabe lo que hace:
%  \mypkg*[no-index]{spintent} no revisa lo que se escriba en él.
% \end{important}
%
% \subsection{De \texttt{mathml-SE} a \texttt{mathml-AF}}
%
% \begin{enumerate}[nosep,itemsep=\parskip]
%   \item Escriba y revise el documento con \texttt{mathml-SE}.
%   \item Cuando esté terminado, ejecute en una terminal |spmmli-tool archivo.tex|.
%     Se genera el archivo \myfile{archivo-mathml.html}.
%   \item En |\DocumentMetadata|, cambie \texttt{mathml-SE} por \texttt{mathml-AF} y compile
%     de nuevo.
%   \item Si después modifica alguna fórmula, repita desde el segundo paso con
%     la opción |-o|, que reemplaza el archivo existente.
% \end{enumerate}
%
% La línea del tercer paso queda así:
%
% \iffalse
%<*ignore>
% \fi
\begin{examplecode}[frame=single]
\DocumentMetadata
  {
    lang=es-CL,
    pdfversion=2.0,
    pdfstandard=ua-2,
    tagging=on,
    tagging-setup={math/setup={mathml-AF}},
  }
\end{examplecode}
% \iffalse
%</ignore>
% \fi
%
% \subsection{La herramienta \texttt{spmmli-tool}}
%
% El script \myspitool{} se instala junto con el paquete. Prepara los
% archivos de \mynvmath{MathML} que genera \mypkg{luamml}\cite{luamml} para el
% modo \texttt{mathml-AF}: elimina de ellos unos caracteres invisibles,
% \texttt{U+2063}, los \emph{«fantasmas»}, y escribe el resultado en
% \myfile{archivo-mathml.html}. También valida el \texttt{PDF}, siempre que las
% herramientas de la~\S\ref{subsec:comprobar} estén instaladas. Según el
% archivo que reciba, hace lo siguiente:
%
% \begingroup\small
% \begin{longtable}{@{\extracolsep{\fill}}l p{8.5cm}@{}}
% \caption{Modos de \texttt{spmmli-tool}} \label{tab:spmmli-tool-modos} \\
% \toprule
% \textbf{Archivo} & \textbf{Qué hace} \\
% \midrule
% \endfirsthead
% \caption[]{\emph{(Continuación: Modos de \texttt{spmmli-tool})}} \\
% \toprule
% \textbf{Archivo} & \textbf{Qué hace} \\
% \midrule
% \endhead
% \midrule
% \multicolumn{2}{r}{\emph{Continúa en la siguiente página...}} \\
% \endfoot
% \bottomrule
% \endlastfoot
% \texttt{archivo.tex} & Compila una vez en un directorio temporal con
%   |AFghost=true|, obtiene el archivo de \mynvmath{MathML} de \mypkg{luamml},
%   lo limpia y escribe \myfile{archivo-mathml.html}. El \texttt{PDF} temporal se
%   descarta y no se valida. \\
% \texttt{archivo-luamml-mathml.html} & Solo limpia un archivo ya generado. \\
% \texttt{archivo.pdf} & Solo valida el \texttt{PDF} con \texttt{rnv-wrapp} y
%   \texttt{veraPDF} para \texttt{PDF/UA-2}, si están en el \texttt{PATH}. \\
% \end{longtable}
% \endgroup
%
% Las opciones son:
%
% \begingroup\small
% \begin{longtable}{@{\extracolsep{\fill}}l p{8.5cm}@{}}
% \caption{Opciones de \texttt{spmmli-tool}} \label{tab:spmmli-tool-opciones} \\
% \toprule
% \textbf{Opción} & \textbf{Efecto} \\
% \midrule
% \endfirsthead
% \caption[]{\emph{(Continuación: Opciones de \texttt{spmmli-tool})}} \\
% \toprule
% \textbf{Opción} & \textbf{Efecto} \\
% \midrule
% \endhead
% \midrule
% \multicolumn{2}{r}{\emph{Continúa en la siguiente página...}} \\
% \endfoot
% \bottomrule
% \endlastfoot
% |-c|, |--check-only| & Solo valida; el archivo debe ser un \texttt{PDF}. \\
% |-o|, |--overwrite| & Reemplaza los archivos |-mathml.html| existentes. \\
% |-p|, |--path=|\myparg{carpeta} & Carpeta donde se escriben los archivos, que se crea si no existe. \\
% |-e|, |--engine=|\myparg{motor} & Motor con que se compila; por defecto \texttt{lualatex-dev} si existe y, si no, |lualatex|. \\
% |-s|, |--strict| & Se detiene en la primera falla; sin ella procesa todos los archivos. \\
% |-h|, |--help| & Muestra la ayuda. \\
% |-v|, |--version| & Muestra la versión. \\
% \end{longtable}
% \endgroup
%
% Si algún paso falla, el código de salida de \myspitool[no-index]{} es distinto de
% cero. Pueden indicarse varios archivos a la vez y la extensión puede
% omitirse. Algunos ejemplos:
%
% \iffalse
%<*ignore>
% \fi
\begin{examplecode}[frame=single,language={}]
spmmli-tool -c ejemplo.pdf
spmmli-tool -c -s ejemplo
spmmli-tool ejemplo-luamml-mathml.html
spmmli-tool -o -p build/ ejemplo.tex
\end{examplecode}
% \iffalse
%</ignore>
% \fi
%
% \section{Ejemplos de \texttt{PDF} accesible y etiquetado}\label{sec:ejemplos}
%
% Esta sección muestra el uso de \mypkg*[no-index]{spintent} en documentos
% \emph{tagged} \texttt{PDF} con \hologo{LuaLaTeX}. Los archivos están en el
% directorio |sources/test-pkg/|: los cinco primeros prueban \emph{todos} los
% comandos del paquete, agrupados por eje, y los tres últimos son ejemplos
% reales, del tipo de documento que haría un docente de aula. Cada uno se
% compila con \texttt{lualatex-dev} en los dos modos, \texttt{mathml-SE} y \texttt{mathml-AF}
% (\S\ref{sec:af}), y los \texttt{PDF} resultantes se validan con
% \texttt{rnv-wrapp} y \texttt{veraPDF} (\S\ref{subsec:comprobar}).
%
% \begin{important}*
%  This section is just to show the compatibility of \mypkg*[no-index]{spintent}
%  with tagged \texttt{PDF} using \hologo{LuaLaTeX}. The attached files are
%  intended as examples and, in a way, to save time for Matthew Bertucci
%  (@mbertucci), who runs the tests on the \hologo{LaTeX} side, when he sees
%  this package and adds it to The \hologo{LaTeX} Tagged \texttt{PDF} repository.
% \end{important}
%
% Para compilar los ejemplos con \texttt{lualatex-dev} se requieren los
% paquetes \mypkg{babel} con la opción \texttt{spanish}, \mypkg{spintent}
% y \mypkg{hyperref}, junto con la línea de |\DocumentMetadata| (\S\ref{subsec:carga}).
% Las versiones \texttt{mathml-AF} se obtienen cambiando \texttt{mathml-SE}
% por \texttt{mathml-AF} después de ejecutar \myspitool[no-index]{}.
%
% Los archivos \texttt{.tex} van adjuntos a este \texttt{PDF}, junto con
% los \texttt{PDF} ya compilados en cada modo: al final de cada descripción
% están el archivo fuente y los \texttt{PDF} en \texttt{mathml-SE} y en
% \texttt{mathml-AF}.
%
% \begin{itemize}[left=0pt,itemsep=\parskip]
%   \item El archivo \myfile{spintent-01.tex} contiene las pruebas de |\spnum|,
%     |\spunit|, |\spmoney| y |\spshort|, junto con |\spnewunit| y |\spunitalias|.
%     \myattachfile{spintent-01}
%   \item El archivo \myfile{spintent-02.tex} contiene las pruebas de las utilidades
%     generales, que son |\spsiglo|, |\sptime| y |\spdate|, y de las utilidades para
%     símbolos, que son |\spnewread|, |\spread|, |\spusep|, |\spdsym| y |\spmsym|.
%     \myattachfile{spintent-02}
%   \item El archivo \myfile{spintent-03.tex} contiene las pruebas de las utilidades
%     para números: |\spdiv|, |\spmul|, |\spnD|, |\spnM|, |\spmcd|, |\spmcm|,
%     |\spnZ|, |\spmQ|, |\sprazon| y |\spProp|.
%     \myattachfile{spintent-03}
%   \item El archivo \myfile{spintent-04.tex} contiene las pruebas de las utilidades
%     para geometría: |\spcoord|, |\spPoint|, |\spvec|, |\sprecta|, |\sprayo|,
%     |\spside|, |\spmside|, |\spangle|, |\spmangle|, |\spang|, |\sparc|,
%     |\spmarc|, |\spPoly|, |\spAfig|, |\spPfig|, |\sprad|, |\spdiam|,
%     |\spcirc|, |\spAcirc|, |\spPcirc| y |\spVol|.
%     \myattachfile{spintent-04}
%   \item El archivo \myfile{spintent-05.tex} contiene las pruebas de las utilidades
%     para álgebra: |\spinterval|, |\spfun|, |\spdom|, |\sprec|, |\spcod|,
%     |\spfuncomp| y |\spabs|; y para estadística: |\spProm|, |\spmedia|,
%     |\spmediana|, |\spmoda|, |\spdesv|, |\spvarz|, |\sprango|, |\spriq|,
%     |\spmin|, |\spmax|, |\spcuartil|, |\spdecil|, |\spPerc|, |\spFrec|,
%     |\spcase|, |\spOmega|, |\spPval|, |\spProb|, |\spcomp|, |\spcomb| y |\spfact|.
%     \myattachfile{spintent-05}
%   \item El archivo \myfile{spintent-06.tex} es un ejemplo real: una guía de
%     ejercicios de divisibilidad, fracciones, razones y enteros.
%     \myattachfile{spintent-06}
%   \item El archivo \myfile{spintent-07.tex} es un ejemplo real: una guía de
%     ejercicios de geometría con ángulos, áreas y perímetros.
%     \myattachfile{spintent-07}
%   \item El archivo \myfile{spintent-08.tex} es un ejemplo real: una guía de
%     ejercicios de estadística y probabilidad.
%     \myattachfile{spintent-08}
% \end{itemize}
%
% \newpage
%
% \section*{A Work in Progress}
%
% Todo comenzó cuando salió la versión de desarrollo de \hologo{LaTeX}
% release 2025-11-01 \cite{ltnews}, la cual implementó |\MathMLintent| y |\MathMLarg|.
% Mi idea inicial era modesta: un paquete llamado \texttt{spunit}, pensado para las
% unidades de medida. Pero una unidad casi nunca viene sola, y de ahí nació
% |\spnum|; luego vinieron |\spmoney| y |\spshort| en reemplazo de |\sptext|,
% hasta formar \emph{«los cuatro grandes»}, que cubren lo esencial de un
% documento en español con \mypkg{babel}.
%
% Durante un tiempo fui desarrollando pequeñas utilidades, una a una, pero
% pronto quedaron cortas para lo que necesita un docente de enseñanza básica y
% media. Lo que partió como un conjunto de utilidades sueltas se transformó en
% un trabajo por ejes: números, geometría, álgebra y estadística. El nombre
% también cambió. Después de leer mucho sobre el tema, vi una y otra vez la
% palabra \emph{intent}, y lo que yo \emph{intentaba} hacer sonaba mejor en mi
% cabeza como \mypkg*[no-index]{spintent} que como \texttt{spunit}.
%
% En el camino aprendí bastante más que Lua y \hologo{LaTeX}: a validar un
% \texttt{PDF} etiquetado con \texttt{rnv} y \texttt{veraPDF}\cite{verapdf}, a
% construir baterías de tests de regresión, a comprender los modos \texttt{mathml-SE} y
% \texttt{mathml-AF}, a escuchar con \mynvmath{NVDA/MathCAT}\cite{nvda,mathcat}
% una y otra vez, a revisar los \emph{intents} del núcleo antes de crear uno
% propio, a definir una interfaz de usuario amigable y, sobre todo, a
% documentar, y documentar otra vez.
%
% Esta sección es para mí, el de hoy y el de mañana. Hoy el proyecto está
% terminado: el conjunto de comandos está cerrado y no se agregarán más. Lo que
% sí seguiré haciendo es seguirle el paso al proyecto de etiquetado de
% \hologo{LaTeX}\cite{tagged-project} y a \mypkg{luamml}\cite{luamml}, para que
% el paquete siga funcionando con ellos. El código y la documentación están
% pensados para quien necesite un comando que aquí no existe: los comandos
% actuales son las plantillas para copiar. Por eso esta sección se
% llama \emph{A Work in Progress}: porque un trabajo en progreso es el corazón de \mypkg*[no-index]{spintent}.
%
% Queda además una idea abierta. El atributo \emph{intent} no nació en este
% paquete: viene de una discusión más amplia sobre cómo eliminar la
% ambigüedad de \mynvmath{MathML} dejando que sea quien escribe el documento
% quien diga lo que quiso expresar\cite{authorintent}. Lo que hace
% \mypkg*[no-index]{spanish-mathml-intent} es poner esa idea al servicio de
% la matemática y demás en el ámbito escolar en español, pero nada impide
% que otros idiomas tengan su propio paquete. Me gustaría ver algún día algo
% como un hipotético \mypkg[no-index]{ukenglish-mathml-intent}, con una
% interfaz pensada para su lengua y su escuela. Si alguien se anima,
% esta sección es una invitación, y la~\S\ref{subsec:other-languages}, en inglés,
% dice por dónde empezar.
%
% \let\stdsection\section
% \def\section*#1{\stdsection{#1}}
% \expandafter\let\expandafter\thebibliography\csname HDorg@thebibliography\endcsname
%
% \begin{thebibliography}{10}
%
% \bibitem{scontents} \textsc{González, Pablo}. \enquote{\textsf{scontents} - Stores
% \hologo{LaTeX} contents in memory or files}. Available from
% \textsc{ctan}, \url{https://www.ctan.org/pkg/scontents}, 2026.
%
% \bibitem{enumext} \textsc{González, Pablo}. \enquote{\textsf{enumext} -
% Enumerate exercise sheets}. Available from \textsc{ctan},
% \url{https://www.ctan.org/pkg/enumext}, 2026.
%
% \bibitem{tagged-project} The \hologo{LaTeX} Project. \enquote{\textsf{LaTeX
% Tagged \texttt{PDF} Project} - Documentation and resources}. Available from
% \textsc{latex project}, \url{https://latex3.github.io/tagging-project/documentation/},
% 2026.
%
% \bibitem{verapdf} \textsc{veraPDF Consortium}. \enquote{\textsf{veraPDF} -
% Industry supported \texttt{PDF}/A and \texttt{PDF/UA} validator}. Available from
% \textsc{verapdf}, \url{https://verapdf.org/}, 2026.
%
% \bibitem{ngpdf} \textsc{Dual Lab}. \enquote{\textsf{ngPDF} -
% Next Generation \texttt{PDF} demonstrator and validator}. Available from
% \textsc{ngpdf}, \url{https://ngpdf.com/}, 2026.
%
% \bibitem{nvda} \textsc{NV Access}. \enquote{\textsf{NVDA} - NonVisual
% Desktop Access screen reader}. Available from \textsc{nv access},
% \url{https://www.nvaccess.org/}, 2026.
%
% \bibitem{mathcat} \textsc{Soiffer, Neil}. \enquote{\textsf{MathCAT} -
% Math to speech and braille translation}. Available from \textsc{github},
% \url{https://github.com/daisy/MathCAT}, 2026.
%
% \bibitem{pdf20} \textsc{ISO}. \enquote{\textsf{ISO 32000-2:2020} - Portable
% Document Format (\texttt{PDF} 2.0)}. Available from \texttt{PDF} \textsc{association},
% \url{https://pdfa.org/resource/iso-32000-2/}, 2020.
%
% \bibitem{pdf20-impact} \textsc{Johnson, Duff}. \enquote{\textsf{\texttt{PDF} 2.0:
% New Features, Real-World Impact}}. Available from \textsc{pdfa},
% \url{https://pdfa.org/PDF-2-0-new-features-real-world-impact/}, 2026.
%
% \bibitem{pdf20-errata} \texttt{PDF} \textsc{association}. \enquote{\textsf{\texttt{PDF} 2.0
% Errata Collection 3}}. Available from \textsc{pdfa},
% \url{https://pdfa.org/PDF-2-0-errata-collection-3-now-available/}, 2026.
%
% \bibitem{wtpdf} \texttt{PDF} \textsc{association}. \enquote{\textsf{WTPDF} -
% Well-Tagged \texttt{PDF} specification}. Available from \textsc{pdfa},
% \url{https://pdfa.org/wtpdf/}, 2024.
%
% \bibitem{bpg-math} \textsc{LaTeX Project LWG}. \enquote{\textsf{Best
% Practice Guide: Math in PDF}}. Available from \textsc{pdfa},
% \url{https://pdfa.org/resource/best-practice-guide-math-in-PDF/},
% 2026.
%
% \bibitem{unicode-math} \textsc{Robertson, Will}. \enquote{unicode-math
% - Unicode mathematics support for \hologo{XeTeX} and \hologo{LuaTeX}}. Available from
% \textsc{ctan}, \url{https://www.ctan.org/pkg/unicode-math}, 2023.
%
% \bibitem{acrobat} \textsc{Adobe Inc.} \enquote{\textsf{Adobe Acrobat
% Reader} - Reliable \texttt{PDF} viewer}. Available from \textsc{adobe},
% \url{https://get.adobe.com/reader/}, 2026.
%
% \bibitem{mathcatdemo} \textsc{Soiffer, Neil}. \enquote{\textsf{MathCAT Demo} -
% Online demonstration of \textsf{MathCAT}}. Available from \textsc{github},
% \url{https://daisy.github.io/MathCATDemo/}, 2026.
%
% \bibitem{userguide} The \hologo{LaTeX} Project. \enquote{\hologo{LaTeX} for authors
% current version}. Available from
% \textsc{ctan}, \url{https://ctan.org/pkg/latex-base}, 2026.
%
% \bibitem{learnlatex} \textsc{learnlatex.org}. \enquote{\textsf{Learn \hologo{LaTeX}} -
% Your first steps in \hologo{LaTeX}, in short lessons}. Available from
% \textsc{learnlatex.org}, \url{https://www.learnlatex.org/es/}, 2026.
%
% \bibitem{latexref} \textsc{Berry, Karl}. \enquote{\hologo{LaTeX2e}: An
% Unofficial Reference Manual}. Available from \textsc{ctan},
% \url{https://ctan.org/pkg/latex2e-help-texinfo}, 2026.
%
% \bibitem{latex2e} The \hologo{LaTeX} Project. \enquote{The \hologo{LaTeX2e}
% sources}. Available from
% \textsc{ctan}, \url{https://ctan.org/tex-archive/macros/latex/base}, 2026.
%
% \bibitem{latexlab} The \hologo{LaTeX} Project. \enquote{\textsf{latex-lab} – \hologo{LaTeX}
% laboratory}. Available from
% \textsc{ctan}, \url{https://www.ctan.org/pkg/latex-lab}, 2026.
%
% \bibitem{luamml} \textsc{Krueger, Marcel}. \enquote{LuaMML%
% - Automatically generate MathML from \hologo{LuaTeX} math mode material}.
% Available from \textsc{ctan}, \url{https://ctan.org/pkg/luamml}, 2026.
%
% \bibitem{tagpdf} \textsc{Fischer, Ulrike}. \enquote{\textsf{tagpdf} – \hologo{LaTeX}
% kernel code for \texttt{PDF} tagging}. Available from
% \textsc{ctan}, \url{https://www.ctan.org/pkg/tagpdf}, 2026.
%
% \bibitem{babel-spanish} \textsc{Bezos, Javier}.
% \enquote{\textsf{babel-spanish} - Babel support for Spanish}.
% Available from \textsc{ctan}, \url{https://www.ctan.org/pkg/babel-spanish},
% 2026.
%
% \bibitem{babel} \textsc{Braams, Johannes and Bezos, Javier}.
% \enquote{\textsf{babel} - Multilingual support for \hologo{LaTeX},
% \hologo{LuaLaTeX}, \hologo{XeLaTeX}, and Plain \TeX}. Available from
% \textsc{ctan}, \url{https://www.ctan.org/pkg/babel}, 2026.
%
% \bibitem{fontspec} The \hologo{LaTeX} Project. \enquote{fontspec%
% - Advanced font selection for \hologo{XeLaTeX} and \hologo{LuaTeX}}.
% Available from \textsc{ctan}, \url{https://www.ctan.org/pkg/fontspec},%
% 2025.
%
% \bibitem{libertinus-otf} \textsc{Voß, Herbert}. \enquote{libertinus-otf%
% - Support for Libertinus OpenType}. Available from \textsc{ctan},%
% \url{https://www.ctan.org/pkg/libertinus-otf}, 2025.
%
% \bibitem{erewhon-math} \textsc{Flipo, Daniel}. \enquote{erewhon-math%
% - Utopia based OpenType Math font}. Available from%
% \textsc{ctan}, \url{https://ctan.org/pkg/erewhon-math}, 2026.
%
% \bibitem{sourcecodepro} \textsc{Hofstra, Silke}. \enquote{sourcecodepro%
% - Use SourceCodePro with \hologo{TeX}(-alike) systems}. Available from%
% \textsc{ctan}, \url{https://ctan.org/pkg/sourcecodepro}, 2025.
%
% \bibitem{newcomputermodern} \textsc{Tsolomitis, Antonis}. \enquote{newcomputermodern%
% - The New Computer Modern family of fonts}. Available from \textsc{ctan},
% \url{https://ctan.org/pkg/newcomputermodern}, 2026.
%
% \bibitem{hyperref} The \hologo{LaTeX} Project. \enquote{Extensive support
% for hypertext in \hologo{LaTeX}}. Available from
% \textsc{ctan}, \url{https://www.ctan.org/pkg/hyperref}, 2026.
%
% \bibitem{expl3} The \hologo{LaTeX} Project. \enquote{The \textsf{expl3}
% package}. Available from \textsc{ctan}, \url{https://www.ctan.org/pkg/l3kernel}, 2026.
%
% \bibitem{inter3} The \hologo{LaTeX} Project. \enquote{The \hologo{LaTeX}3
% Interfaces}. Available from \textsc{ctan}, \url{https://www.ctan.org/pkg/l3kernel}, 2026.
%
% \bibitem{ltnews} The \hologo{LaTeX} Project. \enquote{\hologo{LaTeX} News,
% Issue 42, November 2025}. Available from
% \textsc{ctan}, \url{https://ctan.org/tex-archive/macros/latex/base}, 2025.
%
% \bibitem{authorintent} \textsc{Carlisle, David and Libbrecht, Paul and
% Schubotz, Moritz and Soiffer, Neil}. \enquote{Author Intent: Eliminating
% Ambiguity in MathML}. Available from \textsc{arXiv},
% \url{https://arxiv.org/abs/2407.06720}, 2025.
%
% \end{thebibliography}
%
% \let\section\stdsection
%
% \section{Change history}
% \label{sec:changes}
%
% \setlist[itemize,1]{label=\textendash,leftmargin=*,labelsep=0.5em,nosep,noitemsep}
% \newlength\descrwidth
% \settowidth{\descrwidth}{\textsf{v1.0, (ctan), 2026-10-20} }
% \begin{description}[font=\small\sffamily,wide=0pt,style=multiline,leftmargin=1.1\descrwidth,nosep,noitemsep]
%  \item [\fileversion{} (ctan), \filedate]
%    \begin{itemize}
%      \item The geometry commands put an invisible separator between
%        consecutive points (|AB|, |AOB|, |ABC|\ldots), so that
%        \mynvmath{MathCAT} 0.7.7 reads each point on its own instead of merging
%        the letters into a word.
%    \end{itemize}
%  \item [v1.3 (ctan), 2026-11-01]
%    \begin{itemize}
%      \item Documentation: the variables of the implementation are described
%        in groups, by purpose.
%      \item Documentation: new note on the plural of acronyms, which explains
%        why the entry \texttt{ongs} prints \enquote{ONGs}.
%      \item The Spanish text of the manual follows the \textsc{rae} rules.
%    \end{itemize}
%  \item [v1.2 (ctan), 2026-10-24]
%    \begin{itemize}
%      \item New subsection \enquote{Other languages} in the implementation,
%        linked from \enquote{A Work in Progress}.
%      \item The title and the \texttt{PDF} metadata of the manual and of the
%        example use the name \enquote{Spanish MathML intent}.
%      \item |\spfact| prints the exclamation mark with |\mathexclam|, as the
%        other commands do with their symbols.
%      \item Internal: the function that builds the intent of the statistics
%        commands is split from the one that typesets them, and all function
%        bodies follow the same layout.
%      \item Internal: the Lua module has no unused code, sends its variables
%        to \TeX{} through helper functions, and is organized in indexed
%        sections that keep their local variables to themselves.
%    \end{itemize}
%  \item [v1.1 (ctan), 2026-10-22]
%    \begin{itemize}
%      \item The command-line tool |spitool| is renamed \myspitool[no-index]{}
%        (script and man page).
%      \item The message |pos-invalid-index| is renamed |stat-pos-invalid-index|.
%      \item New abstract of the manual, in line with the description of the
%        \texttt{README}.
%    \end{itemize}
%  \item [v1.0 (ctan), 2026-10-20]
%    \begin{itemize}
%      \item First public release.
%    \end{itemize}
% \end{description}
%
% \newpage
%
% \newgeometry{top=0.5in,bottom=0.3in,left=1.0in,right=0.5in,footskip=0.2in,headheight=1cm,headsep=0.27cm}
%
% \indexprologue{
% Los números en cursiva indican las páginas donde se describe la entrada
% correspondiente.}
%
% \printindex[userdoc]
%
% \newpage
%
% \selectlanguage{english}
%
% \StartImplementation
%
% \StopEventually{^^A
% \newgeometry{top=0.5in,bottom=0.3in,left=1.0in,right=0.5in,footskip=0.2in,headheight=1cm,headsep=0.27cm}
% \addtocontents{toc}{\protect\setcounter{tocdepth}{2}}
% \cleardoublepage
% \phantomsection
% \indexprologue{
% The italic numbers denote the pages where the corresponding entry is
% described, the numbers underlined and all others indicate the line on
% which they are implemented in the package code.
% }
% \printindex
% }
%
% \newgeometry{top=0.5in,bottom=0.3in,left=1.95in,right=0.5in,footskip=0.2in,headheight=1cm,headsep=0.27cm}
%
% \section{Implementation}
% \label{sec:implementation}
% \addtocontents{toc}{\protect\setcounter{tocdepth}{0}}
%
% The most recent publicly released version of \mypkg*[no-index]{spintent} is
% available at \textsc{ctan}: \url{https://www.ctan.org/pkg/spanish-mathml-intent}.
% While general feedback via email is welcomed, specific bugs or
% feature requests should be reported through the issue
% tracker: \textcolor{gray}{\scriptsize\faIcon[regular]{github}} \url{https://github.com/pablgonz/spanish-mathml-intent/issues}.
%
% \begin{important}*
% The documentation presented here is far from professional, it contains
% a lot of obvious information that to the eye of a \hologo{TeX}pert are
% superfluous, but, after so many years developing this project is the
% only way to remember what does what.
% \end{important}
%
% \subsection{General conventions}
%
% Functions of the form \myhlc|\@@_luafun_some:n| or \myhlc|\@@_luafun_some:nn|
% are created in the Lua module \myluamod{} and only their variants
% are declared in \mypkg[no-index]{expl3}.
%
% In the same way, variables of the form \myhlc|\l_@@_luaset_some_tl|,
% \myhlc|\l_@@_luaset_some_str| or \myhlc|\l_@@_luaset_some_clist|, also
% with the prefix of a command, such as \myhlc|\l_@@_spcomb_luaset_n_str|, are only
% declared in \mypkg[no-index]{expl3}; their values are set from Lua.
%
% \begin{important}*
%  All variables and functions defined in this package are private and
%  are NOT intended to work or be used by another package or module.
% \end{important}
%
% \subsection{Other languages}\label{subsec:other-languages}
%
% The \emph{intent} attribute is not tied to Spanish\cite{authorintent}. What
% belongs to this package is the Spanish reading of each concept, written in
% the names of the intents, such as |_valor-absoluto($x)|, and the set of
% commands for the school curriculum it targets. A package for another
% language would keep the same idea, a command that typesets an expression and
% annotates it with an intent, but would choose its own intent names and its
% own commands.
%
% Every command in this implementation follows the same order: variables,
% error messages, keys and internal functions, with the public command at the
% end. The code written in Lua only parses the argument and sets variables that
% \mypkg{expl3} reads. That order is meant to be reused, under the terms of
% the \textsc{lppl}. Anyone who wants to start a package for another language
% is welcome to do so, as a separate package with its own name.
%
% \subsection{Initial set up}
%
% Start the \pkg{DocStrip} guards.
%    \begin{macrocode}
%<*package>
%    \end{macrocode}
% Identify the internal prefix, following the \LaTeX3 \pkg{DocStrip} convention, for
% \myclass{l3doc} class.
%    \begin{macrocode}
%<@@=spintent>
%    \end{macrocode}
%
% \subsection{Declaration of the package}
%
% First we will make sure we have a minimum release version
% of \hologo{LaTeX} to work correctly, otherwise, we will return an \enquote{error}.
%    \begin{macrocode}
\NeedsTeXFormat{LaTeX2e}[2026-11-01]
\IfFormatAtLeastF { 2026-11-01 }
  {
    \PackageError { spanish-mathml-intent }
      { LaTeX~kernel~too~old~need~2026-11-01~or~later }
      {
        spanish-mathml-intent~requires~a~LaTeX2e~release~2026-11-01~or~
        later.~Update~your~TeX~distribution.
      }
  }
%    \end{macrocode}
% Now declare the \mypkg*[no-index]{spintent} package and add only two
% keys: \mykey[usepackage]{mathcal} and \mykey[usepackage]{AFghost}, which can be passed as package
% options.
%    \begin{macrocode}
\ProvidesExplPackage {spanish-mathml-intent} {2026-11-02} {1.4} {Spanish MathML intent}
\keys_define:nn { spintent }
  {
    mathcal .usage:n = load,
    mathcal .bool_set:N = \l_@@_mathcal_bool,
    mathcal .initial:n = true,
    mathcal .value_required:n = true,
    AFghost .usage:n = load,
    AFghost .bool_set:N = \l_@@_debug_ghost_bool,
    AFghost .initial:n = false,
    AFghost .value_required:n = true,
  }
\ProcessKeyOptions [ spintent ]
%    \end{macrocode}
% We check if the requirements for running the \mypkg*[no-index]{spintent} package
% are met; that is: |\DocumentMetadata| must be active, \hologo{LuaTeX}
% must be running, and the \mypkg{unicode-math} or \mypkg{lua-unicode-math}
% package must be loaded. If neither is present, we will load \mypkg{unicode-math}.
% If the conditions cannot be met, we will return a \enquote{fatal error}.
%    \begin{macrocode}
\IfDocumentMetadataTF
  {
    \sys_if_engine_luatex:TF
      {
        \msg_new:nnn { spintent } { package-not-load }
          {
            The~'#1'~package~will~be~loaded~as~a~dependency.
          }
        \IfPackageLoadedF { unicode-math }
          {
            \IfPackageLoadedF { lua-unicode-math }
              {
                \msg_info:nnn { spintent } { package-not-load } { unicode-math }
                \RequirePackage{unicode-math}
              }
          }
        \msg_new:nnn { spintent } { env-success }
          {
            Everything~looks~correct,~spanish-mathml-intent~will~make~the~intent!
          }
        \msg_info:nn { spintent } { env-success }
      }
      {
        \msg_new:nnnn { spintent } { fatal-engine-requires-luatex }
          {
            The~spanish-mathml-intent~package~requires~LuaLaTeX.
          }
          {
            This~package~relies~on~Lua~module.~Compile~with~lualatex.
          }
        \msg_fatal:nn { spintent } { fatal-engine-requires-luatex }
      }
  }
  {
    \msg_new:nnnn { spintent } { fatal-metadata-missing }
      {
        '\token_to_str:N \DocumentMetadata'~is~missing.
      }
      {
        The~spanish-mathml-intent~package~requires~tagged~PDF~active.\\
        You~must~declare~'\token_to_str:N \DocumentMetadata { tagging=on }'.
      }
    \msg_fatal:nn { spintent } { fatal-metadata-missing }
  }
%    \end{macrocode}
%
% \subsection{Some kernel variants}
%
% \begin{macro}[int]{\str_lowercase:V, \seq_set_from_clist:Ne, \int_set:NV}
%    Non-standard kernel variants.
%    \begin{macrocode}
\cs_generate_variant:Nn \str_lowercase:n       { V }
\cs_generate_variant:Nn \seq_set_from_clist:Nn { Ne }
\cs_generate_variant:Nn \int_set:Nn            { NV }
%    \end{macrocode}
% \end{macro}
%
% \subsection{Variables for the key \texttt{mathcal}, \texttt{\textbackslash rightanglesqr} and \texttt{\textbackslash attribute}}
%
% \begin{variable}[int]
%   {
%     \l_@@_luaset_cal_fam_ok_str,
%     \l_@@_luaset_sym_fam_ok_str,
%     \l_@@_luaset_scope_attr_str,
%   }
%   First, we declare the string variable \myhlc|\l_@@_luaset_cal_fam_ok_str|
%   used by the function \myhlc|\@@_luafun_classic_mathcal:nn|, controlled
%   by the key \mykey{mathcal}, the string variable \myhlc|\l_@@_luaset_sym_fam_ok_str|
%   used by the function \myhlc|\@@_luafun_right_angle_sqr:| to copy |\rightanglesqr|
%   alongside the string variable \myhlc|\l_@@_luaset_scope_attr_str|
%   used by |\attribute| before loading the Lua module \myluamod{}.
%   This is necessary due to the implementation of the functions within
%   the Lua module; otherwise, \mypkg[no-index]{expl3} would issue a warning
%   regarding duplicate variable declarations.
%    \begin{macrocode}
\str_new:N \l_@@_luaset_cal_fam_ok_str
\str_new:N \l_@@_luaset_sym_fam_ok_str
\str_new:N \l_@@_luaset_scope_attr_str
%    \end{macrocode}
% \end{variable}
%
% \subsection{Loading the Lua module and functions}
%
% \begin{macro}[int]{spintent.lua}
%   Loads the Lua module \myluamod{}, where every \myhlc|_luafun_| function
%   used throughout the package is registered.
%    \begin{macrocode}
\lua_load_module:n { spintent.lua }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_luafun_set_debug_ghost:n}
%   Passes \mykey{AFghost} to Lua as a plain string. If it is |true|, the two
%   ghost characters are marked with |sptmp="outer"| or |sptmp="inner"| in the
%   file |-luamml-mathml.html|, so that the Lua script \myspitool{} can
%   remove them before generating the |Associated~Files| structure of the
%   \emph{tagged} \texttt{PDF} for \texttt{mathml-AF} mode.
%    \begin{macrocode}
\bool_if:NTF \l_@@_debug_ghost_bool
  { \@@_luafun_set_debug_ghost:n { true } }
  { \@@_luafun_set_debug_ghost:n { false } }
%    \end{macrocode}
% \end{macro}
%
% \begin{variable}[int]{\l_@@_scope_attr_int}
%   A \hologo{LuaTeX} attribute number allocated from Lua with
%   |luatexbase.new_attribute| and exposed as an integer. Every command that
%   uses the function \myhlc|\@@_luafun_mrow_intent_arg:nn| sets |\attribute|
%   \myhlc|\l_@@_scope_attr_int| |= 1| inside its own group, which marks the
%   nodes typeset within it as eligible for the queue.
%    \begin{macrocode}
\int_new:N  \l_@@_scope_attr_int
\int_set:NV \l_@@_scope_attr_int \l_@@_luaset_scope_attr_str
%    \end{macrocode}
% \end{variable}
%
% \begin{macro}[int]
%   {
%     \@@_luafun_spunit_parse_product:nV,
%     \@@_luafun_spmoney_lookup_data:V,
%     \@@_luafun_spmQ_scale_int:Vn,
%     \@@_luafun_spfun_parse_and_set:v,
%   }
%   Variants generated for \myhlc|\@@_luafun_spunit_parse_product:nn|,
%   used by |\spunit|; \myhlc|\@@_luafun_spmoney_lookup_data:n|, used
%   by |\spmoney|; \myhlc|\@@_luafun_spmQ_scale_int:nn|, used by
%   |\spmQ|; and \myhlc|\@@_luafun_spfun_parse_and_set:n|, used by
%   |\spfun|.
%    \begin{macrocode}
\cs_generate_variant:Nn \@@_luafun_spunit_parse_product:nn { nV }
\cs_generate_variant:Nn \@@_luafun_spmoney_lookup_data:n   { V }
\cs_generate_variant:Nn \@@_luafun_spmQ_scale_int:nn       { Vn }
\cs_generate_variant:Nn \@@_luafun_spfun_parse_and_set:n   { v }
%    \end{macrocode}
% \end{macro}
%
% \subsection{Internal implementation for \texttt{\textbackslash mathcal} and \texttt{\textbackslash rightanglesqr}}
%
% To simplify user configuration and obtain the expected output style
% \hologo{pdfTeX} for |\mathcal|, we will provide a fallback to the font
% \texttt{NewCMMath-Regular.otf}, which contains the glyphs for these
% characters. Using this same font, we will obtain the symbol
% |\rightanglesqr| for the key \mykey{recto}.
%
% \begin{macro}[int]{mathcal-fallback, right-angle-sqr-fallback}
%   First, we define two warnings in case our fallback fails: the
%   \enquote{warning} |mathcal-fallback| is raised when no suitable font or
%   free math family is available, and the \enquote{warning}
%   |right-angle-sqr-fallback| is raised when the right-angle glyph is
%   replaced by |\rightangle|.
%    \begin{macrocode}
\hook_gput_code:nnn { begindocument } { spintent }
  {
    \@@_luafun_init_math_fallbacks:
  }
\msg_new:nnn { spintent } { mathcal-fallback }
  {
    Using~classic~'\token_to_str:N \mathcal'~as~fallback~
    (font~not~found~or~no~free~math~family~available)~\msg_line_context:.
  }
\msg_new:nnn { spintent } { right-angle-sqr-fallback }
  {
    Using~'\token_to_str:N \rightangle'~as~fallback~
    for~the~right-angle~glyph~\msg_line_context:.
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_mathcal:n}
%   Now we define the internal function \myhlc|\@@_mathcal:n| for
%   |\mathcal| which checks the state of the variable \myhlc|\l_@@_mathcal_bool|
%   set by the package key \mykey{mathcal}; if its state is true, we check
%   the string variable \myhlc|\l_@@_luaset_cal_fam_ok_str| and if the latter has the value
%   true, we call the function \myhlc|\@@_luafun_classic_mathcal:n| and
%   otherwise we return a warning
%   message and use |\mathcal|. Otherwise, that is,
%   \mykey{mathcal}|=false| we use |\mathcal|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_mathcal:n
  {
    \bool_if:NTF \l_@@_mathcal_bool
      {
        \str_if_eq:VnTF \l_@@_luaset_cal_fam_ok_str { true }
          { \@@_luafun_classic_mathcal:n {#1} }
          {
            \msg_warning:nn { spintent } { mathcal-fallback }
            \mathcal {#1}
          }
      }
      { \mathcal {#1} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_right_angle_sqr:}
%   The internal function \myhlc|\@@_right_angle_sqr:| establishes the
%   internal copy of the command |\rightanglesqr|. First, we check
%   the string variable \myhlc|\l_@@_luaset_sym_fam_ok_str|. If it
%   is true, we call the function \myhlc|\@@_luafun_right_angle_sqr:|;
%   otherwise we return a warning
%   message and use |\rightangle| instead.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_right_angle_sqr:
  {
    \str_if_eq:VnTF \l_@@_luaset_sym_fam_ok_str { true }
      { \@@_luafun_right_angle_sqr: }
      {
        \msg_warning:nn { spintent } { right-angle-sqr-fallback }
        \rightangle
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{Shared annotation mechanisms: the queue and the ghosts}
%
% Almost all commands in the \mypkg*[no-index]{spintent} package share
% three annotation functions, the mechanism of which is defined in the
% Lua module \myluamod{}.
%
% \begin{macro}[int]{\@@_luafun_mrow_intent_arg:nn}
% The internal function \myhlc|\@@_luafun_mrow_intent_arg:nn| is called
% directly, not through an \mypkg[no-index]{expl3} function. It pushes onto a queue
% one entry formed by a concept |intent| in `|#1|', which may be empty, and a Lua
% table literal |arg=| or |intent=| in `|#2|'. A |pre_mlist_to_hlist_filter|
% callback, installed before the one of \mypkg{luamml}\cite{luamml}, walks the
% top-level noads of the formula in order. Each noad whose nucleus or subscript
% was typeset inside the group of a \mypkg*[no-index]{spintent} command, marked
% with the \hologo{LuaTeX} |\attribute| activated by every command that uses
% this queue, takes the next pending entry: |arg| or the literal |intent| is
% assigned to that leaf and, if the concept |intent| is not empty, the leaf is
% wrapped in a native |<mrow>| carrying it. The entries are consumed in order
% and those left when the formula ends are discarded. The queue does not see the
% claims of a native |\MathMLarg| or |\MathMLintent|.
% \end{macro}
%
% The other two are the \enquote{ghost} characters, an invisible |U+2063| tagged
% |:silent| through their own |mathml_filter|, which the queue never claims. A
% ghost is added as an extra sibling of the |<mrow>| so that it is not the only
% top-level content of the formula; otherwise \mypkg{luamml} would promote it to
% |<math intent=...>|, as in issue
% \href{https://github.com/latex3/luamml/issues/26}{\#26}.
%
% \begin{macro}[int]{\@@_invisible_sep:}
%   Places the ghost before the outer |\MathMLintent| of the formula. Only the
%   first call in the document has effect; the following ones, in any command,
%   do nothing.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_invisible_sep:
  {
    \@@_luafun_invisible_sep:
  }
%    \end{macrocode}
% \begin{important}*
%   Executing this function writes the following line to the
%   output file |-luamml-mathml.html|:
% \iffalse
%<*example>
% \fi
\begin{examplecode}
 <mi intent=":silent" mathvariant="normal">⁣</mi>
\end{examplecode}
% \iffalse
%</example>
% \fi
% and if the package key \mykey{AFghost}|=true| is used, the output is:
% \iffalse
%<*example>
% \fi
\begin{examplecode}
 <mi intent=":silent" mathvariant="normal" sptmp="outer">⁣</mi>
\end{examplecode}
% \iffalse
%</example>
% \fi
% If you wish to use \texttt{mathml-AF} mode, you can remove the line generated by
% \mykey{AFghost}|=true| using the Lua script \myspitool{}.
% \end{important}
% \end{macro}
%
% \begin{macro}[int]{\@@_inner_sep:}
%   Places the ghost just inside a native |\MathMLintent| group. Every call has
%   effect, one for each such group of the formula, nested or sibling.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_inner_sep:
  {
    \@@_luafun_inner_sep:
  }
%    \end{macrocode}
% \begin{important}*
%   Executing this function writes the following line to the
%   output file |-luamml-mathml.html|:
% \iffalse
%<*example>
% \fi
\begin{examplecode}
 <mi intent=":silent" mathvariant="normal">⁣</mi>
\end{examplecode}
% \iffalse
%</example>
% \fi
% and if the package key \mykey{AFghost}|=true| is used, the output is:
% \iffalse
%<*example>
% \fi
\begin{examplecode}
 <mi intent=":silent" mathvariant="normal" sptmp="inner">⁣</mi>
\end{examplecode}
% \iffalse
%</example>
% \fi
% If you wish to use \texttt{mathml-AF} mode, you can remove the line generated by
% \mykey{AFghost}|=true| using the Lua script \myspitool{}.
% \end{important}
% \end{macro}
%
% \begin{macro}[int]{\@@_point_sep:}
%   Places an invisible separator (U+2063) between two consecutive points
%   of a geometric command, written to MathML as
%   |<mo intent=":silent" mathvariant="normal">|.
%   It must be an |<mo>|: MathCAT merges consecutive single-letter |<mi>|
%   elements into a word before reading the intent, even when they carry
%   |arg|, and a silent |<mi>| between them does not prevent it.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_point_sep:
  {
    \@@_luafun_point_sep:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{variable}[int]{\c_@@_invisible_sep_int}
%   A raw, untagged copy of the character |U+2063| defined with the
%   \hologo{LuaTeX} primitive |\Umathchardef|. It is used as content inside an
%   explicit |\MathMLintent|\mymarg{intent}, typically |:silent|.
%    \begin{macrocode}
\hook_gput_code:nnn { begindocument } { spintent }
  {
    \Umathchardef \c_@@_invisible_sep_int = "0 "0 "2063
  }
%    \end{macrocode}
% \begin{important}*
% Documented in \mypkg[no-index]{source3} on page 887 for \myhlc|\int_const:Nn|:
% ``As stated, most constants can be defined as |\chardef| or |\mathchardef|
% but that’s engine dependent.'' We follow this recommendation.
% \end{important}
% \end{variable}
%
% \subsection{Normalize argument and affixes}
%
% We normalize the \mymarg{argument} to support prefixes and suffixes
% by trimming leading and trailing spaces, converting internal spaces to
% hyphens, collapsing consecutive hyphens, and removing leading or
% trailing hyphens.
%
% \begin{macro}[int]{\@@_collapse_hyphens:N}
%   A recursive function to collapse multiple hyphens into a single one.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_collapse_hyphens:N
  {
    \tl_if_in:NnT #1 { -- }
      {
        \tl_replace_all:Nnn #1 { -- } { - }
        \@@_collapse_hyphens:N #1
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_sanitize_affix:Nn, \@@_sanitize_affix:cn}
%   The function \myhlc|\@@_sanitize_affix:Nn| checks if the \mymarg{argument}
%   passed in `|#2|' is \emph{blank}. If so, it clears the target token list
%   variable passed in `|#1|'. Otherwise, it converts the \mymarg{argument}
%   into a string with every character in category code~12, so it no longer
%   matters how an underscore `\myhlc|_|' was typed, trims the bounds, converts
%   spaces `\verb*+ +' and underscores `\myhlc|_|' to hyphens `|-|', collapses
%   multiple hyphens, and strips leading/trailing hyphens. Because
%   underscores are converted \emph{before} collapsing, any run mixing
%   hyphens and underscores is reduced to a single hyphen, and the stored
%   value never starts or ends with `|-|' or `\myhlc|_|': the command that uses the
%   value is the one that places the leading `\myhlc|_|' of the concept name. For
%   example, if you pass a target variable and raw text:
%
%   \begin{center}
%     \myhlc|\@@_sanitize_affix:Nn| \myhlc|\l_@@_spsomesym_prefix_tl||{ foo bar baz }|
%   \end{center}
%
%   The function will \emph{\enquote{store}} the normalized string
%   |foo-bar-baz| inside \myhlc|\l_@@_spsomesym_prefix_tl|, ready to be
%   conditionally appended. In the same way, |{ __foo___bar__ }| is stored
%   as |foo-bar|.
%
%   The only exception is the exact value `\myhlc|_-|', once the bounds are
%   trimmed, which is stored as a single hyphen `|-|'. After the command
%   prepends its own leading `\myhlc|_|', this yields the \emph{silent} literal
%   `\myhlc|_-|' documented for |\spread|. Any other value made only of hyphens,
%   underscores and spaces is stored as \emph{empty}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sanitize_affix:Nn
  {
    \tl_if_blank:nTF {#2}
      { \tl_clear:N #1 }
      {
        \tl_set:Ne #1 { \tl_to_str:e {#2} }
        \tl_trim_spaces:N #1
        \str_if_eq:VnTF #1 { _- }
          { \tl_set:Nn #1 { - } }
          {
            \tl_replace_all:Nnn #1 { ~ } { - }
            \tl_replace_all:Nee #1 { \tl_to_str:n { _ } } { - }
            \@@_collapse_hyphens:N #1
            \tl_put_left:Nn  #1 { \q_mark }
            \tl_put_right:Nn #1 { \q_stop }
            \tl_replace_all:Nnn #1 { \q_mark - } { \q_mark }
            \tl_replace_all:Nnn #1 { - \q_stop } { \q_stop }
            \tl_remove_all:Nn #1 { \q_mark }
            \tl_remove_all:Nn #1 { \q_stop }
          }
      }
  }
\cs_generate_variant:Nn \@@_sanitize_affix:Nn { cn }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{setspintent}}\label{cmd:setspintent}
%
% The |\setspintent| command is used to configure \mymeta{keys}
% \emph{\enquote{globally}} for the utilities provided by \mypkg*[no-index]{spintent}.
%
% \subsubsection{Definition of variables and error messages for \cs[no-index]{setspintent}}
%
% \begin{macro}[int]{\l_@@_setspintent_input_arg_cmd_tl, unknown-command-setup}
%   The variable \myhlc|\l_@@_setspintent_input_arg_cmd_tl| will \emph{store} the
%   command or command name passed to the \emph{\enquote{first}} \mymarg{argument} `|#1|'
%   of the command |\setspintent|.
%    \begin{macrocode}
\tl_new:N \l_@@_setspintent_input_arg_cmd_tl
%    \end{macrocode}
%   The \enquote{error} |unknown-command-setup| is raised when the
%   \mymarg{argument} `|#1|' passed to |\setspintent| is a
%   \emph{\enquote{non-existent}} command.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { unknown-command-setup }
  {
    Cannot~configure~unknown~command~'\c_backslash_str #1'~\msg_line_context:.
  }
  {
    The~command~'\c_backslash_str #1'~is~not~defined.~Check~the~spelling~of~the~command~
    name.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{setspintent}}
%
% \begin{macro}[int]{\@@_setspintent_set_keys:nn, \@@_setspintent_set_keys:en}
%   The function \myhlc|\@@_setspintent_set_keys:nn| will check if the
%   command passed as \mymarg{argument} exists in `|#1|' and set the
%   \mymeta{keys} passed as \mymarg{argument} in `|#2|' to the correct
%   path; otherwise, it will return an error.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_setspintent_set_keys:nn
  {
    \cs_if_exist:cTF {#1}
      {
        \keys_set:nn { spintent / #1 } {#2}
      }
      {
        \msg_error:nnn { spintent } { unknown-command-setup } {#1}
      }
  }
\cs_generate_variant:Nn \@@_setspintent_set_keys:nn { en }
%    \end{macrocode}
% \end{macro}
% \begin{macro}[int]{\@@_setspintent_parse_args:nn, \@@_setspintent_parse_args:Vn}
%   The function \myhlc|\@@_setspintent_parse_args:nn| will check if the
%   argument passed in |#1| begins with `|\|' or is just a \emph{command name}. If it
%   begins with `|\|', it will remove it using \myhlc|\cs_to_str:N| and then
%   execute \myhlc|\@@_setspintent_set_keys:en|. Otherwise, i.e., if `|\|'
%   was not passed, it will execute \myhlc|\@@_setspintent_set_keys:nn|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_setspintent_parse_args:nn
  {
    \tl_if_single:nTF {#1}
      {
        \token_if_cs:NTF #1
          {
            \@@_setspintent_set_keys:en { \cs_to_str:N #1 } {#2}
          }
          {
            \@@_setspintent_set_keys:nn {#1} {#2}
          }
      }
      {
        \@@_setspintent_set_keys:nn {#1} {#2}
      }
  }
\cs_generate_variant:Nn \@@_setspintent_parse_args:nn { Vn }
%    \end{macrocode}
% \end{macro}
% \begin{macro}{\setspintent}
%   Now we define the user command \ics*{setspintent}.
%    \begin{macrocode}
\NewDocumentCommand \setspintent { m m }
  {
    \tl_if_blank:nTF {#1}
      {
        \msg_error:nnn { spintent } { unknown-command-setup } { <empty> }
      }
      {
        \tl_set:Nn \l_@@_setspintent_input_arg_cmd_tl {#1}
        \tl_trim_spaces:N \l_@@_setspintent_input_arg_cmd_tl
        \@@_setspintent_parse_args:Vn \l_@@_setspintent_input_arg_cmd_tl
          {#2}
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{Handling unknown keys}\label{key:unknown-keys}
%
% \begin{macro}[int]
%   {
%     \l_@@_command_name_str,
%     unknown-choice,
%     cmd-key-unknown,
%     cmd-key-value-unknown,
%   }
%   We define the variable \myhlc|\l_@@_command_name_str| which will \emph{store}
%   the \emph{\enquote{name}} of each command within the implementation along with the
%   messages |unknown-choice| used by \mymeta{keys} that use \myhlc|.choice:n| in
%   their implementation and the messages |cmd-key-unknown|, |cmd-key-value-unknown|
%   used by the \mymeta{key} |unknown| in commands that use \myoarg{key\textnormal{\textcolor{gray}{=}}val}.
%    \begin{macrocode}
\str_new:N \l_@@_command_name_str
\msg_new:nnnn { spintent } { unknown-choice }
  {
    Invalid~value~'#3'~for~key~'#1';~use~one~of:~#2~\msg_line_context:.
  }
  {
    The~key~'#1'~only~accepts~one~of:~#2.
  }
\msg_new:nnnn { spintent } { cmd-key-unknown }
  {
    The~key~'#1'~is~unknown~by~command~
      '\c_backslash_str \l_@@_command_name_str'~and~is~being~ignored~
      \msg_line_context:.
  }
  {
    Check~the~spelling~of~the~key~name.
  }
\msg_new:nnnn { spintent } { cmd-key-value-unknown }
  {
    The~key~'#1=#2'~is~unknown~by~command~
      '\c_backslash_str \l_@@_command_name_str'~and~is~being~ignored~
      \msg_line_context:.
  }
  {
    Check~the~spelling~of~the~key~name.
  }
%    \end{macrocode}
% \end{macro}
% \begin{macro}[int]
%   {
%     \@@_unknown_keys_cmd:nn,
%     \@@_unknown_keys_cmd:Vn,
%     \@@_unknown_keys_cmd:n,
%   }
%   The internal function \myhlc|\@@_unknown_keys_cmd:nn| raises the
%   \enquote{error} for an unknown \mymeta{key}. Its first \mymarg{argument}
%   is the name of the key and the second one its value. If the value is blank
%   it issues the message |cmd-key-unknown|; otherwise it issues
%   |cmd-key-value-unknown|. The variant \myhlc|\@@_unknown_keys_cmd:Vn| takes
%   the name of the key from a variable. The function
%   \myhlc|\@@_unknown_keys_cmd:n| is the one used by the \mymeta{key}
%   |unknown| of each command: it receives the value and takes the name of the
%   key from \myhlc|\l_keys_key_str|. The name of the command shown in the
%   message is stored in \myhlc|\l_@@_command_name_str|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_unknown_keys_cmd:nn
  {
    \tl_if_blank:nTF {#2}
      { \msg_error:nnn { spintent } { cmd-key-unknown } {#1} }
      { \msg_error:nnnn { spintent } { cmd-key-value-unknown } {#1} {#2} }
  }
\cs_generate_variant:Nn \@@_unknown_keys_cmd:nn { Vn }
\cs_new_protected:Nn \@@_unknown_keys_cmd:n
  {
    \@@_unknown_keys_cmd:Vn \l_keys_key_str {#1}
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{Definition of core base variables}\label{subsec:core-base-vars}
%
% \begin{macro}[int]{\@@_luafun_clean_split_and_set:n}
%   The function \myhlc|\@@_luafun_clean_split_and_set:n| defined in the
%   Lua module \myluamod{}, is common to several commands in this
%   implementation. It analyzes, processes, cleans and splits the
%   \mymarg{argument} passed to commands using internal functions defined
%   in the Lua module \myluamod. It isolates signs, integer components, decimal
%   separators, decimal part, decimal periodic part, exponents and final physical
%   \emph{\enquote{units}}, assigning them to \mypkg{expl3} variables.
%
%   This function assigns the states of the variables `\myhlc|_str|' or `\myhlc|_tl|'
%   described below to |true|, |false|, |error| or \emph{stores}
%   information according to the role it fulfills within the
%   implementation.
% \end{macro}
%
% \begin{variable}[int]
%   {
%     \l_@@_luaset_sign_tl,
%     \l_@@_luaset_part_int_tl,
%     \l_@@_luaset_dec_sep_tl,
%     \l_@@_luaset_part_dec_tl,
%     \l_@@_luaset_part_period_tl,
%     \l_@@_luaset_has_integer_str,
%   }
%   The variable \myhlc|\l_@@_luaset_sign_tl| \emph{stores} the explicit
%   `$\mathcolor{red}{+}$' or `$\mathcolor{red}{-}$' sign if present.
%   The variable \myhlc|\l_@@_luaset_part_int_tl| \emph{stores} the raw digit
%   sequence of the \emph{\enquote{integer part}}. The variable
%   \myhlc|\l_@@_luaset_dec_sep_tl| \emph{stores} the matched
%   `\textcolor{red}{\texttt{,}}' or `\textcolor{red}{\texttt{.}}' used as
%   the decimal separator. The variable \myhlc|\l_@@_luaset_part_dec_tl|
%   \emph{stores} the raw digits of the \emph{\enquote{finite decimal part}}
%   that follow the separator, and the variable \myhlc|\l_@@_luaset_part_period_tl|
%   \emph{stores} the digits of the \emph{\enquote{infinite periodic decimal part}}
%   that follow a semicolon `\textcolor{red}{\texttt{;}}'.
%
%    The variable \myhlc|\l_@@_luaset_has_integer_str| \emph{store} the state
%   |true| when it is \emph{only} an \emph{\enquote{integer}}.
%    \begin{macrocode}
\tl_new:N  \l_@@_luaset_sign_tl
\tl_new:N  \l_@@_luaset_part_int_tl
\tl_new:N  \l_@@_luaset_dec_sep_tl
\tl_new:N  \l_@@_luaset_part_dec_tl
\tl_new:N  \l_@@_luaset_part_period_tl
\str_new:N \l_@@_luaset_has_integer_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_luaset_only_part_int_str,
%     \l_@@_luaset_only_part_dec_str,
%     \l_@@_luaset_only_part_period_str,
%     \l_@@_luaset_format_part_int_str,
%     \l_@@_luaset_format_part_dec_str,
%   }
%   The literal entries of the numbers that we will pass to |\MathMLintent|
%   with the \emph{intent} \emph{property} |:number| are \emph{stored}
%   in \myhlc|\l_@@_luaset_only_part_int_str| for the
%   \emph{\enquote{integer part}}, in \myhlc|\l_@@_luaset_only_part_dec_str| for the
%   \emph{\enquote{finite decimal part}}, and in \myhlc|\l_@@_luaset_only_part_period_str|
%   for the \emph{\enquote{infinite periodic decimal part}}.
%
%   Formatted versions, \mykey[spnum]{cifras-sep}|=true|, are \emph{stored}
%   in \myhlc|\l_@@_luaset_format_part_int_str| for the
%   \emph{\enquote{integer part}} and \myhlc|\l_@@_luaset_format_part_dec_str| for
%   the \emph{\enquote{finite decimal part}}.
%    \begin{macrocode}
\str_new:N \l_@@_luaset_only_part_int_str
\str_new:N \l_@@_luaset_only_part_dec_str
\str_new:N \l_@@_luaset_only_part_period_str
\str_new:N \l_@@_luaset_format_part_int_str
\str_new:N \l_@@_luaset_format_part_dec_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_luaset_millons_str,
%     \l_@@_luaset_decimal_period_str,
%     \l_@@_luaset_not_decimal_period_str,
%     \l_@@_luaset_not_decimal_not_period_str,
%     \l_@@_luaset_dec_is_all_zeros_str,
%     \l_@@_luaset_has_exponent_str,
%     \l_@@_luaset_exponent_tl,
%   }
%
%   The flag variable \myhlc|\l_@@_luaset_millons_str| indicates whether the
%   integer part translates exactly to a multiple of one million when
%   passed to |\MathMLintent|. To properly structure the reading and writing of numbers in
%   \mynvmath{MathML}, we use the flag variables \myhlc|\l_@@_luaset_decimal_period_str|,
%   \myhlc|\l_@@_luaset_not_decimal_period_str|, and \myhlc|\l_@@_luaset_not_decimal_not_period_str|.
%   The flag variable \myhlc|\l_@@_luaset_dec_is_all_zeros_str| monitors whether the \emph{\enquote{finite
%   decimal part}} is completely zero, useful for \emph{\enquote{pure periodic decimals}}.
%
%   The flag variable \myhlc|\l_@@_luaset_has_exponent_str| indicates
%   whether active \emph{\enquote{scientific notation}} was detected at the end of the
%   entered number, passed with `\textcolor{red}{\texttt{e}}' or
%   `\textcolor{red}{\texttt{E}}'. If true, it will store the digits of the
%   exponent in \myhlc|\l_@@_luaset_exponent_tl|.
%    \begin{macrocode}
\str_new:N \l_@@_luaset_millons_str
\str_new:N \l_@@_luaset_decimal_period_str
\str_new:N \l_@@_luaset_not_decimal_period_str
\str_new:N \l_@@_luaset_not_decimal_not_period_str
\str_new:N \l_@@_luaset_dec_is_all_zeros_str
\str_new:N \l_@@_luaset_has_exponent_str
\tl_new:N  \l_@@_luaset_exponent_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_luaset_has_units_str,
%     \l_@@_luaset_status_str,
%     \l_@@_luaset_arg_above_tl,
%     \l_@@_luaset_arg_below_tl,
%     \l_@@_luaset_denom_has_number_str,
%   }
%
% Since the function \myhlc|\@@_luafun_clean_split_and_set:n| is common to
% |\spnum| (\S\ref{cmd:spnum}) and |\spunit| (\S\ref{cmd:spunit}), it determines the following
% unit-related variables after processing the \mymarg{arguments} via the Lua module \myluamod.
%
% First, it checks if there is any residual text \emph{after} the processed number
% that is a candidate for physical \emph{\enquote{units}}. It sets the flag variable
% \myhlc|\l_@@_luaset_has_units_str| accordingly. Following this, it
% verifies if this text contains more than one \enquote{/}. It then sets
% the variable \myhlc|\l_@@_luaset_status_str| to |valid| or |multislash|.
% If the structure is |valid|, it splits the unit into
% \myhlc|\l_@@_luaset_arg_above_tl| and \myhlc|\l_@@_luaset_arg_below_tl|.
% Finally, it analyzes \myhlc|\l_@@_luaset_arg_below_tl|; if the denominator
% starts with a number, it sets \myhlc|\l_@@_luaset_denom_has_number_str| to
% |true|, otherwise to |false|.
%    \begin{macrocode}
\str_new:N \l_@@_luaset_has_units_str
\str_new:N \l_@@_luaset_status_str
\tl_new:N  \l_@@_luaset_arg_above_tl
\tl_new:N  \l_@@_luaset_arg_below_tl
\str_new:N \l_@@_luaset_denom_has_number_str
%    \end{macrocode}
% \end{variable}
%
% \subsection{Common messages for text mode and math mode}
%
% The \enquote{error}s |need-math-mode| and |need-text-mode| are raised when a
% command that requires math mode or text mode is used in the other one.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { need-math-mode }
  {
    Math~mode~required~for~'\tl_trim_spaces:n {#1}'~\msg_line_context:.
  }
  {
    Use~the~command~inside~'$...$'~or~in~a~math~environment.
  }
\msg_new:nnnn { spintent } { need-text-mode }
  {
    Text~mode~required~for~'\tl_trim_spaces:n {#1}'~\msg_line_context:.
  }
  {
    Use~the~command~outside~of~'$...$'~and~math~environments.
  }
%    \end{macrocode}
%
% \subsection{The command \cs{spnum}}\label{cmd:spnum}
%
% The user command |\spnum| is \emph{first} of \emph{\enquote{the big four}}
% provided by the \mypkg*[no-index]{spintent} package and is fundamental to the
% implementation of other commands. It handles the processing of numerical
% input, scientific notation, and units of measurement, using the Lua module \myluamod,
% which is responsible for separating and assigning variables defined in
% \mypkg{expl3}.
%
% From the \mypkg{expl3} side, we handle the printing logic and the
% intents passed to |\MathMLintent| to achieve a valid
% \mynvmath{MathML} structure that is accepted by \mynvmath{MathCAT}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spnum}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spnum_read_terse_bool,
%     \l_@@_spnum_read_period_sep_str,
%     \l_@@_spnum_notation_sym_tl,
%     \l_@@_spnum_intent_read_sign_str,
%     \l_@@_spnum_intent_read_dec_sep_str,
%   }
%   The variable \myhlc|\l_@@_spnum_read_terse_bool| handled by the key \mykey[spnum]{read-sign}, controls the
%   |intent| passed to |\MathMLintent| to speak the `$\mathcolor{red}{+}$'
%   or `$\mathcolor{red}{-}$'. If set to |clear|, it will say
%   \emph{\enquote{positivo}} or \emph{\enquote{negativo}} \emph{after} reading the
%   number; if set to |terse|, it will say \emph{\enquote{más}} or \emph{\enquote{menos}}
%   \emph{before} reading the number.
%
%   The variable \myhlc|\l_@@_spnum_read_period_sep_str| handled by the key \mykey[spnum]{read-period-sep}, controls how the
%   decimal separator `\textcolor{red}{\texttt{;}}', periodic part, is printed
%   and spoken when the \mymarg{argument} is a \emph{pure decimal periodic} and
%   is passed the |intent| to |\MathMLintent|. If set to |coma|, it will say
%   \emph{\enquote{coma}} and print `\textcolor{red}{,}', if set to |punto| it
%   will say \emph{\enquote{punto}} and print `\textcolor{red}{.}'.
%
%   The variable \myhlc|\l_@@_spnum_notation_sym_tl|, handled by the key
%   \mykey[spnum]{notation-sym}, determines whether `$\mathcolor{red}{\times}$'
%   will be printed when set to |times| or `$\mathcolor{red}{\cdot}$'
%   when set to |cdot|.
%
%   The variable \myhlc|\l_@@_spnum_intent_read_sign_str| will \emph{store} the
%   spoken translation used in the |intent| function passed to
%   |\MathMLintent| for the `$\mathcolor{red}{+}$' or
%   `$\mathcolor{red}{-}$' according to the value passed to the key
%   \mykey[spnum]{read-sign}.
%
%   The variable \myhlc|\l_@@_spnum_intent_read_dec_sep_str| \emph{stores} the
%   spoken translation that will be used in the |intent| function passed to
%   |\MathMLintent| for reading the decimal separation as a
%   \emph{\enquote{coma}} or a \emph{\enquote{punto}} according to the
%   value found in the variable \myhlc|\l_@@_luaset_dec_sep_tl|.
%    \begin{macrocode}
\bool_new:N \l_@@_spnum_read_terse_bool
\str_new:N  \l_@@_spnum_read_period_sep_str
\tl_new:N   \l_@@_spnum_notation_sym_tl
\str_new:N  \l_@@_spnum_intent_read_sign_str
\str_new:N  \l_@@_spnum_intent_read_dec_sep_str
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spnum}}
%
% \begin{macro}[int]{spnum-has-units, spnum-no-digits}
%   The \enquote{error} |spnum-has-units| is raised when the user includes
%   \emph{\enquote{units}} in the \mymarg{argument} passed in `|#1|', advising
%   them to use the |\spunit| command instead. The \enquote{error}
%   |spnum-no-digits| is raised when the provided \mymarg{argument} contains no
%   numerical digits. Since other commands, such as |\spdiv| and |\spmul|,
%   delegate to |\spnum|, these messages do not mention the command name.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spnum-has-units }
  {
    Units~are~not~allowed~in~a~numerical~value~('#1')~\msg_line_context:.
  }
  {
    Use~'\token_to_str:N \spunit'~to~print~numbers~with~units.
  }
\msg_new:nnnn { spintent } { spnum-no-digits }
  {
    Invalid~numerical~value~'#1'~\msg_line_context:.
  }
  {
    A~numerical~value~must~contain~at~least~one~digit.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spnum}}
%
% \begin{macro}[int]{cifras-sep, read-sign, read-period-sep, notation-sym, unknown}
%   We define the \mymeta{keys} \mykey[spnum]{cifras-sep}, \mykey[spnum]{read-sign},
%   \mykey[spnum]{read-period-sep} and \mykey[spnum]{notation-sym}.
%    \begin{macrocode}
\keys_define:nn { spintent / spnum }
  {
    cifras-sep      .bool_set:N = \l_@@_spnum_cifras_sep_bool,
    cifras-sep      .initial:n = true,
    cifras-sep      .value_required:n = true,
    read-sign       .choices:nn =
      { terse , clear }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \bool_set_true:N  \l_@@_spnum_read_terse_bool }
            {2} { \bool_set_false:N \l_@@_spnum_read_terse_bool }
          }
      },
    read-sign       .initial:n = terse,
    read-sign       .value_required:n = true,
    read-sign / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-sign } { terse , ~clear } { \exp_not:n {#1} },
    read-period-sep .choices:nn =
      { coma , punto }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spnum_read_period_sep_str { coma } }
            {2} { \str_set:Nn \l_@@_spnum_read_period_sep_str { punto } }
          }
      },
    read-period-sep .initial:n = coma,
    read-period-sep .value_required:n = true,
    read-period-sep / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-period-sep } { coma, ~punto } { \exp_not:n {#1} },
    notation-sym    .choices:nn =
      { times , cdot }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \tl_set:Nn \l_@@_spnum_notation_sym_tl { \times } }
            {2} { \tl_set:Nn \l_@@_spnum_notation_sym_tl { \cdot } }
          }
      },
    notation-sym    .initial:n = cdot,
    notation-sym    .value_required:n = true,
    notation-sym / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { notation-sym } { times , ~ cdot } { \exp_not:n {#1} },
    unknown         .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spnum }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spnum}}
%
% \begin{macro}[int]{\@@_spnum_render_part:n, \@@_spnum_render_int:, \@@_spnum_render_dec:}
%   The function \myhlc|\@@_spnum_render_part:n| processes the
%   \mymarg{argument} passed in `|#1|' used by the functions \myhlc|\@@_spnum_render_int:| and
%   \myhlc|\@@_spnum_render_dec:| to define the \emph{intent property} |:number| for
%   the integer and decimal part passed to |\MathMLintent|. It evaluates
%   whether the key \mykey[spnum]{cifras-sep} is active or not.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spnum_render_part:n
  {
    \MathMLintent { \str_use:c { l_@@_luaset_only_part_ #1 _str } :number }
      {
        \bool_if:NTF \l_@@_spnum_cifras_sep_bool
          { \str_use:c { l_@@_luaset_format_part_ #1 _str } }
          { \tl_use:c { l_@@_luaset_part_ #1 _tl } }
      }
  }
\cs_new_protected:Nn \@@_spnum_render_int:
  {
    \@@_spnum_render_part:n { int }
  }
\cs_new_protected:Nn \@@_spnum_render_dec:
  {
    \@@_spnum_render_part:n { dec }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spnum_render_only_period:, \@@_spnum_print_period_bar:,\@@_spnum_render_period_sep:}
%   The function \myhlc|\@@_spnum_render_only_period:| isolates the
%   \emph{\enquote{infinite periodic decimal part}} from the
%   \emph{\enquote{finite decimal part}}, assigning the
%   \emph{intent property} |:number| passed to |\MathMLintent| which will be used
%   by |\MathMLarg| in the function \myhlc|\@@_spnum_print_period_bar:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spnum_render_only_period:
  {
    \MathMLintent { \l_@@_luaset_only_part_period_str :number }
      {
        \l_@@_luaset_part_period_tl
      }
  }
%    \end{macrocode}
%   The function \myhlc|\@@_spnum_print_period_bar:| inserts the function
%   \myhlc|\@@_spnum_render_only_period:| as an argument within the
%   \emph{intent} \myhlc|_periódico:postfix($dp)| passed to |\MathMLintent|, generating
%   the correct spoken for the \emph{\enquote{infinite periodic decimal
%   part}}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spnum_print_period_bar:
  {
    \MathMLintent { _periódico:postfix($dp) }
      {
        {
          \overline
            {
              \MathMLarg { dp } { \@@_spnum_render_only_period: }
            }
        }
      }
  }
%    \end{macrocode}
%   The function \myhlc|\@@_spnum_render_period_sep:| sets how the decimal
%   separator handled by the key \mykey[spnum]{read-period-sep} will be
%   printed and spoken when a \emph{\enquote{pure periodic decimal}} is passed.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spnum_render_period_sep:
  {
    \str_if_eq:VnTF \l_@@_spnum_read_period_sep_str { coma }
      {
        \MathMLintent { _coma:prefix($pp) }
          {
            \mathord{,}
            \MathMLarg { pp } { \@@_spnum_print_period_bar: }
          }
      }
      {
        \MathMLintent { _punto:prefix($pp) }
          {
            \mathord{.}
            \MathMLarg { pp } { \@@_spnum_print_period_bar: }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spnum_render_decimal_sep:}
%  The function \myhlc|\@@_spnum_render_decimal_sep:| sets the variable
%  \myhlc|\l_@@_spnum_intent_read_dec_sep_str| with the function
%  \myhlc|_coma:prefix($dp)| for reading a \emph{\enquote{coma}} or \myhlc|_punto:prefix($dp)| for
%  reading a \emph{\enquote{punto}} to |\MathMLintent|, after parsing the value of
%  \myhlc|\l_@@_luaset_dec_sep_tl|. We print the decimal separator
%  stored in \myhlc|\l_@@_luaset_dec_sep_tl| using |\mathord| to remove
%  spaces around it and then use the command |\MathMLarg| where we insert
%  \myhlc|\@@_invisible_sep:| for compatibility with \mynvmath{MathML} and
%  finally call the function \myhlc|\@@_spnum_render_dec:| to print the
%  finite decimal part.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spnum_render_decimal_sep:
  {
    \str_if_eq:VnTF \l_@@_luaset_dec_sep_tl { , }
      { \str_set:Nn \l_@@_spnum_intent_read_dec_sep_str { _coma } }
      { \str_set:Nn \l_@@_spnum_intent_read_dec_sep_str { _punto } }
    \MathMLintent { \l_@@_spnum_intent_read_dec_sep_str :prefix($dp) }
      {
        \mathord { \l_@@_luaset_dec_sep_tl }
        \MathMLarg { dp }
          {
            % keep "dp" in mathml-AF
            \MathMLintent { :silent } { \c_@@_invisible_sep_int }
            \@@_spnum_render_dec:
          }
      }
    % period part
    \str_if_eq:VnT \l_@@_luaset_decimal_period_str { true }
      {
        \str_if_eq:VnTF \l_@@_luaset_dec_is_all_zeros_str { true }
          {
            \@@_inner_sep:
            \@@_spnum_print_period_bar:
          }
          {
            \MathMLintent { _con:prefix($pnum) }
              {
                \@@_inner_sep:
                \MathMLarg { pnum }
                  {
                    \@@_spnum_print_period_bar:
                  }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spnum_print_part_dec:, \@@_spnum_print_part_period:}
%   The function \myhlc|\@@_spnum_print_part_dec:| checks if a decimal part exists.
%   If it does, it determines whether the decimal separator is a comma or a period
%   to set the correct speech intent, \emph{\enquote{coma}} or \emph{\enquote{punto}}.
%   It then prints the separator, renders the decimal digits, and if a periodic
%   sequence follows, it decides how to attach the periodic bar.
%
%   The function \myhlc|\@@_spnum_print_part_period:| handles cases where a
%   periodic part exists. It evaluates if it should render the periodic separator,
%   particularly when the number has a periodic part but lacks a standard
%   decimal part.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spnum_print_part_dec:
  {
    \tl_if_empty:NF \l_@@_luaset_part_dec_tl
      {
        \@@_spnum_render_decimal_sep:
      }
  }
\cs_new_protected:Nn \@@_spnum_print_part_period:
  {
    \tl_if_empty:NF \l_@@_luaset_part_period_tl
      {
        \str_if_eq:VnTF \l_@@_luaset_not_decimal_period_str { true }
          { \@@_spnum_render_period_sep: }
          {
            \tl_if_empty:NT \l_@@_luaset_part_dec_tl
              {
                \@@_spnum_render_period_sep:
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spnum_print_integer:, \@@_spnum_print_num_start:, \@@_spnum_mathml:}
%   The function \myhlc|\@@_spnum_print_integer:| checks if the number consists
%   only of an integer. If so, it renders it. If a decimal or periodic
%   part exists, it sets up an intent to link the integer part, respecting
%   digit separators if enabled, to the subsequent decimal or periodic functions.
%
%   The function \myhlc|\@@_spnum_print_num_start:| checks if an explicit sign
%   is present. If it finds one, it evaluates the terse reading flag to decide
%   whether the sign should be read as a prefix, \emph{\enquote{más}} or
%   \emph{\enquote{menos}}, or as a postfix, \emph{\enquote{positivo}} or
%   \emph{\enquote{negativo}}, and wraps the intent around the rest of the number.
%
%   The function \myhlc|\@@_spnum_mathml:| is the main function.
%   It triggers the sign and base number evaluation, and then checks if a scientific
%   exponent is present. If it is, it appends the correct base-10 notation symbol
%   and exponent to complete the sequence.
%
% \begin{important}*
%   Note for \myhlc|\@@_invisible_sep:|.
% \end{important}
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spnum_print_integer:
  {
    \str_if_eq:VnTF \l_@@_luaset_not_decimal_not_period_str { true }
      { \@@_spnum_render_int: }
      {
        \MathMLintent { \l_@@_luaset_only_part_int_str :number:prefix($next) }
          {
            \bool_if:NTF \l_@@_spnum_cifras_sep_bool
              { \l_@@_luaset_format_part_int_str }
              { \l_@@_luaset_part_int_tl }
            \MathMLarg { next }
              {
                \tl_if_empty:NTF \l_@@_luaset_part_dec_tl
                  { \@@_spnum_print_part_period: }
                  { \@@_spnum_print_part_dec: }
              }
          }
      }
  }
\cs_new_protected:Nn \@@_spnum_print_num_start:
  {
    \tl_if_empty:NTF \l_@@_luaset_sign_tl
      {
        \@@_invisible_sep: % need
        \@@_spnum_print_integer:
      }
      {
        \bool_if:NTF \l_@@_spnum_read_terse_bool
          {
            \str_if_eq:VnTF \l_@@_luaset_sign_tl { + }
              { \str_set:Nn \l_@@_spnum_intent_read_sign_str { _más:prefix($n)} }
              {
                \str_set:Nn \l_@@_spnum_intent_read_sign_str { _menos:prefix($n)}
              }
          }
          {
            \str_if_eq:VnTF \l_@@_luaset_sign_tl { + }
              {
                \str_set:Nn \l_@@_spnum_intent_read_sign_str
                  { _positivo:postfix($n) }
              }
              {
                \str_set:Nn \l_@@_spnum_intent_read_sign_str
                  { _negativo:postfix($n) }
              }
          }
        \@@_invisible_sep: % need
        \MathMLintent { \l_@@_spnum_intent_read_sign_str }
          {
            \l_@@_luaset_sign_tl
            \MathMLarg { n } { \@@_spnum_print_integer: }
          }
      }
  }
\cs_new_protected:Nn \@@_spnum_mathml:
  {
    \@@_spnum_print_num_start:
    \str_if_eq:VnT \l_@@_luaset_has_exponent_str { true }
      {
        \MathMLintent { _por } { \l_@@_spnum_notation_sym_tl }
        10^{ \l_@@_luaset_exponent_tl }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spnum}
%   The public user command \ics*{spnum} accepts an optional \myoarg{key%
%   \textnormal{\textcolor{gray}{=}}val} argument in `|#1|' and a mandatory
%   \mymarg{rational number} in `|#2|'. It first checks if it is executed inside
%   math mode, raising an error if it is not. If correct, it opens a local group
%   to apply the options and passes the input to Lua for parsing via
%   \myhlc|\@@_luafun_clean_split_and_set:n|.
%
%   Before printing, it validates the extracted data. It throws an error if it
%   finds trailing units in the input or if the integer part is completely empty,
%   that is, it has no valid digits. If all checks pass, it calls \myhlc|\@@_spnum_mathml:|
%   to render the final number, and finally closes the group.
%    \begin{macrocode}
\NewDocumentCommand \spnum { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \keys_set:nn { spintent / spnum } {#1}
          \@@_luafun_clean_split_and_set:n { \exp_not:n {#2} }
          \str_if_eq:VnTF \l_@@_luaset_has_units_str { true }
            { \msg_error:nnn { spintent } { spnum-has-units } {#2} }
            {
              \tl_if_empty:NTF \l_@@_luaset_part_int_tl
                { \msg_error:nnn { spintent } { spnum-no-digits } {#2} }
                { \@@_spnum_mathml: }
            }
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spnum } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spunit}}\label{cmd:spunit}
%
% The user command |\spunit| is the \emph{second} of \emph{\enquote{the big four}}
% provided by the \mypkg*[no-index]{spintent} package and is used to parse and print physical units
% and angular systems. It sends the \mymarg{argument} to Lua module \myluamod{} via
% \myhlc|\@@_luafun_clean_split_and_set:n| to separate and clean the \mymarg{argument}.
%
% All the \emph{\enquote{units}}, including SÍ fractions, products, and sexagesimal
% angles, are processed in the same way by the \myluamod{}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spunit}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spunit_luaset_above_n_str,
%     \l_@@_spunit_luaset_above_found_str,
%     \l_@@_spunit_luaset_above_sexa_str,
%     \l_@@_spunit_luaset_below_n_str,
%     \l_@@_spunit_luaset_below_found_str,
%   }
% These string variables store the summary returned by the Lua module \myluamod{}
% for each of the two products of a \emph{\enquote{unit expression}}, the one
% before the slash, |above|, and the one after it, |below|: the number of
% \emph{\enquote{units}} in the product, whether all of them exist in the dictionary
% and, for |above|, whether it is a single sexagesimal symbol. For each unit
% number |<i>| of a product |<p>|, Lua also leaves the canonical symbol, the
% speech text and the exponent. The canonical symbol is stored in
% \myhlc|\l_@@_spunit_luaset_<p>_<i>_canonical_str|, the speech text in
% \myhlc|\l_@@_spunit_luaset_<p>_<i>_read_str| and the exponent, which is
% empty when there is none, in \myhlc|\l_@@_spunit_luaset_<p>_<i>_exp_tl|.
%    \begin{macrocode}
\str_new:N \l_@@_spunit_luaset_above_n_str
\str_new:N \l_@@_spunit_luaset_above_found_str
\str_new:N \l_@@_spunit_luaset_above_sexa_str
\str_new:N \l_@@_spunit_luaset_below_n_str
\str_new:N \l_@@_spunit_luaset_below_found_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spunit_luaset_canonical_str,
%     \l_@@_spunit_luaset_read_str,
%     \l_@@_spunit_luaset_status_str,
%   }
% The first two hold the data of the \emph{\enquote{unit}} being printed, a copy
% of the one stored by Lua for it: the canonical \emph{\enquote{unit symbol}} and
% the correct speech text for \mynvmath{NVDA/MathCAT}. The last one stores the
% result of defining a new \emph{\enquote{unit}} or alias in Lua.
%    \begin{macrocode}
\str_new:N \l_@@_spunit_luaset_canonical_str
\str_new:N \l_@@_spunit_luaset_read_str
\str_new:N \l_@@_spunit_luaset_status_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spunit_exponent_tl,
%     \l_@@_spunit_read_slash_str,
%   }
% These variables are used to format the \emph{\enquote{unit}}: its exponent,
% a copy of the one stored by Lua, and the speech text of the slash.
%    \begin{macrocode}
\tl_new:N  \l_@@_spunit_exponent_tl
\str_new:N \l_@@_spunit_read_slash_str
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spunit}}
%
% \begin{macro}[int]{spunit-invalid-unit, spunit-no-units, spunit-inline-numeric-denominator}
%   The \enquote{error} |spunit-invalid-unit| is raised when the
%   \emph{\enquote{unit expression}} provided in `|#1|' is unknown or
%   malformed. The \enquote{error} |spunit-no-units| is raised when no
%   \emph{\enquote{units}} are found in the \mymarg{argument}, advising the
%   user to use |\spnum| for pure numbers. The \enquote{error}
%   |spunit-inline-numeric-denominator| is raised when a numeric coefficient is
%   illegally placed in the denominator of an inline fraction.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spunit-invalid-unit }
  {
    Invalid~unit~expression~'#1'~\msg_line_context:.
  }
  {
    The~expression~contains~an~unknown~or~malformed~unit.~Check~the~spelling~or~verify~
    that~the~unit~is~defined.
  }
\msg_new:nnnn { spintent } { spunit-no-units }
  {
    No~unit~found~in~'#1'~\msg_line_context:.
  }
  {
    At~least~one~unit~is~required;~use~'\token_to_str:N \spnum'~for~pure~
    numbers.
  }
\msg_new:nnnn { spintent } { spunit-inline-numeric-denominator }
  {
    Invalid~numeric~coefficient~in~the~inline~denominator~'#1'~\msg_line_context:.
  }
  {
    Numbers~are~not~allowed~after~the~slash~'/'~in~the~inline~mode~of~units.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spunit}}
%
% \begin{macro}[int]{print-cdot, read-cdot, read-slash, cifras-sep, read-period-sep, notation-sym, unknown}
%   We define the \mymeta{keys} \mykey[spunit]{print-cdot}, \mykey[spunit]{read-cdot}, \mykey[spunit]{read-slash},
%   \mykey[spunit]{cifras-sep}, \mykey[spunit]{read-period-sep} and \mykey[spunit]{notation-sym}.
%    \begin{macrocode}
\keys_define:nn { spintent / spunit }
  {
    print-cdot      .bool_set:N = \l_@@_spunit_print_cdot_bool,
    print-cdot      .initial:n = false,
    print-cdot      .value_required:n = true,
    read-cdot       .bool_set:N = \l_@@_spunit_read_cdot_bool,
    read-cdot       .initial:n = false,
    read-cdot       .value_required:n = true,
    read-slash      .choices:nn =
      { por , partido-por }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spunit_read_slash_str { _por } }
            {2} { \str_set:Nn \l_@@_spunit_read_slash_str { _partido-por } }
          }
      },
    read-slash      .initial:n = por,
    read-slash      .value_required:n = true,
    read-slash / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-slash } { por, ~partido-por } { \exp_not:n {#1} },
    cifras-sep      .meta:nn = { spintent / spnum } { cifras-sep = {#1} },
    cifras-sep      .value_required:n = true,
    read-period-sep .meta:nn = { spintent / spnum } { read-period-sep = {#1} },
    read-period-sep .value_required:n = true,
    notation-sym    .meta:nn = { spintent / spnum } { notation-sym = {#1} },
    notation-sym    .value_required:n = true,
    unknown         .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spunit }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spunit}}
%
% \begin{macro}[int]{\@@_spunit_format_canonical:}
%   The function \myhlc|\@@_spunit_format_canonical:| handles the visual formatting
%   of the \emph{\enquote{unit symbol}}. It specifically checks for the \emph{liter symbol}
%   `ℓ' to print it as |\ell|, and formats all other valid \emph{\enquote{units}}
%   in standard upright text using |\mathrm|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spunit_format_canonical:
  {
    \str_case:VnF \l_@@_spunit_luaset_canonical_str
      {
        { ℓ } { \ell }
      }
      { \mathrm { \l_@@_spunit_luaset_canonical_str } }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spunit_load_term:nn}
%   The function \myhlc|\@@_spunit_load_term:nn| copies the data that Lua stored
%   for the unit number `|#2|' of the product `|#1|', which is |above| or |below|, into
%   the variables used to print it.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spunit_load_term:nn
  {
    \str_set_eq:Nc \l_@@_spunit_luaset_canonical_str
      { l_@@_spunit_luaset_ #1 _ #2 _canonical_str }
    \str_set_eq:Nc \l_@@_spunit_luaset_read_str
      { l_@@_spunit_luaset_ #1 _ #2 _read_str }
    \tl_set_eq:Nc \l_@@_spunit_exponent_tl
      { l_@@_spunit_luaset_ #1 _ #2 _exp_tl }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spunit_print_term:nn}
%   The function \myhlc|\@@_spunit_print_term:nn| prints the unit number `|#2|'
%   of the product `|#1|', along with its exponent. Unless this is the first
%   unit of the product, it handles the spacing and multiplication dot:
%   |\cdot| when \mykey{print-cdot} is |true|, or a thin space |\,|
%   when it is |false|. In both cases it is read as \emph{\enquote{por}} when
%   \mykey{read-cdot} is |true|, and it is silent otherwise.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spunit_print_term:nn
  {
    \int_compare:nNnT {#2} > { 1 }
      {
        \bool_if:NTF \l_@@_spunit_print_cdot_bool
          {
            \bool_if:NTF \l_@@_spunit_read_cdot_bool
              { \MathMLintent { _por } { \cdot } }
              { \MathMLintent { :silent } { \cdot } }
          }
          {
            \,
            \bool_if:NT \l_@@_spunit_read_cdot_bool
              { \MathMLintent { _por } { \mathopen { \c_@@_invisible_sep_int } } }
          }
      }
%    \end{macrocode}
%   Then it renders the base unit and, if it has one, its exponent.
%    \begin{macrocode}
    \@@_spunit_load_term:nn {#1} {#2}
    \tl_if_empty:NTF \l_@@_spunit_exponent_tl
      {
        \MathMLintent { \l_@@_spunit_luaset_read_str }
          {
            \@@_spunit_format_canonical:
          }
      }
      {
        {
          \MathMLintent { \l_@@_spunit_luaset_read_str }
            {
              \@@_spunit_format_canonical:
            }
        }
        ^ { \l_@@_spunit_exponent_tl }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spunit_print_product:n}
%   The function \myhlc|\@@_spunit_print_product:n| prints all the units of
%   the product `|#1|', |above| or |below|, in the order in which Lua found them.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spunit_print_product:n
  {
    \int_step_inline:nn { \str_use:c { l_@@_spunit_luaset_ #1 _n_str } }
      { \@@_spunit_print_term:nn {#1} {##1} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_print_spunit_hierarchy:}
%   The function \myhlc|\@@_print_spunit_hierarchy:| handles the layout of inline
%   \emph{\enquote{unit}} fractions. It first prints the \emph{\enquote{units}}
%   before the slash. If a denominator exists, it prints the slash |/| `$\mathcolor{red}{/}$'
%   with its corresponding speech intent, and then prints the remaining \emph{\enquote{units}}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_print_spunit_hierarchy:
  {
    \tl_if_empty:NF \l_@@_luaset_arg_above_tl
      {
        \@@_spunit_print_product:n { above }
        \tl_if_empty:NF \l_@@_luaset_arg_below_tl
          {
            \MathMLintent { \l_@@_spunit_read_slash_str } { / }
            \@@_spunit_print_product:n { below }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spunit_exec:n}
%   The function \myhlc|\@@_spunit_exec:n| performs the main validation
%   and layout for \emph{\enquote{units}}. It first checks if the \mymarg{argument}
%   has units, is a valid expression, and does not contain illegal numbers
%   in the denominator, throwing the appropriate \enquote{error}s if any check fails.
%
%   Then it asks Lua to split each product of the expression into its units
%   and to look them up; if any of them is unknown, it raises the
%   \enquote{error} for an invalid expression.
%
%   If the units are valid, it calls \myhlc|\@@_spunit_mathml:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spunit_exec:n
  {
    \str_if_eq:VnTF \l_@@_luaset_has_units_str { false }
      { \msg_error:nnn { spintent } { spunit-no-units } {#1} }
      {
        \str_if_eq:VnTF \l_@@_luaset_status_str { valid }
          {
            \str_if_eq:VnTF
              \l_@@_luaset_denom_has_number_str { true }
              {
                \msg_error:nnn { spintent }
                  { spunit-inline-numeric-denominator } {#1}
              }
              {
                \@@_luafun_spunit_parse_product:nV { above }
                  \l_@@_luaset_arg_above_tl
                \tl_if_empty:NF \l_@@_luaset_arg_below_tl
                  {
                    \@@_luafun_spunit_parse_product:nV { below }
                      \l_@@_luaset_arg_below_tl
                  }
                \bool_lazy_and:nnTF
                  {
                    \str_if_eq_p:ee { \l_@@_spunit_luaset_above_found_str }
                      { true }
                  }
                  {
                    \bool_lazy_or_p:nn
                      { \tl_if_empty_p:N \l_@@_luaset_arg_below_tl }
                      {
                        \str_if_eq_p:ee { \l_@@_spunit_luaset_below_found_str }
                          { true }
                      }
                  }
                  {
                    \@@_spunit_mathml:
                  }
                  { \msg_error:nnn { spintent } { spunit-invalid-unit } {#1} }
              }
          }
          { \msg_error:nnn { spintent } { spunit-invalid-unit } {#1} }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spunit_mathml:}
%   The function \myhlc|\@@_spunit_mathml:| builds the \mynvmath{MathML}
%   structure of a validated expression. If it is a sexagesimal angle in
%   degrees, minutes, or seconds, it prints the number and the symbol
%   \emph{\enquote{without any space in between}}, applying a slight negative
%   space `|\!|' for |arcseconds|. When the number is exactly |1|, with no
%   decimal part, the reading becomes the singular literal \myhlc|_grado:unit|,
%   \myhlc|_minuto:unit| or \myhlc|_segundo:unit|, since neither the fixed plural names nor
%   \mynvmath{MathCAT} can see the number. For standard units, it prints the
%   number followed by a thin space `|\,|' and then processes the unit
%   sequence.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spunit_mathml:
  {
    \str_if_eq:VnTF \l_@@_spunit_luaset_above_sexa_str { true }
      {
        \@@_spunit_load_term:nn { above } { 1 }
        \tl_if_empty:NF \l_@@_luaset_part_int_tl
          {
            \@@_spnum_mathml:
          }
        \bool_lazy_and:nnT
          { \str_if_eq_p:ee { \l_@@_luaset_part_int_tl } { 1 } }
          { \tl_if_empty_p:N \l_@@_luaset_part_dec_tl }
          {
            \str_set:Ne \l_@@_spunit_luaset_read_str
              {
                \str_case:Vn \l_@@_spunit_luaset_canonical_str
                  {
                    { ° } { _grado:unit   }
                    { ′ } { _minuto:unit  }
                    { ″ } { _segundo:unit }
                  }
              }
          }
        \MathMLintent { \l_@@_spunit_luaset_read_str }
          {
            \mathord
              {
                \str_case:Vn \l_@@_spunit_luaset_canonical_str
                  {
                    { ″ } { \prime \! \prime }
                    { ′ } { \prime           }
                    { ° } { °                }
                  }
              }
          }
      }
      {
        \tl_if_empty:NF \l_@@_luaset_part_int_tl
          {
            \@@_spnum_mathml: \,
          }
        \@@_print_spunit_hierarchy:
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spunit}
%   The public user command \ics*{spunit} accepts an optional \myoarg{key%
%   \textnormal{\textcolor{gray}{=}}val} argument in `|#1|' and a mandatory
%   \mymarg{unit expression} in `|#2|'. It first checks if it is executed inside
%   \emph{math mode}, raising an \enquote{error} if it is not. If correct, it opens a local group
%   to apply the options and passes the \mymarg{argument} to Lua module for parsing via
%   \myhlc|\@@_luafun_clean_split_and_set:n| function.
%
%   Finally, it delegates the execution to the function \myhlc|\@@_spunit_exec:n|
%   to perform the safety checks and render the parsed data, before closing the group.
%    \begin{macrocode}
\NewDocumentCommand \spunit { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \keys_set:nn { spintent / spunit } {#1}
          \@@_luafun_clean_split_and_set:n { \exp_not:n {#2} }
          \@@_spunit_exec:n {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spunit } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The commands \cs{spnewunit} and \cs{spunitalias}}\label{cmd:spnewunit}
%
% The user commands |\spnewunit| and |\spunitalias| allow users to add
% new custom \emph{\enquote{units}} or \emph{\enquote{aliases}} to the
% package's dictionary. Similar to |\spunit|, these commands delegate
% the processing and validation to the \myluamod{} via
% \myhlc|\@@_luafun_define_custom_unit:nn| and \myhlc|\@@_luafun_define_spunit_alias:nn|.
%
% During the definition, the Lua module checks if the new \emph{\enquote{unit}}
% or \emph{\enquote{alias}} is valid and does not conflict with existing \emph{\enquote{units}}.
% It returns the result of this operation to \TeX{} using the \myhlc|\l_@@_spunit_luaset_status_str| variable.
% If a collision is detected or the \mymarg{argument} is invalid, the package throws the
% corresponding \enquote{error} message.
%
% \subsubsection{Definition of error messages for \cs[no-index]{spnewunit} and \cs[no-index]{spunitalias}}
%
% \begin{macro}[int]{spnewunit-duplicate, spunitalias-duplicate, spunitalias-invalid-expr}
%   The \enquote{error}s |spnewunit-duplicate| and |spunitalias-duplicate| are
%   raised when the user attempts to define a \emph{\enquote{new unit}} or
%   alias passed in `|#1|' that already exists in the dictionary. The
%   \enquote{error} |spunitalias-invalid-expr| is raised by |\spunitalias| when
%   the target expression provided in `|#2|' is invalid, for example, because it
%   contains unknown \emph{\enquote{units}} or incorrect syntax.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spnewunit-duplicate }
  {
    The~unit~symbol~or~alias~'#1'~is~already~defined~\msg_line_context:.
  }
  {
    Existing~units~and~aliases~cannot~be~redefined~with~
    '\token_to_str:N \spnewunit'.
  }
\msg_new:nnnn { spintent } { spunitalias-duplicate }
  {
    The~alias~'#1'~is~already~defined~or~reserved~\msg_line_context:.
  }
  {
    Existing~units~and~aliases~cannot~be~redefined.
  }
\msg_new:nnnn { spintent } { spunitalias-invalid-expr }
  {
    Invalid~expression~'#2'~for~alias~'#1'~\msg_line_context:.
  }
  {
    The~expression~must~contain~only~defined~units,~exponents~and~one~'/'.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spnewunit} and \cs[no-index]{spunitalias}}
%
% \begin{macro}{\spnewunit}
%   The public command \ics*{spnewunit} defines a new custom unit. It passes
%   the new unit symbol `|#1|' and its speech text `|#2|' to Lua via
%   \myhlc|\@@_luafun_define_custom_unit:nn|. If the module reports that the
%   unit already exists, it throws the corresponding duplicate error.
%    \begin{macrocode}
\NewDocumentCommand \spnewunit { m m }
  {
    \mode_if_math:TF
      {
        \msg_error:nnn { spintent } { need-text-mode } { \spnewunit }
      }
      {
        \@@_luafun_define_custom_unit:nn {#1} {#2}
        \str_if_eq:VnT \l_@@_spunit_luaset_status_str { duplicate }
          {
            \msg_error:nnn { spintent } { spnewunit-duplicate } {#1}
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spunitalias}
%   The public command \ics*{spunitalias} creates a shortcut for a complex
%   unit expression. It passes the new alias name `|#1|' and the target
%   expression `|#2|' to Lua via \myhlc|\@@_luafun_define_spunit_alias:nn|.
%   It throws an error if the alias already exists, or if the provided target
%   expression contains invalid syntax or unknown units.
%    \begin{macrocode}
\NewDocumentCommand \spunitalias { m m }
  {
    \mode_if_math:TF
      {
        \msg_error:nnn { spintent } { need-text-mode } { \spunitalias }
      }
      {
        \@@_luafun_define_spunit_alias:nn {#1} {#2}
        \str_if_eq:VnT \l_@@_spunit_luaset_status_str { duplicate }
          {
            \msg_error:nnn { spintent } { spunitalias-duplicate } {#1}
          }
        \str_if_eq:VnT \l_@@_spunit_luaset_status_str { invalid-expr }
          {
            \msg_error:nnnn { spintent } { spunitalias-invalid-expr } {#1} {#2}
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spmoney}}\label{cmd:spmoney}
%
% The user command |\spmoney| is the \emph{third} of
% \emph{\enquote{the big four}} provided by the \mypkg*[no-index]{spintent} package,
% designed to format monetary values and currencies. It sends the
% \mymarg{argument} to the Lua module \myluamod{} with
% \myhlc|\@@_luafun_clean_split_and_set:n|, and then gets the
% \emph{\enquote{currency symbol}}, the names of the currency and its layout
% from the database.
%
% The command accepts \emph{optional arguments} to change the default
% \emph{\enquote{currency symbol}} or to write the \emph{\enquote{subunit}}.
% Finally, it builds the intent for \mynvmath{MathCAT}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spmoney}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spmoney_luaset_symbol_str,
%     \l_@@_spmoney_luaset_position_str,
%   }
% These variables are set by the Lua module \myluamod{}. The variable
% \myhlc|\l_@@_spmoney_luaset_symbol_str| stores the currency symbol and
% \myhlc|\l_@@_spmoney_luaset_position_str| stores its position, |pre| or
% |post|.
%    \begin{macrocode}
\str_new:N \l_@@_spmoney_luaset_symbol_str
\str_new:N \l_@@_spmoney_luaset_position_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spmoney_luaset_sing_str,
%     \l_@@_spmoney_luaset_plur_str,
%     \l_@@_spmoney_luaset_conde_str,
%     \l_@@_spmoney_luaset_subsing_str,
%     \l_@@_spmoney_luaset_subplur_str,
%   }
% The variables \myhlc|\l_@@_spmoney_luaset_sing_str|,
% \myhlc|\l_@@_spmoney_luaset_plur_str| and
% \myhlc|\l_@@_spmoney_luaset_conde_str| store the name of the main unit in
% singular, in plural and in the form with \emph{«de»}. The variables
% \myhlc|\l_@@_spmoney_luaset_subsing_str| and
% \myhlc|\l_@@_spmoney_luaset_subplur_str| store the name of the subunit in
% singular and in plural, and are empty if the currency has no subunits.
%    \begin{macrocode}
\str_new:N \l_@@_spmoney_luaset_sing_str
\str_new:N \l_@@_spmoney_luaset_plur_str
\str_new:N \l_@@_spmoney_luaset_conde_str
\str_new:N \l_@@_spmoney_luaset_subsing_str
\str_new:N \l_@@_spmoney_luaset_subplur_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spmoney_luaset_print_iso_str,
%     \l_@@_spmoney_luaset_currency_arg_str,
%     \l_@@_spmoney_luaset_status_str,
%   }
% The variable \myhlc|\l_@@_spmoney_luaset_print_iso_str| stores |true| or
% |false| according to whether the \texttt{ISO} code is printed. The variable
% \myhlc|\l_@@_spmoney_luaset_currency_arg_str| stores the name of the
% currency, or the text as given if it was not found, and
% \myhlc|\l_@@_spmoney_luaset_status_str| stores |found| or |notfound|.
%    \begin{macrocode}
\str_new:N \l_@@_spmoney_luaset_print_iso_str
\str_new:N \l_@@_spmoney_luaset_currency_arg_str
\str_new:N \l_@@_spmoney_luaset_status_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spmoney_luaset_extracted_curr_str,
%     \l_@@_spmoney_luaset_extracted_num_tl,
%   }
% The variables \myhlc|\l_@@_spmoney_luaset_extracted_curr_str| and
% \myhlc|\l_@@_spmoney_luaset_extracted_num_tl| store the currency and the
% number that the module extracts from the \mymarg{argument}.
%    \begin{macrocode}
\str_new:N \l_@@_spmoney_luaset_extracted_curr_str
\tl_new:N  \l_@@_spmoney_luaset_extracted_num_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spmoney_sub_units_bool,
%     \l_@@_spmoney_intent_money_str,
%     \l_@@_spmoney_intent_sign_str,
%     \l_@@_spmoney_intent_sub_unit_str,
%     \l_@@_spmoney_res_main_voice_str,
%     \l_@@_spmoney_subnum_int,
%   }
% The variable \myhlc|\l_@@_spmoney_sub_units_bool| stores whether the
% subunits are written; it is created by the key \mykey[spmoney]{sub-units}.
% The variables
% \myhlc|\l_@@_spmoney_intent_money_str|,
% \myhlc|\l_@@_spmoney_intent_sign_str| and
% \myhlc|\l_@@_spmoney_intent_sub_unit_str| store the intent of the main
% unit, of the sign and of the subunit. The variable
% \myhlc|\l_@@_spmoney_res_main_voice_str| stores the text read for the main
% unit. The variable \myhlc|\l_@@_spmoney_subnum_int| stores the number of
% subunits, to choose between singular and plural.
%    \begin{macrocode}
\str_new:N \l_@@_spmoney_intent_money_str
\str_new:N \l_@@_spmoney_intent_sign_str
\str_new:N \l_@@_spmoney_intent_sub_unit_str
\str_new:N \l_@@_spmoney_res_main_voice_str
\int_new:N \l_@@_spmoney_subnum_int
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spmoney}}
%
% \begin{macro}[int]{spmoney-no-subunits, spmoney-invalid-currency}
%   The \enquote{warning} |spmoney-no-subunits| is raised when subunit rendering
%   is requested for a currency that does not support fractional divisions,
%   such as JPY or CLP, ignoring the flag. The \enquote{error}
%   |spmoney-invalid-currency| is raised when the provided currency identifier
%   is not registered in the Lua database or is not written validly: an alias
%   or symbol with any uppercase letter, or an \texttt{ISO} code that is
%   neither all lowercase nor all uppercase.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spmoney-no-subunits }
  {
    The~option~'sub-units=true'~is~ignored~for~the~currency~'#1'~\msg_line_context:.
  }
  {
    Currencies~like~'JPY'~or~'CLP'~have~no~subunits~in~the~Lua~module.
  }
\msg_new:nnnn { spintent } { spmoney-invalid-currency }
  {
    Invalid~currency~'#1'~for~'\token_to_str:N \spmoney'~\msg_line_context:.
  }
  {
    The~symbol,~alias~or~ISO~code~does~not~exist~in~the~Lua~module.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spmoney}}
%
% \begin{macro}[int]
%   {
%     currency,
%     sub-units,
%     money-math,
%     cifras-sep,
%     unknown,
%   }
%   We define the \mymeta{keys} \mykey[spmoney]{currency}, \mykey[spmoney]{sub-units},
%   \mykey[spmoney]{money-math} and \mykey[spmoney]{cifras-sep}.
%    \begin{macrocode}
\keys_define:nn { spintent / spmoney }
  {
    currency   .code:n = { \@@_spmoney_set_currency:n {#1} },
    currency   .value_required:n = true,
    sub-units  .bool_set:N = \l_@@_spmoney_sub_units_bool,
    sub-units  .initial:n = { false },
    sub-units  .value_required:n = true,
    money-math .bool_set:N = \l_@@_spmoney_money_math_bool,
    money-math .initial:n = { true },
    money-math .value_required:n = true,
    cifras-sep .meta:nn = { spintent / spnum } { cifras-sep = {#1} },
    cifras-sep .value_required:n = true,
    unknown    .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spmoney }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spmoney}}
%
% \begin{macro}[int]{\@@_spmoney_set_currency:n, \@@_spmoney_normalize_key:n, \@@_spmoney_normalize_key:V}
%   The function \myhlc|\@@_spmoney_set_currency:n| checks the currency
%   identifier in `|#1|' against the database. It calls
%   \myhlc|\@@_spmoney_normalize_key:n|, which uses the Lua module \myluamod{}.
%   If the module returns |notfound|, it raises an \enquote{error}. Otherwise,
%   it stores the identifier in
%   \myhlc|\l_@@_spmoney_luaset_currency_arg_str|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmoney_set_currency:n
  {
    \@@_spmoney_normalize_key:n {#1}
    \str_if_eq:VnTF \l_@@_spmoney_luaset_status_str { notfound }
      {
        \msg_error:nnn { spintent } { spmoney-invalid-currency } {#1}
      }
      { \str_set:Nn \l_@@_spmoney_luaset_currency_arg_str {#1} }
  }
\cs_new_protected:Nn \@@_spmoney_normalize_key:n
  {
    \@@_luafun_spmoney_normalize_key:n { \exp_not:n {#1} }
  }
\cs_generate_variant:Nn \@@_spmoney_normalize_key:n { V }
%    \end{macrocode}
%   Since the key \mykey[spmoney]{currency} calls
%   \myhlc|\@@_spmoney_set_currency:n|, its default value is set once the
%   function exists.
%    \begin{macrocode}
\keys_set:nn { spintent / spmoney } { currency = pesos }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmoney_validate_money:}
%   The function \myhlc|\@@_spmoney_validate_money:| verifies if the parsed
%   input contains an embedded currency identifier by checking
%   \myhlc|\l_@@_luaset_has_units_str|. If a currency is detected, it extracts
%   and normalizes the key from \myhlc|\l_@@_luaset_arg_above_tl|. It throws
%   an \enquote{error} if the currency is not found in the database. If valid, it
%   updates \myhlc|\l_@@_spmoney_luaset_currency_arg_str| and retrieves the
%   full currency metadata from the \myluamod{} via \myhlc|\@@_luafun_spmoney_lookup_data:V|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmoney_validate_money:
  {
    \str_if_eq:VnT \l_@@_luaset_has_units_str { true }
      {
        \@@_spmoney_normalize_key:V \l_@@_luaset_arg_above_tl
        \str_if_eq:VnTF \l_@@_spmoney_luaset_status_str { notfound }
          {
            \msg_error:nne { spintent } { spmoney-invalid-currency }
              { \l_@@_luaset_arg_above_tl }
          }
          {
            \str_set:NV
              \l_@@_spmoney_luaset_currency_arg_str
              \l_@@_luaset_arg_above_tl
          }
      }
    \str_set:Ne \l_@@_spmoney_luaset_status_str { found }
    \@@_luafun_spmoney_lookup_data:V \l_@@_spmoney_luaset_currency_arg_str
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmoney_print_symbol:}
%   The function \myhlc|\@@_spmoney_print_symbol:| renders the \emph{\enquote{currency symbol}}.
%   If \myhlc|\l_@@_spmoney_luaset_print_iso_str| is |true|, it outputs the ISO
%   code using |\mathrm|. Otherwise, it prints the \emph{\enquote{standard symbol}},
%   directly in math mode when \myhlc|\l_@@_spmoney_money_math_bool| is |true|,
%   or wrapped inside |\text| when it is |false|. For dollar signs `\$', the
%   symbol is typeset through the control sequence |\$| in both cases.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmoney_print_symbol:
  {
    \str_if_eq:VnTF \l_@@_spmoney_luaset_print_iso_str { true }
      { \mathrm { \l_@@_spmoney_luaset_currency_arg_str } }
      {
        \str_if_eq:VnTF \l_@@_spmoney_luaset_symbol_str { $ }
          {
            \bool_if:NTF \l_@@_spmoney_money_math_bool
              { \$ } { \text { \$ } }
          }
          {
            \bool_if:NTF \l_@@_spmoney_money_math_bool
              { \l_@@_spmoney_luaset_symbol_str }
              { \text { \l_@@_spmoney_luaset_symbol_str } }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmoney_render_layout:}
%   The function \myhlc|\@@_spmoney_render_layout:| determines the layout
%   order of the \emph{\enquote{currency symbol}} relative to the numeric
%   value. It evaluates \myhlc|\l_@@_spmoney_luaset_position_str|; if set to |pre|,
%   it renders the \emph{\enquote{currency symbol}} \emph{before} the number
%   node. Otherwise, it places the number first, followed by the
%   \emph{\enquote{currency symbol}}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmoney_render_layout:
  {
    \str_if_eq:VnTF \l_@@_spmoney_luaset_position_str { pre }
      {
        \@@_spmoney_print_symbol:
        \MathMLarg { n } { \@@_spmoney_render_number_node: }
      }
      {
        \MathMLarg { n } { \@@_spmoney_render_number_node: }
        \@@_spmoney_print_symbol:
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmoney_render_number_node:, \@@_spmoney_render_only_integer_node:}
%   The function \myhlc|\@@_spmoney_render_number_node:| renders the whole
%   number, with the integer and the decimal part. The function
%   \myhlc|\@@_spmoney_render_only_integer_node:| renders only the integer
%   part.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmoney_render_number_node:
  {
    \@@_spnum_render_int:
    \@@_spnum_print_part_dec:
  }
\cs_new_protected:Nn \@@_spmoney_render_only_integer_node:
  {
    \@@_spnum_render_int:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmoney_mathml:}
%   The function \myhlc|\@@_spmoney_mathml:| checks if there is an explicit
%   sign in \myhlc|\l_@@_luaset_sign_tl|. If there is one, it sets the prefix
%   intent `|más|' or `|menos|' and puts the sign and the rest inside a
%   |\MathMLintent|. If there is no sign, it continues according to whether
%   the subunits are enabled.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmoney_mathml:
  {
    \tl_if_empty:NTF \l_@@_luaset_sign_tl
      {
        \bool_if:NTF \l_@@_spmoney_sub_units_bool
          { \@@_spmoney_set_and_print_sub_units: }
          { \@@_spmoney_intent_int: }
      }
      {
        \str_if_eq:VnTF \l_@@_luaset_sign_tl { + }
          {
            \str_set:Nn \l_@@_spmoney_intent_sign_str { _más:prefix ($n) }
          }
          {
            \str_set:Nn \l_@@_spmoney_intent_sign_str { _menos:prefix ($n) }
          }
        \@@_invisible_sep:
        \MathMLintent { \l_@@_spmoney_intent_sign_str }
          {
            \l_@@_luaset_sign_tl
            \MathMLarg { n }
              {
                \bool_if:NTF \l_@@_spmoney_sub_units_bool
                  { \@@_spmoney_set_and_print_sub_units: }
                  { \@@_spmoney_intent_int: }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmoney_intent_int:}
%   The function \myhlc|\@@_spmoney_intent_int:| chooses the form of the
%   currency name: singular, plural, or the form with `|de|' for exact
%   millions. It looks at the integer and the decimal part to choose it, and
%   sets it as a postfix intent over the layout rendered by
%   \myhlc|\@@_spmoney_render_layout:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmoney_intent_int:
  {
    \tl_if_empty:NTF \l_@@_luaset_dec_sep_tl
      {
        \str_if_eq:VnTF \l_@@_luaset_only_part_int_str { 1 }
          {
            \str_set:NV \l_@@_spmoney_intent_money_str
              \l_@@_spmoney_luaset_sing_str
          }
          {
            \str_set:NV \l_@@_spmoney_intent_money_str
              \l_@@_spmoney_luaset_plur_str
          }
        \str_if_eq:VnT \l_@@_luaset_millons_str { true }
          {
            \str_set:NV \l_@@_spmoney_intent_money_str
              \l_@@_spmoney_luaset_conde_str
          }
      }
      {
        \str_set:NV \l_@@_spmoney_intent_money_str
          \l_@@_spmoney_luaset_plur_str
        \str_if_eq:VnT \l_@@_luaset_millons_str { true }
          {
            \str_if_eq:VnTF \l_@@_luaset_dec_is_all_zeros_str { true }
              {
                \str_set:NV \l_@@_spmoney_intent_money_str
                  \l_@@_spmoney_luaset_conde_str
              }
              {
                \str_set:NV \l_@@_spmoney_intent_money_str
                  \l_@@_spmoney_luaset_plur_str
              }
          }
      }
    \@@_invisible_sep:
    \MathMLintent { _ \l_@@_spmoney_intent_money_str :postfix($n) }
      {
        \@@_spmoney_render_layout:
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmoney_check_sub_units:, \@@_spmoney_set_and_print_sub_units:}
%   The function \myhlc|\@@_spmoney_check_sub_units:| verifies whether the
%   selected currency supports \emph{\enquote{subunits}}. If the database returns empty
%   subunit data, it triggers a warning and disables the
%   \emph{\enquote{subunit}} rendering flag. The flag is also disabled,
%   without a warning, when the amount has no decimal part, since there
%   are no \emph{\enquote{subunits}} to voice.
%
%   The function \myhlc|\@@_spmoney_set_and_print_sub_units:| sets the intents
%   when the \emph{\enquote{subunits}} are enabled. It completes a
%   single-digit decimal like `.5` to `50`, chooses the singular or the plural
%   for the main \emph{\enquote{currency}} and for the
%   \emph{\enquote{subunits}}, and calls \myhlc|\@@_spmoney_build_tree:| to
%   build the \mynvmath{MathML} tree.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmoney_check_sub_units:
  {
    \str_if_empty:NT \l_@@_spmoney_luaset_subsing_str
      {
        \msg_warning:nne { spintent } { spmoney-no-subunits }
          { \l_@@_spmoney_luaset_currency_arg_str }
        \bool_set_false:N \l_@@_spmoney_sub_units_bool
      }
    \tl_if_empty:NT \l_@@_luaset_dec_sep_tl
      { \bool_set_false:N \l_@@_spmoney_sub_units_bool }
  }
\cs_new_protected:Nn \@@_spmoney_set_and_print_sub_units:
  {
    \int_compare:nNnTF { \str_count:N \l_@@_luaset_only_part_dec_str } = { 1 }
      {
        \int_set:Nn \l_@@_spmoney_subnum_int
          { \l_@@_luaset_only_part_dec_str * 10 }
        \str_set:Ne \l_@@_luaset_only_part_dec_str
          { \l_@@_luaset_only_part_dec_str 0 }
      }
      {
        \int_set:Nn \l_@@_spmoney_subnum_int
          { \l_@@_luaset_only_part_dec_str }
      }
    \int_compare:nNnTF { \l_@@_spmoney_subnum_int } = { 1 }
      {
        \str_set:Ne \l_@@_spmoney_intent_sub_unit_str
          { _ \l_@@_spmoney_luaset_subsing_str :postfix($sub) }
      }
      {
        \str_set:Ne \l_@@_spmoney_intent_sub_unit_str
          { _ \l_@@_spmoney_luaset_subplur_str :postfix($sub) }
      }
    \str_if_eq:VnTF \l_@@_luaset_only_part_int_str { 1 }
      {
        \str_set:NV \l_@@_spmoney_res_main_voice_str
          \l_@@_spmoney_luaset_sing_str
      }
      {
        \str_set:NV \l_@@_spmoney_res_main_voice_str
          \l_@@_spmoney_luaset_plur_str
      }
    \str_if_eq:VnT \l_@@_luaset_millons_str { true }
      {
        \str_set:NV \l_@@_spmoney_res_main_voice_str
          \l_@@_spmoney_luaset_conde_str
      }
    \@@_spmoney_build_tree:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]
%   {
%     \@@_spmoney_build_tree:,
%     \@@_spmoney_print_sep_con:,
%     \@@_spmoney_print_dec_sub_unit:,
%   }
%   The function \myhlc|\@@_spmoney_build_tree:| builds the \mynvmath{MathML}
%   tree for currencies with \emph{\enquote{subunits}}. It places the integer
%   part, the decimal separator and the \emph{\enquote{subunit}} according to
%   the position, |pre| or |post|, of the \emph{\enquote{currency symbol}}.
%
%   \myhlc|\@@_spmoney_print_sep_con:| also applies the specific
%   speech intent `\myhlc|_con|' to the decimal separator, while
%   \myhlc|\@@_spmoney_print_dec_sub_unit:| binds the computed \emph{\enquote{subunit}}
%   intent to the decimal digits.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmoney_build_tree:
  {
    \str_if_eq:VnTF \l_@@_spmoney_luaset_position_str { pre }
      {
        \MathMLintent { _ \l_@@_spmoney_res_main_voice_str :postfix($n) }
          {
            \@@_spmoney_print_symbol:
            \MathMLarg { n } { \@@_spmoney_render_only_integer_node: }
          }
        \@@_spmoney_print_sep_con:
        \@@_spmoney_print_dec_sub_unit:
      }
      {
        \MathMLintent { _ \l_@@_spmoney_res_main_voice_str :postfix($n) }
          {
            \@@_inner_sep:
            \MathMLarg { n } { \@@_spmoney_render_only_integer_node: }
          }
        \@@_spmoney_print_sep_con:
        \@@_spmoney_print_dec_sub_unit:
        \MathMLintent { :silent } { \@@_spmoney_print_symbol: }
      }
  }
\cs_new_protected:Nn \@@_spmoney_print_sep_con:
  {
    \MathMLintent { _con }
      {
        \mathord { \l_@@_luaset_dec_sep_tl }
      }
  }
\cs_new_protected:Nn \@@_spmoney_print_dec_sub_unit:
  {
    \MathMLintent { \l_@@_spmoney_intent_sub_unit_str }
      {
        \@@_inner_sep:
        \MathMLarg { sub }
          {
            \@@_spnum_render_dec:
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmoney_parse_and_route:n}
%   The internal function \myhlc|\@@_spmoney_parse_and_route:n| processes
%   the \mymarg{argument} passed in `|#1|` using Lua module to extract
%   the numeric value and any \emph{\enquote{currency}} identifier. If a
%   \emph{\enquote{currency}} is found in \myhlc|\l_@@_spmoney_luaset_extracted_curr_str|,
%   it sets the corresponding variables. It then calls \myhlc|\@@_spmoney_validate_money:|
%   to verify the \emph{\enquote{currency}}. If \emph{\enquote{subunit}}
%   rendering is enabled in \myhlc|\l_@@_spmoney_sub_units_bool|,
%   it calls \myhlc|\@@_spmoney_check_sub_units:|. Finally, it calls
%   \myhlc|\@@_spmoney_mathml:| to format the output.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmoney_parse_and_route:n
  {
    \@@_luafun_spmoney_prepare_input:n { \exp_not:n {#1} }
    \@@_luafun_clean_split_and_set:n
      { \exp_not:V \l_@@_spmoney_luaset_extracted_num_tl }
    \str_if_empty:NF \l_@@_spmoney_luaset_extracted_curr_str
      {
        \str_set:Ne \l_@@_luaset_has_units_str { true }
        \str_set:NV \l_@@_luaset_arg_above_tl
          \l_@@_spmoney_luaset_extracted_curr_str
      }
    \@@_spmoney_validate_money:
    \bool_if:NT \l_@@_spmoney_sub_units_bool
      { \@@_spmoney_check_sub_units: }
    \@@_spmoney_mathml:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spmoney}
%   The public command \ics*{spmoney} formats monetary values. It accepts an
%   optional argument `|#1|' with the \mymeta{keys} and a mandatory argument
%   `|#2|' with the number and, optionally, the currency. It raises an
%   \enquote{error} if it is used outside \emph{math mode}. Then it opens a
%   local group, sets the keys, calls \myhlc|\@@_spmoney_parse_and_route:n|
%   with the \mymarg{argument}, and closes the group.
%    \begin{macrocode}
\NewDocumentCommand \spmoney { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \keys_set:nn { spintent / spmoney } {#1}
          \@@_spmoney_parse_and_route:n {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spmoney } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spshort}}\label{cmd:spshort}
%
% The user command |\spshort| is the \emph{fourth} and \emph{final} of
% \emph{\enquote{the big four}}. It writes abbreviations, ordinals and
% acronyms in \emph{text mode} and makes \mynvmath{NVDA} read them in full.
% Unlike the \emph{math mode} commands, which use \mynvmath{MathML}
% attributes, this command uses the \texttt{PDF} \emph{tagging tools}
% provided by the \mypkg{tagpdf}\cite{tagpdf} package: the key \mykey{E}
% gives the full form of the abbreviation, ordinal or acronym.
%
% \subsubsection{Definition of variables for \cs[no-index]{spshort}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spshort_luaset_status_str,
%     \l_@@_spshort_luaset_layout_str,
%     \l_@@_spshort_luaset_expand_str,
%     \l_@@_spshort_luaset_output_str,
%     \l_@@_spshort_luaset_base_str,
%     \l_@@_spshort_luaset_suffix_str,
%   }
% These variables store the values returned by the Lua module \myluamod{}.
% The variable \myhlc|\l_@@_spshort_luaset_status_str| stores |success|,
% |fallback| or |error|, and \myhlc|\l_@@_spshort_luaset_layout_str| stores
% the way the abbreviation is typeset. The variable
% \myhlc|\l_@@_spshort_luaset_expand_str| stores the text read, which is also
% the one used by the key \mykey{E}, and
% \myhlc|\l_@@_spshort_luaset_output_str| stores the text that is printed. For
% ordinals, \myhlc|\l_@@_spshort_luaset_base_str| stores the number and
% \myhlc|\l_@@_spshort_luaset_suffix_str| stores the suffix.
%    \begin{macrocode}
\str_new:N \l_@@_spshort_luaset_status_str
\str_new:N \l_@@_spshort_luaset_layout_str
\str_new:N \l_@@_spshort_luaset_expand_str
\str_new:N \l_@@_spshort_luaset_output_str
\str_new:N \l_@@_spshort_luaset_base_str
\str_new:N \l_@@_spshort_luaset_suffix_str
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spshort}}
%
% \begin{macro}[int]{spshort-unknown-abbreviation}
%   The \enquote{error} |spshort-unknown-abbreviation| is raised when the
%   \mymarg{argument} is not an entry of the database in \myluamod{} and it
%   cannot be expanded as an ordinal.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spshort-unknown-abbreviation }
  {
    The~abbreviation~or~ordinal~'#1'~is~invalid~or~not~recognized~\msg_line_context:.
  }
  {
    Check~the~spelling~or~the~grammar~rules~for~ordinals.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spshort}}
%
% \begin{macro}[int]{small-caps, read-arg, unknown}
%   We define the \mymeta{keys} \mykey[spshort]{small-caps} and \mykey[spshort]{read-arg}.
%    \begin{macrocode}
\keys_define:nn { spintent / spshort }
  {
    small-caps .bool_set:N = \l_@@_spshort_smallcaps_bool,
    small-caps .initial:n = false,
    small-caps .value_required:n = true,
    read-arg   .str_set:N = \l_@@_spshort_read_arg_str,
    read-arg   .initial:n = {},
    read-arg   .value_required:n = true,
    unknown    .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spshort }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spshort}}
%
% \begin{macro}[int]{\@@_spshort_tag_span:nn, \@@_spshort_tag_span:en}
%   The internal function \myhlc|\@@_spshort_tag_span:nn| prints the
%   \mymarg{argument} in `|#2|' with the tools of \mypkg{tagpdf}. With the
%   keys |tag| and \mykey{E} it creates the \texttt{PDF} structure
%   |/E (#1)|, so that \mynvmath{NVDA} reads the text in `|#1|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spshort_tag_span:nn
  {
    \tag_mc_end_push:
      \tag_struct_begin:n { tag=Span, E={#1} }
        \tag_mc_begin:n { tag=Span }
          \tag_suspend:n { \spshort }
            #2
          \tag_resume:n { \spshort }
        \tag_mc_end:
      \tag_struct_end:
    \tag_mc_begin_pop:n {}
  }
\cs_generate_variant:Nn \@@_spshort_tag_span:nn { en }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spshort_print:nn}
%   The internal function \myhlc|\@@_spshort_print:nn| prints the
%   abbreviations. It clears \myhlc|\l_@@_spshort_read_arg_str| and sets the
%   \mymeta{keys} in `|#1|'. If that string variable is still empty, it calls
%   \myhlc|\@@_spshort_exec:n| with the \mymarg{argument} in `|#2|'.
%   Otherwise, it calls \myhlc|\@@_spshort_render_bypass:n|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spshort_print:nn
  {
    \str_clear:N \l_@@_spshort_read_arg_str
    \keys_set:nn { spintent / spshort } {#1}
    \str_if_empty:NTF \l_@@_spshort_read_arg_str
      {
        \@@_spshort_exec:n {#2}
      }
      {
        \@@_spshort_render_bypass:n {#2}
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spshort_exec:n}
%   The internal function \myhlc|\@@_spshort_exec:n| resolves the
%   abbreviation or ordinal in `|#1|' with the Lua function
%   \myhlc|\@@_luafun_spshort_process:n|. Then it reads
%   \myhlc|\l_@@_spshort_luaset_status_str| and calls the rendering function
%   for |success|, |fallback| or |error|. Any other status is handled as
%   |error|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spshort_exec:n
  {
    \@@_luafun_spshort_process:n {#1}
    \str_case:VnF \l_@@_spshort_luaset_status_str
      {
        { success }  { \@@_spshort_render_success: }
        { fallback } { \@@_spshort_render_fallback: }
        { error }    { \@@_spshort_render_error:n {#1} }
      }
      { \@@_spshort_render_error:n {#1} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]
%   {
%     \@@_spshort_render_success:,
%     \@@_spshort_render_fallback:,
%     \@@_spshort_render_bypass:n,
%     \@@_spshort_render_error:n,
%   }
%   The function \myhlc|\@@_spshort_render_success:| uses the key \mykey{E}
%   and the layout returned by Lua. The function
%   \myhlc|\@@_spshort_render_fallback:| prints the terms that are not in the
%   database, with \textsc{small caps} if the option is active.
%
%   The function \myhlc|\@@_spshort_render_bypass:n| uses the reading given
%   by the user for the \mymarg{argument} and does not look in the database.
%   The function \myhlc|\@@_spshort_render_error:n| raises the
%   \enquote{error} and prints the \mymarg{argument} as it is, so that the
%   compilation does not stop.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spshort_render_success:
  {
    \@@_spshort_tag_span:en
      { \l_@@_spshort_luaset_expand_str }
      { \@@_spshort_execute_layout: }
  }
\cs_new_protected:Nn \@@_spshort_render_fallback:
  {
    \@@_spshort_tag_span:en
      { \l_@@_spshort_luaset_expand_str }
      {
        \bool_if:NTF \l_@@_spshort_smallcaps_bool
          { \textsc{ \l_@@_spshort_luaset_output_str } }
          { \l_@@_spshort_luaset_output_str }
      }
  }
\cs_new_protected:Nn \@@_spshort_render_bypass:n
  {
    \@@_spshort_tag_span:en { \l_@@_spshort_read_arg_str } {#1}
  }
\cs_new_protected:Nn \@@_spshort_render_error:n
  {
    \msg_error:nnn { spintent } { spshort-unknown-abbreviation } {#1}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spshort_execute_layout:}
%   The internal function \myhlc|\@@_spshort_execute_layout:| applies the
%   layout. It reads \myhlc|\l_@@_spshort_luaset_layout_str|, with a value
%   such as |linear_regular|, |superscript| or |small_caps_pure|, and applies
%   the corresponding formatting. It also combines superscripts with
%   \textsc{small caps} according to the boolean variables.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spshort_execute_layout:
  {
    \str_case:VnF \l_@@_spshort_luaset_layout_str
      {
        { linear_regular }
          {
            \bool_if:NTF \l_@@_spshort_smallcaps_bool
              { \textsc{ \l_@@_spshort_luaset_output_str } }
              { \l_@@_spshort_luaset_output_str }
          }
        { linear_caps }
          { \l_@@_spshort_luaset_output_str }
        { linear_mixed }
          { \l_@@_spshort_luaset_output_str }
        { superscript }
          {
            \l_@@_spshort_luaset_base_str
            \bool_if:NTF \l_@@_spshort_smallcaps_bool
              {
                \textsuperscript{
                  \textsc{
                    \str_lowercase:V \l_@@_spshort_luaset_suffix_str
                  }
                }
              }
              {
                \textsuperscript{ \l_@@_spshort_luaset_suffix_str }
              }
          }
        { small_caps_pure }
          {
            \bool_if:NTF \l_@@_spshort_smallcaps_bool
              {
                \textsc{
                  \str_lowercase:V \l_@@_spshort_luaset_output_str
                }
              }
              { \l_@@_spshort_luaset_output_str }
          }
        { acronym_long }
          { \l_@@_spshort_luaset_output_str }
      }
      { \l_@@_spshort_luaset_output_str }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spshort}
%   The public command \ics*{spshort} writes abbreviations, acronyms and
%   ordinals in \emph{text mode}. It raises an \enquote{error} in \emph{math
%   mode}. Otherwise, it calls \myhlc|\mode_leave_vertical:|, opens a local
%   group and calls \myhlc|\@@_spshort_print:nn|.
%    \begin{macrocode}
\NewDocumentCommand \spshort { O{} m }
  {
    \mode_if_math:TF
      {
        \msg_error:nnn { spintent } { need-text-mode } { \spshort }
      }
      {
        \mode_leave_vertical:
        \group_begin:
          \@@_spshort_print:nn {#1} {#2}
        \group_end:
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spsiglo}}\label{cmd:spsiglo}
%
% The public command |\spsiglo| writes century designations such as siglo
% XXI. It accepts Arabic and Roman numerals in the \mymarg{argument} and
% uses the Lua module \myluamod{} to validate and convert them.
%
% This command works in \emph{text mode} and in \emph{math mode}. In
% \emph{math mode} it builds a \mynvmath{MathML} \texttt{intent} expression.
% In \emph{text mode} it uses a \texttt{PDF} tagging structure, such as
% |/Alt (#1)|, with the key |alt|.
%
% \subsubsection{Definition of variables for \cs[no-index]{spsiglo}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spsiglo_luaset_error_str,
%     \l_@@_spsiglo_luaset_arabic_str,
%     \l_@@_spsiglo_luaset_roman_min_str,
%     \l_@@_spsiglo_print_era_str,
%   }
%   The variable \myhlc|\l_@@_spsiglo_luaset_error_str| stores |true| or
%   |false| if the Lua module \myluamod{} could not read the \mymarg{argument}.
%   The variable \myhlc|\l_@@_spsiglo_luaset_arabic_str| stores the number in
%   Arabic numerals, used for the speech. The variable
%   \myhlc|\l_@@_spsiglo_luaset_roman_min_str| stores the number in lowercase
%   Roman numerals, which is printed with |\textsc|. The variable
%   \myhlc|\l_@@_spsiglo_print_era_str| stores the value set by the key
%   \mykey[spsiglo]{print-era}.
%    \begin{macrocode}
\str_new:N \l_@@_spsiglo_luaset_error_str
\str_new:N \l_@@_spsiglo_luaset_arabic_str
\str_new:N \l_@@_spsiglo_luaset_roman_min_str
\str_new:N \l_@@_spsiglo_print_era_str
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spsiglo}}
%
% \begin{macro}[int]{spsiglo-invalid-arg}
%   The \enquote{error} |spsiglo-invalid-arg| is raised when the
%   Lua module \myluamod{} cannot map the \mymarg{argument} to an
%   integer within the valid range of |1| to |4000|, preventing incorrect
%   speech synthesis.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spsiglo-invalid-arg }
  {
    Invalid~argument~'#2'~for~'\tl_trim_spaces:n {#1}'~\msg_line_context:.
  }
  {
    The~argument~must~be~an~Arabic~or~Roman~numeral~between~'1'~and~'4000'.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spsiglo}}
%
% \begin{macro}[int]{print-era, unknown}
%   We define the \mymeta{key} \mykey[spsiglo]{print-era}.
%    \begin{macrocode}
\keys_define:nn { spintent / spsiglo }
  {
    print-era .choices:nn =
      { none , ac , dc }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spsiglo_print_era_str { none } }
            {2} { \str_set:Nn \l_@@_spsiglo_print_era_str { ac } }
            {3} { \str_set:Nn \l_@@_spsiglo_print_era_str { dc } }
          }
      },
    print-era .initial:n = none,
    print-era .value_required:n = true,
    print-era / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { print-era } { none, ~ac, ~dc } { \exp_not:n {#1} },
    unknown   .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spsiglo }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spsiglo}}
%
% \begin{macro}[int]
%   {
%     \@@_spsiglo_render_simple:nn,
%     \@@_spsiglo_render_simple:VV,
%     \@@_spsiglo_render_with_era:nnn,
%     \@@_spsiglo_render_with_era:VVV,
%   }
%   The functions \myhlc|\@@_spsiglo_render_simple:nn| and
%   \myhlc|\@@_spsiglo_render_with_era:nnn| build the \mynvmath{MathML}
%   \texttt{intent} structures in \emph{math mode}, with the Arabic numeral
%   as the speech. \myhlc|\@@_spsiglo_render_with_era:nnn| also adds the era
%   |ac| or |dc|, with invisible spacing and postfix intents so that
%   \mynvmath{NVDA/MathCAT} reads it correctly.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spsiglo_render_simple:nn
  {
    \MathMLintent { #1:number } { \text { \textsc {#2} } }
  }
\cs_generate_variant:Nn \@@_spsiglo_render_simple:nn { VV }
\cs_new_protected:Nn \@@_spsiglo_render_with_era:nnn
  {
    \str_if_eq:nnTF {#1} { ac }
      {
        \MathMLintent { _antes-de-cristo:postfix($z) }
          {
            \@@_inner_sep:
            \MathMLarg { z }
              {
                \MathMLintent { #2:number }
                  {
                    \text { \textsc {#3}~a.~C. }
                  }
              }
          }
      }
      {
        \MathMLintent { _después-de-cristo:postfix($z) }
          {
            \@@_inner_sep:
            \MathMLarg { z }
              {
                \MathMLintent { #2:number }
                  {
                    \text { \textsc {#3}~d.~C. }
                  }
              }
          }
      }
  }
\cs_generate_variant:Nn \@@_spsiglo_render_with_era:nnn { VVV }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spsiglo_tag_span:nn, \@@_spsiglo_tag_span:en}
%   The function \myhlc|\@@_spsiglo_tag_span:nn| writes the century in
%   \emph{text mode}. It uses \mypkg{tagpdf} to create the \texttt{PDF}
%   structure |/Alt (#1)| with the key |alt|, so that \mynvmath{NVDA/MathCAT}
%   reads the value correctly without intents.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spsiglo_tag_span:nn
  {
    \tag_mc_end_push:
      \tag_struct_begin:n { tag=Span, alt={#1} }
        \tag_mc_begin:n { tag=Span }
          \tag_suspend:n { \spsiglo }
            #2
          \tag_resume:n { \spsiglo }
        \tag_mc_end:
      \tag_struct_end:
    \tag_mc_begin_pop:n {}
  }
\cs_generate_variant:Nn \@@_spsiglo_tag_span:nn { en }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]
%   {
%     \@@_spsiglo_exec:n,
%     \@@_spsiglo_mathml:,
%     \@@_spsiglo_text:,
%   }
%   The function \myhlc|\@@_spsiglo_exec:n| formats century designations
%   based on the active mode. It first checks the \enquote{error} status in
%   the variable \myhlc|\l_@@_spsiglo_luaset_error_str| returned by the Lua
%   module \myluamod{} and issues an \enquote{error} if validation fails.
%
%   If the \mymarg{argument} is valid, in \emph{math mode} it calls
%   \myhlc|\@@_spsiglo_mathml:|, which applies the \mynvmath{MathML}
%   structures depending on whether an era modifier is present. In
%   \emph{text mode} it leaves vertical mode first and calls
%   \myhlc|\@@_spsiglo_text:|, which uses the keys |tag| and |alt| to
%   expand the value of the \mykey[spsiglo]{print-era} option into full text
%   such as \enquote{antes de cristo} for \mynvmath{NVDA/MathCAT}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spsiglo_exec:n
  {
    \mode_if_math:F { \mode_leave_vertical: }
    \str_if_eq:VnTF \l_@@_spsiglo_luaset_error_str { true }
      {
        \msg_error:nnnn { spintent } { spsiglo-invalid-arg }
          { \spsiglo } {#1}
      }
      {
        \mode_if_math:TF
          { \@@_spsiglo_mathml: }
          { \@@_spsiglo_text: }
      }
  }
\cs_new_protected:Nn \@@_spsiglo_mathml:
  {
    \str_if_eq:VnTF \l_@@_spsiglo_print_era_str { none }
      {
        \@@_spsiglo_render_simple:VV
          \l_@@_spsiglo_luaset_arabic_str
          \l_@@_spsiglo_luaset_roman_min_str
      }
      {
        \@@_spsiglo_render_with_era:VVV
          \l_@@_spsiglo_print_era_str
          \l_@@_spsiglo_luaset_arabic_str
          \l_@@_spsiglo_luaset_roman_min_str
      }
  }
\cs_new_protected:Nn \@@_spsiglo_text:
  {
    \str_case:Vn \l_@@_spsiglo_print_era_str
      {
        { none }
        {
          \@@_spsiglo_tag_span:en
            { \l_@@_spsiglo_luaset_arabic_str }
            { \textsc { \l_@@_spsiglo_luaset_roman_min_str } }
        }
        { ac }
        {
          \@@_spsiglo_tag_span:en
            { \l_@@_spsiglo_luaset_arabic_str ~ antes ~ de ~ cristo }
            { \textsc { \l_@@_spsiglo_luaset_roman_min_str } ~ a.~C. }
        }
        { dc }
        {
          \@@_spsiglo_tag_span:en
            { \l_@@_spsiglo_luaset_arabic_str ~ después ~ de ~ cristo }
            { \textsc { \l_@@_spsiglo_luaset_roman_min_str } ~ d.~C. }
        }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spsiglo}
%   The public command \ics*{spsiglo} opens a local group and sets the
%   \mymeta{keys} in `|#1|'. Then it calls
%   \myhlc|\@@_luafun_spsiglo_parse:n| of the Lua module \myluamod{} with the
%   \mymarg{argument} in `|#2|', and calls \myhlc|\@@_spsiglo_exec:n| for
%   \emph{math mode} or \emph{text mode}.
%    \begin{macrocode}
\NewDocumentCommand \spsiglo { O{} m }
  {
    \group_begin:
      \keys_set:nn { spintent / spsiglo } {#1}
      \@@_luafun_spsiglo_parse:n {#2}
      \@@_spsiglo_exec:n {#2}
    \group_end:
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{sptime}}\label{cmd:sptime}
%
% The command |\sptime| is the main public interface for typesetting and
% tagging time expressions. It parses time string formats, such as |HH:MM|
% or |HH:MM:SS|, using the Lua function \myhlc|\@@_luafun_sptime_parse:n|.
% This separates the hours, minutes, seconds, and fractions to construct
% a \mynvmath{MathML} |intent| tree for \mynvmath{NVDA/MathCAT}.
%
% \subsubsection{Definition of variables for \cs[no-index]{sptime}}
%
% \begin{variable}[int]
%   {
%     \l_@@_sptime_luaset_seconds_str,
%     \l_@@_sptime_luaset_has_seconds_str,
%     \l_@@_sptime_luaset_error_str,
%     \l_@@_sptime_luaset_is_fraction_str,
%     \l_@@_sptime_luaset_base_str,
%   }
% These variables store the values returned by the Lua module \myluamod{}.
% The variable \myhlc|\l_@@_sptime_luaset_error_str| stores |true| or |false|
% if the \mymarg{argument} is invalid. The variable
% \myhlc|\l_@@_sptime_luaset_base_str| stores the hours and minutes, and
% \myhlc|\l_@@_sptime_luaset_seconds_str| stores the seconds, which is empty
% if there are none. The variables
% \myhlc|\l_@@_sptime_luaset_has_seconds_str| and
% \myhlc|\l_@@_sptime_luaset_is_fraction_str| store |true| or |false| if the
% time has seconds and if the seconds have a decimal fraction.
%    \begin{macrocode}
\str_new:N \l_@@_sptime_luaset_seconds_str
\str_new:N \l_@@_sptime_luaset_has_seconds_str
\str_new:N \l_@@_sptime_luaset_error_str
\str_new:N \l_@@_sptime_luaset_is_fraction_str
\str_new:N \l_@@_sptime_luaset_base_str
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{sptime}}
%
% \begin{macro}[int]{sptime-invalid-arg}
%   The \enquote{error} |sptime-invalid-arg| is raised when an argument
%   fails the format checks or contains values outside normal ranges, such as
%   hours exceeding 23 or minutes and seconds exceeding 59.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { sptime-invalid-arg }
  {
    Invalid~argument~'#2'~for~'\tl_trim_spaces:n {#1}'~\msg_line_context:.
  }
  {
    Allowed~formats:~'HH:MM',~'HH:MM:SS'~or~'HH:MM:SS.ss'.~
    Hours~must~be~'0-23'~and~minutes/seconds~'0-59'.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{sptime}}
%
% \begin{macro}[int]{\@@_sptime_build_seconds:nn}
%   The internal function \myhlc|\@@_sptime_build_seconds:nn| builds the \mynvmath{MathML}
%   structure for the seconds component of a time expression. It wraps the
%   numeric value `|#2|' in a speech intent, applying a connector prefix,
%   passed in `|#1|', and the "segundos" postfix so it is read correctly by
%   \mynvmath{NVDA/MathCAT}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sptime_build_seconds:nn
  {
    \MathMLintent { #1:prefix($z) }
      {
        \text { : }
        \MathMLarg { z }
          {
            \MathMLintent { _segundos:postfix($m) }
              {
                \@@_inner_sep:
                \MathMLarg { m }
                  {
                    \text {#2}
                  }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_sptime_mathml:}
%   The internal function \myhlc|\@@_sptime_mathml:| builds the complete
%   \mynvmath{MathML} structure for the time expression. It checks the string variables
%   \myhlc|\l_@@_sptime_luaset_has_seconds_str| and
%   \myhlc|\l_@@_sptime_luaset_is_fraction_str| to determine if the time includes
%   seconds or decimal fractions. Based on these values, it inserts the base
%   time string and either calls \myhlc|\@@_sptime_build_seconds:nn| with the
%   appropriate prefix, 'con' or 'minutos-con', or appends the 'minutos'
%   intent if no seconds are present.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sptime_mathml:
  {
    \str_if_eq:VnTF \l_@@_sptime_luaset_has_seconds_str { true }
      {
        \MathMLintent { :time }
          {
            \text { \l_@@_sptime_luaset_base_str }
          }
        \str_if_eq:VnTF \l_@@_sptime_luaset_is_fraction_str { true }
          {
            \@@_sptime_build_seconds:nn { con }
              { \l_@@_sptime_luaset_seconds_str }
          }
          {
            \@@_sptime_build_seconds:nn { _minutos-con }
              { \l_@@_sptime_luaset_seconds_str }
          }
      }
      {
        \MathMLintent { :time }
          {
            \text { \l_@@_sptime_luaset_base_str }
          }
        \str_if_eq:VnT \l_@@_sptime_luaset_is_fraction_str { false }
          {
            \MathMLintent { _minutos }
              {
                \c_@@_invisible_sep_int
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\sptime}
%   The command \ics*{sptime} is the public interface for formatting time
%   expressions. It first checks if it is being used in math mode, issuing
%   an error if not. Inside a local group, it parses the input using
%   \myhlc|\@@_luafun_sptime_parse:n| and checks the error status in
%   \myhlc|\l_@@_sptime_luaset_error_str|. If an invalid format is detected,
%   it issues the |sptime-invalid-arg| error; otherwise, it calls
%   \myhlc|\@@_sptime_mathml:| to assemble the \mynvmath{MathML} output.
%    \begin{macrocode}
\NewDocumentCommand \sptime { m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_luafun_sptime_parse:n {#1}
          \str_if_eq:VnTF \l_@@_sptime_luaset_error_str { true }
            {
              \msg_error:nnnn { spintent } { sptime-invalid-arg }
                { \sptime } {#1}
            }
            {
              \@@_sptime_mathml:
            }
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \sptime } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spdate}}\label{cmd:spdate}
%
% The command |\spdate| is the main public command for typesetting and
% tagging date expressions. It parses date string formats, such as |DD/MM/YYYY|
% or |YYYY-MM-DD|, using the Lua function \myhlc|\@@_luafun_spdate_parse:n|.
% This verifies the input and constructs a \mynvmath{MathML} |intent| tree for
% \mynvmath{NVDA/MathCAT}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spdate}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spdate_luaset_output_str,
%     \l_@@_spdate_luaset_error_str,
%   }
%   These variables store the values returned by the Lua module \myluamod{}.
%   The variable \myhlc|\l_@@_spdate_luaset_output_str| stores the date
%   already formatted, and \myhlc|\l_@@_spdate_luaset_error_str| stores |true|
%   or |false| if the \mymarg{argument} is invalid.
%    \begin{macrocode}
\str_new:N \l_@@_spdate_luaset_output_str
\str_new:N \l_@@_spdate_luaset_error_str
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spdate}}
%
% \begin{macro}[int]{spdate-invalid-arg}
%   The \enquote{error} |spdate-invalid-arg| is raised when an argument
%   fails the format checks or represents an invalid calendar date, such as
%   |31/02/2024| or using an unsupported pattern like |YYYY/MM/DD|.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spdate-invalid-arg }
  {
    Invalid~argument~'#2'~for~'\tl_trim_spaces:n {#1}'~\msg_line_context:.
  }
  {
    Use~'DD/MM/YYYY',~'DD-MM-YYYY'~or~'YYYY-MM-DD',~with~a~valid~day~
    and~month.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spdate}}
%
% \begin{macro}[int]{\@@_spdate_exec:n}
%   The internal function \myhlc|\@@_spdate_exec:n| parses the input using
%   \myhlc|\@@_luafun_spdate_parse:n| and checks the error status in
%   \myhlc|\l_@@_spdate_luaset_error_str|. If an invalid format is detected,
%   it issues the |spdate-invalid-arg| error. Otherwise, it calls
%   \myhlc|\@@_spdate_mathml:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spdate_exec:n
  {
    \@@_luafun_spdate_parse:n {#1}
    \str_if_eq:VnTF \l_@@_spdate_luaset_error_str { true }
      {
        \msg_error:nnnn { spintent } { spdate-invalid-arg }
          { \spdate } {#1}
      }
      { \@@_spdate_mathml: }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spdate_mathml:}
%   The internal function \myhlc|\@@_spdate_mathml:| builds the
%   \mynvmath{MathML} |intent| structure using the formatted date stored in
%   \myhlc|\l_@@_spdate_luaset_output_str|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spdate_mathml:
  {
    \MathMLintent { :date }
      {
        \text { \l_@@_spdate_luaset_output_str }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spdate}
%   The command \ics*{spdate} is the public interface for formatting date
%   expressions. It first checks if it is being used in math mode, issuing
%   an error if not. Inside a local group, it calls \myhlc|\@@_spdate_exec:n|
%   to process the input.
%    \begin{macrocode}
\NewDocumentCommand \spdate { m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spdate_exec:n {#1}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spdate } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spusep}}\label{cmd:spusep}
%
% For the use of parentheses, we will provide the command |\spusep| with
% only two \mymeta{keys} \mykey{size} and \mykey{silent}. Most of the time we can use
% |\left| and |\right| or |\Uleft| and |\Uright|, but both forms create a
% group, and this hinders the code for \emph{tagged} \texttt{PDF} when we
% want to interrupt something between them.
%
% \subsubsection{Definition of variables for \cs[no-index]{spusep}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spusep_arg_tl,
%     \l_@@_spusep_left_tl,
%     \l_@@_spusep_right_tl,
%   }
%   The variable \myhlc|\l_@@_spusep_arg_tl| stores the \mymarg{argument},
%   the variable \myhlc|\l_@@_spusep_left_tl| and \myhlc|\l_@@_spusep_right_tl|
%   stores the parentheses.
%    \begin{macrocode}
\tl_new:N \l_@@_spusep_arg_tl
\tl_new:N \l_@@_spusep_left_tl
\tl_new:N \l_@@_spusep_right_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spusep}}
%
% \begin{macro}[int]{spusep-empty-arg, spusep-invalid-arg}
%   The \enquote{error} |spusep-empty-arg| is raised when |\spusep| receives an
%   empty or blank \mymarg{argument}. The \enquote{error} |spusep-invalid-arg|
%   is raised when the input is invalid.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spusep-empty-arg }
  {
    Empty~argument~for~'\token_to_str:N \spusep'~\msg_line_context:.
  }
  {
    A~non-empty~argument~is~required.
  }
\msg_new:nnnn { spintent } { spusep-invalid-arg }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \spusep'~\msg_line_context:.
  }
  {
    Only~the~delimiters~listed~in~the~documentation~of~'\token_to_str:N \spusep'~are~
      accepted.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spusep}}
%
% \begin{macro}[int]{size, silent, unknown}
%   We define the \mymeta{keys} \mykey[spusep]{size} and \mykey[spusep]{silent}.
%    \begin{macrocode}
\keys_define:nn { spintent / spusep }
  {
    size    .choices:nn =
      { none , big , Big, bigg, Bigg }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            { 1 }
              {
                \tl_clear:N \l_@@_spusep_left_tl
                \tl_clear:N \l_@@_spusep_right_tl
              }
            { 2 }
              {
                \tl_set:Nn \l_@@_spusep_left_tl  { \bigl }
                \tl_set:Nn \l_@@_spusep_right_tl { \bigr }
              }
            { 3 }
              {
                \tl_set:Nn \l_@@_spusep_left_tl  { \Bigl }
                \tl_set:Nn \l_@@_spusep_right_tl { \Bigr }
              }
            { 4 }
              {
                \tl_set:Nn \l_@@_spusep_left_tl  { \biggl }
                \tl_set:Nn \l_@@_spusep_right_tl { \biggr }
              }
            { 5 }
              {
                \tl_set:Nn \l_@@_spusep_left_tl  { \Biggl }
                \tl_set:Nn \l_@@_spusep_right_tl { \Biggr }
              }
          }
      },
    size    .initial:n = none,
    size    .value_required:n = true,
    size / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { size } { none,~big,~Big,~bigg,~Bigg } { \exp_not:n {#1} },
    silent  .bool_set:N = \l_@@_spusep_silent_bool,
    silent  .initial:n = false,
    silent  .value_required:n = true,
    unknown .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spusep }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spusep}}
%
% \begin{macro}[int]{\@@_spusep_mathml:nn}
%   The function \myhlc|\@@_spusep_mathml:nn| will place the argument
%   passed in `|#2|' to the right of the variables \myhlc|\l_@@_spusep_left_tl|
%   and \myhlc|\l_@@_spusep_right_tl| and then check the state of the variable
%   \myhlc|\l_@@_spusep_silent_bool| handled by the key \mykey{silent} to
%   print.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spusep_mathml:nn
  {
    \tl_put_right:cn { l_@@_spusep_ #1 _tl } {#2}
    \bool_if:NTF \l_@@_spusep_silent_bool
      {
        \MathMLintent { :silent } { \tl_use:c { l_@@_spusep_ #1 _tl } }
      }
      {
        \tl_use:c { l_@@_spusep_ #1 _tl }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spusep_exec:n}
%   The function \myhlc|\@@_spusep_exec:n| captures and stores the
%   \mymarg{argument} passed in `|#1|` in the variable \myhlc|\l_@@_spusep_arg_tl|,
%   validates the cases by converting them to the macros provided by
%   \mypkg{unicode-math}, and passes them to the function \myhlc|\@@_spusep_mathml:nn|
%   for printing. If the input is not supported or is empty, we return an \enquote{error}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spusep_exec:n
  {
    \tl_set:Ne \l_@@_spusep_arg_tl { \tl_trim_spaces:n {#1} }
    \tl_if_empty:NT \l_@@_spusep_arg_tl
      {
        \msg_error:nn { spintent } { spusep-empty-arg }
      }
    \str_case:VnF \l_@@_spusep_arg_tl
      {
        { (  }
          {
            \@@_spusep_mathml:nn { left } { \lparen }
          }
        { )  }
          {
            \@@_spusep_mathml:nn { right } { \rparen }
          }
        { [  }
          {
            \@@_spusep_mathml:nn { left } { \lbrack }
          }
        { ]  }
          {
            \@@_spusep_mathml:nn { right } { \rbrack }
          }
        { \{ }
          {
            \@@_spusep_mathml:nn { left } { \lbrace }
          }
        { \} }
          {
            \@@_spusep_mathml:nn { right } { \rbrace }
          }
      }
      {
        \tl_if_empty:NF \l_@@_spusep_arg_tl
          { \msg_error:nnn { spintent } { spusep-invalid-arg } {#1} }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spusep}
%   The command \ics*{spusep} checks that it is running in math mode, opens a
%   local group, sets the keys passed in `|#1|' and processes the
%   \mymarg{argument} passed in `|#2|' by means of the function
%   \myhlc|\@@_spusep_exec:n|.
%    \begin{macrocode}
\NewDocumentCommand \spusep { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \keys_set:nn { spintent / spusep } {#1}
          \@@_spusep_exec:n {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spusep } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spread}}\label{cmd:spread}
%
% The command |\spread| provides a semantic read for \mynvmath{MathCAT}
% of mathematical symbols for situations in which it is necessary to
% specify gender or plurality in Spanish.
%
% \subsubsection{Definition of variables for \cs[no-index]{spread}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spread_luaset_status_str,
%     \l_@@_spread_luaset_reading_str,
%     \l_@@_spread_read_arg_tl,
%     \l_@@_spread_read_tmp_tl,
%   }
%   The variables \myhlc|\l_@@_spread_luaset_status_str| and
%   \myhlc|\l_@@_spread_luaset_reading_str| are set by
%   \myhlc|\@@_luafun_spread_new:nn|/\myhlc|\@@_luafun_spread_lookup:n|.
%   The variable \myhlc|\l_@@_spread_read_arg_tl| stores \mykey{read-arg}.
%    \begin{macrocode}
\str_new:N \l_@@_spread_luaset_status_str
\str_new:N \l_@@_spread_luaset_reading_str
\tl_new:N  \l_@@_spread_read_arg_tl
\tl_new:N  \l_@@_spread_read_tmp_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spread}}
%
% \begin{macro}[int]{spnewread-empty-args, spnewread-duplicate, spread-empty-sym, spread-unknown-sym}
%   The \enquote{error} |spnewread-empty-args| is raised when
%   |\spnewread| receives a blank symbol or a reading that is empty
%   once normalized. The
%   \enquote{error} |spnewread-duplicate| is raised when it registers a
%   symbol already in the dictionary. The \enquote{error}
%   |spread-empty-sym| is raised when |\spread| receives a blank symbol.
%   The \enquote{error} |spread-unknown-sym| is raised when |\spread| is
%   given a symbol that is neither registered nor accompanied by
%   \mykey{read-arg}.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spnewread-empty-args }
  {
    Empty~argument~for~'\token_to_str:N \spnewread'~\msg_line_context:.
  }
  {
    Two~non-empty~arguments~are~required.
  }
\msg_new:nnnn { spintent } { spnewread-duplicate }
  {
    The~symbol~'#1'~is~already~defined~\msg_line_context:.
  }
  {
    Existing~symbols~cannot~be~redefined~with~
    '\token_to_str:N \spnewread'.
  }
\msg_new:nnnn { spintent } { spread-empty-sym }
  {
    Empty~argument~for~'\token_to_str:N \spread'~\msg_line_context:.
  }
  {
    A~non-empty~symbol~is~required.
  }
\msg_new:nnnn { spintent } { spread-unknown-sym }
  {
    The~symbol~'#1'~is~not~registered~and~no~'read-arg'~was~given~\msg_line_context:.
  }
  {
    Register~the~symbol~first~with~'\token_to_str:N \spnewread'~or~pass~'read-arg'.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spread}}
%
% \begin{macro}[int]{read-arg, silent, unknown}
%   We define the \mymeta{keys} \mykey[spread]{read-arg} and
%   \mykey[spread]{silent}.
%    \begin{macrocode}
\keys_define:nn { spintent / spread }
  {
    read-arg .code:n =
      {
        \@@_sanitize_affix:Nn \l_@@_spread_read_arg_tl {#1}
      },
    read-arg .initial:n = ,
    read-arg .value_required:n = true,
    silent   .bool_set:N = \l_@@_spread_silent_bool,
    silent   .initial:n = false,
    silent   .value_required:n = true,
    unknown  .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spread }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spread}}
%
% \begin{macro}[int]{\@@_spread_exec:nn}
%   The function \myhlc|\@@_spread_exec:nn| receives the optional \mymeta{keys}
%   in `|#1|' and the \mymarg{symbol} in `|#2|'. With \mykey{silent}|=true|,
%   it passes the literal reading `|-|' to \myhlc|\@@_spread_mathml:nn|.
%   Otherwise, with \mykey{read-arg} given, that reading is used for this call,
%   overriding any registered reading. With \mykey{read-arg} empty, it calls
%   \myhlc|\@@_luafun_spread_lookup:n|, using the registered reading when
%   found and reporting |spread-unknown-sym| otherwise.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spread_exec:nn
  {
    \keys_set:nn { spintent / spread } {#1}
    \tl_if_blank:nTF {#2}
      { \msg_error:nn { spintent } { spread-empty-sym } }
      {
        \bool_if:NTF \l_@@_spread_silent_bool
          { \@@_spread_mathml:nn {#2} { - } }
          {
            \tl_if_empty:NTF \l_@@_spread_read_arg_tl
              {
                \@@_luafun_spread_lookup:n { \exp_not:n {#2} }
                \str_if_eq:VnTF \l_@@_spread_luaset_status_str { found }
                  {
                    \@@_spread_mathml:nV {#2}
                      \l_@@_spread_luaset_reading_str
                  }
                  {
                    \msg_error:nne { spintent } { spread-unknown-sym }
                      { \exp_not:n {#2} }
                  }
              }
              { \@@_spread_mathml:nV {#2} \l_@@_spread_read_arg_tl }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spread_mathml:nn, \@@_spread_mathml:nV}
%   The function \myhlc|\@@_spread_mathml:nn| receives the \mymarg{symbol}
%   in `|#1|' and the reading in `|#2|'. It typesets the symbol, preceded by
%   \myhlc|\@@_inner_sep:|, inside a |\MathMLintent| whose literal is the
%   reading preceded by `\myhlc|_|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spread_mathml:nn
  {
    \@@_invisible_sep:
    \MathMLintent { _ #2 } { \@@_inner_sep: #1 }
  }
\cs_generate_variant:Nn \@@_spread_mathml:nn { nV }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spread}
%   The public command \ics*{spread} enforces \emph{math mode}, opens a
%   local group, and delegates execution to \myhlc|\@@_spread_exec:nn|.
%    \begin{macrocode}
\NewDocumentCommand \spread { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spread_exec:nn {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spread } }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spnewread}
%   The public command \ics*{spnewread} registers a new symbol/reading
%   pair. It first normalizes the reading `|#2|' with
%   \myhlc|\@@_sanitize_affix:Nn|, storing the result in the scratch
%   variable \myhlc|\l_@@_spread_read_tmp_tl|, and reports |spnewread-empty-args|
%   when the symbol `|#1|' is blank or the normalized reading is empty.
%   Otherwise it passes the symbol and the normalized reading to Lua via
%   \myhlc|\@@_luafun_spread_new:nn|, reporting
%   |spnewread-duplicate| when the symbol is already registered.
%    \begin{macrocode}
\NewDocumentCommand \spnewread { m m }
  {
    \mode_if_math:TF
      {
        \msg_error:nnn { spintent } { need-text-mode } { \spnewread }
      }
      {
        \@@_sanitize_affix:Nn \l_@@_spread_read_tmp_tl {#2}
        \bool_lazy_or:nnTF
          { \tl_if_blank_p:n {#1} } { \tl_if_empty_p:N \l_@@_spread_read_tmp_tl }
          { \msg_error:nn { spintent } { spnewread-empty-args } }
          {
            \@@_luafun_spread_new:nn { \exp_not:n {#1} }
              { \exp_not:V \l_@@_spread_read_tmp_tl }
            \str_if_eq:VnT \l_@@_spread_luaset_status_str { duplicate }
              {
                \msg_error:nnn { spintent } { spnewread-duplicate } {#1}
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spdsym}}\label{cmd:spdsym}
%
% The command |\spdsym| prints the division symbol with a configurable
% reading for \mynvmath{MathCAT}. The \mymeta{keys} choose the printed symbol,
% the reading, and the optional prefix and suffix of the \emph{intent}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spdsym}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spdsym_symbol_tl,
%     \l_@@_spdsym_read_sym_tl,
%     \l_@@_spdsym_prefix_tl,
%     \l_@@_spdsym_suffix_tl,
%   }
% The variable \myhlc|\l_@@_spdsym_symbol_tl| stores the symbol that is
% printed, chosen by the key \mykey[spdsym]{set-sym}, and
% \myhlc|\l_@@_spdsym_read_sym_tl| stores the word used in the \emph{intent},
% chosen by the key \mykey[spdsym]{read-sym}. The variables
% \myhlc|\l_@@_spdsym_prefix_tl| and \myhlc|\l_@@_spdsym_suffix_tl| store the
% values of the keys \mykey[spdsym]{prefix} and \mykey[spdsym]{suffix}.
%    \begin{macrocode}
\tl_new:N \l_@@_spdsym_symbol_tl
\tl_new:N \l_@@_spdsym_read_sym_tl
\tl_new:N \l_@@_spdsym_prefix_tl
\tl_new:N \l_@@_spdsym_suffix_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of keys for \cs[no-index]{spdsym}}
%
% \begin{macro}[int]{prefix, suffix, set-sym, read-sym, unknown}
%   We define the \mymeta{keys} \mykey[spdsym]{prefix}, \mykey[spdsym]{suffix},
%   \mykey[spdsym]{set-sym} and \mykey[spdsym]{read-sym} for |\spdsym|.
%    \begin{macrocode}
\keys_define:nn { spintent / spdsym }
  {
    prefix   .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spdsym_prefix_tl {#1} },
    prefix   .initial:n = ,
    prefix   .value_required:n = true,
    suffix   .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spdsym_suffix_tl {#1} },
    suffix   .initial:n = ,
    suffix   .value_required:n = true,
    set-sym  .choices:nn =
      { old-style , two-dots , slash }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \tl_set:Nn \l_@@_spdsym_symbol_tl { \div } }
            {2} { \tl_set:Nn \l_@@_spdsym_symbol_tl {   :  } }
            {3} { \tl_set:Nn \l_@@_spdsym_symbol_tl {   /  } }
          }
      },
    set-sym  .initial:n = two-dots,
    set-sym  .value_required:n = true,
    set-sym / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { set-sym } { old-style, ~two-dots, ~slash } { \exp_not:n {#1} },
    read-sym .choices:nn =
      { dividido , dividido-por , dividido-entre }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \tl_set:Nn \l_@@_spdsym_read_sym_tl { dividido       } }
            {2} { \tl_set:Nn \l_@@_spdsym_read_sym_tl { dividido-por   } }
            {3} { \tl_set:Nn \l_@@_spdsym_read_sym_tl { dividido-entre } }
          }
      },
    read-sym .initial:n = dividido,
    read-sym .value_required:n = true,
    read-sym / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-sym } { dividido, ~dividido-por, ~dividido-entre }
        { \exp_not:n {#1} },
    unknown  .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spdsym }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spdsym}}
%
% \begin{macro}[int]{\@@_spdsym_mathml:}
%   The internal function \myhlc|\@@_spdsym_mathml:| outputs the configured
%   intent structure, attaching prefixes and suffixes if present.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spdsym_mathml:
  {
    \MathMLintent
      {
        _
        \tl_if_empty:NF \l_@@_spdsym_prefix_tl
          { \l_@@_spdsym_prefix_tl - }
        \l_@@_spdsym_read_sym_tl
        \tl_if_empty:NF \l_@@_spdsym_suffix_tl
          { - \l_@@_spdsym_suffix_tl }
      }
      { \l_@@_spdsym_symbol_tl }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spdsym}
%   Defines the \ics*{spdsym} operator. It takes a single optional argument
%   for local setup and delegates the output to \myhlc|\@@_spdsym_mathml:|.
%    \begin{macrocode}
\NewDocumentCommand \spdsym { O{} }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \keys_set:nn { spintent / spdsym } {#1}
          \@@_spdsym_mathml:
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spdsym } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spmsym}}\label{cmd:spmsym}
%
% The command |\spmsym| prints the multiplication symbol with a configurable
% reading for \mynvmath{MathCAT}. The \mymeta{keys} choose the printed symbol,
% the reading, whether the symbol is printed, and the optional prefix and
% suffix of the \emph{intent}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spmsym}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spmsym_symbol_tl,
%     \l_@@_spmsym_read_sym_tl,
%     \l_@@_spmsym_prefix_tl,
%     \l_@@_spmsym_suffix_tl,
%   }
% The variable \myhlc|\l_@@_spmsym_symbol_tl| stores the symbol that is
% printed, chosen by the key \mykey[spmsym]{set-sym}, and
% \myhlc|\l_@@_spmsym_read_sym_tl| stores the word used in the \emph{intent},
% chosen by the key \mykey[spmsym]{read-sym}. The variables
% \myhlc|\l_@@_spmsym_prefix_tl| and \myhlc|\l_@@_spmsym_suffix_tl| store the
% values of the keys \mykey[spmsym]{prefix} and \mykey[spmsym]{suffix}.
%    \begin{macrocode}
\tl_new:N \l_@@_spmsym_symbol_tl
\tl_new:N \l_@@_spmsym_read_sym_tl
\tl_new:N \l_@@_spmsym_prefix_tl
\tl_new:N \l_@@_spmsym_suffix_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of keys for \cs[no-index]{spmsym}}
%
% \begin{macro}[int]{prefix, suffix, set-sym, read-sym, print-sym, unknown}
%   We define the \mymeta{keys} \mykey[spmsym]{prefix}, \mykey[spmsym]{suffix},
%   \mykey[spmsym]{set-sym}, \mykey[spmsym]{read-sym} and \mykey[spmsym]{print-sym}
%   for |\spmsym|.
%    \begin{macrocode}
\keys_define:nn { spintent / spmsym }
  {
    prefix    .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spmsym_prefix_tl {#1} },
    prefix    .initial:n = ,
    prefix    .value_required:n = true,
    suffix    .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spmsym_suffix_tl {#1} },
    suffix    .initial:n = ,
    suffix    .value_required:n = true,
    set-sym   .choices:nn =
      { cross , dot , asterisk }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \tl_set:Nn \l_@@_spmsym_symbol_tl { \times } }
            {2} { \tl_set:Nn \l_@@_spmsym_symbol_tl { \cdot  } }
            {3} { \tl_set:Nn \l_@@_spmsym_symbol_tl { \ast   } }
          }
      },
    set-sym   .initial:n = dot,
    set-sym   .value_required:n = true,
    set-sym / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { set-sym } { cross, ~dot, ~asterisk } { \exp_not:n {#1} },
    read-sym  .choices:nn =
      { por , multiplicado-por , veces }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \tl_set:Nn \l_@@_spmsym_read_sym_tl { por } }
            {2} { \tl_set:Nn \l_@@_spmsym_read_sym_tl { multiplicado-por } }
            {3} { \tl_set:Nn \l_@@_spmsym_read_sym_tl { veces } }
          }
      },
    read-sym  .initial:n = por,
    read-sym  .value_required:n = true,
    read-sym / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-sym } { por, ~multiplicado-por, ~veces } { \exp_not:n {#1} },
    print-sym .bool_set:N = \l_@@_spmsym_print_sym_bool,
    print-sym .initial:n = true,
    print-sym .value_required:n = true,
    unknown   .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spmsym }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spmsym}}
%
% \begin{macro}[int]{\@@_spmsym_mathml:}
%   The internal function \myhlc|\@@_spmsym_mathml:| outputs the correct
%   mathematical glyph based on the local \mymeta{keys}, attaching prefixes and
%   suffixes if present. With \mykey[spmsym]{print-sym} false, it prints an
%   invisible separator instead of the glyph.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmsym_mathml:
  {
    \MathMLintent
      {
        _
        \tl_if_empty:NF \l_@@_spmsym_prefix_tl
          { \l_@@_spmsym_prefix_tl - }
        \l_@@_spmsym_read_sym_tl
        \tl_if_empty:NF \l_@@_spmsym_suffix_tl
          { - \l_@@_spmsym_suffix_tl }
      }
      {
        \bool_if:NTF \l_@@_spmsym_print_sym_bool
          { \l_@@_spmsym_symbol_tl }
          {
            \mathopen { \c_@@_invisible_sep_int }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spmsym}
%   Defines the \ics*{spmsym} operator. Similar to |\spdsym|, it takes a single
%   optional argument for local setup and delegates the output to
%   \myhlc|\@@_spmsym_mathml:|.
%    \begin{macrocode}
\NewDocumentCommand \spmsym { O{} }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \keys_set:nn { spintent / spmsym } {#1}
          \@@_spmsym_mathml:
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spmsym } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spdiv}}\label{cmd:spdiv}
%
% The user-level command |\spdiv| formats division representations. It
% takes two mandatory arguments: the \mymarg{dividend} and the
% \mymarg{divisor}. Both \mymarg{arguments} are internally processed by the
% |\spnum| command to ensure proper digit separation. This command checks
% that neither the \mymarg{dividend} nor
% the \mymarg{divisor} is \emph{blank}.
%
% \subsubsection{Definition of error messages for \cs[no-index]{spdiv}}
%
% \begin{macro}[int]{spdiv-empty-arg}
%   The \enquote{error} |spdiv-empty-arg| is raised when either the
%   \mymarg{dividend} or the \mymarg{divisor} is missing or \emph{blank}.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spdiv-empty-arg }
  {
    Empty~argument~for~'\token_to_str:N \spdiv'~\msg_line_context:.
  }
  {
    A~dividend~and~a~divisor~are~required.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spdiv}}
%
% \begin{macro}[int]
%   {
%     cifras-sep,
%     read-sign,
%     read-period-sep,
%     notation-sym,
%     set-sym,
%     read-sym,
%     wrap-parens,
%     read-parens,
%     unknown,
%   }
%   We define the \mymeta{keys} \mykey[spdiv]{cifras-sep},
%   \mykey[spdiv]{read-sign}, \mykey[spdiv]{read-period-sep},
%   \mykey[spdiv]{notation-sym}, \mykey[spdiv]{set-sym},
%   \mykey[spdiv]{read-sym}, \mykey[spdiv]{wrap-parens} and
%   \mykey[spdiv]{read-parens}.
%    \begin{macrocode}
\keys_define:nn { spintent / spdiv }
  {
    cifras-sep      .meta:nn = { spintent / spnum  } { cifras-sep      = {#1} },
    read-sign       .meta:nn = { spintent / spnum  } { read-sign       = {#1} },
    read-period-sep .meta:nn = { spintent / spnum  } { read-period-sep = {#1} },
    notation-sym    .meta:nn = { spintent / spnum  } { notation-sym    = {#1} },
    set-sym         .meta:nn = { spintent / spdsym } { set-sym         = {#1} },
    read-sym        .meta:nn = { spintent / spdsym } { read-sym        = {#1} },
    wrap-parens     .bool_set:N = \l_@@_spdiv_wrap_parens_bool,
    wrap-parens     .initial:n = false,
    wrap-parens     .value_required:n = true,
    read-parens     .bool_set:N = \l_@@_spdiv_read_parens_bool,
    read-parens     .initial:n = true,
    read-parens     .value_required:n = true,
    unknown         .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spdiv }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spdiv}}
%
% \begin{macro}[int]{\@@_spdiv_render_op:nn}
%   The function \myhlc|\@@_spdiv_render_op:nn| parses both \mymarg{arguments}
%   through the |\spnum| command and embeds the semantic operator from
%   |\spdsym|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spdiv_render_op:nn
  {
    \spnum {#1}
    \MathMLintent { _ \l_@@_spdsym_read_sym_tl }
      {
        \l_@@_spdsym_symbol_tl
      }
    \spnum {#2}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spdiv_mathml:nn}
%   The function \myhlc|\@@_spdiv_mathml:nn| builds the \mynvmath{MathML}
%   structure of the division. If \mykey{wrap-parens} is true, it adds the
%   structural parentheses around \myhlc|\@@_spdiv_render_op:nn|, read or
%   silenced according to \mykey{read-parens}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spdiv_mathml:nn
  {
    \bool_if:NTF \l_@@_spdiv_wrap_parens_bool
      {
        \bool_if:NTF \l_@@_spdiv_read_parens_bool
          {
            \lparen \@@_spdiv_render_op:nn {#1} {#2} \rparen
          }
          {
            \MathMLintent { :silent } { \lparen }
            \@@_spdiv_render_op:nn {#1} {#2}
            \MathMLintent { :silent } { \rparen }
          }
      }
      { \@@_spdiv_render_op:nn {#1} {#2} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spdiv_exec:nnn}
%   The function \myhlc|\@@_spdiv_exec:nnn| sets \mymeta{keys}, validates
%   \mymarg{arguments}, and calls \myhlc|\@@_spdiv_mathml:nn|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spdiv_exec:nnn
  {
    \keys_set:nn { spintent / spdiv } {#1}
    \tl_if_blank:nTF {#2}
      { \msg_error:nn { spintent } { spdiv-empty-arg } }
      {
        \tl_if_blank:nTF {#3}
          { \msg_error:nn { spintent } { spdiv-empty-arg } }
          {
            \@@_spdiv_mathml:nn {#2} {#3}
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spdiv}
%   Defines the user command \ics*{spdiv}. It requires \emph{math mode}.
%    \begin{macrocode}
\NewDocumentCommand \spdiv { O{} m m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spdiv_exec:nnn {#1} {#2} {#3}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spdiv } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spmul}}\label{cmd:spmul}
%
% The user-level command |\spmul| formats multiplication representations. It
% takes two mandatory arguments: the \mymarg{first factor} and the
% \mymarg{second factor}. Both \mymarg{arguments} are internally processed by the
% |\spnum| command to ensure proper digit separation. This command checks
% that neither the \mymarg{first factor} nor
% the \mymarg{second factor} is \emph{blank}.
%
% \subsubsection{Definition of error messages for \cs[no-index]{spmul}}
%
% \begin{macro}[int]{spmul-empty-arg}
%   The \enquote{error} |spmul-empty-arg| is raised when either the
%   \mymarg{first factor} or the \mymarg{second factor} is missing or
%   \emph{blank}.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spmul-empty-arg }
  {
    Empty~argument~for~'\token_to_str:N \spmul'~\msg_line_context:.
  }
  {
    A~first~factor~and~a~second~factor~are~required.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spmul}}
%
% \begin{macro}[int]
%   {
%     cifras-sep,
%     read-sign,
%     read-period-sep,
%     notation-sym,
%     set-sym,
%     read-sym,
%     wrap-parens,
%     read-parens,
%     unknown,
%   }
%   We define the \mymeta{keys} \mykey[spmul]{cifras-sep},
%   \mykey[spmul]{read-sign}, \mykey[spmul]{read-period-sep},
%   \mykey[spmul]{notation-sym}, \mykey[spmul]{set-sym},
%   \mykey[spmul]{read-sym}, \mykey[spmul]{wrap-parens} and
%   \mykey[spmul]{read-parens}.
%    \begin{macrocode}
\keys_define:nn { spintent / spmul }
  {
    cifras-sep      .meta:nn = { spintent / spnum  } { cifras-sep       = {#1} },
    read-sign       .meta:nn = { spintent / spnum  } { read-sign        = {#1} },
    read-period-sep .meta:nn = { spintent / spnum  } { read-period-sep  = {#1} },
    notation-sym    .meta:nn = { spintent / spnum  } { notation-sym     = {#1} },
    set-sym         .meta:nn = { spintent / spmsym } { set-sym          = {#1} },
    read-sym        .meta:nn = { spintent / spmsym } { read-sym         = {#1} },
    wrap-parens     .bool_set:N = \l_@@_spmul_wrap_parens_bool,
    wrap-parens     .initial:n = false,
    wrap-parens     .value_required:n = true,
    read-parens     .bool_set:N = \l_@@_spmul_read_parens_bool,
    read-parens     .initial:n = true,
    read-parens     .value_required:n = true,
    unknown         .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spmul }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spmul}}
%
% \begin{macro}[int]{\@@_spmul_render_op:nn}
%   The function \myhlc|\@@_spmul_render_op:nn| parses both \mymarg{arguments}
%   through the |\spnum| command and embeds the semantic operator from
%   |\spmsym|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmul_render_op:nn
  {
    \spnum {#1}
    \MathMLintent { _ \l_@@_spmsym_read_sym_tl }
      {
        \l_@@_spmsym_symbol_tl
      }
    \spnum {#2}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmul_mathml:nn}
%   The function \myhlc|\@@_spmul_mathml:nn| builds the \mynvmath{MathML}
%   structure of the multiplication. If \mykey{wrap-parens} is true, it adds the
%   structural parentheses around \myhlc|\@@_spmul_render_op:nn|, read or
%   silenced according to \mykey{read-parens}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmul_mathml:nn
  {
    \bool_if:NTF \l_@@_spmul_wrap_parens_bool
      {
        \bool_if:NTF \l_@@_spmul_read_parens_bool
          {
            \lparen \@@_spmul_render_op:nn {#1} {#2} \rparen
          }
          {
            \MathMLintent { :silent } { \lparen }
            \@@_spmul_render_op:nn {#1} {#2}
            \MathMLintent { :silent } { \rparen }
          }
      }
      { \@@_spmul_render_op:nn {#1} {#2} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmul_exec:nnn}
%   The function \myhlc|\@@_spmul_exec:nnn| sets \mymeta{keys}, validates
%   \mymarg{arguments}, and calls \myhlc|\@@_spmul_mathml:nn|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmul_exec:nnn
  {
    \keys_set:nn { spintent / spmul } {#1}
    \tl_if_blank:nTF {#2}
      { \msg_error:nn { spintent } { spmul-empty-arg } }
      {
        \tl_if_blank:nTF {#3}
          { \msg_error:nn { spintent } { spmul-empty-arg } }
          {
            \@@_spmul_mathml:nn {#2} {#3}
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spmul}
%   Defines the user command \ics*{spmul}. It requires \emph{math mode}.
%    \begin{macrocode}
\NewDocumentCommand \spmul { O{} m m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spmul_exec:nnn {#1} {#2} {#3}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spmul } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The commands \cs{spmcd} and \cs{spmcm}}\label{cmd:spmcd}
%
% The user-level commands |\spmcd| and |\spmcm| provide a semantic interface
% for the Greatest Common Divisor, MCD in Spanish, and the Least Common
% Multiple, MCM in Spanish. They send the validation of the arguments and the calculation
% to the Lua module \myluamod{}, constructing a valid intent.
%
% \subsubsection{Definition of variables for \cs[no-index]{spmcd} and \cs[no-index]{spmcm}}
%
% \begin{variable}[int]
%   {
%     \l_@@_mc_luaset_types_clist,
%     \l_@@_mc_luaset_all_numeric_str,
%   }
% The variable \myhlc|\l_@@_mc_luaset_types_clist| stores the type of each
% argument, |number| or |other|, in the same order and separated by commas.
% The variable \myhlc|\l_@@_mc_luaset_all_numeric_str| stores |true| when all
% the arguments are natural numbers. Both are set by the Lua module
% \myluamod{}.
%    \begin{macrocode}
\clist_new:N \l_@@_mc_luaset_types_clist
\str_new:N   \l_@@_mc_luaset_all_numeric_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_mc_raw_args_seq,
%     \l_@@_mc_types_seq,
%     \l_@@_mc_args_seq,
%     \l_@@_mc_idx_int,
%   }
% The variable \myhlc|\l_@@_mc_raw_args_seq| stores the arguments as they
% were given and \myhlc|\l_@@_mc_types_seq| stores their types. The variable
% \myhlc|\l_@@_mc_args_seq| stores the arguments ready to be typeset, with
% |\spnum| applied to those of type |number|. The variable
% \myhlc|\l_@@_mc_idx_int| stores the position of the argument being
% processed.
%    \begin{macrocode}
\seq_new:N \l_@@_mc_raw_args_seq
\seq_new:N \l_@@_mc_types_seq
\seq_new:N \l_@@_mc_args_seq
\int_new:N \l_@@_mc_idx_int
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spmcd_luaset_mcd_value_tl,
%     \l_@@_spmcm_luaset_mcm_value_tl,
%     \l_@@_spmcd_read_terse_bool,
%     \l_@@_spmcm_read_terse_bool,
%     \l_@@_spmcd_prefix_tl,
%     \l_@@_spmcm_prefix_tl,
%     \l_@@_spmcd_print_result_bool,
%     \l_@@_spmcm_print_result_bool,
%     \l_@@_spmcd_print_upper_bool,
%     \l_@@_spmcm_print_upper_bool,
%   }
% Each command has its own copy of these variables. The variables
% \myhlc|\l_@@_spmcd_luaset_mcd_value_tl| and
% \myhlc|\l_@@_spmcm_luaset_mcm_value_tl| store the result of the greatest
% common divisor and of the least common multiple, set by the Lua module
% \myluamod{}. The variables \myhlc|_read_terse_bool| and
% \myhlc|_prefix_tl| store the values of the keys \mykey{read-sty} and
% \mykey{prefix}. The variables \myhlc|_print_result_bool| and
% \myhlc|_print_upper_bool| store the values of the keys \mykey{result} and
% \mykey{upper-case}, and are created by those keys.
%    \begin{macrocode}
\clist_map_inline:nn { mcd , mcm }
  {
    \tl_new:c   { l_@@_sp #1 _luaset_ #1 _value_tl }
    \bool_new:c { l_@@_sp #1 _read_terse_bool      }
    \tl_new:c   { l_@@_sp #1 _prefix_tl            }
  }
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of keys for \cs[no-index]{spmcd} and \cs[no-index]{spmcm}}
%
% \begin{macro}[int]
%   {
%     result,
%     prefix,
%     read-sty,
%     upper-case,
%     concept,
%     silent,
%     unknown,
%   }
%   We define the shared \mymeta{keys} \mykey[spmcd,spmcm]{result},
%   \mykey[spmcd,spmcm]{prefix}, \mykey[spmcd,spmcm]{read-sty},
%   \mykey[spmcd,spmcm]{upper-case}, \mykey[spmcd,spmcm]{concept} and
%   \mykey[spmcd,spmcm]{silent}.
%    \begin{macrocode}
\clist_map_inline:nn { mcd , mcm }
  {
    \keys_define:nn { spintent / sp #1 }
      {
        result     .bool_set:c = { l_@@_sp #1 _print_result_bool },
        result     .initial:n = false,
        result     .value_required:n = true,
        prefix     .code:n =
          { \@@_sanitize_affix:cn { l_@@_sp #1 _prefix_tl } {##1} },
        prefix     .initial:n = ,
        prefix     .value_required:n = true,
        read-sty   .choices:nn =
          { terse , clear }
          {
            \int_case:nn { \l_keys_choice_int }
              {
                {1} { \bool_set_true:c  { l_@@_sp #1 _read_terse_bool } }
                {2} { \bool_set_false:c { l_@@_sp #1 _read_terse_bool } }
              }
          },
        read-sty   .initial:n = clear,
        read-sty   .value_required:n = true,
        read-sty / unknown .code:n =
          \msg_error:nneee { spintent } { unknown-choice }
            { read-sty } { terse , ~clear } { \exp_not:n {##1} },
        upper-case .bool_set:c = { l_@@_sp #1 _print_upper_bool },
        upper-case .initial:n = false,
        upper-case .value_required:n = true,
        concept    .bool_set:c = { l_@@_sp #1 _concept_bool },
        concept    .initial:n = true,
        concept    .value_required:n = true,
        silent     .bool_set:c = { l_@@_sp #1 _silent_bool },
        silent     .initial:n = false,
        silent     .value_required:n = true,
        unknown    .code:n =
          {
            \str_set:Nn \l_@@_command_name_str { sp #1 }
            \@@_unknown_keys_cmd:n {##1}
          },
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spmcd} and \cs[no-index]{spmcm}}
%
% \begin{macro}[int]{\@@_mc_process_args:nn}
%   The function \myhlc|\@@_mc_process_args:nn| parses the operation `|#1|'
%   and its raw argument list `|#2|'. Calls \myhlc|\@@_luafun_mc_classify_args:n|,
%   splits `|#2|' into \myhlc|\l_@@_mc_raw_args_seq| and \myhlc|\l_@@_mc_luaset_types_clist| into
%   \myhlc|\l_@@_mc_types_seq|, then walks the former by position,
%   fetching the matching type from the latter through
%   \myhlc|\seq_item:Nn|, to build \myhlc|\l_@@_mc_args_seq|: each
%   argument wrapped through |\spnum| when its type is |number|,
%   verbatim otherwise. Then it calls \myhlc|\@@_mc_mathml_args:n|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_mc_process_args:nn
  {
    \@@_luafun_mc_classify_args:n {#2}
    \seq_set_from_clist:Nn \l_@@_mc_raw_args_seq {#2}
    \seq_set_from_clist:NN \l_@@_mc_types_seq \l_@@_mc_luaset_types_clist
    \seq_clear:N \l_@@_mc_args_seq
    \int_zero:N \l_@@_mc_idx_int
    \seq_map_inline:Nn \l_@@_mc_raw_args_seq
      {
        \int_incr:N \l_@@_mc_idx_int
        \str_if_eq:eeTF
          { \seq_item:Nn \l_@@_mc_types_seq { \l_@@_mc_idx_int } }
            { number }
          {
            \seq_put_right:Nn \l_@@_mc_args_seq { \spnum {##1} }
          }
          { \seq_put_right:Nn \l_@@_mc_args_seq {##1} }
      }
    \@@_mc_mathml_args:n {#1}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_mc_mathml_args:n}
%   The function \myhlc|\@@_mc_mathml_args:n| receives the short identifier
%   in `|#1|' and puts the intents around the
%   already-formatted \myhlc|\l_@@_mc_args_seq|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_mc_mathml_args:n
  {
    \bool_if:cTF { l_@@_sp #1 _silent_bool }
      {
        \seq_set_map:NNn \l_@@_mc_args_seq \l_@@_mc_args_seq
          {
            \MathMLintent { _- } { \@@_inner_sep: ##1 }
          }
        \MathMLintent { :silent } { \c_@@_invisible_sep_int } % prevent "por"
        \MathMLintent { :silent } { \lparen }
        % prevent "intervalo"
        \MathMLintent { :silent } { \c_@@_invisible_sep_int }
        \seq_use:Nnnn \l_@@_mc_args_seq
          { \MathMLintent { :silent } { , } }
          { \MathMLintent { :silent } { , } }
          { \MathMLintent { :silent } { , } }
      }
      {
        \MathMLintent { :silent } { \c_@@_invisible_sep_int } % prevent doble "de"
        \bool_if:cTF { l_@@_sp #1 _concept_bool }
          {
            \MathMLintent { _entre } { \lparen }
          }
          {
            \MathMLintent { :silent } { \lparen }
          }
        % prevent "intervalo"
        \MathMLintent { :silent } { \c_@@_invisible_sep_int }
        \seq_use:Nnnn \l_@@_mc_args_seq
          {
            \MathMLintent { :silent } { , }
            \MathMLintent { _y- } { \c_@@_invisible_sep_int } % "y" medio
          }
          {
            \MathMLintent { :silent } { , }
          }
          {
            \MathMLintent { :silent } { , }
            \MathMLintent { _y- } { \c_@@_invisible_sep_int } % "y" final
          }
      }
    \MathMLintent { :silent } { \rparen }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmc_exec:nnnn}
%   The internal function \myhlc|\@@_spmc_exec:nnnn| is the shared
%   function for both |\spmcd| and |\spmcm|. Takes the short identifier
%   in `|#1|', its full reading string in `|#2|', the local
%   \mymeta{keys} in `|#3|', and the optional arguments in `|#4|'.
%
%   After setting the \mymeta{keys}, it calls \myhlc|\@@_spmc_mathml:nn| to
%   print the operator name.
%
%   With `|#4|' given, classifies it through \myhlc|\@@_luafun_mc_classify_args:n|.
%   Only when every argument is a number does it call the Lua calculation
%   |calculate_|\myhlc|#1| and, when \mykey{result} is |true|, print the
%   computed value with \myhlc|\@@_spmc_result_mathml:n|.
%   \myhlc|\@@_mc_process_args:nn| prints the argument list itself either way.
%   With \mykey{silent}|=true|, it wraps each argument in a literal `\myhlc|_-|'
%   intent and the printed result is silenced as well.
%
% \begin{important}*
%   We should use |\mathrm| instead of |\operatorname|, which generates
%   unwanted spacing when \myhlc|\c__spintent_invisible_sep_int| is
%   applied.
% \end{important}
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmc_exec:nnnn
  {
    \keys_set:nn { spintent / sp #1 } {#3}
    \@@_spmc_mathml:nn {#1} {#2}
%    \end{macrocode}
%   With `|#4|' given, it is classified with
%   \myhlc|\@@_luafun_mc_classify_args:n|. If every argument is a number, the
%   Lua calculation |calculate_|\myhlc|#1| is called, and the computed value is
%   printed when \mykey{result} is |true|. If any argument is not a number, no calculation is
%   attempted. In both cases \myhlc|\@@_mc_process_args:n| prints the argument
%   list.
%    \begin{macrocode}
    \tl_if_novalue:nF {#4}
      {
        \seq_set_from_clist:Nn \l_@@_mc_raw_args_seq {#4}
        \int_compare:nNnTF { \seq_count:N \l_@@_mc_raw_args_seq } < { 2 }
          {
            \msg_error:nne { spintent } { mc-too-few-args } { \exp_not:c { sp #1 } }
          }
          {
            \@@_luafun_mc_classify_args:n {#4}
            \str_if_eq:VnT \l_@@_mc_luaset_all_numeric_str { true }
              {
                \use:c { @@_luafun_calculate_ #1 :n } {#4}
              }
            \@@_mc_process_args:nn {#1} {#4}
            \@@_spmc_result_mathml:n {#1}
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmc_mathml:nn}
%   The function \myhlc|\@@_spmc_mathml:nn| receives the short identifier in
%   `|#1|' and its full reading string in `|#2|', and builds the
%   \mynvmath{MathML} structure of the operator name. With \mykey{silent}|=true|
%   or \mykey{concept}|=false|, the operator name is silenced. With
%   \mykey{concept}|=true|, the literal |intent| is |#1| or |#2| according to
%   \mykey{read-sty}, prefixed with \mykey{prefix} when given.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmc_mathml:nn
  {
    \bool_if:cTF { l_@@_sp #1 _silent_bool }
      {
        \MathMLintent { :silent }
          {
            \mathrm
              {
                \bool_if:cTF { l_@@_sp #1 _print_upper_bool }
                  { \text_uppercase:n {#1} } {#1}
              }
          }
      }
      {
        \bool_if:cTF { l_@@_sp #1 _concept_bool }
          {
            \MathMLintent
              {
                _
                \tl_if_empty:cF { l_@@_sp #1 _prefix_tl }
                  {
                    \tl_use:c { l_@@_sp #1 _prefix_tl } -
                  }
                \bool_if:cTF { l_@@_sp #1 _read_terse_bool }
                  {#1} {#2}
              }
              {
                \mathrm
                  {
                    \bool_if:cTF { l_@@_sp #1 _print_upper_bool }
                      { \text_uppercase:n {#1} } {#1}
                  }
              }
          }
          {
            \MathMLintent { :silent }
              {
                \mathrm
                  {
                    \bool_if:cTF { l_@@_sp #1 _print_upper_bool }
                      { \text_uppercase:n {#1} } {#1}
                  }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmc_result_mathml:n}
%   The function \myhlc|\@@_spmc_result_mathml:n| receives the short
%   identifier in `|#1|' and builds the \mynvmath{MathML} structure of the
%   computed value, when \mykey{result} is |true|. With \mykey{silent}|=true|,
%   the result is silenced as well.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmc_result_mathml:n
  {
    \str_if_eq:VnT \l_@@_mc_luaset_all_numeric_str { true }
      {
        \bool_if:cT { l_@@_sp #1 _print_result_bool }
          {
            \bool_if:cTF { l_@@_sp #1 _silent_bool }
              {
                \MathMLintent { :silent } { = }
                \MathMLintent { _- }
                  {
                    \@@_inner_sep:
                    \tl_use:c { l_@@_sp #1 _luaset_ #1 _value_tl }
                  }
              }
              {
                \MathMLintent { _es-igual-a } { = }
                \tl_use:c { l_@@_sp #1 _luaset_ #1 _value_tl }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spmcd, \spmcm}
%   The commands \ics*{spmcd} and \ics*{spmcm} are the public commands
%   for typesetting the greatest common divisor and least common
%   multiple. They enforce math mode, open a local group, and defer
%   execution to the main processing macro \myhlc|\@@_spmc_exec:nnnn|,
%   passing along the appropriate reading strings and arguments.
%    \begin{macrocode}
\NewDocumentCommand \spmcd { O{} d() }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spmc_exec:nnnn { mcd } { máximo-común-divisor } {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spmcd } }
  }
\NewDocumentCommand \spmcm { O{} d() }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spmc_exec:nnnn { mcm } { mínimo-común-múltiplo } {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spmcm } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The commands \cs{spnM} and \cs{spnD}}\label{cmd:spnM}
%
% The user-level commands |\spnM| and |\spnD| provide a semantic interface
% for the Multiples of a number, \emph{\enquote{múltiplos}} in Spanish,
% and the Divisors of a number, \emph{\enquote{divisores}} in Spanish.
% They send the validation of the arguments and the calculation to the Lua module \myluamod{},
% constructing a valid intent.
%
% \subsubsection{Definition of variables for \cs[no-index]{spnM} and \cs[no-index]{spnD}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spnmd_luaset_is_number_str,
%     \l_@@_spnM_spnD_luaset_error_str,
%   }
% The variable \myhlc|\l_@@_spnmd_luaset_is_number_str| stores |true| when the
% \mymarg{argument} is a positive integer. The variable
% \myhlc|\l_@@_spnM_spnD_luaset_error_str| stores |true| when the
% \mymarg{argument} is zero. Both are set by the Lua module \myluamod{}.
%    \begin{macrocode}
\str_new:N \l_@@_spnM_spnD_luaset_error_str
\str_new:N \l_@@_spnmd_luaset_is_number_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spnM_luaset_result_tl,
%     \l_@@_spnD_luaset_result_tl,
%     \l_@@_spnM_luaset_is_truncated_str,
%     \l_@@_spnD_luaset_is_truncated_str,
%     \l_@@_spnM_prefix_tl,
%     \l_@@_spnD_prefix_tl,
%     \l_@@_spnM_print_result_num_tl,
%     \l_@@_spnD_print_result_num_tl,
%   }
% Each command has its own copy of these variables. The variables
% \myhlc|_luaset_result_tl| store the multiples or the divisors, separated by
% commas, and the variables \myhlc|_luaset_is_truncated_str| store |true| when
% the list is cut, that is, when not all the results are printed; both are set
% by the Lua module \myluamod{}. The variables \myhlc|_prefix_tl| and
% \myhlc|_print_result_num_tl| store the values of the keys \mykey{prefix} and
% \mykey{result-num}, and the second ones are created by the key.
%    \begin{macrocode}
\clist_map_inline:nn { nM , nD }
  {
    \tl_new:c   { l_@@_sp #1 _luaset_result_tl        }
    \str_new:c  { l_@@_sp #1 _luaset_is_truncated_str }
    \tl_new:c   { l_@@_sp #1 _prefix_tl               }
  }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_md_out_seq,
%     \l_@@_spnM_spnD_intent_tl,
%   }
% The variable \myhlc|\l_@@_md_out_seq| stores the results to be printed, one
% by one. The variable \myhlc|\l_@@_spnM_spnD_intent_tl| holds the intent that
% \myhlc|\@@_spnM_spnD_intent:nn| builds.
%    \begin{macrocode}
\seq_new:N \l_@@_md_out_seq
\tl_new:N  \l_@@_spnM_spnD_intent_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spnM} and \cs[no-index]{spnD}}
%
% \begin{macro}[int]
%   {
%     md-invalid-arg,
%     mc-too-few-args,
%   }
%   The \enquote{error} |md-invalid-arg| is raised when the argument for
%   multiples or divisors is invalid.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { md-invalid-arg }
  {
    Invalid~argument~for~'\tl_trim_spaces:n {#1}'~\msg_line_context:.
  }
  {
    The~argument~must~be~a~single~positive~integer.
  }
%    \end{macrocode}
%   The \enquote{error} |mc-too-few-args| is raised when fewer than two
%   arguments are provided.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { mc-too-few-args }
  {
    Invalid~arguments~for~'\tl_trim_spaces:n {#1}'~\msg_line_context:.
  }
  {
    At~least~two~arguments~are~required.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spnM} and \cs[no-index]{spnD}}
%
% \begin{macro}[int]
%   {
%     result,
%     silent,
%     result-num,
%     prefix,
%     concept,
%     unknown,
%   }
%   We define the shared \mymeta{keys} \mykey[spnM,spnD]{result},
%   \mykey[spnM,spnD]{silent}, \mykey[spnM,spnD]{result-num},
%   \mykey[spnM,spnD]{prefix} and \mykey[spnM,spnD]{concept}.
%    \begin{macrocode}
\clist_map_inline:nn { nM , nD }
  {
    \keys_define:nn { spintent / sp #1 }
      {
        result     .bool_set:c = { l_@@_sp #1 _print_result_bool },
        result     .initial:n = false,
        result     .value_required:n = true,
        silent     .bool_set:c = { l_@@_sp #1 _silent_bool },
        silent     .initial:n = false,
        silent     .value_required:n = true,
        result-num .tl_set:c = { l_@@_sp #1 _print_result_num_tl },
        result-num .initial:n = ,
        result-num .value_required:n = true,
        prefix     .code:n =
          { \@@_sanitize_affix:cn { l_@@_sp #1 _prefix_tl } {##1} },
        prefix     .initial:n = ,
        prefix     .value_required:n = true,
        concept    .bool_set:c = { l_@@_sp #1 _concept_bool },
        concept    .initial:n = true,
        concept    .value_required:n = true,
        unknown    .code:n =
          {
            \str_set:Nn \l_@@_command_name_str { sp #1 }
            \@@_unknown_keys_cmd:n {##1}
          },
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spnM} and \cs[no-index]{spnD}}
%
% \begin{macro}[int]{\@@_spnM_spnD_intent:nn}
%   The internal function \myhlc|\@@_spnM_spnD_intent:nn| resolves the main
%   reading intent. It takes the command identifier `|#1|', such as
%   ``nM'' or ``nD'', and the base reading string `|#2|'. It
%   prepends the localized prefix if defined, normalizes accentuation for
%   \emph{\enquote{múltiplos}}, appends `|-de|', and stores the result in
%   \myhlc|\l_@@_spnM_spnD_intent_tl|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spnM_spnD_intent:nn
  {
    \tl_set:Ne \l_@@_spnM_spnD_intent_tl
      {
        _
        \tl_if_empty:cF { l_@@_sp #1 _prefix_tl }
          { \tl_use:c { l_@@_sp #1 _prefix_tl } - }
        \str_case:nnF {#2} { { multiplos } { múltiplos } }
          {#2}
        -de
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spnM_spnD_silent:nn}
%   The internal function \myhlc|\@@_spnM_spnD_silent:nn| prints the atomized
%   notation structure in full silent mode. It takes the mathematical symbol
%   `|#1|', such as ``D'' or ``M'', and the variable or explicit argument `|#2|'.
%   Every component is wrapped in a silent intent, and an invisible separator
%   is put between the parentheses to prevent \mynvmath{NVDA/MathCAT} from
%   applying default contextual reading rules. The argument is wrapped in a
%   literal `\myhlc|_-|' intent, not in |:silent|, so that intents of its own, such as
%   those of |\spnum|, are silenced too.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spnM_spnD_silent:nn
  {
    \MathMLintent { :silent } { \operatorname {#1} }
    \MathMLintent { :silent } { \lparen }
    \MathMLintent { :silent } { \c_@@_invisible_sep_int } % prevent doble "de"
    \MathMLintent { _- } { \@@_inner_sep: #2 }
    \MathMLintent { :silent } { \rparen }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spnM_spnD_result:nn}
%   The internal function \myhlc|\@@_spnM_spnD_result:nn| calculates and formats
%   the computed set. It takes the command identifier `|#1|' and the classified
%   numeric argument `|#2|'. It invokes the Lua calculation, reporting
%   |md-invalid-arg| if it fails. On success, it maps the resulting clist into a
%   sequence and formats the printed output with custom intents for the
%   separators and the conjunctions.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spnM_spnD_result:nn
  {
    \use:c { @@_luafun_calcular_ #1 :nn } {#2}
      {
        \tl_use:c { l_@@_sp #1 _print_result_num_tl }
      }
    \str_if_eq:VnTF \l_@@_spnM_spnD_luaset_error_str { true }
      {
        \msg_error:nne { spintent } { md-invalid-arg } { \exp_not:c { sp #1 } }
      }
      {
        \MathMLintent { :silent } { \c_@@_invisible_sep_int } % prevent "por"
        \MathMLintent { _son } { = \{ }
        \seq_set_from_clist:Ne
          \l_@@_md_out_seq { \tl_use:c { l_@@_sp #1 _luaset_result_tl } }
        \seq_use:Nnnn \l_@@_md_out_seq
          {
            \MathMLintent { :silent } { , }
            \str_if_eq:vnT { l_@@_sp #1 _luaset_is_truncated_str } { false }
              {
                \MathMLintent { _y- } { \c_@@_invisible_sep_int } % "y" (solo dos)
              }
          }
          { \MathMLintent { :silent } { , } }
          {
            \MathMLintent { :silent } { , }
            \str_if_eq:vnT { l_@@_sp #1 _luaset_is_truncated_str } { false }
              {
                \MathMLintent { _y- } { \c_@@_invisible_sep_int } % "y" final
              }
          }
        \str_if_eq:vnT { l_@@_sp #1 _luaset_is_truncated_str } { true }
          {
            \MathMLintent { :silent } { , } ~ \dots
          }
        \MathMLintent { :silent } { \} }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spnM_spnD_exec:nnnnn}
%   The internal function \myhlc|\@@_spnM_spnD_exec:nnnnn| is the one called
%   by the commands |\spnM| and |\spnD|. It takes
%   the command identifier `|#1|', its printed mathematical symbol `|#2|',
%   its base reading string `|#3|', the local key-value options `|#4|', and
%   the optional argument `|#5|'. It first sets the provided keys within the
%   current scope. With `|#5|' given,
%   \myhlc|\@@_luafun_spnmd_classify:n| classifies it, and a zero is rejected
%   with |md-invalid-arg|. Then it calls \myhlc|\@@_spnM_spnD_mathml:nnnn|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spnM_spnD_exec:nnnnn
  {
    \keys_set:nn { spintent / sp #1 } {#4}
    \tl_if_novalue:nF {#5}
      {
        \@@_luafun_spnmd_classify:n { \exp_not:n {#5} }
        \str_if_eq:VnT \l_@@_spnM_spnD_luaset_error_str { true }
          { \msg_error:nne { spintent } { md-invalid-arg } { \exp_not:c { sp #1 } } }
      }
    \@@_spnM_spnD_mathml:nnnn {#1} {#2} {#3} {#5}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spnM_spnD_mathml:nnnn}
%   The function \myhlc|\@@_spnM_spnD_mathml:nnnn| takes the command
%   identifier in `|#1|', the symbol in `|#2|', the base reading string in
%   `|#3|', and the optional argument in `|#4|', and builds the
%   \mynvmath{MathML} structure. The parentheses are always silenced.
%
%   With `|#4|' absent, the placeholder |n| stands in for the argument. With
%   `|#4|' given, the classification stored in
%   \myhlc|\l_@@_spnmd_luaset_is_number_str| decides whether it is wrapped
%   with |\spnum|, and then \mykey{result} decides whether
%   \myhlc|\@@_spnM_spnD_result:nn| prints the computed set; otherwise the
%   argument is typeset verbatim and no result is attempted. With
%   \mykey{silent}|=true| the printed result is wrapped in a literal `\myhlc|_-|'
%   intent.
%
%   Either way, \mykey{silent} silences the whole thing through
%   \myhlc|\@@_spnM_spnD_silent:nn|; otherwise \mykey{concept}|=true|
%   carries the literal |intent| from \myhlc|\@@_spnM_spnD_intent:nn|,
%   |false| silences only the operator symbol, leaving the argument to
%   read normally.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spnM_spnD_mathml:nnnn
  {
    \tl_if_novalue:nTF {#4}
      {
        \bool_if:cTF { l_@@_sp #1 _silent_bool }
          { \@@_spnM_spnD_silent:nn {#2} { n } }
          {
            \bool_if:cTF { l_@@_sp #1 _concept_bool }
              {
                \@@_spnM_spnD_intent:nn {#1} {#3}
                \MathMLintent
                  { \l_@@_spnM_spnD_intent_tl }
                  { \operatorname {#2} }
              }
              { \MathMLintent { :silent } { \operatorname {#2} } }
            \MathMLintent { :silent } { \lparen }
            % prevent doble "de"
            \MathMLintent { :silent } { \c_@@_invisible_sep_int }
            n
            \MathMLintent { :silent } { \rparen }
          }
      }
      {
        \bool_if:cTF { l_@@_sp #1 _silent_bool }
          {
            \str_if_eq:VnTF \l_@@_spnmd_luaset_is_number_str { true }
              { \@@_spnM_spnD_silent:nn {#2} { \spnum {#4} } }
              { \@@_spnM_spnD_silent:nn {#2} {#4} }
          }
          {
            \bool_if:cTF { l_@@_sp #1 _concept_bool }
              {
                \@@_spnM_spnD_intent:nn {#1} {#3}
                \MathMLintent
                  { \l_@@_spnM_spnD_intent_tl }
                  { \operatorname {#2} }
              }
              { \MathMLintent { :silent } { \operatorname {#2} } }
            \MathMLintent { :silent } { \lparen }
            % prevent doble "de"
            \MathMLintent { :silent } { \c_@@_invisible_sep_int }
            \str_if_eq:VnTF \l_@@_spnmd_luaset_is_number_str { true }
              { \spnum {#4} }
              {#4}
            \MathMLintent { :silent } { \rparen }
          }
        \str_if_eq:VnT \l_@@_spnmd_luaset_is_number_str { true }
          {
            \bool_if:cT { l_@@_sp #1 _print_result_bool }
              {
                \bool_if:cTF { l_@@_sp #1 _silent_bool }
                  {
                    % prevent "por"
                    \MathMLintent { :silent } { \c_@@_invisible_sep_int }
                    \MathMLintent { _- }
                      {
                        \@@_inner_sep:
                        \@@_spnM_spnD_result:nn {#1} {#4}
                      }
                  }
                  {
                    \@@_spnM_spnD_result:nn {#1} {#4}
                  }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spnM, \spnD}
%   The commands \ics*{spnM} and \ics*{spnD} are the public commands
%   for typesetting multiples and divisors, respectively. They
%   enforce math mode, open a local group, and defer execution to the
%   function \myhlc|\@@_spnM_spnD_exec:nnnnn|, passing along
%   the optional \mymeta{keys} `|#1|' and the numeric \mymarg{argument} `|#2|'.
%    \begin{macrocode}
\NewDocumentCommand \spnM { O{} d() }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spnM_spnD_exec:nnnnn { nM } { M } { multiplos } {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spnM } }
  }
\NewDocumentCommand \spnD { O{} d() }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spnM_spnD_exec:nnnnn { nD } { D } { divisores } {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spnD } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{sprazon}}\label{cmd:sprazon}
%
% The user command |\sprazon| typesets a ratio |a|:|b| as a fraction, using |\dfrac|
% or |\frac| according to \mykey{use-dfrac}, read natively, without parsing in
% Lua.
%
% \subsubsection{Definition of variables for \cs[no-index]{sprazon}}
%
% \begin{variable}[int]{\l_@@_sprazon_cmd_str}
%   The string variable \myhlc|\l_@@_sprazon_cmd_str| holds the name of the
%   command that the message of an unknown key reports: |sprazon|, or
%   |spProp|, which reuses the keys of |\sprazon|.
%    \begin{macrocode}
\str_new:N \l_@@_sprazon_cmd_str
\str_set:Nn \l_@@_sprazon_cmd_str { sprazon }
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of keys for \cs[no-index]{sprazon}}
%
% \begin{macro}[int]{silent, use-dfrac, inline, unknown}
%   We define the \mymeta{keys} \mykey[sprazon]{silent},
%   \mykey[sprazon]{use-dfrac} and \mykey[sprazon]{inline}.
%    \begin{macrocode}
\keys_define:nn { spintent / sprazon }
  {
    silent    .bool_set:N = \l_@@_sprazon_silent_bool,
    silent    .initial:n = false,
    silent    .value_required:n = true,
    use-dfrac .bool_set:N = \l_@@_sprazon_use_dfrac_bool,
    use-dfrac .initial:n = false,
    use-dfrac .value_required:n = true,
    inline    .bool_set:N = \l_@@_sprazon_inline_bool,
    inline    .initial:n = false,
    inline    .value_required:n = true,
    unknown   .code:n =
      {
        \str_set_eq:NN \l_@@_command_name_str \l_@@_sprazon_cmd_str
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{sprazon}}
%
% \begin{macro}[int]{\@@_sprazon_exec:nnn}
%   The function \myhlc|\@@_sprazon_exec:nnn| receives the optional
%   \mymeta{keys} in `|#1|', the numerator in `|#2|' and the denominator in
%   `|#3|'. It processes the keys and passes the numerator and the denominator
%   to \myhlc|\@@_sprazon_mathml:nn|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sprazon_exec:nnn
  {
    \keys_set:nn { spintent / sprazon } {#1}
    \@@_sprazon_mathml:nn {#2} {#3}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_sprazon_mathml:nn}
%   The function \myhlc|\@@_sprazon_mathml:nn| receives the numerator in
%   `|#1|' and the denominator in `|#2|'. With \mykey{silent}, the whole
%   expression is silenced as one unit: |\frac| or |\dfrac|, according to
%   \mykey{use-dfrac}, or |#1 : #2| with \mykey{inline}.
%
%   Otherwise, with \mykey{inline}|=false|, the default, the numerator and the
%   denominator are tagged with a native |\MathMLarg|, as |num| and |denom|, inside
%   |\frac| or |\dfrac|, and the outer |\MathMLintent| reads
%   \myhlc|_es-a:infix($num,$denom)|. With \mykey{inline}|=true|, the two
%   references are tagged side by side and the |:| between them carries the
%   \myhlc|_es-a:infix($num,$denom)| intent.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sprazon_mathml:nn
  {
    \@@_invisible_sep:
    \bool_if:NTF \l_@@_sprazon_silent_bool
      {
        \MathMLintent { _- }
          {
            \@@_inner_sep:
            \bool_if:NTF \l_@@_sprazon_inline_bool
              { #1 : #2 }
              {
                \bool_if:NTF \l_@@_sprazon_use_dfrac_bool
                  { \dfrac {#1} {#2} }
                  { \frac  {#1} {#2} }
              }
          }
      }
      {
        \bool_if:NTF \l_@@_sprazon_inline_bool
          {
            \MathMLintent { _es-a:infix($num,$denom) }
              {
                \MathMLarg { num } {#1}
                :
                \MathMLarg { denom } {#2}
              }
          }
          {
            \MathMLintent { _es-a:infix($num,$denom) }
              {
                \@@_inner_sep:
                \bool_if:NTF \l_@@_sprazon_use_dfrac_bool
                  {
                    \dfrac { \MathMLarg { num } {#1} } { \MathMLarg { denom } {#2} }
                  }
                  { \frac  { \MathMLarg { num } {#1} } { \MathMLarg { denom } {#2} } }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\sprazon}
%   The public command \ics*{sprazon} enforces \emph{math mode} and
%   delegates execution to \myhlc|\@@_sprazon_exec:nnn|.
%    \begin{macrocode}
\NewDocumentCommand \sprazon { O{} m m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_sprazon_exec:nnn {#1} {#2} {#3}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \sprazon } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spProp}}\label{cmd:spProp}
%
% The user command |\spProp| typesets a proportion |a|:|b| |=| |c|:|d|
% as two fractions joined by |=|, reusing
% \myhlc|\@@_sprazon_exec:nnn| directly, not the public |\sprazon|
% command, for each ratio.
%
% \subsubsection{Internal functions for \cs[no-index]{spProp}}
%
% \begin{macro}{\spProp}
%   The public command \ics*{spProp} enforces \emph{math mode}, and
%   forwards the same \mymeta{keys} in `|#1|' to both ratios: |a/b|,
%   built from `|#2|' and `|#3|', and |c/d|, built from `|#4|' and `|#5|', joined by a native
%   |\MathMLintent| reading \myhlc|_como| over the |=| sign.
%    \begin{macrocode}
\NewDocumentCommand \spProp { O{} m m m m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \str_set:Nn \l_@@_sprazon_cmd_str { spProp }
          \@@_sprazon_exec:nnn {#1} {#2} {#3}
          \bool_if:NTF \l_@@_sprazon_silent_bool
            {
              \MathMLintent { :silent } { = }
            }
            { \MathMLintent { _como } { = } }
          \@@_sprazon_exec:nnn {#1} {#4} {#5}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spProp } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spang}}\label{cmd:spang}
%
% The command |\spang| is the main public command for typesetting and
% tagging sexagesimal angle expressions.
% It parses the input using the Lua function \myhlc|\@@_luafun_spang_parse:n|.
% This separates the degrees, minutes, and seconds to construct a \mynvmath{MathML}
% |intent| tree with the correct postfixes for \mynvmath{NVDA/MathCAT}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spang}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spang_luaset_grado_str,
%     \l_@@_spang_luaset_minuto_str,
%     \l_@@_spang_luaset_segundo_str,
%     \l_@@_spang_luaset_error_str,
%   }
% These variables store the values returned by the Lua module \myluamod{}.
% The variables \myhlc|\l_@@_spang_luaset_grado_str|,
% \myhlc|\l_@@_spang_luaset_minuto_str| and
% \myhlc|\l_@@_spang_luaset_segundo_str| store the degrees, the minutes and
% the seconds, and are empty if missing. The variable
% \myhlc|\l_@@_spang_luaset_error_str| stores |true| or |false| if the
% \mymarg{argument} is invalid.
%    \begin{macrocode}
\str_new:N \l_@@_spang_luaset_grado_str
\str_new:N \l_@@_spang_luaset_minuto_str
\str_new:N \l_@@_spang_luaset_segundo_str
\str_new:N \l_@@_spang_luaset_error_str
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spang}}
%
% \begin{macro}[int]{spang-invalid-arg}
%   The \enquote{error} |spang-invalid-arg| is raised when an argument
%   fails the format checks, does not match the expected sexagesimal pattern,
%   or contains invalid measurement symbols.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spang-invalid-arg }
  {
    Invalid~argument~'#2'~for~'\tl_trim_spaces:n {#1}'~\msg_line_context:.
  }
  {
    The~argument~must~have~the~form~'degrees',~'degrees:minutes'~or~
    'degrees:minutes:seconds'.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spang}}
%
% \begin{macro}[int]{\@@_spang_exec:n}
%   The internal function \myhlc|\@@_spang_exec:n| parses the angle input using
%   \myhlc|\@@_luafun_spang_parse:n| after resetting the error flag in
%   \myhlc|\l_@@_spang_luaset_error_str|. If an invalid format is detected, it
%   issues the |spang-invalid-arg| error. Otherwise, it calls
%   \myhlc|\@@_spang_mathml:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spang_exec:n
  {
    \str_set:Nn \l_@@_spang_luaset_error_str { false }
    \@@_luafun_spang_parse:n {#1}
    \str_if_eq:VnTF \l_@@_spang_luaset_error_str { true }
      {
        \msg_error:nnnn { spintent } { spang-invalid-arg } { \spang } {#1}
      }
      { \@@_spang_mathml: }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spang_mathml:}
%   The internal function \myhlc|\@@_spang_mathml:| checks the extracted
%   string variables \myhlc|\l_@@_spang_luaset_grado_str|,
%   \myhlc|\l_@@_spang_luaset_minuto_str|, and
%   \myhlc|\l_@@_spang_luaset_segundo_str|. For each non-empty variable, it
%   constructs the corresponding \mynvmath{MathML} |intent| structure with the
%   correct measurement symbols and postfixes for \mynvmath{NVDA/MathCAT}.
%   Each value is expanded with \myhlc|\exp_args:Ne| before it reaches |\spunit|,
%   which protects its \mymarg{argument} from expansion.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spang_mathml:
  {
    \str_if_empty:NF \l_@@_spang_luaset_grado_str
      {
        \exp_args:Ne \spunit { \l_@@_spang_luaset_grado_str ° }
      }
    \str_if_empty:NF \l_@@_spang_luaset_minuto_str
      {
        \MathMLintent { _minutos:postfix($m) }
          {
            \MathMLarg { m }
              {
                \exp_args:Ne \spunit { \l_@@_spang_luaset_minuto_str ′ }
              }
          }
      }
    \str_if_empty:NF \l_@@_spang_luaset_segundo_str
      {
        \MathMLintent { _segundos:postfix($s) }
          {
            \MathMLarg { s }
              {
                \exp_args:Ne \spunit { \l_@@_spang_luaset_segundo_str ″ }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spang}
%   The command \ics*{spang} is the public interface for formatting
%   sexagesimal angle expressions. It first checks if it is being used in
%   math mode, issuing an error if not. Inside a local group, it calls
%   \myhlc|\@@_spang_exec:n| to process the \mymarg{argument}.
%    \begin{macrocode}
\NewDocumentCommand \spang { m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spang_exec:n {#1}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spang } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spnZ}}\label{cmd:spnZ}
%
% The user-level command |\spnZ| formats integers, the set
% $\mathbb{Z}$. It includes specific options to enclose
% numbers, particularly negative ones, in visual parentheses and allows
% you to control whether these parentheses are read by \mynvmath{MathCAT}.
%
% This command verifies that the \mymarg{argument} is indeed an \emph{integer},
% checking for the complete absence of decimal separators or attached
% units of measurement.
%
% \subsubsection{Definition of error messages for \cs[no-index]{spnZ}}
%
% \begin{macro}[int]{spnZ-not-integer}
%   The \enquote{error} |spnZ-not-integer| is raised when the
%   \mymarg{argument} contains decimal separators or attached units, which
%   conflict with the constraints of a pure integer representation, or when it
%   is empty.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spnZ-not-integer }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \spnZ'~\msg_line_context:.
  }
  {
    Only~integers~are~accepted;~decimal~separators~and~units~are~not~allowed.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spnZ}}
%
% \begin{macro}[int]{cifras-sep, read-sign, wrap-parens, read-parens, unknown}
%   We define the \mymeta{keys} \mykey[spnZ]{cifras-sep},
%   \mykey[spnZ]{read-sign}, \mykey[spnZ]{wrap-parens} and
%   \mykey[spnZ]{read-parens}.
%    \begin{macrocode}
\keys_define:nn { spintent / spnZ }
  {
    cifras-sep  .meta:nn = { spintent / spnum } { cifras-sep = {#1} },
    read-sign   .meta:nn = { spintent / spnum } { read-sign  = {#1} },
    wrap-parens .bool_set:N = \l_@@_spnz_wrap_parens_bool,
    wrap-parens .initial:n = false,
    wrap-parens .value_required:n = true,
    read-parens .bool_set:N = \l_@@_spnz_read_parens_bool,
    read-parens .initial:n = true,
    read-parens .value_required:n = true,
    unknown     .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spnZ }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spnZ}}
%
% \begin{macro}[int]{\@@_spnz_mathml:}
%   The function \myhlc|\@@_spnz_mathml:| builds the \mynvmath{MathML}
%   structure of the integer. If \mykey[spnZ]{wrap-parens} is true, it prints
%   the number surrounded by parentheses, checking the state of
%   \myhlc|\l_@@_spnz_read_parens_bool|, which the
%   \mykey[spnZ]{read-parens} key controls: if it is |true|, the parentheses are read;
%   otherwise it applies the |:silent| \emph{intent property} to them. If
%   \mykey[spnZ]{wrap-parens} is false, it prints the number alone with
%   \myhlc|\@@_spnum_print_num_start:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spnz_mathml:
  {
    \bool_if:NTF \l_@@_spnz_wrap_parens_bool
      {
        \bool_if:NTF \l_@@_spnz_read_parens_bool
          {
            \lparen \@@_spnum_print_num_start: \rparen
          }
          {
            \MathMLintent { :silent } { \lparen }
            \@@_spnum_print_num_start:
            \MathMLintent { :silent } { \rparen }
          }
      }
      { \@@_spnum_print_num_start: }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spnz_exec:nn}
%   The function \myhlc|\@@_spnz_exec:nn| first processes the
%   \mymeta{keys} passed in `|#1|' and then calls
%   \myhlc|\@@_luafun_clean_split_and_set:n| on `|#2|'. It verifies
%   that the input is a valid integer by checking if the variable
%   \myhlc|\l_@@_luaset_has_integer_str| is |true|. If not |true|,
%   an \enquote{error} is raised.
%
%   If it is |true|, it calls \myhlc|\@@_spnz_mathml:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spnz_exec:nn
  {
    \keys_set:nn { spintent / spnZ } {#1}
    \@@_luafun_clean_split_and_set:n { \exp_not:n {#2} }
    \str_if_eq:VnTF \l_@@_luaset_has_integer_str { true }
      { \@@_spnz_mathml: }
      { \msg_error:nnn { spintent } { spnZ-not-integer } {#2} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spnZ}
%   Defines the user command \ics*{spnZ}, which is executed within a
%   \emph{group} and enforces \emph{math mode}.
%    \begin{macrocode}
\NewDocumentCommand \spnZ { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spnz_exec:nn {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spnZ } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spmQ}}\label{cmd:spmQ}
%
% The command |\spmQ| typesets \emph{\enquote{mixed numbers}} within Spanish
% mathematical contexts such as $3\frac{1}{2}$. It sends the scaling
% of the integer part to the Lua module \myluamod{} via
% \myhlc|\@@_luafun_spmQ_scale_int:Vn|, which measures both the integer
% glyph and the fraction box at the node level and computes a scaling factor
% to align their heights visually. Finally, it constructs the correct
% intent for \mynvmath{MathCAT}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spmQ}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spmQ_join_word_str,
%     \l_@@_spmQ_intent_read_sign_str,
%   }
% The variable \myhlc|\l_@@_spmQ_join_word_str| stores the value set by the
% key \mykey[spmQ]{join-word}, which \mynvmath{MathCAT} verbalizes as a
% grammatical connector between the integer and fractional parts. The variable
% \myhlc|\l_@@_spmQ_intent_read_sign_str| stores the |:intent| passed to
% |\MathMLintent| when the signs `$\mathcolor{red}{+}$' or
% `$\mathcolor{red}{-}$' are present in the integer part, used by the function
% \myhlc|\@@_spmQ_print_scale_int:n|.
%    \begin{macrocode}
\str_new:N \l_@@_spmQ_join_word_str
\str_new:N \l_@@_spmQ_intent_read_sign_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spmQ_wrap_parens_l_tl,
%     \l_@@_spmQ_wrap_parens_r_tl,
%   }
% The variables \myhlc|\l_@@_spmQ_wrap_parens_l_tl| and
% \myhlc|\l_@@_spmQ_wrap_parens_r_tl| store the sized parentheses set by the
% function \myhlc|\@@_spmQ_set_wrap_parens_size:| and used by the key
% \mykey[spmQ]{wrap-parens}.
%    \begin{macrocode}
\tl_new:N \l_@@_spmQ_wrap_parens_l_tl
\tl_new:N \l_@@_spmQ_wrap_parens_r_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_spmQ_error_bool}
% The boolean variable \myhlc|\l_@@_spmQ_error_bool| is set to |true| by
% \myhlc|\@@_spmQ_check_part:nn| when any of the three arguments is rejected,
% so that nothing is typeset.
%    \begin{macrocode}
\bool_new:N \l_@@_spmQ_error_bool
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_saved_luamml_flag_int}
% The integer variable \myhlc|\l_@@_saved_luamml_flag_int| temporarily stores
% the current value of \myhlc|\l__luamml_flag_int| before disabling it during
% the injection of the scaled integer node, and restores it afterwards. This
% prevents \mypkg{luamml} from generating a spurious \lstinline+<math>+
% element around the |sub_box| processed in isolation, before |\frac| has
% expanded.
%    \begin{macrocode}
\int_new:N \l_@@_saved_luamml_flag_int
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spmQ}}
%
% \begin{macro}[int]{spmQ-zero-denominator, spmQ-not-integer, spmQ-signed-part}
%   The \enquote{error} |spmQ-zero-denominator| is raised when a zero
%   denominator is passed to |\spmQ|. The \enquote{error} |spmQ-not-integer| is
%   raised when any of the three arguments is not actually an integer. The
%   \enquote{error} |spmQ-signed-part| is raised when the numerator or the
%   denominator carries a sign: only the integer part may be signed.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spmQ-zero-denominator }
  {
    Invalid~denominator~for~'\token_to_str:N \spmQ'~\msg_line_context:.
  }
  {
    The~denominator~of~a~fraction~cannot~be~zero.
  }
\msg_new:nnnn { spintent } { spmQ-not-integer }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \spmQ'~\msg_line_context:.
  }
  {
    Only~integers~are~accepted.
  }
\msg_new:nnnn { spintent } { spmQ-signed-part }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \spmQ'~\msg_line_context:.
  }
  {
    The~numerator~and~the~denominator~must~be~integers~without~sign;~only~the~integer~
    part~may~have~one.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spmQ}}
%
% \begin{macro}[int]
%   {
%     join-word,
%     use-spnZ,
%     wrap-parens,
%     read-parens,
%     use-dfrac,
%     silent,
%     unknown,
%   }
%   We define the \mymeta{keys} \mykey[spmQ]{join-word}, \mykey[spmQ]{use-spnZ},
%   \mykey[spmQ]{wrap-parens}, \mykey[spmQ]{read-parens},
%   \mykey[spmQ]{use-dfrac} and \mykey[spmQ]{silent}.
%    \begin{macrocode}
\keys_define:nn { spintent / spmQ }
  {
    join-word   .choices:nn =
      { entero , enteros , y }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spmQ_join_word_str { entero } }
            {2} { \str_set:Nn \l_@@_spmQ_join_word_str { enteros } }
            {3} { \str_set:Nn \l_@@_spmQ_join_word_str { y } }
          }
      },
    join-word   .initial:n = y,
    join-word   .value_required:n = true,
    join-word / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { join-word } { entero, ~enteros, ~y } { \exp_not:n {#1} },
    use-spnZ    .bool_set:N = \l_@@_spmQ_use_spnZ_bool,
    use-spnZ    .initial:n = true,
    use-spnZ    .value_required:n = true,
    use-dfrac   .bool_set:N = \l_@@_spmQ_use_dfrac_bool,
    use-dfrac   .initial:n = false,
    use-dfrac   .value_required:n = true,
    wrap-parens .bool_set:N = \l_@@_spmQ_wrap_parens_bool,
    wrap-parens .initial:n = false,
    wrap-parens .value_required:n = true,
    read-parens .bool_set:N = \l_@@_spmQ_read_parens_bool,
    read-parens .initial:n = true,
    read-parens .value_required:n = true,
    silent      .bool_set:N = \l_@@_spmQ_silent_bool,
    silent      .initial:n = false,
    silent      .value_required:n = true,
    unknown     .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spmQ }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spmQ}}
%
% \begin{macro}[int]{\@@_spmQ_set_wrap_parens_size:}
%   The function \myhlc|\@@_spmQ_set_wrap_parens_size:| captures the
%   current mathematical style using the primitive |\mathstyle| from
%   \hologo{LuaTeX} and sets the variables \myhlc|\l_@@_spmQ_wrap_parens_l_tl|
%   and \myhlc|\l_@@_spmQ_wrap_parens_r_tl| with the appropriate size for the
%   parentheses. Display styles |0| and |1| receive |\Bigl\lparen| and
%   |\Bigr\rparen|; text styles |2| and |3| receive |\bigl\lparen| and
%   |\bigr\rparen|; any other style falls back to unsized |\lparen| and
%   |\rparen|.
%
%   If the key \mykey[spmQ]{use-dfrac} is active, the size is always
%   forced to |\Bigl|/|\Bigr| regardless of the current mathematical style,
%   since |\dfrac| always produces a display-height fraction.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmQ_set_wrap_parens_size:
  {
    \int_case:nnF { \mathstyle }
      {
        { 0 }
          {
            \tl_set:Nn \l_@@_spmQ_wrap_parens_l_tl { \Bigl \lparen }
            \tl_set:Nn \l_@@_spmQ_wrap_parens_r_tl { \Bigr \rparen }
          }
        { 1 }
          {
            \tl_set:Nn \l_@@_spmQ_wrap_parens_l_tl { \Bigl \lparen }
            \tl_set:Nn \l_@@_spmQ_wrap_parens_r_tl { \Bigr \rparen }
          }
        { 2 }
          {
            \tl_set:Nn \l_@@_spmQ_wrap_parens_l_tl { \bigl \lparen }
            \tl_set:Nn \l_@@_spmQ_wrap_parens_r_tl { \bigr \rparen }
          }
        { 3 }
          {
            \tl_set:Nn \l_@@_spmQ_wrap_parens_l_tl { \bigl \lparen }
            \tl_set:Nn \l_@@_spmQ_wrap_parens_r_tl { \bigr \rparen }
          }
      }
      {
        \tl_set:Nn \l_@@_spmQ_wrap_parens_l_tl { \lparen }
        \tl_set:Nn \l_@@_spmQ_wrap_parens_r_tl { \rparen }
      }
%    \end{macrocode}
%   If the key \mykey[spmQ]{use-dfrac} is active, we must reset the
%   variables \myhlc|\l_@@_spmQ_wrap_parens_l_tl| and
%   \myhlc|\l_@@_spmQ_wrap_parens_r_tl| to |\Bigl|/|\Bigr| unconditionally.
%    \begin{macrocode}
    \bool_if:NT \l_@@_spmQ_use_dfrac_bool
      {
        \tl_set:Nn \l_@@_spmQ_wrap_parens_l_tl { \Bigl \lparen }
        \tl_set:Nn \l_@@_spmQ_wrap_parens_r_tl { \Bigr \rparen }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmQ_start_wrap_parens:, \@@_spmQ_stop_wrap_parens:}
%   The functions \myhlc|\@@_spmQ_start_wrap_parens:| and
%   \myhlc|\@@_spmQ_stop_wrap_parens:| open and close the parentheses
%   around the \emph{\enquote{mixed number}} when the key
%   \mykey[spmQ]{wrap-parens} is active. They first call
%   \myhlc|\@@_spmQ_set_wrap_parens_size:| to determine the correct size
%   for the current mathematical style, then print the appropriate
%   parenthesis stored in \myhlc|\l_@@_spmQ_wrap_parens_l_tl| or
%   \myhlc|\l_@@_spmQ_wrap_parens_r_tl|.
%
%   If the variable \myhlc|\l_@@_spmQ_read_parens_bool|, controlled by the
%   key \mykey[spmQ]{read-parens}, is \emph{false}, each parenthesis is
%   wrapped inside |\MathMLintent{:silent}| so that
%   \mynvmath{NVDA/MathCAT} does not verbalize them.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmQ_start_wrap_parens:
  {
    \bool_if:NT \l_@@_spmQ_wrap_parens_bool
      {
        \@@_spmQ_set_wrap_parens_size:
        \bool_if:NTF \l_@@_spmQ_read_parens_bool
          { \l_@@_spmQ_wrap_parens_l_tl }
          { \MathMLintent { :silent } { \l_@@_spmQ_wrap_parens_l_tl } }
      }
  }
\cs_new_protected:Nn \@@_spmQ_stop_wrap_parens:
  {
    \bool_if:NT \l_@@_spmQ_wrap_parens_bool
      {
        \@@_spmQ_set_wrap_parens_size:
        \bool_if:NTF \l_@@_spmQ_read_parens_bool
          { \l_@@_spmQ_wrap_parens_r_tl }
          { \MathMLintent { :silent } { \l_@@_spmQ_wrap_parens_r_tl } }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmQ_set_join_word:}
%   The function \myhlc|\@@_spmQ_set_join_word:| sets the grammatical
%   connector between the integer part and the fraction by branching on
%   the value stored in \myhlc|\l_@@_spmQ_join_word_str|, set by the key
%   \mykey[spmQ]{join-word}. In all three cases the visual output is the
%   Unicode invisible separator |U+2063| via \myhlc|\c_@@_invisible_sep_tl|,
%   which produces a \lstinline+<mi>+ node in the \mynvmath{MathML}
%   structure; what differs is the |:intent| text attached to it via
%   |\MathMLintent|, which \mynvmath{NVDA/MathCAT} will verbalize as
%   \emph{«entero»}, \emph{«enteros»} or \emph{«y»} respectively.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmQ_set_join_word:
  {
    \str_case:Vn \l_@@_spmQ_join_word_str
      {
        { entero  } { \MathMLintent { _entero  } { \c_@@_invisible_sep_int } }
        { enteros } { \MathMLintent { _enteros } { \c_@@_invisible_sep_int } }
        { y       } { \MathMLintent { _y-      } { \c_@@_invisible_sep_int } }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmQ_print_scale_int:}
%   The function \myhlc|\@@_spmQ_print_scale_int:| prints the integer part
%   of the \emph{\enquote{mixed number}} scaled to match the visual height
%   of the accompanying fraction. Before calling the Lua function
%   \myhlc|\@@_luafun_spmQ_scale_int:Vn|, it temporarily disables
%   \mypkg{luamml} by saving the current value of \myhlc|\l__luamml_flag_int|
%   into \myhlc|\l_@@_saved_luamml_flag_int| and setting it to |0|, which skips it
%   completely. This prevents \mypkg{luamml} from generating a spurious
%   \lstinline+<math>+ wrapper around the |sub_box| node that holds the
%   scaled integer, which is processed in isolation before |\frac| has
%   expanded. Once the Lua function returns, the original flag value is
%   restored via \myhlc|\l_@@_saved_luamml_flag_int|.
%
%   The Lua function receives the integer digits from
%   \myhlc|\l_@@_luaset_part_int_tl| and the fraction command name,
%   determined by the key \mykey[spmQ]{use-dfrac},
%   as its two arguments, and inserts the appropriately scaled |sub_box|
%   node into the current horizontal list.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmQ_print_scale_int:
  {
    \int_set_eq:Nc \l_@@_saved_luamml_flag_int { l__luamml_flag_int }
    \int_set:Nn \l__luamml_flag_int { 0 }
    \bool_if:NTF \l_@@_spmQ_use_dfrac_bool
      { \@@_luafun_spmQ_scale_int:Vn \l_@@_luaset_part_int_tl { dfrac } }
      { \@@_luafun_spmQ_scale_int:Vn \l_@@_luaset_part_int_tl { frac  } }
    \int_set_eq:cN { l__luamml_flag_int } \l_@@_saved_luamml_flag_int
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmQ_print_scale_int:n}
%   The function \myhlc|\@@_spmQ_print_scale_int:n| takes the integer part
%   of the \emph{\enquote{mixed number}} passed in `|#1|'. It first calls
%   \myhlc|\@@_luafun_clean_split_and_set:n| on `|#1|' to parse and
%   set the shared variables from Lua, then checks the status of
%   \myhlc|\l_@@_luaset_has_integer_str|.
%
%   If the status is |true|, it inspects \myhlc|\l_@@_luaset_sign_tl|. When
%   the sign is \emph{empty}, it delegates to
%   \myhlc|\@@_spmQ_print_scale_int:| to render the scaled integer. When
%   a sign is present, it sets \myhlc|\l_@@_spmQ_intent_read_sign_str| to
%   either \myhlc|_más:prefix($e)| or \myhlc|_menos:prefix($e)| and wraps the sign
%   glyph together with a \mynvmath{MathML} argument |($e)| containing the
%   scaled integer inside a |\MathMLintent|, so that
%   \mynvmath{NVDA/MathCAT} verbalizes the sign as a prefix before the
%   integer value. An \myhlc|\@@_invisible_sep:| call between the sign and
%   the |MathMLarg| forces the opening of a new \lstinline+<mrow>+ in the
%   \mynvmath{MathML} structure, which is required for the intent tree to
%   be well-formed.
%
%   If the status is |false|, the |spmQ-not-integer| \enquote{error} is
%   raised immediately.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmQ_print_scale_int:n
  {
    \@@_luafun_clean_split_and_set:n { \exp_not:n {#1} }
    \str_if_eq:VnTF \l_@@_luaset_has_integer_str { true }
      {
        \tl_if_empty:NTF \l_@@_luaset_sign_tl
          {
            \@@_spmQ_print_scale_int:
          }
          {
            \str_if_eq:VnTF \l_@@_luaset_sign_tl { + }
              {
                \str_set:Nn
                  \l_@@_spmQ_intent_read_sign_str { _más:prefix($e) }
              }
              {
                \str_set:Nn
                  \l_@@_spmQ_intent_read_sign_str { _menos:prefix($e) }
              }
            \MathMLintent { \l_@@_spmQ_intent_read_sign_str }
              {
                \l_@@_luaset_sign_tl \c_@@_invisible_sep_int
                \MathMLarg { e } { \@@_spmQ_print_scale_int: }
              }
          }
      }
      { \msg_error:nnn { spintent } { spmQ-not-integer } {#1} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmQ_print_spnZ_dfrac:nn}
%   The function \myhlc|\@@_spmQ_print_spnZ_dfrac:nn| typesets the
%   fractional part of the \emph{\enquote{mixed number}}. It takes the
%   numerator in `|#1|' and the denominator in `|#2|', both already
%   validated by \myhlc|\@@_spmQ_check_part:nn|. The function branches
%   on the two boolean variables \myhlc|\l_@@_spmQ_use_spnZ_bool| and
%   \myhlc|\l_@@_spmQ_use_dfrac_bool|, controlled respectively by the keys
%   \mykey[spmQ]{use-spnZ} and \mykey[spmQ]{use-dfrac}. When
%   \mykey[spmQ]{use-spnZ} is |true|, both the numerator and denominator
%   are passed through |\spnZ| to gain \mynvmath{MathML} integer semantics;
%   otherwise, the raw token lists `|#1|' and `|#2|' are used as given.
%   In both branches, the fraction command is selected with
%   |\use:c|: if \mykey[spmQ]{use-dfrac} is |true|, |dfrac| is
%   assembled; otherwise, |frac| is used.
%   Inside the fraction the outer invisible separator is not typeset, only
%   recorded as already placed, since the |\spnZ| of the numerator would put
%   it there as the first element, and \mynvmath{MathCAT} would no longer
%   read the fraction as a simple one.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmQ_print_spnZ_dfrac:nn
  {
    \bool_if:NTF \l_@@_spmQ_use_spnZ_bool
      {
        \group_begin:
          \cs_set_protected:Nn \@@_invisible_sep:
            {
              \@@_luafun_invisible_sep_mark:
            }
          \use:c { \bool_if:NT \l_@@_spmQ_use_dfrac_bool { d } frac }
            { \spnZ {#1} } { \spnZ {#2} }
        \group_end:
      }
      {
        \use:c { \bool_if:NT \l_@@_spmQ_use_dfrac_bool { d } frac }
          {#1} {#2}
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmQ_print:nnn}
%   The function \myhlc|\@@_spmQ_print:nnn| is the main function of |\spmQ|.
%   It takes three arguments: the integer part
%   `|#1|', the numerator `|#2|', and the denominator `|#3|'. It opens
%   the optional parentheses via \myhlc|\@@_spmQ_start_wrap_parens:|,
%   then calls \myhlc|\@@_spmQ_print_scale_int:n| to scale and render the
%   integer part. Next, it sets the grammatical connector between the
%   integer and the fraction via \myhlc|\@@_spmQ_set_join_word:|. Then it
%   calls \myhlc|\@@_spmQ_print_spnZ_dfrac:nn| to typeset the fraction
%   using the keys \mykey[spmQ]{use-dfrac} and \mykey[spmQ]{use-spnZ}.
%   Finally, it closes the optional parentheses via
%   \myhlc|\@@_spmQ_stop_wrap_parens:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmQ_print:nnn
  {
    \@@_spmQ_start_wrap_parens:
    \@@_spmQ_print_scale_int:n {#1}
    \@@_spmQ_set_join_word:
    \@@_spmQ_print_spnZ_dfrac:nn {#2} {#3}
    \@@_spmQ_stop_wrap_parens:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmQ_check_part:nn}
%   The function \myhlc|\@@_spmQ_check_part:nn| validates one argument of
%   |\spmQ|. It receives the role of the argument in `|#1|', which is |int|,
%   |num| or |den|, and the argument itself in `|#2|', and calls
%   \myhlc|\@@_luafun_clean_split_and_set:n| on it. If
%   \myhlc|\l_@@_luaset_has_integer_str| is |false|, the |spmQ-not-integer|
%   \enquote{error} is raised. If the argument is an integer but carries a
%   sign and its role is not |int|, the |spmQ-signed-part| \enquote{error}
%   is raised. If the role is |den| and the value is zero, the
%   |spmQ-zero-denominator| \enquote{error} is raised. Every rejected
%   argument sets \myhlc|\l_@@_spmQ_error_bool| to |true|; the arguments are
%   rejected whatever the value of the key \mykey[spmQ]{silent}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmQ_check_part:nn
  {
    \@@_luafun_clean_split_and_set:n { \exp_not:n {#2} }
    \str_if_eq:VnTF \l_@@_luaset_has_integer_str { true }
      {
        \bool_lazy_or:nnTF
          { \tl_if_empty_p:N \l_@@_luaset_sign_tl }
          { \str_if_eq_p:nn {#1} { int } }
          {
            \str_if_eq:nnT {#1} { den }
              {
                \int_compare:nNnT { \l_@@_luaset_part_int_tl } = { 0 }
                  {
                    \msg_error:nn { spintent } { spmQ-zero-denominator }
                    \bool_set_true:N \l_@@_spmQ_error_bool
                  }
              }
          }
          {
            \msg_error:nnn { spintent } { spmQ-signed-part } {#2}
            \bool_set_true:N \l_@@_spmQ_error_bool
          }
      }
      {
        \msg_error:nnn { spintent } { spmQ-not-integer } {#2}
        \bool_set_true:N \l_@@_spmQ_error_bool
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmQ_exec:nnnn}
%   The internal function \myhlc|\@@_spmQ_exec:nnnn| takes four arguments:
%   the optional \mymeta{keys} `|#1|', the integer `|#2|', the numerator
%   `|#3|', and the denominator `|#4|'. First sets the optional \mymeta{keys}
%   from `|#1|', then validates the three arguments with
%   \myhlc|\@@_spmQ_check_part:nn|. If any of them is rejected, nothing is
%   typeset.
%
%   Otherwise, it calls \myhlc|\@@_spmQ_mathml:nnn|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmQ_exec:nnnn
  {
    \keys_set:nn { spintent / spmQ } {#1}
    \bool_set_false:N \l_@@_spmQ_error_bool
    \@@_spmQ_check_part:nn { int } {#2}
    \@@_spmQ_check_part:nn { num } {#3}
    \@@_spmQ_check_part:nn { den } {#4}
    \bool_if:NF \l_@@_spmQ_error_bool
      { \@@_spmQ_mathml:nnn {#2} {#3} {#4} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spmQ_mathml:nnn}
%   The internal function \myhlc|\@@_spmQ_mathml:nnn| takes the integer
%   `|#1|', the numerator `|#2|', and the denominator `|#3|', and calls
%   \myhlc|\@@_spmQ_print:nnn| to format the \emph{\enquote{mixed number}}.
%   With the key \mykey[spmQ]{silent} set to |true|, the whole
%   \emph{\enquote{mixed number}} is wrapped in a single
%   |\MathMLintent{_-}|, preceded by the outer invisible separator and with
%   the inner one first inside it, so \mynvmath{MathCAT} verbalizes nothing.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spmQ_mathml:nnn
  {
    \bool_if:NTF \l_@@_spmQ_silent_bool
      {
        \@@_invisible_sep:
        \MathMLintent { _- }
          {
            \@@_inner_sep:
            \@@_spmQ_print:nnn {#1} {#2} {#3}
          }
      }
      { \@@_spmQ_print:nnn {#1} {#2} {#3} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spmQ}
%   The command \ics*{spmQ} is the public interface for formatting
%   \emph{\enquote{mixed numbers}}. It first checks if it is being used in
%   \emph{math mode}, issuing an \enquote{error} if not. Inside a local
%   group, it calls \myhlc|\@@_spmQ_exec:nnnn| to process the optional
%   \mymeta{keys} and the \mymarg{arguments}.
%    \begin{macrocode}
\NewDocumentCommand \spmQ { O{} m m m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spmQ_exec:nnnn {#1} {#2} {#3} {#4}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spmQ } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The commands of the \texttt{geo} family}\label{cmd:geo}
%
% The commands described in this section provide a semantic interface for
% geometric objects in Spanish mathematical contexts. They delegate the parsing
% to \myluamod{} and share the variables \myhlc{\l_@@_geo_luaset_error_str},
% \myhlc{\l_@@_geo_luaset_intent_str}, \myhlc{\l_@@_geo_luaset_print_tl},
% \myhlc{\l_@@_geo_luaset_letra_str}, \myhlc{\l_@@_geo_luaset_sub_str} and
% \myhlc{\l_@@_geo_luaset_tipo_str}, with which the intent is
% built. A single point is always annotated with
% \myhlc|\@@_mathmlarg_point:nnn|. Every command activates
% \myhlc|\l_@@_scope_attr_int| inside its own group, so that the shared queue
% only claims content typeset by a \texttt{geo} command.
%
% \subsubsection{Definition of variables for the \texttt{geo} family}
%
% \begin{variable}[int]
%   {
%     \l_@@_geo_luaset_error_str,
%     \l_@@_geo_luaset_intent_str,
%     \l_@@_geo_luaset_print_tl,
%   }
%   These variables are shared by all the commands of the family and are set
%   by the Lua module \myluamod{}. The variable
%   \myhlc|\l_@@_geo_luaset_error_str| stores |true| or |false| if the
%   \mymarg{argument} is invalid. The variable
%   \myhlc|\l_@@_geo_luaset_intent_str| stores the body of the intent and
%   \myhlc|\l_@@_geo_luaset_print_tl| stores what is printed.
%    \begin{macrocode}
\str_new:N \l_@@_geo_luaset_error_str
\str_new:N \l_@@_geo_luaset_intent_str
\tl_new:N  \l_@@_geo_luaset_print_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_geo_luaset_points_clist,
%     \l_@@_geo_luaset_is_name_str,
%     \l_@@_geo_luaset_letra_str,
%     \l_@@_geo_luaset_sub_str,
%     \l_@@_geo_luaset_tipo_str,
%   }
%   The variable \myhlc|\l_@@_geo_luaset_points_clist| stores the points of
%   the \mymarg{argument} separated by commas and not joined, such as
%   |A,B,C|. The variable \myhlc|\l_@@_geo_luaset_is_name_str| stores |true|
%   when the \mymarg{argument} is the name of a line, ray or segment and
%   |false| when it is a list of points. When the \mymarg{argument} is a
%   single point or name, \myhlc|\l_@@_geo_luaset_letra_str| stores its
%   letter, \myhlc|\l_@@_geo_luaset_sub_str| stores its subscript or prime
%   and \myhlc|\l_@@_geo_luaset_tipo_str| stores |sub| or |prima|, and is
%   empty if there is none.
%    \begin{macrocode}
\clist_new:N \l_@@_geo_luaset_points_clist
\str_new:N   \l_@@_geo_luaset_is_name_str
\str_new:N   \l_@@_geo_luaset_letra_str
\str_new:N   \l_@@_geo_luaset_sub_str
\str_new:N   \l_@@_geo_luaset_tipo_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_geo_luaset_poly_sym_str,
%     \l_@@_geo_luaset_concept_str,
%   }
%   The variable \myhlc|\l_@@_geo_luaset_poly_sym_str| stores |triangle|,
%   |square| or nothing, according to the number of vertices of the polygon.
%   The variable \myhlc|\l_@@_geo_luaset_concept_str| stores the name of the
%   polygon or of the concept, such as |triángulo| or |área-del-triángulo|.
%    \begin{macrocode}
\str_new:N \l_@@_geo_luaset_poly_sym_str
\str_new:N \l_@@_geo_luaset_concept_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_geo_luaset_angulo_case_str,
%     \l_@@_geo_luaset_nombre_es_num_str,
%   }
%   The variable \myhlc|\l_@@_geo_luaset_angulo_case_str| stores the kind of
%   \mymarg{argument} that Lua recognized for an angle: |tres-puntos|,
%   |un-punto| or |nombre|. The variable
%   \myhlc|\l_@@_geo_luaset_nombre_es_num_str| stores |true| when that name
%   is a number.
%    \begin{macrocode}
\str_new:N \l_@@_geo_luaset_angulo_case_str
\str_new:N \l_@@_geo_luaset_nombre_es_num_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_geo_luaset_letra_word_str,
%     \l_@@_geo_luaset_letra_conector_str,
%     \l_@@_geo_luaset_sub_spoken_str,
%   }
%   These variables are used by the commands for areas and perimeters of
%   figures. When the \mymarg{argument} is a letter of the table, |F|, |P|,
%   |B|, |L| or |T|, the variable \myhlc|\l_@@_geo_luaset_letra_word_str|
%   stores its word, such as |figura| or |lateral|, and
%   \myhlc|\l_@@_geo_luaset_letra_conector_str| stores the connector that the
%   word needs, such as |de-la|, which is empty if it needs none. The variable
%   \myhlc|\l_@@_geo_luaset_sub_spoken_str| stores the subscript as it is
%   spoken.
%    \begin{macrocode}
\str_new:N \l_@@_geo_luaset_letra_word_str
\str_new:N \l_@@_geo_luaset_letra_conector_str
\str_new:N \l_@@_geo_luaset_sub_spoken_str
%    \end{macrocode}
% \end{variable}
%
% \subsection{The command \cs{spcoord}}\label{cmd:spcoord}
%
% The user command |\spcoord| typesets an ordered pair |(x,y)|. The comma or
% semicolon between the coordinates carries its own |:infix| intent, and each
% coordinate that starts with |+| or |-| is wrapped with |:prefix($vp)| or
% |:prefix($vn)|, so that the sign is read as \emph{\enquote{más}} or
% \emph{\enquote{menos}}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spcoord}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcoord_luaset_error_str,
%     \l_@@_spcoord_luaset_sep_str,
%   }
% These variables are set by the function
% \myhlc|\@@_luafun_spcoord_parse_and_set:n|. The variable
% \myhlc|\l_@@_spcoord_luaset_error_str| stores |true| or |false| if the
% \mymarg{argument} is invalid. The variable
% \myhlc|\l_@@_spcoord_luaset_sep_str| stores |,| or |;|, whichever the
% argument actually used.
%    \begin{macrocode}
\str_new:N \l_@@_spcoord_luaset_error_str
\str_new:N \l_@@_spcoord_luaset_sep_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcoord_luaset_x_sign_str,
%     \l_@@_spcoord_luaset_x_type_str,
%     \l_@@_spcoord_luaset_x_value_str,
%     \l_@@_spcoord_luaset_x_has_frac_str,
%     \l_@@_spcoord_luaset_x_composite_str,
%     \l_@@_spcoord_luaset_y_sign_str,
%     \l_@@_spcoord_luaset_y_type_str,
%     \l_@@_spcoord_luaset_y_value_str,
%     \l_@@_spcoord_luaset_y_has_frac_str,
%     \l_@@_spcoord_luaset_y_composite_str,
%   }
% The variables for the abscissa |x| and the ordinate |y| have the same
% meaning and are set by the same function: \myhlc|_sign_str| is |+|, |-|, or
% empty; \myhlc|_type_str| is |number| or |raw|; \myhlc|_value_str| is the
% value with the sign already removed; \myhlc|_has_frac_str| is |none|,
% |frac|, or |dfrac|; and \myhlc|_composite_str| is |true| when the value is a
% sole subscripted, superscripted or radical node, |false| otherwise.
%    \begin{macrocode}
\clist_map_inline:nn { x , y }
  {
    \str_new:c { l_@@_spcoord_luaset_ #1 _sign_str      }
    \str_new:c { l_@@_spcoord_luaset_ #1 _type_str      }
    \str_new:c { l_@@_spcoord_luaset_ #1 _value_str     }
    \str_new:c { l_@@_spcoord_luaset_ #1 _has_frac_str  }
    \str_new:c { l_@@_spcoord_luaset_ #1 _composite_str }
  }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcoord_raw_arg_tl,
%     \l_@@_spcoord_intent_str,
%     \l_@@_spcoord_wrap_parens_l_tl,
%     \l_@@_spcoord_wrap_parens_r_tl,
%   }
%   The variable \myhlc|\l_@@_spcoord_raw_arg_tl| holds the \mymarg{argument} exactly as
%   given. The variable \myhlc|\l_@@_spcoord_intent_str| holds the |:infix|
%   word for the separator \myhlc|_coma:infix| or \myhlc|_punto-y-coma:infix|. The variables
%   \myhlc|\l_@@_spcoord_wrap_parens_l_tl|/\myhlc|\l_@@_spcoord_wrap_parens_r_tl|
%   hold the opening/closing delimiter, sized by the function \myhlc|\@@_spcoord_set_wrap_parens_size:|.
%    \begin{macrocode}
\tl_new:N  \l_@@_spcoord_raw_arg_tl
\str_new:N \l_@@_spcoord_intent_str
\tl_new:N  \l_@@_spcoord_wrap_parens_l_tl
\tl_new:N  \l_@@_spcoord_wrap_parens_r_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spcoord}}
%
% \begin{macro}[int]{spcoord-invalid-arg}
%   The \enquote{error} |spcoord-invalid-arg| is raised when the
%   \mymarg{argument} does not have the form \emph{abscissa,ordinate} or
%   \emph{abscissa;ordinate}.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spcoord-invalid-arg }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \spcoord'~\msg_line_context:.
  }
  {
    The~argument~must~have~the~form~'abscissa,~ordinate'~or~'abscissa;~ordinate'.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spcoord}}
%
% \begin{macro}[int]{silent, unknown}
%   We define the \mymeta{key} \mykey[spcoord]{silent}.
%    \begin{macrocode}
\keys_define:nn { spintent / spcoord }
  {
    silent  .bool_set:N = \l_@@_spcoord_silent_bool,
    silent  .initial:n = false,
    silent  .value_required:n = true,
    unknown .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spcoord }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spcoord}}
%
% \begin{macro}[int]{\@@_spcoord_set_wrap_parens_size:}
%   With neither side carrying a fraction, the parentheses stay
%   unsized. A fraction counts wherever it appears in the value,
%   be it |\frac|, |\dfrac| or a |\spmQ|, which prints one, except inside
%   a subscript or superscript, where it does not make the line taller.
%   Otherwise, |\mathstyle| picks |\Bigl|/|\Bigr| for
%   display, |\bigl|/|\bigr| for text. When either side is
%   |dfrac|, the parentheses are forced to |\Bigl|/|\Bigr| regardless
%   of |\mathstyle|, since |\dfrac| always renders at display
%   height.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcoord_set_wrap_parens_size:
  {
    \tl_set:Nn \l_@@_spcoord_wrap_parens_l_tl { \lparen }
    \tl_set:Nn \l_@@_spcoord_wrap_parens_r_tl { \rparen }
    \bool_if:nT
      {
        \bool_lazy_or_p:nn
          {
            \bool_not_p:n
              { \str_if_eq_p:Vn \l_@@_spcoord_luaset_x_has_frac_str { none } }
          }
          {
            \bool_not_p:n
              { \str_if_eq_p:Vn \l_@@_spcoord_luaset_y_has_frac_str { none } }
          }
      }
      {
        \int_case:nnF { \mathstyle }
          {
            { 0 }
              {
                \tl_set:Nn \l_@@_spcoord_wrap_parens_l_tl { \Bigl \lparen }
                \tl_set:Nn \l_@@_spcoord_wrap_parens_r_tl { \Bigr \rparen }
              }
            { 1 }
              {
                \tl_set:Nn \l_@@_spcoord_wrap_parens_l_tl { \Bigl \lparen }
                \tl_set:Nn \l_@@_spcoord_wrap_parens_r_tl { \Bigr \rparen }
              }
            { 2 }
              {
                \tl_set:Nn \l_@@_spcoord_wrap_parens_l_tl { \bigl \lparen }
                \tl_set:Nn \l_@@_spcoord_wrap_parens_r_tl { \bigr \rparen }
              }
            { 3 }
              {
                \tl_set:Nn \l_@@_spcoord_wrap_parens_l_tl { \bigl \lparen }
                \tl_set:Nn \l_@@_spcoord_wrap_parens_r_tl { \bigr \rparen }
              }
          }
          { }
        \bool_if:nT
          {
            \bool_lazy_or_p:nn
              { \str_if_eq_p:Vn \l_@@_spcoord_luaset_x_has_frac_str { dfrac } }
              { \str_if_eq_p:Vn \l_@@_spcoord_luaset_y_has_frac_str { dfrac } }
          }
          {
            \tl_set:Nn \l_@@_spcoord_wrap_parens_l_tl { \Bigl \lparen }
            \tl_set:Nn \l_@@_spcoord_wrap_parens_r_tl { \Bigr \rparen }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcoord_value:n}
%   With the variable \myhlc|_type_str| of the endpoint in `|#1|' equal to
%   |number|, it calls |\spnum| on its variable \myhlc|_value_str|. Otherwise,
%   it typesets it as given, a |\frac|, a |\dfrac| or a letter expression.
%   Receives |x| or |y| in `|#1|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcoord_value:n
  {
    \str_if_eq:eeTF
      { \str_use:c { l_@@_spcoord_luaset_ #1 _type_str } } { number }
      {
        \exp_args:Ne \spnum { \str_use:c { l_@@_spcoord_luaset_ #1 _value_str } }
      }
      { \str_use:c { l_@@_spcoord_luaset_ #1 _value_str } }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcoord_print:n}
%   With the variable \myhlc|_sign_str| of the endpoint in `|#1|' equal to |+|
%   or |-|, it wraps the sign and \myhlc|\@@_spcoord_value:n| with
%   \myhlc|_más:prefix($vp)| or \myhlc|_menos:prefix($vn)|, referencing the value with
%   \MathMLarg. With no sign, it calls \myhlc|\@@_spcoord_value:n| directly.
%   Receives |x| or |y| in `|#1|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcoord_print:n
  {
    \str_case:enF { \str_use:c { l_@@_spcoord_luaset_ #1 _sign_str } }
      {
        { + }
          {
            \MathMLintent { _más:prefix($vp) }
              {
                \str_use:c { l_@@_spcoord_luaset_ #1 _sign_str }
                \MathMLarg{ vp } { \@@_spcoord_value:n {#1} }
              }
          }
        { - }
          {
            \MathMLintent { _menos:prefix($vn) }
              {
                \str_use:c { l_@@_spcoord_luaset_ #1 _sign_str }
                \MathMLarg{ vn } { \@@_spcoord_value:n {#1} }
              }
          }
      }
      { \@@_spcoord_value:n {#1} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcoord_inner_sep:n}
%   Receives |x| or |y| in `|#1|'. With the variable \myhlc|_composite_str| of that
%   endpoint equal to |true|, places
%   \myhlc|\@@_inner_sep:| first, so that the group |\MathMLarg| wraps holds
%   two children and keeps its |arg|: luamml drops it from a sole node that
%   carries a subscript, a superscript or a radical.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcoord_inner_sep:n
  {
    \str_if_eq:eeT
      { \str_use:c { l_@@_spcoord_luaset_ #1 _composite_str } } { true }
      { \@@_inner_sep: }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcoord_intent_infix:}
%   Sets \myhlc|\l_@@_spcoord_intent_str| to \myhlc|_coma:infix| or
%   \myhlc|_punto-y-coma:infix|, according to \myhlc|\l_@@_spcoord_luaset_sep_str|,
%   and wraps both sides, each referenced with \MathMLarg, with it. The names
%   |$x| and |$y| stand for the output of \myhlc|\@@_spcoord_print:n|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcoord_intent_infix:
  {
    \str_if_eq:VnTF \l_@@_spcoord_luaset_sep_str { ; }
      { \str_set:Nn \l_@@_spcoord_intent_str { _punto-y-coma:infix } }
      { \str_set:Nn \l_@@_spcoord_intent_str { _coma:infix } }
    \MathMLintent { \l_@@_spcoord_intent_str ($x, $y) }
      {
        \MathMLarg{x}
          { \@@_spcoord_inner_sep:n { x } \@@_spcoord_print:n { x } }
        \MathMLintent { :silent } { \l_@@_spcoord_luaset_sep_str }
        \MathMLarg{y}
          { \@@_spcoord_inner_sep:n { y } \@@_spcoord_print:n { y } }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcoord_mathml:N}
%   The argument is the boolean variable that holds the value of \mykey{silent}
%   of the command that calls it. With \mykey{silent}|=true|, the parentheses
%   and the argument are wrapped in a single `\myhlc|_-|' intent, and
%   \myhlc|\l_@@_spcoord_raw_arg_tl| is typeset as given. Otherwise, the
%   parentheses are silenced individually and
%   \myhlc|\@@_spcoord_intent_infix:| provides the reading between them.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcoord_mathml:N
  {
    \bool_if:NTF #1
      {
        \MathMLintent { _- }
          {
            \l_@@_spcoord_wrap_parens_l_tl
            \l_@@_spcoord_raw_arg_tl
            \l_@@_spcoord_wrap_parens_r_tl
          }
      }
      {
        \MathMLintent { :silent } { \l_@@_spcoord_wrap_parens_l_tl }
          \@@_spcoord_intent_infix:
        \MathMLintent { :silent } { \l_@@_spcoord_wrap_parens_r_tl }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcoord_exec:nn}
%   Sets the \mymeta{keys} in `|#1|' and \myhlc|\l_@@_spcoord_raw_arg_tl| to
%   `|#2|'. Calls \myhlc|\@@_luafun_spcoord_parse_and_set:n| and reports
%   |spcoord-invalid-arg| on error, whatever the value of \mykey{silent}. Then
%   it sizes the parentheses and, if there was no error, calls
%   \myhlc|\@@_spcoord_mathml:N| preceded by an invisible separator placed
%   \emph{before} the opening parenthesis. The whole pair is wrapped in
%   |\mathord|, which prevents \mynvmath{MathCAT} from reading |(1,2)| as an
%   interval when an equals sign or |\in| follows it.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcoord_exec:nn
  {
    \keys_set:nn { spintent / spcoord } {#1}
    \tl_set:Nn \l_@@_spcoord_raw_arg_tl {#2}
    \@@_luafun_spcoord_parse_and_set:n { \exp_not:n {#2} }
    \str_if_eq:VnT \l_@@_spcoord_luaset_error_str { true }
      { \msg_error:nne { spintent } { spcoord-invalid-arg } { \exp_not:n {#2} } }
    \@@_spcoord_set_wrap_parens_size:
    \str_if_eq:VnF \l_@@_spcoord_luaset_error_str { true }
      {
        \mathord
          {
            % prevent "intervalo"
            \MathMLintent { :silent } { \c_@@_invisible_sep_int }
            \@@_spcoord_mathml:N \l_@@_spcoord_silent_bool
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spcoord}
%   The public command \ics*{spcoord}
%   enforces \emph{math mode}, opens a local group,
%   and delegates to \myhlc|\@@_spcoord_exec:nn|.
%    \begin{macrocode}
\NewDocumentCommand \spcoord { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_spcoord_exec:nn {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spcoord } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spinterval}}\label{cmd:spinterval}
%
% The user command |\spinterval| receives the \mymarg{argument} written as a
% left delimiter |(|, |[| or |]|, two endpoints separated by a semicolon or by
% a single comma, and a right delimiter |)|, |]| or |[|. A delimiter |(| or |]|
% on the left, and |)| or |[| on the right, excludes its endpoint; the others
% include it. The type of interval, the endpoints and the separator are
% detected by the Lua module.
%
% \subsubsection{Definition of variables for \cs[no-index]{spinterval}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spinterval_luaset_error_str,
%     \l_@@_spinterval_luaset_error_code_str,
%   }
% These variables are set by the function
% \myhlc|\@@_luafun_spinterval_parse_and_set:n|. The variable
% \myhlc|\l_@@_spinterval_luaset_error_str| stores |true| or |false| if the
% \mymarg{argument} is invalid. The variable
% \myhlc|\l_@@_spinterval_luaset_error_code_str| stores the kind of error:
% |delim|, |vacio|, |llaves|, |sep|, |ambiguo|, |inf-orden| or |inf-cerrado|,
% and is empty if there is none.
%    \begin{macrocode}
\str_new:N \l_@@_spinterval_luaset_error_str
\str_new:N \l_@@_spinterval_luaset_error_code_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spinterval_luaset_left_sym_str,
%     \l_@@_spinterval_luaset_right_sym_str,
%     \l_@@_spinterval_luaset_type_str,
%     \l_@@_spinterval_luaset_sep_str,
%     \l_@@_spinterval_luaset_tall_str,
%   }
% The variables \myhlc|\l_@@_spinterval_luaset_left_sym_str| and
% \myhlc|\l_@@_spinterval_luaset_right_sym_str| store the delimiters of the
% interval as they were written. The variable
% \myhlc|\l_@@_spinterval_luaset_type_str| stores |open|, |closed|,
% |open-closed| or |closed-open|. The variable
% \myhlc|\l_@@_spinterval_luaset_sep_str| stores the separator, |,| or |;|.
% The variable \myhlc|\l_@@_spinterval_luaset_tall_str| stores |dfrac|,
% |frac| or |none|, according to the tallest fraction in the endpoints.
%    \begin{macrocode}
\str_new:N \l_@@_spinterval_luaset_left_sym_str
\str_new:N \l_@@_spinterval_luaset_right_sym_str
\str_new:N \l_@@_spinterval_luaset_type_str
\str_new:N \l_@@_spinterval_luaset_sep_str
\str_new:N \l_@@_spinterval_luaset_tall_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spinterval_luaset_a_sign_str,
%     \l_@@_spinterval_luaset_a_type_str,
%     \l_@@_spinterval_luaset_a_value_str,
%     \l_@@_spinterval_luaset_a_inf_str,
%     \l_@@_spinterval_luaset_a_tall_str,
%     \l_@@_spinterval_luaset_a_composite_str,
%     \l_@@_spinterval_luaset_a_integer_str,
%     \l_@@_spinterval_luaset_a_short_str,
%     \l_@@_spinterval_luaset_a_braced_str,
%     \l_@@_spinterval_luaset_b_sign_str,
%     \l_@@_spinterval_luaset_b_type_str,
%     \l_@@_spinterval_luaset_b_value_str,
%     \l_@@_spinterval_luaset_b_inf_str,
%     \l_@@_spinterval_luaset_b_tall_str,
%     \l_@@_spinterval_luaset_b_composite_str,
%     \l_@@_spinterval_luaset_b_integer_str,
%     \l_@@_spinterval_luaset_b_short_str,
%     \l_@@_spinterval_luaset_b_braced_str,
%   }
%   The same variables for each endpoint, |a| and |b|, set by the same
%   function. The variable \myhlc|_sign_str| is |+|, |-|, or empty;
%   \myhlc|_type_str| is |number|, |raw| or |infinity|; \myhlc|_value_str| is
%   the value with the sign already removed; \myhlc|_inf_str| is |pos|, |neg|
%   or |none|; \myhlc|_tall_str| is |dfrac|, |frac| or |none|; and
%   \myhlc|_composite_str| is |true| when the value is a sole subscripted,
%   superscripted or radical node. The variables \myhlc|_integer_str| and
%   \myhlc|_short_str| are |true| when the value is an integer and when it is
%   an integer of at most three digits, respectively; \myhlc|_braced_str|
%   holds the value with each comma protected by braces.
%    \begin{macrocode}
\clist_map_inline:nn { a , b }
  {
    \str_new:c { l_@@_spinterval_luaset_ #1 _sign_str      }
    \str_new:c { l_@@_spinterval_luaset_ #1 _type_str      }
    \str_new:c { l_@@_spinterval_luaset_ #1 _value_str     }
    \str_new:c { l_@@_spinterval_luaset_ #1 _inf_str       }
    \str_new:c { l_@@_spinterval_luaset_ #1 _tall_str      }
    \str_new:c { l_@@_spinterval_luaset_ #1 _composite_str }
    \str_new:c { l_@@_spinterval_luaset_ #1 _integer_str   }
    \str_new:c { l_@@_spinterval_luaset_ #1 _short_str     }
    \str_new:c { l_@@_spinterval_luaset_ #1 _braced_str    }
  }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spinterval_prefix_tl,
%     \l_@@_spinterval_read_arg_tl,
%     \l_@@_spinterval_mode_str,
%     \l_@@_spinterval_size_str,
%   }
% The variables \myhlc|\l_@@_spinterval_mode_str|,
% \myhlc|\l_@@_spinterval_prefix_tl|, \myhlc|\l_@@_spinterval_read_arg_tl| and
% \myhlc|\l_@@_spinterval_size_str| store the values set by the keys
% \mykey[spinterval]{read-cmd}, \mykey[spinterval]{prefix},
% \mykey[spinterval]{read-arg} and \mykey[spinterval]{size}.
%    \begin{macrocode}
\tl_new:N  \l_@@_spinterval_prefix_tl
\tl_new:N  \l_@@_spinterval_read_arg_tl
\str_new:N \l_@@_spinterval_mode_str
\str_new:N \l_@@_spinterval_size_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_spinterval_reason_str}
% The variable \myhlc|\l_@@_spinterval_reason_str| stores the text of the
% error reason, set from the error code returned by the Lua module.
%    \begin{macrocode}
\str_new:N \l_@@_spinterval_reason_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spinterval_word_w_tl,
%     \l_@@_spinterval_word_g_tl,
%     \l_@@_spinterval_intent_arg_tl,
%     \l_@@_spinterval_intent_group_tl,
%   }
% The variables \myhlc|\l_@@_spinterval_word_w_tl| and
% \myhlc|\l_@@_spinterval_word_g_tl| store the word that names the interval
% and the word that joins its endpoints. The variables
% \myhlc|\l_@@_spinterval_intent_arg_tl| and
% \myhlc|\l_@@_spinterval_intent_group_tl| store the intents of the whole
% interval and of the group of endpoints.
%    \begin{macrocode}
\tl_new:N \l_@@_spinterval_word_w_tl
\tl_new:N \l_@@_spinterval_word_g_tl
\tl_new:N \l_@@_spinterval_intent_arg_tl
\tl_new:N \l_@@_spinterval_intent_group_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spinterval_sym_l_tl,
%     \l_@@_spinterval_sym_r_tl,
%     \l_@@_spinterval_size_l_tl,
%     \l_@@_spinterval_size_r_tl,
%     \l_@@_spinterval_delim_l_tl,
%     \l_@@_spinterval_delim_r_tl,
%   }
% The variables \myhlc|\l_@@_spinterval_sym_l_tl| and
% \myhlc|\l_@@_spinterval_sym_r_tl| store the delimiter symbols, the variables
% \myhlc|\l_@@_spinterval_size_l_tl| and \myhlc|\l_@@_spinterval_size_r_tl|
% store their size commands, and the variables
% \myhlc|\l_@@_spinterval_delim_l_tl| and
% \myhlc|\l_@@_spinterval_delim_r_tl| store the delimiters ready to be
% typeset.
%    \begin{macrocode}
\tl_new:N \l_@@_spinterval_sym_l_tl
\tl_new:N \l_@@_spinterval_sym_r_tl
\tl_new:N \l_@@_spinterval_size_l_tl
\tl_new:N \l_@@_spinterval_size_r_tl
\tl_new:N \l_@@_spinterval_delim_l_tl
\tl_new:N \l_@@_spinterval_delim_r_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_spinterval_tmp_tl}
% The variable \myhlc|\l_@@_spinterval_tmp_tl| is a scratch variable.
%    \begin{macrocode}
\tl_new:N \l_@@_spinterval_tmp_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spinterval}}
%
% \begin{macro}[int]{spinterval-invalid-arg}
%   The \enquote{error} |spinterval-invalid-arg| is raised when the Lua module
%   rejects the \mymarg{argument}; it reports the argument and the reason set by
%   \myhlc|\@@_spinterval_set_reason:|.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spinterval-invalid-arg }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \spinterval'~(#2)~
    \msg_line_context:.
  }
  {
    Use~'(a;~b)'~or~'(a,~b)',~with~'(',~'['~or~']'~on~the~left~and~')',~']'~or~'['~
    on~the~right.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spinterval}}
%
% \begin{macro}[int]{read-cmd, prefix, read-arg, size, silent, concept, unknown}
%   We define the \mymeta{keys} \mykey[spinterval]{read-cmd}, \mykey[spinterval]{prefix},
%   \mykey[spinterval]{read-arg}, \mykey[spinterval]{size}, \mykey[spinterval]{silent}
%   and \mykey[spinterval]{concept}.
%    \begin{macrocode}
\keys_define:nn { spintent / spinterval }
  {
    read-cmd .choices:nn = { school , mathcat }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spinterval_mode_str { school  } }
            {2} { \str_set:Nn \l_@@_spinterval_mode_str { mathcat } }
          }
      },
    read-cmd .initial:n = school,
    read-cmd .value_required:n = true,
    read-cmd / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-cmd } { school,~mathcat } { \exp_not:n {#1} },
    prefix   .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spinterval_prefix_tl {#1} },
    prefix   .initial:n = ,
    prefix   .value_required:n = true,
    read-arg .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spinterval_read_arg_tl {#1} },
    read-arg .initial:n = ,
    read-arg .value_required:n = true,
    size     .choices:nn = { auto , none , big , Big , bigg , Bigg }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spinterval_size_str { auto } }
            {2} { \str_set:Nn \l_@@_spinterval_size_str { none } }
            {3} { \str_set:Nn \l_@@_spinterval_size_str { big  } }
            {4} { \str_set:Nn \l_@@_spinterval_size_str { Big  } }
            {5} { \str_set:Nn \l_@@_spinterval_size_str { bigg } }
            {6} { \str_set:Nn \l_@@_spinterval_size_str { Bigg } }
          }
      },
    size     .initial:n = auto,
    size     .value_required:n = true,
    size / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { size } { auto,~none,~big,~Big,~bigg,~Bigg } { \exp_not:n {#1} },
    silent   .bool_set:N = \l_@@_spinterval_silent_bool,
    silent   .initial:n = false,
    silent   .value_required:n = true,
    concept  .bool_set:N = \l_@@_spinterval_concept_bool,
    concept  .initial:n = true,
    concept  .value_required:n = true,
    unknown  .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spinterval }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spinterval}}
%
% \begin{macro}[int]{\@@_spinterval_set_reason:}
%   The internal function \myhlc|\@@_spinterval_set_reason:| stores in
%   \myhlc|\l_@@_spinterval_reason_str| the text that describes the error code
%   returned by the Lua module, or |invalid argument| when the code is not known.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spinterval_set_reason:
  {
    \str_case:VnF \l_@@_spinterval_luaset_error_code_str
      {
        { delim }
          {
            \str_set:Nn \l_@@_spinterval_reason_str
              { it~must~start~with~'(',~'['~or~']'~and~end~with~')',~']'~or~'[' }
          }
        { vacio }
          { \str_set:Nn \l_@@_spinterval_reason_str { an~endpoint~is~empty } }
        { sep }
          {
            \str_set:Nn \l_@@_spinterval_reason_str
              { there~is~no~separator:~use~a~comma~or~a~semicolon }
          }
        { ambiguo }
          {
            \str_set:Nn \l_@@_spinterval_reason_str
              {
                more~than~one~separator:~use~a~single~semicolon~and~the~commas~are~
                  decimal
              }
          }
        { llaves }
          { \str_set:Nn \l_@@_spinterval_reason_str { unbalanced~braces } }
        { inf-orden }
          {
            \str_set:Nn \l_@@_spinterval_reason_str
              { minus~infinity~goes~on~the~left~and~infinity~on~the~right }
          }
        { inf-cerrado }
          {
            \str_set:Nn \l_@@_spinterval_reason_str
              {
                an~infinite~endpoint~cannot~be~included:~use~'('~or~']'~on~the~left,~
                ')'~or~'['~on~the~right
              }
          }
      }
      { \str_set:Nn \l_@@_spinterval_reason_str { invalid~argument } }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spinterval_set_delims:}
%   The internal function \myhlc|\@@_spinterval_set_delims:| sets the symbols of the
%   left and right delimiters from the characters returned by the Lua module, and
%   their size commands from the key \mykey[spinterval]{size}. With |big|, |Big|,
%   |bigg| and |Bigg| the size is fixed, and with |none| there is no size command.
%   With |auto| there is none unless an endpoint contains a fraction, in which case it
%   is |\Bigl|/|\Bigr| in display style and |\bigl|/|\bigr| in text style, and always
%   |\Bigl|/|\Bigr| when the fraction is a |\dfrac|. Without a size command each
%   delimiter is wrapped in |\mathopen| or |\mathclose|; with one, the delimiter
%   follows the size command.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spinterval_set_delims:
  {
    \str_case:Vn \l_@@_spinterval_luaset_left_sym_str
      {
        { ( } { \tl_set:Nn \l_@@_spinterval_sym_l_tl { \lparen } }
        { [ } { \tl_set:Nn \l_@@_spinterval_sym_l_tl { \lbrack } }
        { ] } { \tl_set:Nn \l_@@_spinterval_sym_l_tl { \rbrack } }
      }
    \str_case:Vn \l_@@_spinterval_luaset_right_sym_str
      {
        { ) } { \tl_set:Nn \l_@@_spinterval_sym_r_tl { \rparen } }
        { [ } { \tl_set:Nn \l_@@_spinterval_sym_r_tl { \lbrack } }
        { ] } { \tl_set:Nn \l_@@_spinterval_sym_r_tl { \rbrack } }
      }
    \tl_clear:N \l_@@_spinterval_size_l_tl
    \tl_clear:N \l_@@_spinterval_size_r_tl
    \str_case:Vn \l_@@_spinterval_size_str
      {
        { big }
          {
            \tl_set:Nn \l_@@_spinterval_size_l_tl { \bigl }
            \tl_set:Nn \l_@@_spinterval_size_r_tl { \bigr }
          }
        { Big }
          {
            \tl_set:Nn \l_@@_spinterval_size_l_tl { \Bigl }
            \tl_set:Nn \l_@@_spinterval_size_r_tl { \Bigr }
          }
        { bigg }
          {
            \tl_set:Nn \l_@@_spinterval_size_l_tl { \biggl }
            \tl_set:Nn \l_@@_spinterval_size_r_tl { \biggr }
          }
        { Bigg }
          {
            \tl_set:Nn \l_@@_spinterval_size_l_tl { \Biggl }
            \tl_set:Nn \l_@@_spinterval_size_r_tl { \Biggr }
          }
        { auto }
          {
            \str_if_eq:VnF \l_@@_spinterval_luaset_tall_str { none }
              {
                \int_case:nnF { \mathstyle }
                  {
                    { 0 }
                      {
                        \tl_set:Nn \l_@@_spinterval_size_l_tl { \Bigl }
                        \tl_set:Nn \l_@@_spinterval_size_r_tl { \Bigr }
                      }
                    { 1 }
                      {
                        \tl_set:Nn \l_@@_spinterval_size_l_tl { \Bigl }
                        \tl_set:Nn \l_@@_spinterval_size_r_tl { \Bigr }
                      }
                    { 2 }
                      {
                        \tl_set:Nn \l_@@_spinterval_size_l_tl { \bigl }
                        \tl_set:Nn \l_@@_spinterval_size_r_tl { \bigr }
                      }
                    { 3 }
                      {
                        \tl_set:Nn \l_@@_spinterval_size_l_tl { \bigl }
                        \tl_set:Nn \l_@@_spinterval_size_r_tl { \bigr }
                      }
                  }
                  { }
                \str_if_eq:VnT \l_@@_spinterval_luaset_tall_str { dfrac }
                  {
                    \tl_set:Nn \l_@@_spinterval_size_l_tl { \Bigl }
                    \tl_set:Nn \l_@@_spinterval_size_r_tl { \Bigr }
                  }
              }
          }
      }
    \tl_if_empty:NTF \l_@@_spinterval_size_l_tl
      {
        \tl_set:Ne \l_@@_spinterval_delim_l_tl
          { \exp_not:N \mathopen { \exp_not:V \l_@@_spinterval_sym_l_tl } }
        \tl_set:Ne \l_@@_spinterval_delim_r_tl
          { \exp_not:N \mathclose { \exp_not:V \l_@@_spinterval_sym_r_tl } }
      }
      {
        \tl_set:Ne \l_@@_spinterval_delim_l_tl
          {
            \exp_not:V \l_@@_spinterval_size_l_tl \exp_not:V
              \l_@@_spinterval_sym_l_tl
          }
        \tl_set:Ne \l_@@_spinterval_delim_r_tl
          {
            \exp_not:V \l_@@_spinterval_size_r_tl \exp_not:V
              \l_@@_spinterval_sym_r_tl
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spinterval_set_intents:}
%   The internal function \myhlc|\@@_spinterval_set_intents:| sets the two words used
%   in the intents from the type returned by the Lua module: |abierto-de| and |a| for
%   an open interval, |cerrado-de| and |a| for a closed one, |abierto-en| and
%   |y-cerrado-en| for an open-closed one, and |cerrado-en| and |y-abierto-en| for a
%   closed-open one. The intent of the group of endpoints is \myhlc|_<word>:infix($a,$b)|
%   with the second word. The intent of the whole interval is `\myhlc|_-|' when
%   \mykey[spinterval]{silent} is |true|, \myhlc|_| followed by the value of
%   \mykey[spinterval]{read-arg} when it is not empty, and otherwise \myhlc|_<word>:prefix($ab)|
%   with the first word, preceded by the \mykey[spinterval]{prefix}, if any, and
%   |intervalo-| when \mykey[spinterval]{concept} is |true|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spinterval_set_intents:
  {
    \str_case:Vn \l_@@_spinterval_luaset_type_str
      {
        { open }
          {
            \tl_set:Nn \l_@@_spinterval_word_w_tl { abierto-de }
            \tl_set:Nn \l_@@_spinterval_word_g_tl { a }
          }
        { closed }
          {
            \tl_set:Nn \l_@@_spinterval_word_w_tl { cerrado-de }
            \tl_set:Nn \l_@@_spinterval_word_g_tl { a }
          }
        { open-closed }
          {
            \tl_set:Nn \l_@@_spinterval_word_w_tl { abierto-en }
            \tl_set:Nn \l_@@_spinterval_word_g_tl { y-cerrado-en }
          }
        { closed-open }
          {
            \tl_set:Nn \l_@@_spinterval_word_w_tl { cerrado-en }
            \tl_set:Nn \l_@@_spinterval_word_g_tl { y-abierto-en }
          }
      }
    \tl_set:Ne \l_@@_spinterval_intent_group_tl
      { _ \l_@@_spinterval_word_g_tl :infix($a,$b) }
    \bool_if:NTF \l_@@_spinterval_silent_bool
      { \tl_set:Nn \l_@@_spinterval_intent_arg_tl { _- } }
      {
        \tl_if_empty:NTF \l_@@_spinterval_read_arg_tl
          {
            \tl_set:Ne \l_@@_spinterval_intent_arg_tl
              {
                _
                \bool_if:NT \l_@@_spinterval_concept_bool
                  {
                    \tl_if_empty:NF \l_@@_spinterval_prefix_tl
                      { \tl_use:N \l_@@_spinterval_prefix_tl - }
                    intervalo-
                  }
                \l_@@_spinterval_word_w_tl :prefix($ab)
              }
          }
          {
            \tl_set:Ne \l_@@_spinterval_intent_arg_tl
              { _ \l_@@_spinterval_read_arg_tl }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spinterval_inner_sep:n}
%   The internal function \myhlc|\@@_spinterval_inner_sep:n| calls
%   \myhlc|\@@_inner_sep:| when the endpoint |a| or |b| passed in `|#1|' is composite.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spinterval_inner_sep:n
  {
    \str_if_eq:eeT
      { \str_use:c { l_@@_spinterval_luaset_ #1 _composite_str } } { true }
      { \@@_inner_sep: }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spinterval_plain_number:n}
%   The internal function \myhlc|\@@_spinterval_plain_number:n| typesets the value of
%   the endpoint passed in `|#1|' as a number, with each comma protected by braces.
%   Unless the value is an integer it is preceded by \myhlc|\@@_inner_sep:|. An
%   integer is preceded by a permanent silent ghost instead, which survives in mathml-AF
%   and keeps MathCAT from merging |1,5| into a single decimal number.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spinterval_plain_number:n
  {
    \str_if_eq:eeTF { \str_use:c { l_@@_spinterval_luaset_ #1 _integer_str } }
      { true }
      % keep "a,b" apart in mathml-AF
      { \MathMLintent { :silent } { \c_@@_invisible_sep_int } }
      { \@@_inner_sep: }
    \str_use:c { l_@@_spinterval_luaset_ #1 _braced_str }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spinterval_value:n}
%   The internal function \myhlc|\@@_spinterval_value:n| typesets the value of the
%   endpoint passed in `|#1|' according to its type: |\infty| for infinity; for a number,
%   \myhlc|\@@_inner_sep:| followed by |\spnum| when \mykey[spinterval]{read-cmd} is
%   |school| and the value is not an integer of at most three digits, or
%   \myhlc|\@@_spinterval_plain_number:n| otherwise, since |\spnum| reads such an
%   integer as MathCAT does; and the value as written for any other type.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spinterval_value:n
  {
    \str_case:enF { \str_use:c { l_@@_spinterval_luaset_ #1 _type_str } }
      {
        { infinity } { \infty }
        { number }
          {
            \str_if_eq:VnTF \l_@@_spinterval_mode_str { school }
              {
                \tl_set:Ne \l_@@_spinterval_tmp_tl
                  { \str_use:c { l_@@_spinterval_luaset_ #1 _value_str } }
                \str_if_eq:eeTF
                  { \str_use:c { l_@@_spinterval_luaset_ #1 _short_str } }
                  { true }
                  { \@@_spinterval_plain_number:n {#1} }
                  {
                    \@@_inner_sep:
                    \exp_args:NV \spnum \l_@@_spinterval_tmp_tl
                  }
              }
              { \@@_spinterval_plain_number:n {#1} }
          }
      }
      { \str_use:c { l_@@_spinterval_luaset_ #1 _value_str } }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spinterval_print:n}
%   The internal function \myhlc|\@@_spinterval_print:n| typesets the endpoint passed
%   in `|#1|' with its sign. With \mykey{read-cmd}|=school| a sign |+| or |-| is wrapped, with
%   its value, in a native |\MathMLintent| with the intent \myhlc|_más:prefix($vp)| or
%   \myhlc|_menos:prefix($vn)|, the value tagged with |\MathMLarg| and preceded by
%   \myhlc|\@@_spinterval_inner_sep:n|; an endpoint without sign is typeset through
%   \myhlc|\@@_spinterval_value:n|. With \mykey{read-cmd}|=mathcat| the sign is typeset as is,
%   followed by the value.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spinterval_print:n
  {
    \str_if_eq:VnTF \l_@@_spinterval_mode_str { school }
      {
        \str_case:enF { \str_use:c { l_@@_spinterval_luaset_ #1 _sign_str } }
          {
            { + }
              {
                \MathMLintent { _más:prefix($vp) }
                  {
                    +
                    \MathMLarg { vp }
                      {
                        \@@_spinterval_inner_sep:n {#1}
                          \@@_spinterval_value:n {#1}
                      }
                  }
              }
            { - }
              {
                \MathMLintent { _menos:prefix($vn) }
                  {
                    -
                    \MathMLarg { vn }
                      {
                        \@@_spinterval_inner_sep:n {#1}
                          \@@_spinterval_value:n {#1}
                      }
                  }
              }
          }
          { \@@_spinterval_value:n {#1} }
      }
      {
        \str_use:c { l_@@_spinterval_luaset_ #1 _sign_str }
        \@@_spinterval_value:n {#1}
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spinterval_mathml:}
%   The internal function \myhlc|\@@_spinterval_mathml:| builds the annotated interval
%   with a native |\MathMLintent| that carries the intent of the whole interval. It
%   contains the left delimiter silenced, a |\MathMLarg| named |ab| that wraps
%   \myhlc|\@@_inner_sep:| and a |\MathMLintent| with the intent of the group of
%   endpoints, and the right delimiter silenced. The group holds the endpoints |a| and
%   |b|, each tagged with |\MathMLarg| and typeset by \myhlc|\@@_spinterval_print:n|,
%   separated by the silenced separator.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spinterval_mathml:
  {
    \MathMLintent { \l_@@_spinterval_intent_arg_tl }
      {
        \MathMLintent { :silent } { \l_@@_spinterval_delim_l_tl }
        \MathMLarg { ab }
          {
            \@@_inner_sep:
            \MathMLintent { \l_@@_spinterval_intent_group_tl }
              {
                \MathMLarg { a }
                  {
                    \@@_spinterval_inner_sep:n { a }
                      \@@_spinterval_print:n { a }
                  }
                \MathMLintent { :silent } { \l_@@_spinterval_luaset_sep_str }
                \MathMLarg { b }
                  {
                    \@@_spinterval_inner_sep:n { b }
                      \@@_spinterval_print:n { b }
                  }
              }
          }
        \MathMLintent { :silent } { \l_@@_spinterval_delim_r_tl }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spinterval_exec:nn}
%   The internal function \myhlc|\@@_spinterval_exec:nn| receives the optional
%   \mymeta{keys} passed in `|#1|' and the \mymarg{argument} passed in `|#2|'. It calls
%   \myhlc|\@@_luafun_spinterval_parse_and_set:n|; if the Lua module reports an error,
%   it reports |spinterval-invalid-arg| whatever the value of
%   \mykey[spinterval]{silent}: an invalid \mymarg{argument} is always
%   rejected. When there is no error, it calls \myhlc|\@@_spinterval_set_delims:| and
%   \myhlc|\@@_spinterval_set_intents:|, places \myhlc|\@@_invisible_sep:| and
%   delegates the rendering to \myhlc|\@@_spinterval_mathml:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spinterval_exec:nn
  {
    \keys_set:nn { spintent / spinterval } {#1}
    \@@_luafun_spinterval_parse_and_set:n { \exp_not:n {#2} }
    \str_if_eq:VnTF \l_@@_spinterval_luaset_error_str { true }
      {
        \@@_spinterval_set_reason:
        \msg_error:nnee { spintent } { spinterval-invalid-arg }
          { \exp_not:n {#2} } { \l_@@_spinterval_reason_str }
      }
      {
        \@@_spinterval_set_delims:
        \@@_spinterval_set_intents:
        \@@_invisible_sep:
        \@@_spinterval_mathml:
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spinterval}
%   The public command \ics*{spinterval}
%   enforces \emph{math mode}, opens a local group,
%   and delegates to \myhlc|\@@_spinterval_exec:nn|.
%    \begin{macrocode}
\NewDocumentCommand \spinterval { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_spinterval_exec:nn {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spinterval } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spfun}}\label{cmd:spfun}
%
% The user command |\spfun| typesets a function call such as |f(x)|,
% |f_{1}(x)|, |f''(x,y)|. The Lua function
% \myhlc|\@@_luafun_spfun_parse_and_set:n| splits the \mymarg{argument}
% into the function name, which is a letter with an optional subscript or primes
% left in the shared \myhlc|\l_@@_geo_luaset_*| variables, and the list of
% arguments, cut at the top-level commas, each one stored in
% \myhlc|\l_@@_spfun_luaset_arg_<i>_tl| together with a flag
% \myhlc|\l_@@_spfun_luaset_simple_<i>_str| telling whether it is a single
% node. The whole call is tagged with a native |\MathMLintent| whose
% |intent| is |$f($a1,$a2,...)|; the name is tagged |arg="f"| and each
% argument |arg="a<i>"|, while the parentheses and the commas are silenced.
% The name may carry the inverse exponent |^{-1}|, which sets the flag
% \myhlc|\l_@@_spfun_luaset_inv_str|; in that case the literal |intent| of the
% name ends in |-inversa|, the exponent is typeset silenced and the
% parentheses may be omitted, with no arguments and |$f| as the whole |intent|.
% An argument that is itself a call, |f(g(x))| or |f(g^{-1}(y))|, is analysed
% recursively by Lua, kind |call|, and typeset by
% \myhlc|\@@_spfun_nested:n|, which runs the same code in a group, one level
% deeper; the |arg| names carry the depth.
%
% \subsubsection{Definition of variables for \cs[no-index]{spfun}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spfun_luaset_error_str,
%     \l_@@_spfun_luaset_nargs_str,
%     \l_@@_spfun_luaset_inv_str,
%   }
% These variables are set by the Lua module \myluamod{}. The variable
% \myhlc|\l_@@_spfun_luaset_error_str| stores |true| or |false| if the
% \mymarg{argument} is invalid. The variable
% \myhlc|\l_@@_spfun_luaset_nargs_str| stores the number of arguments of the
% function and \myhlc|\l_@@_spfun_luaset_inv_str| stores |true| when the name
% carries the inverse exponent |^{-1}|.
%    \begin{macrocode}
\str_new:N \l_@@_spfun_luaset_error_str
\str_new:N \l_@@_spfun_luaset_nargs_str
\str_new:N \l_@@_spfun_luaset_inv_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spfun_mode_str,
%     \l_@@_spfun_prefix_tl,
%     \l_@@_spfun_silent_bool,
%     \l_@@_spfun_concept_bool,
%   }
% The variables \myhlc|\l_@@_spfun_mode_str|, \myhlc|\l_@@_spfun_prefix_tl|,
% \myhlc|\l_@@_spfun_silent_bool| and \myhlc|\l_@@_spfun_concept_bool| store
% the values set by the keys \mykey[spfun]{read-cmd}, \mykey[spfun]{prefix},
% \mykey[spfun]{silent} and \mykey[spfun]{concept}. The last two are created
% by the keys.
%    \begin{macrocode}
\str_new:N \l_@@_spfun_mode_str
\tl_new:N  \l_@@_spfun_prefix_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spfun_nargs_int,
%     \l_@@_spfun_depth_int,
%     \l_@@_spfun_tmp_str,
%   }
% The variable \myhlc|\l_@@_spfun_nargs_int| stores the number of arguments
% of the function, copied from \myhlc|\l_@@_spfun_luaset_nargs_str|. The
% variable \myhlc|\l_@@_spfun_depth_int| stores the nesting level of
% \myhlc|\@@_spfun_nested:n|. The variable \myhlc|\l_@@_spfun_tmp_str| is a
% scratch variable that holds the value Lua left for the argument being
% typeset.
%    \begin{macrocode}
\int_new:N \l_@@_spfun_nargs_int
\int_new:N \l_@@_spfun_depth_int
\str_new:N \l_@@_spfun_tmp_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spfun_refs_tl,
%     \l_@@_spfun_call_tl,
%     \l_@@_spfun_intent_arg_tl,
%     \l_@@_spfun_kv_tl,
%     \l_@@_spfun_fname_tl,
%     \l_@@_spfun_aname_tl,
%   }
% The variable \myhlc|\l_@@_spfun_refs_tl| stores the list of references to
% the arguments and \myhlc|\l_@@_spfun_call_tl| stores the |$f($a1,...)| text
% of the |intent|. The variable \myhlc|\l_@@_spfun_intent_arg_tl| stores the
% |intent| of the whole call. The variable \myhlc|\l_@@_spfun_kv_tl| stores
% the key-value list with the attributes |arg| and |intent| of the name. The
% variables \myhlc|\l_@@_spfun_fname_tl| and \myhlc|\l_@@_spfun_aname_tl|
% hold the names of the |arg| attributes built by
% \myhlc|\@@_spfun_fname:| and \myhlc|\@@_spfun_aname:n|.
%    \begin{macrocode}
\tl_new:N \l_@@_spfun_refs_tl
\tl_new:N \l_@@_spfun_call_tl
\tl_new:N \l_@@_spfun_intent_arg_tl
\tl_new:N \l_@@_spfun_kv_tl
\tl_new:N \l_@@_spfun_fname_tl
\tl_new:N \l_@@_spfun_aname_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spfun}}
%
% \begin{macro}[int]{spfun-invalid-arg}
%   The \enquote{error} |spfun-invalid-arg| is raised when the argument
%   fails to parse: no valid name, no parentheses unless it is the inverse
%   function, a malformed or misplaced inverse exponent, unbalanced
%   delimiters, an empty argument or text after the closing parenthesis. It
%   is raised whatever the value of \mykey{silent}.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spfun-invalid-arg }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \spfun'~\msg_line_context:.
  }
  {
    The~argument~must~have~the~form~'f(x)'~or~'f(x,y,...)',~where~'f'~is~a~letter~with~
    optional~primes~or~subscript.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spfun}}
%
% \begin{macro}[int]{read-cmd, prefix, concept, silent, unknown}
%   We define the \mymeta{keys} \mykey[spfun]{read-cmd}, \mykey[spfun]{prefix},
%   \mykey[spfun]{concept} and \mykey[spfun]{silent}.
%    \begin{macrocode}
\keys_define:nn { spintent / spfun }
  {
    read-cmd .choices:nn = { school , mathcat }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spfun_mode_str { school  } }
            {2} { \str_set:Nn \l_@@_spfun_mode_str { mathcat } }
          }
      },
    read-cmd .initial:n = school,
    read-cmd .value_required:n = true,
    read-cmd / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-cmd } { school,~mathcat } { \exp_not:n {#1} },
    prefix   .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spfun_prefix_tl {#1} },
    prefix   .initial:n = ,
    prefix   .value_required:n = true,
    silent   .bool_set:N = \l_@@_spfun_silent_bool,
    silent   .initial:n = false,
    silent   .value_required:n = true,
    concept  .bool_set:N = \l_@@_spfun_concept_bool,
    concept  .initial:n = false,
    concept  .value_required:n = true,
    unknown  .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spfun }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spfun}}
%
% \begin{macro}[int]{\@@_spfun_fname:, \@@_spfun_aname:n}
%   Names of the |arg| attributes at the current nesting depth
%   \myhlc|\l_@@_spfun_depth_int|. At depth~1 they are |f| and |a<i>|; at a
%   deeper level, |f<d>| and |a<d>x<i>|. They are stored in
%   \myhlc|\l_@@_spfun_fname_tl| and \myhlc|\l_@@_spfun_aname_tl|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfun_fname:
  {
    \tl_set:Ne \l_@@_spfun_fname_tl
      {
        f
        \int_compare:nNnF { \l_@@_spfun_depth_int } = { 1 }
          { \int_use:N \l_@@_spfun_depth_int }
      }
  }
\cs_new_protected:Nn \@@_spfun_aname:n
  {
    \tl_set:Ne \l_@@_spfun_aname_tl
      {
        a
        \int_compare:nNnF { \l_@@_spfun_depth_int } = { 1 }
          { \int_use:N \l_@@_spfun_depth_int x }
        #1
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfun_name_plain:}
%   Typesets the name without any annotation: the letter, followed by the
%   subscript or by the primes when there are any.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfun_name_plain:
  {
    \l_@@_geo_luaset_letra_str
    \str_case:VnF \l_@@_geo_luaset_tipo_str
      {
        { sub }   { \c_math_subscript_token { \l_@@_geo_luaset_sub_str } }
        { prima } { \l_@@_geo_luaset_sub_str }
      }
      { }
    \str_if_eq:VnT \l_@@_spfun_luaset_inv_str { true }
      { \c_math_superscript_token { -1 } }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfun_name_school:}
%   For \mykey{read-cmd}|=school| the name is tagged |arg="f"| with a literal
%   |intent| such as \myhlc|_f-sub-uno-| or \myhlc|_F-doble-prima-| through the shared
%   queue, so \mynvmath{MathCAT} neither announces a capital letter nor reads
%   the Unicode double prime as seconds. A subscript is typeset silenced; a
%   prime is not, since it is not spoken on the base. The inverse exponent
%   |^{-1}|, when present, is typeset silenced too: the literal |intent|
%   \myhlc|_f-inversa-| already says everything.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfun_name_school:
  {
    \@@_spfun_fname:
    \tl_set:Ne \l_@@_spfun_kv_tl
      {
        arg = " \l_@@_spfun_fname_tl ", intent =
          "_ \l_@@_geo_luaset_intent_str -"
      }
    \@@_luafun_mrow_intent_arg:nn { } { \l_@@_spfun_kv_tl }
    \str_case:VnF \l_@@_geo_luaset_tipo_str
      {
        { sub }
          {
            \l_@@_geo_luaset_letra_str
            \c_math_subscript_token
              { \MathMLintent { :silent } { \l_@@_geo_luaset_sub_str } }
          }
        { prima }
          { \l_@@_geo_luaset_letra_str \l_@@_geo_luaset_sub_str }
      }
      { \l_@@_geo_luaset_letra_str }
    \str_if_eq:VnT \l_@@_spfun_luaset_inv_str { true }
      {
        \c_math_superscript_token
          { \MathMLintent { :silent } { -1 } }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfun_name_mathcat:}
%   For \mykey{read-cmd}|=mathcat| the name is tagged |arg="f"| with a native
%   |\MathMLarg|, without using the shared queue. With a subscript or primes,
%   the inner separator keeps the |arg| on a wrapping |<mrow>|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfun_name_mathcat:
  {
    \@@_spfun_fname:
    \bool_lazy_and:nnTF
      { \str_if_eq_p:Vn \l_@@_geo_luaset_tipo_str { } }
      { \str_if_eq_p:Vn \l_@@_spfun_luaset_inv_str { false } }
      { \MathMLarg { \l_@@_spfun_fname_tl } { \@@_spfun_name_plain: } }
      {
        \MathMLarg { \l_@@_spfun_fname_tl }
          { \@@_inner_sep: \@@_spfun_name_plain: }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfun_arg:n}
%   Tags argument number |#1| with |arg="a<i>"|, named by \myhlc|\@@_spfun_aname:n|.
%   A single node, which is a letter, a number or a command such as |\alpha|, carries its
%   |arg| directly; any other argument starts with the inner separator, which
%   forces an |<mrow>| to hold the |arg|. An argument that is itself a call,
%   such as |g(x)| or |g^{-1}(y)|, is typeset by \myhlc|\@@_spfun_nested:n|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfun_arg:n
  {
    \@@_spfun_aname:n {#1}
    \str_set_eq:Nc \l_@@_spfun_tmp_str
      { l_@@_spfun_luaset_simple_ #1 _str }
    \str_case:VnF \l_@@_spfun_tmp_str
      {
        { true }
          {
            \MathMLarg { \l_@@_spfun_aname_tl }
              { \tl_use:c { l_@@_spfun_luaset_arg_ #1 _tl } }
          }
        { call }
          {
            \MathMLarg { \l_@@_spfun_aname_tl }
              { \@@_inner_sep: \@@_spfun_nested:n {#1} }
          }
      }
      {
        \MathMLarg { \l_@@_spfun_aname_tl }
          { \@@_inner_sep: \tl_use:c { l_@@_spfun_luaset_arg_ #1 _tl } }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfun_args:}
%   Walks the arguments, silencing the comma between them.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfun_args:
  {
    \int_step_inline:nn { \l_@@_spfun_nargs_int }
      {
        \int_compare:nNnF { ##1 } = { 1 }
          { \MathMLintent { :silent } { , } }
        \@@_spfun_arg:n {##1}
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfun_args_plain:}
%   Typesets the arguments with no annotation for \mykey{silent}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfun_args_plain:
  {
    \int_step_inline:nn { \l_@@_spfun_nargs_int }
      {
        \int_compare:nNnF { ##1 } = { 1 } { , }
        \tl_use:c { l_@@_spfun_luaset_arg_ ##1 _tl }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfun_set_intent:}
%   Builds the concept |intent| in \myhlc|\l_@@_spfun_intent_arg_tl|:
%   |$f($a1,$a2,...)| when \mykey{concept} is |false|, and
%   \myhlc|_prefix-función:prefix($f($a1,$a2,...))| otherwise. Without arguments,
%   as for the inverse function written bare, the call is just |$f|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfun_set_intent:
  {
    \tl_clear:N \l_@@_spfun_refs_tl
    \int_step_inline:nn { \l_@@_spfun_nargs_int }
      {
        \@@_spfun_aname:n {##1}
        \tl_put_right:Ne \l_@@_spfun_refs_tl
          { \int_compare:nNnF { ##1 } = { 1 } { , } $ \l_@@_spfun_aname_tl }
      }
    \@@_spfun_fname:
    \int_compare:nNnTF { \l_@@_spfun_nargs_int } = { 0 }
      { \tl_set:Ne \l_@@_spfun_call_tl { $ \l_@@_spfun_fname_tl } }
      {
        \tl_set:Ne \l_@@_spfun_call_tl
          { $ \l_@@_spfun_fname_tl ( \l_@@_spfun_refs_tl ) }
      }
    \bool_if:NTF \l_@@_spfun_concept_bool
      {
        \tl_set:Ne \l_@@_spfun_intent_arg_tl
          {
            _
            \tl_if_empty:NF \l_@@_spfun_prefix_tl
              { \tl_use:N \l_@@_spfun_prefix_tl - }
            función:prefix( \l_@@_spfun_call_tl )
          }
      }
      {
        \tl_set_eq:NN \l_@@_spfun_intent_arg_tl \l_@@_spfun_call_tl
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfun_mathml:}
%   Calls \myhlc|\@@_invisible_sep:| first, then wraps name, parentheses and
%   arguments in a native |\MathMLintent|. With \mykey{silent} set to |true|
%   the intent is `\myhlc|_-|' and nothing inside is annotated.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfun_mathml:
  {
    \@@_invisible_sep:
    \@@_spfun_body:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfun_body:}
%   The call itself, without the outer separator: used by the top-level call
%   and by every call nested in an argument.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfun_body:
  {
    \bool_if:NTF \l_@@_spfun_silent_bool
      {
        \MathMLintent { _- }
          {
            \@@_inner_sep:
            \@@_spfun_name_plain:
            \int_compare:nNnF { \l_@@_spfun_nargs_int } = { 0 }
              {
                \lparen
                \@@_spfun_args_plain:
                \rparen
              }
          }
      }
      {
        \@@_spfun_set_intent:
        \MathMLintent { \l_@@_spfun_intent_arg_tl }
          {
            \int_compare:nNnT { \l_@@_spfun_nargs_int } = { 0 }
              { \@@_inner_sep: }
            \str_if_eq:VnTF \l_@@_spfun_mode_str { school }
              { \@@_spfun_name_school: }
              { \@@_spfun_name_mathcat: }
            \int_compare:nNnF { \l_@@_spfun_nargs_int } = { 0 }
              {
                \MathMLintent { :silent } { \lparen }
                \@@_spfun_args:
                \MathMLintent { :silent } { \rparen }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfun_nested:n}
%   Typesets argument number |#1| as a call of its own. It opens a group, so the
%   values that Lua leaves in the variables for the nested call, namely the name, the
%   arguments and their number, are discarded when it ends. The nested call
%   raises the depth, does not apply \mykey{concept} and goes through
%   \myhlc|\@@_spfun_body:| with no outer separator. It cannot fail to parse,
%   since the first analysis already validated every level.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfun_nested:n
  {
    \group_begin:
      \@@_luafun_spfun_parse_and_set:v { l_@@_spfun_luaset_arg_ #1 _tl }
      \int_incr:N \l_@@_spfun_depth_int
      \bool_set_false:N \l_@@_spfun_concept_bool
      \int_set:NV \l_@@_spfun_nargs_int \l_@@_spfun_luaset_nargs_str
      \@@_spfun_body:
    \group_end:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfun_exec:nn}
%   Receives the optional \mymeta{keys} in `|#1|' and the \mymarg{argument}
%   in `|#2|'. Parses first, reporting |spfun-invalid-arg| on error whatever
%   \mykey{silent} is.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfun_exec:nn
  {
    \keys_set:nn { spintent / spfun } {#1}
    \@@_luafun_spfun_parse_and_set:n { \exp_not:n {#2} }
    \str_if_eq:VnTF \l_@@_spfun_luaset_error_str { true }
      {
        \msg_error:nne { spintent } { spfun-invalid-arg } { \exp_not:n {#2} }
      }
      {
        \int_set:NV \l_@@_spfun_nargs_int \l_@@_spfun_luaset_nargs_str
        \int_set:Nn \l_@@_spfun_depth_int { 1 }
        \@@_spfun_mathml:
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spfun}
%   The public command \ics*{spfun}
%   enforces \emph{math mode}, opens a local group,
%   and delegates to \myhlc|\@@_spfun_exec:nn|.
%    \begin{macrocode}
\NewDocumentCommand \spfun { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_spfun_exec:nn {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spfun } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spfuncomp}}\label{cmd:spfuncomp}
%
% The user command |\spfuncomp| typesets the linear composition of
% functions: |f\circ g|, |f\circ g\circ h|, |(f\circ g)| and the
% composition applied to arguments, |(f\circ g)(x)|. The Lua function
% \myhlc|\@@_luafun_spfuncomp_parse_and_set:n| splits the \mymarg{argument}
% at the top-level |\circ|, or the Unicode character U+2218; every function is a name as in
% |\spfun| and gets its literal |intent|, so
% \mynvmath{MathCAT} neither announces a capital letter nor reads the letter h, U+210E, as
% Planck's constant. The arguments of the application, if any, are left in
% the |\spfun| variables and typeset with its own functions, so an
% argument may be a call. The whole is tagged with a native
% |\MathMLintent| whose |intent| is \myhlc|_compuesta-con:infix($k1,$k2,...)|; the
% application adds |$c($a1,...)| around it, with the composition tagged
% |arg="c"|. With two or more arguments they are grouped under their own
% |intent|, \myhlc|_coma:infix($a1,$a2,...)|, tagged |arg="l"|, because
% \mynvmath{MathCAT} drops the commas of a call whose head is not a single
% letter. The |\circ| and the parentheses are silenced.
%
% \subsubsection{Definition of variables for \cs[no-index]{spfuncomp}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spfuncomp_luaset_error_str,
%     \l_@@_spfuncomp_luaset_n_str,
%     \l_@@_spfuncomp_luaset_paren_str,
%     \l_@@_spfuncomp_luaset_apply_str,
%   }
% These variables are set by the Lua module \myluamod{}. The variable
% \myhlc|\l_@@_spfuncomp_luaset_error_str| stores |true| or |false| if the
% \mymarg{argument} is invalid. The variable
% \myhlc|\l_@@_spfuncomp_luaset_n_str| stores the number of functions. The
% variables \myhlc|\l_@@_spfuncomp_luaset_paren_str| and
% \myhlc|\l_@@_spfuncomp_luaset_apply_str| store |true| when the composition
% is in parentheses and when it is applied to arguments.
%    \begin{macrocode}
\str_new:N \l_@@_spfuncomp_luaset_error_str
\str_new:N \l_@@_spfuncomp_luaset_n_str
\str_new:N \l_@@_spfuncomp_luaset_paren_str
\str_new:N \l_@@_spfuncomp_luaset_apply_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spfuncomp_n_int,
%     \l_@@_spfuncomp_refs_tl,
%     \l_@@_spfuncomp_intent_tl,
%     \l_@@_spfuncomp_kv_tl,
%     \l_@@_spfuncomp_tipo_str,
%   }
% The variable \myhlc|\l_@@_spfuncomp_n_int| stores the number of functions,
% copied from \myhlc|\l_@@_spfuncomp_luaset_n_str|. The variable
% \myhlc|\l_@@_spfuncomp_refs_tl| stores the references to the functions and
% \myhlc|\l_@@_spfuncomp_intent_tl| stores the |intent| being built. The
% variable \myhlc|\l_@@_spfuncomp_kv_tl| stores the key-value list with the
% attributes |arg| and |intent| of the function being typeset and
% \myhlc|\l_@@_spfuncomp_tipo_str| stores the kind of its name.
%    \begin{macrocode}
\int_new:N \l_@@_spfuncomp_n_int
\tl_new:N  \l_@@_spfuncomp_refs_tl
\tl_new:N  \l_@@_spfuncomp_intent_tl
\tl_new:N  \l_@@_spfuncomp_kv_tl
\str_new:N \l_@@_spfuncomp_tipo_str
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spfuncomp}}
%
% \begin{macro}[int]{spfuncomp-invalid-arg}
%   The \enquote{error} |spfuncomp-invalid-arg| is raised when the argument is
%   not a composition of at least two function names, optionally in parentheses
%   and optionally applied to arguments written in parentheses, whatever the
%   value of \mykey{silent}.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spfuncomp-invalid-arg }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \spfuncomp'~\msg_line_context:.
  }
  {
    The~argument~must~be~two~or~more~functions~joined~by~'\token_to_str:N \circ'.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spfuncomp}}
%
% \begin{macro}[int]{silent, unknown}
%   We define the \mymeta{key} \mykey[spfuncomp]{silent}.
%    \begin{macrocode}
\keys_define:nn { spintent / spfuncomp }
  {
    silent  .bool_set:N = \l_@@_spfuncomp_silent_bool,
    silent  .initial:n = false,
    silent  .value_required:n = true,
    unknown .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spfuncomp }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spfuncomp}}
%
% \begin{macro}[int]{\@@_spfuncomp_fun:n}
%   Function number |#1| of the chain. Its first letter is tagged |arg="k<i>"|
%   with the literal |intent| of the name through
%   the shared queue. A subscript is typeset silenced; a prime is not; the
%   inverse exponent |^{-1}| is silenced as well.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfuncomp_fun:n
  {
    \tl_set:Ne \l_@@_spfuncomp_kv_tl
      {
        arg = " k #1 ",
        intent = "_ \use:c { l_@@_spfuncomp_luaset_intent_ #1 _str } -"
      }
    \@@_luafun_mrow_intent_arg:nn { } { \l_@@_spfuncomp_kv_tl }
    \str_set_eq:Nc \l_@@_spfuncomp_tipo_str
      { l_@@_spfuncomp_luaset_tipo_ #1 _str }
    \str_case:VnF \l_@@_spfuncomp_tipo_str
      {
        { sub }
          {
            \use:c { l_@@_spfuncomp_luaset_letra_ #1 _str }
            \c_math_subscript_token
              {
                \MathMLintent { :silent }
                  { \use:c { l_@@_spfuncomp_luaset_sub_ #1 _str } }
              }
          }
        { prima }
          {
            \use:c { l_@@_spfuncomp_luaset_letra_ #1 _str }
            \use:c { l_@@_spfuncomp_luaset_sub_ #1 _str }
          }
      }
      { \use:c { l_@@_spfuncomp_luaset_letra_ #1 _str } }
    \str_set_eq:Nc \l_@@_spfuncomp_tipo_str
      { l_@@_spfuncomp_luaset_inv_ #1 _str }
    \str_if_eq:VnT \l_@@_spfuncomp_tipo_str { true }
      {
        \c_math_superscript_token
          { \MathMLintent { :silent } { -1 } }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfuncomp_group:}
%   The composition, with its own |intent|, in parentheses when the argument
%   was written in parentheses.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfuncomp_group:
  {
    \tl_clear:N \l_@@_spfuncomp_refs_tl
    \int_step_inline:nn { \l_@@_spfuncomp_n_int }
      {
        \tl_put_right:Ne \l_@@_spfuncomp_refs_tl
          { \int_compare:nNnF { ##1 } = { 1 } { , } $k ##1 }
      }
    \tl_set:Ne \l_@@_spfuncomp_intent_tl
      { _compuesta-con:infix( \l_@@_spfuncomp_refs_tl ) }
    \MathMLintent { \l_@@_spfuncomp_intent_tl }
      {
        \str_if_eq:VnT \l_@@_spfuncomp_luaset_paren_str { true }
          { \MathMLintent { :silent } { \lparen } }
        \int_step_inline:nn { \l_@@_spfuncomp_n_int }
          {
            \int_compare:nNnF { ##1 } = { 1 }
              { \MathMLintent { :silent } { \circ } }
            \@@_spfuncomp_fun:n {##1}
          }
        \str_if_eq:VnT \l_@@_spfuncomp_luaset_paren_str { true }
          { \MathMLintent { :silent } { \rparen } }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfuncomp_plain:}
%   Typesets the whole with no annotation for \mykey{silent}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfuncomp_plain:
  {
    \str_if_eq:VnT \l_@@_spfuncomp_luaset_paren_str { true } { \lparen }
    \int_step_inline:nn { \l_@@_spfuncomp_n_int }
      {
        \int_compare:nNnF { ##1 } = { 1 } { \circ }
        \tl_use:c { l_@@_spfuncomp_luaset_print_ ##1 _tl }
      }
    \str_if_eq:VnT \l_@@_spfuncomp_luaset_paren_str { true } { \rparen }
    \str_if_eq:VnT \l_@@_spfuncomp_luaset_apply_str { true }
      {
        \lparen \@@_spfun_args_plain: \rparen
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfuncomp_mathml:}
%   Calls \myhlc|\@@_invisible_sep:| first. With \mykey{silent} the |intent|
%   is `\myhlc|_-|'. Otherwise an application wraps the composition with |arg="c"|, and
%   the arguments in |$c($a1,...)|; the arguments are processed as in |\spfun|,
%   including calls nested in them.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfuncomp_mathml:
  {
    \@@_invisible_sep:
    \str_if_eq:VnT \l_@@_spfuncomp_luaset_apply_str { true }
      {
        \int_set:NV \l_@@_spfun_nargs_int \l_@@_spfun_luaset_nargs_str
      }
    \int_set:Nn \l_@@_spfun_depth_int { 1 }
    \str_set:Nn \l_@@_spfun_mode_str { school }
    \bool_set_false:N \l_@@_spfun_silent_bool
    \bool_set_false:N \l_@@_spfun_concept_bool
    \bool_if:NTF \l_@@_spfuncomp_silent_bool
      {
        \MathMLintent { _- }
          { \@@_inner_sep: \@@_spfuncomp_plain: }
      }
      {
        \str_if_eq:VnTF \l_@@_spfuncomp_luaset_apply_str { true }
          {
            \tl_clear:N \l_@@_spfun_refs_tl
            \int_step_inline:nn { \l_@@_spfun_nargs_int }
              {
                \@@_spfun_aname:n {##1}
                \tl_put_right:Ne \l_@@_spfun_refs_tl
                  {
                    \int_compare:nNnF { ##1 } = { 1 } { , }
                    $ \l_@@_spfun_aname_tl
                  }
              }
            \int_compare:nNnTF { \l_@@_spfun_nargs_int } > { 1 }
              {
                \tl_set:Ne \l_@@_spfun_call_tl { $c ( $l ) }
                \MathMLintent { \l_@@_spfun_call_tl }
                  {
                    \MathMLarg { c }
                      { \@@_inner_sep: \@@_spfuncomp_group: }
                    \MathMLintent { :silent } { \lparen }
                    \MathMLarg { l }
                      {
                        \@@_inner_sep:
                        \MathMLintent
                          { _coma:infix ( \l_@@_spfun_refs_tl ) }
                          { \@@_spfun_args: }
                      }
                    \MathMLintent { :silent } { \rparen }
                  }
              }
              {
                \tl_set:Ne \l_@@_spfun_call_tl
                  { $c ( \l_@@_spfun_refs_tl ) }
                \MathMLintent { \l_@@_spfun_call_tl }
                  {
                    \MathMLarg { c }
                      { \@@_inner_sep: \@@_spfuncomp_group: }
                    \MathMLintent { :silent } { \lparen }
                    \@@_spfun_args:
                    \MathMLintent { :silent } { \rparen }
                  }
              }
          }
          { \@@_spfuncomp_group: }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfuncomp_exec:nn}
%   Receives the optional \mymeta{keys} in `|#1|' and the \mymarg{argument}
%   in `|#2|'. Parses first, reporting |spfuncomp-invalid-arg| on error
%   whatever \mykey{silent} is.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfuncomp_exec:nn
  {
    \keys_set:nn { spintent / spfuncomp } {#1}
    \@@_luafun_spfuncomp_parse_and_set:n { \exp_not:n {#2} }
    \str_if_eq:VnTF \l_@@_spfuncomp_luaset_error_str { true }
      {
        \msg_error:nne { spintent } { spfuncomp-invalid-arg }
          { \exp_not:n {#2} }
      }
      {
        \int_set:NV \l_@@_spfuncomp_n_int \l_@@_spfuncomp_luaset_n_str
        \@@_spfuncomp_mathml:
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spfuncomp}
%   The public command \ics*{spfuncomp} enforces \emph{math mode}, opens a
%   local group with the scope attribute, for the calls that an argument may contain,
%   and delegates to \myhlc|\@@_spfuncomp_exec:nn|.
%    \begin{macrocode}
\NewDocumentCommand \spfuncomp { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_spfuncomp_exec:nn {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spfuncomp } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The commands \cs{spdom}, \cs{sprec} and \cs{spcod}}\label{cmd:spdom}
%
% The user commands |\spdom|, |\sprec| and |\spcod| typeset the domain
% |\operatorname{Dom}(f)|, the range |\operatorname{Rec}(f)| and the
% codomain |\operatorname{Cod}(f)| of a function. They share one
% implementation, \myhlc|\@@_spdom_exec:nnnnn|, which receives the command
% name, the operator, the spoken word, the \mymeta{keys} and the
% \mymarg{argument}. The Lua function \myhlc|\@@_luafun_spdom_parse_and_set:n|
% accepts exactly a function name, as |\spfun| does: a letter with an
% optional subscript or primes, and the optional exponent |^{-1}|. It
% leaves it in the shared \myhlc|\l_@@_geo_luaset_*| variables. The whole is tagged
% with a native |\MathMLintent| whose |intent| is
% \myhlc|_[prefix-]dominio-de:prefix($f)|, with |recorrido| or |codominio| in place of |dominio| for the other two commands; the operator,
% the parentheses are silenced and the name carries |arg="f"| wrapped in its
% own literal |intent|, such as \myhlc|_f-|, \myhlc|_F-doble-prima-| or \myhlc|_f-inversa-|, so
% \mynvmath{MathCAT} neither announces a capital letter nor reads the
% inverse as a power. No \mykey{read-cmd} nor \mykey{concept} keys exist:
% the native reading of |Dom(f)| is not useful, and the concept is already
% the spoken word.
%
% \subsubsection{Definition of variables for \cs[no-index]{spdom}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spdom_cmd_str,
%     \l_@@_spdom_op_tl,
%     \l_@@_spdom_word_tl,
%   }
% Each public command of this family sets these variables before calling
% the common code. The variable \myhlc|\l_@@_spdom_cmd_str| stores the name of
% the command, \myhlc|\l_@@_spdom_op_tl| the operator typeset by it and
% \myhlc|\l_@@_spdom_word_tl| the word used in the |intent|.
%    \begin{macrocode}
\str_new:N \l_@@_spdom_cmd_str
\tl_new:N  \l_@@_spdom_op_tl
\tl_new:N  \l_@@_spdom_word_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spdom_prefix_tl,
%     \l_@@_spdom_intent_arg_tl,
%   }
% The variable \myhlc|\l_@@_spdom_prefix_tl| stores the value set by the key
% \mykey[spdom]{prefix} and \myhlc|\l_@@_spdom_intent_arg_tl| stores the
% |intent| being built.
%    \begin{macrocode}
\tl_new:N \l_@@_spdom_prefix_tl
\tl_new:N \l_@@_spdom_intent_arg_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_spdom_luaset_error_str}
% This variable is set from Lua and stores |true| or |false| if the
% \mymarg{argument} is not a function name.
%    \begin{macrocode}
\str_new:N \l_@@_spdom_luaset_error_str
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spdom}}
%
% \begin{macro}[int]{spdom-invalid-arg}
%   The \enquote{error} |spdom-invalid-arg| is raised when the argument is
%   not a function name. It receives the command name and the argument, and
%   is raised whatever the value of \mykey{silent}.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spdom-invalid-arg }
  {
    Invalid~argument~'#2'~for~'\c_backslash_str #1'~\msg_line_context:.
  }
  {
    The~argument~must~be~a~function~name.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spdom}}
%
% \begin{macro}[int]{prefix, silent, unknown}
%   We define the \mymeta{keys} \mykey[spdom]{prefix} and
%   \mykey[spdom]{silent}.
%    \begin{macrocode}
\keys_define:nn { spintent / spdom }
  {
    prefix  .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spdom_prefix_tl {#1} },
    prefix  .initial:n = ,
    prefix  .value_required:n = true,
    silent  .bool_set:N = \l_@@_spdom_silent_bool,
    silent  .initial:n = false,
    silent  .value_required:n = true,
    unknown .code:n =
      {
        \str_set_eq:NN \l_@@_command_name_str \l_@@_spdom_cmd_str
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spdom}}
%
% \begin{macro}[int]{\@@_spdom_name:}
%   Tags the name with |arg="f"| through a native |\MathMLarg|; inside it, a
%   native |\MathMLintent| gives the name its literal |intent|. Both groups
%   start with the inner separator, so each one gets its own |<mrow>| even
%   when the name is a single node.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spdom_name:
  {
    \MathMLarg { f }
      {
        \@@_inner_sep:
        \MathMLintent { _ \l_@@_geo_luaset_intent_str - }
          { \@@_inner_sep: \@@_spfun_name_plain: }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spdom_mathml:}
%   Calls \myhlc|\@@_invisible_sep:| first, then wraps operator,
%   parentheses and name in a native |\MathMLintent|. With \mykey{silent}
%   set to |true| the |intent| is `\myhlc|_-|' and nothing inside is annotated.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spdom_mathml:
  {
    \@@_invisible_sep:
    \bool_if:NTF \l_@@_spdom_silent_bool
      {
        \MathMLintent { _- }
          {
            \@@_inner_sep:
            \operatorname { \l_@@_spdom_op_tl }
            \lparen \@@_spfun_name_plain: \rparen
          }
      }
      {
        \tl_set:Ne \l_@@_spdom_intent_arg_tl
          {
            _
            \tl_if_empty:NF \l_@@_spdom_prefix_tl
              { \tl_use:N \l_@@_spdom_prefix_tl - }
            \l_@@_spdom_word_tl -de:prefix( $f )
          }
        \MathMLintent { \l_@@_spdom_intent_arg_tl }
          {
            \MathMLintent { :silent } { \operatorname { \l_@@_spdom_op_tl } }
            \MathMLintent { :silent } { \lparen }
            \@@_spdom_name:
            \MathMLintent { :silent } { \rparen }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spdom_exec:nnnnn}
%   Receives the command name in `|#1|', the operator in `|#2|', the spoken
%   word in `|#3|', the optional \mymeta{keys} in `|#4|' and the
%   \mymarg{argument} in `|#5|'. Parses first, reporting
%   |spdom-invalid-arg| on error whatever \mykey{silent} is.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spdom_exec:nnnnn
  {
    \str_set:Nn \l_@@_spdom_cmd_str {#1}
    \tl_set:Nn  \l_@@_spdom_op_tl   {#2}
    \tl_set:Nn  \l_@@_spdom_word_tl {#3}
    \keys_set:nn { spintent / spdom } {#4}
    \@@_luafun_spdom_parse_and_set:n { \exp_not:n {#5} }
    \str_if_eq:VnTF \l_@@_spdom_luaset_error_str { true }
      {
        \msg_error:nnee { spintent } { spdom-invalid-arg }
          {#1} { \exp_not:n {#5} }
      }
      { \@@_spdom_mathml: }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spdom, \sprec, \spcod}
%   The public commands \ics*{spdom}, \ics*{sprec} and \ics*{spcod} enforce
%   \emph{math mode}, open a local group, and delegate to \myhlc|\@@_spdom_exec:nnnnn|.
%    \begin{macrocode}
\NewDocumentCommand \spdom { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spdom_exec:nnnnn { spdom } { Dom } { dominio } {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spdom } }
  }
\NewDocumentCommand \sprec { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spdom_exec:nnnnn { sprec } { Rec } { recorrido } {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \sprec } }
  }
\NewDocumentCommand \spcod { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spdom_exec:nnnnn { spcod } { Cod } { codominio } {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spcod } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spabs}}\label{cmd:spabs}
%
% The user command |\spabs| typesets the absolute value |\lvert x\rvert| and
% reads it with the literal intent |valor-absoluto($x)|. Optionally, a second
% literal intent |fin-valor-absoluto| announces where it ends. It has the keys
% \mykey[spabs]{silent}, \mykey[spabs]{read-arg}, \mykey[spabs]{size} and
% \mykey[spabs]{close}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spabs}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spabs_read_arg_tl,
%     \l_@@_spabs_size_str,
%     \l_@@_spabs_close_mode_str,
%   }
%   The token list variable \myhlc|\l_@@_spabs_read_arg_tl| stores the key
%   \mykey[spabs]{read-arg}, and the string variables
%   \myhlc|\l_@@_spabs_size_str| and \myhlc|\l_@@_spabs_close_mode_str| the
%   keys \mykey[spabs]{size} and \mykey[spabs]{close}.
%    \begin{macrocode}
\tl_new:N  \l_@@_spabs_read_arg_tl
\str_new:N \l_@@_spabs_size_str
\str_new:N \l_@@_spabs_close_mode_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spabs_delim_l_tl,
%     \l_@@_spabs_delim_r_tl,
%   }
%   The opening and closing bars, with their size commands.
%    \begin{macrocode}
\tl_new:N \l_@@_spabs_delim_l_tl
\tl_new:N \l_@@_spabs_delim_r_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spabs_luaset_close_str,
%     \l_@@_spabs_close_bool,
%   }
%   The string variable \myhlc|\l_@@_spabs_luaset_close_str| is set by the
%   function \myhlc|\@@_luafun_spabs_close:n| and is |true| when the
%   argument, without spaces and without a leading sign, has more than one
%   token; the boolean variable
%   \myhlc|\l_@@_spabs_close_bool| tells whether the closing is read.
%    \begin{macrocode}
\str_new:N  \l_@@_spabs_luaset_close_str
\bool_new:N \l_@@_spabs_close_bool
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spabs}}
%
% \begin{macro}[int]{spabs-empty-arg}
%   The \enquote{error} |spabs-empty-arg| is raised when the argument of
%   |\spabs| is missing or \emph{blank}.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spabs-empty-arg }
  {
    Empty~argument~for~'\token_to_str:N \spabs'~\msg_line_context:.
  }
  {
    The~expression~inside~the~absolute~value~is~required.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spabs}}
%
% \begin{macro}[int]{silent, read-arg, size, close, unknown}
%   We define the \mymeta{keys} \mykey[spabs]{silent}, \mykey[spabs]{read-arg},
%   \mykey[spabs]{size} and \mykey[spabs]{close}.
%    \begin{macrocode}
\keys_define:nn { spintent / spabs }
  {
    silent   .bool_set:N = \l_@@_spabs_silent_bool,
    silent   .initial:n = false,
    silent   .value_required:n = true,
    read-arg .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spabs_read_arg_tl {#1} },
    read-arg .initial:n = ,
    read-arg .value_required:n = true,
    size     .choices:nn = { none , big , Big , bigg , Bigg }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spabs_size_str { none } }
            {2} { \str_set:Nn \l_@@_spabs_size_str { big  } }
            {3} { \str_set:Nn \l_@@_spabs_size_str { Big  } }
            {4} { \str_set:Nn \l_@@_spabs_size_str { bigg } }
            {5} { \str_set:Nn \l_@@_spabs_size_str { Bigg } }
          }
      },
    size     .initial:n = none,
    size     .value_required:n = true,
    size / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { size } { none,~big,~Big,~bigg,~Bigg } { \exp_not:n {#1} },
    close    .choices:nn = { auto , true , false }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spabs_close_mode_str { auto  } }
            {2} { \str_set:Nn \l_@@_spabs_close_mode_str { true  } }
            {3} { \str_set:Nn \l_@@_spabs_close_mode_str { false } }
          }
      },
    close    .initial:n = auto,
    close    .value_required:n = true,
    close / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { close } { auto,~true,~false } { \exp_not:n {#1} },
    unknown  .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spabs }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spabs}}
%
% \begin{macro}[int]{\@@_spabs_set_delims:}
%   The function \myhlc|\@@_spabs_set_delims:| sets the bars from the key
%   \mykey[spabs]{size}: the bare |\lvert| and |\rvert| for |none|, and
%   |\bigl|, |\Bigl|, |\biggl| or |\Biggl|, and the closing versions,
%   before the bars otherwise.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spabs_set_delims:
  {
    \str_case:Vn \l_@@_spabs_size_str
      {
        { none }
          {
            \tl_set:Nn \l_@@_spabs_delim_l_tl { \lvert }
            \tl_set:Nn \l_@@_spabs_delim_r_tl { \rvert }
          }
        { big }
          {
            \tl_set:Nn \l_@@_spabs_delim_l_tl { \bigl  \lvert }
            \tl_set:Nn \l_@@_spabs_delim_r_tl { \bigr  \rvert }
          }
        { Big }
          {
            \tl_set:Nn \l_@@_spabs_delim_l_tl { \Bigl  \lvert }
            \tl_set:Nn \l_@@_spabs_delim_r_tl { \Bigr  \rvert }
          }
        { bigg }
          {
            \tl_set:Nn \l_@@_spabs_delim_l_tl { \biggl \lvert }
            \tl_set:Nn \l_@@_spabs_delim_r_tl { \biggr \rvert }
          }
        { Bigg }
          {
            \tl_set:Nn \l_@@_spabs_delim_l_tl { \Biggl \lvert }
            \tl_set:Nn \l_@@_spabs_delim_r_tl { \Biggr \rvert }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spabs_set_close:n}
%   The function \myhlc|\@@_spabs_set_close:n| sets
%   \myhlc|\l_@@_spabs_close_bool| from \mykey[spabs]{close}: always
%   |true| with |true|, always |false| with |false|, and with |auto| |true|
%   when Lua reports that the argument `|#1|', without spaces and without a
%   leading |+| or |-|, has more than one token.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spabs_set_close:n
  {
    \bool_set_false:N \l_@@_spabs_close_bool
    \str_case:Vn \l_@@_spabs_close_mode_str
      {
        { true } { \bool_set_true:N \l_@@_spabs_close_bool }
        { auto }
          {
            \@@_luafun_spabs_close:n { \exp_not:n {#1} }
            \str_if_eq:VnT \l_@@_spabs_luaset_close_str { true }
              { \bool_set_true:N \l_@@_spabs_close_bool }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spabs_exec:nn}
%   The function \myhlc|\@@_spabs_exec:nn| sets the \mymeta{keys} in `|#1|',
%   sets the delimiters with \myhlc|\@@_spabs_set_delims:| and calls
%   \myhlc|\@@_spabs_mathml:n| with the argument in `|#2|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spabs_exec:nn
  {
    \keys_set:nn { spintent / spabs } {#1}
    \@@_spabs_set_delims:
    \@@_spabs_mathml:n {#2}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spabs_mathml:n}
%   The function \myhlc|\@@_spabs_mathml:n| prints the absolute value of `|#1|'
%   inside one native |\MathMLintent|, preceded by the outer ghost
%   \myhlc|\@@_invisible_sep:| and followed by the inner ghost
%   \myhlc|\@@_inner_sep:|. With \mykey{silent}|=true| the
%   intent is `\myhlc|_-|'; with \mykey{read-arg} given it is
%   \myhlc|_|\mymeta{read-arg}|:nofix|; otherwise it is \myhlc|_valor-absoluto($x)|. In this
%   last case, when \myhlc|\@@_spabs_set_close:n| sets the closing, a native
%   |\MathMLintent| with the literal intent \myhlc|_fin-valor-absoluto| follows the bars
%   as a sibling, holding \myhlc|\@@_inner_sep:| and an invisible character, and
%   the outer ghost is omitted because the formula already has two top-level
%   nodes.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spabs_mathml:n
  {
    \bool_if:NTF \l_@@_spabs_silent_bool
      {
        \@@_invisible_sep:
        \MathMLintent { _- }
          {
            \l_@@_spabs_delim_l_tl \@@_inner_sep: #1
              \l_@@_spabs_delim_r_tl
          }
      }
      {
        \tl_if_empty:NTF \l_@@_spabs_read_arg_tl
          {
            \@@_spabs_set_close:n {#1}
            \bool_if:NF \l_@@_spabs_close_bool { \@@_invisible_sep: }
            \MathMLintent { _valor-absoluto($x) }
              {
                \l_@@_spabs_delim_l_tl
                \MathMLarg { x } { \@@_inner_sep: #1 }
                \l_@@_spabs_delim_r_tl
              }
            \bool_if:NT \l_@@_spabs_close_bool
              {
                \MathMLintent { _fin-valor-absoluto }
                  { \@@_inner_sep: \c_@@_invisible_sep_int }
              }
          }
          {
            \@@_invisible_sep:
            \MathMLintent { _ \tl_use:N \l_@@_spabs_read_arg_tl :nofix }
              {
                \l_@@_spabs_delim_l_tl \@@_inner_sep: #1
                  \l_@@_spabs_delim_r_tl
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spabs}
%   The public command \ics*{spabs} enforces \emph{math mode}, opens a local
%   group and delegates execution to \myhlc|\@@_spabs_exec:nn|.
%    \begin{macrocode}
\NewDocumentCommand \spabs { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \tl_if_blank:nTF {#2}
            { \msg_error:nn { spintent } { spabs-empty-arg } }
            { \@@_spabs_exec:nn {#1} {#2} }
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spabs } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The function \cs[no-index]{__spintent_mathmlarg_point:nnn}}\label{fun:mathmlarg-point}
%
% \begin{variable}[int]
%   {
%     \l_@@_mathmlarg_point_ref_tl,
%     \l_@@_mathmlarg_point_kv_tl,
%   }
% These variables are scratch variables of
% \myhlc|\@@_mathmlarg_point:nnn|. The variable
% \myhlc|\l_@@_mathmlarg_point_ref_tl| stores the reference name given in
% |#1| and \myhlc|\l_@@_mathmlarg_point_kv_tl| stores the key-value list with
% the attributes of the point.
%    \begin{macrocode}
\tl_new:N \l_@@_mathmlarg_point_ref_tl
\tl_new:N \l_@@_mathmlarg_point_kv_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_mathmlarg_point_mode_str,
%     \l_@@_mathmlarg_point_silent_bool,
%   }
% The variables \myhlc|\l_@@_mathmlarg_point_mode_str| and
% \myhlc|\l_@@_mathmlarg_point_silent_bool| store the state of the keys
% \mykey{read-cmd} and \mykey{silent} of the calling command, found by the
% name given in |#2|.
%    \begin{macrocode}
\str_new:N  \l_@@_mathmlarg_point_mode_str
\bool_new:N \l_@@_mathmlarg_point_silent_bool
%    \end{macrocode}
% \end{variable}
%
% \begin{macro}[int]{\@@_mathmlarg_point:nnn, \@@_mathmlarg_point:nnV}
%   Annotates the point in `|#3|' with |arg| and, when applicable, a literal
%   |intent|, without wrapping it in its own |<mrow>|. Concept |intent|
%   is passed empty on purpose: the caller applies it externally, via a
%   native |\MathMLintent|.
%
%   With \myhlc|\l_@@_geo_luaset_tipo_str| equal to |sub|, for
%   \mykey{read-cmd}|=school| the letter is tagged with
%   \myhlc|\@@_luafun_mrow_intent_arg:nn| and the subscript is appended
%   separately, silenced. A subscript of more than one digit is typeset plain,
%   with one extra silent claim per digit. For \mykey{read-cmd}|=mathcat|, the
%   letter and the subscript are typeset together and tagged as a whole with a
%   native |\MathMLarg|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_mathmlarg_point:nnn
  {
    \tl_set:Ne \l_@@_mathmlarg_point_ref_tl {#1}
    \@@_luafun_geo_point_parse_and_set:n { \exp_not:n {#3} }
    \bool_set_eq:Nc \l_@@_mathmlarg_point_silent_bool
      { l_@@_ #2 _silent_bool }
    \str_set_eq:Nc  \l_@@_mathmlarg_point_mode_str  { l_@@_ #2 _mode_str }
    \@@_invisible_sep:
    \str_case:VnF \l_@@_mathmlarg_point_ref_tl
      { { pt } { } { pt1 } { } { pts1 } { } }
      { \@@_point_sep: }
    \str_case:VnF \l_@@_geo_luaset_tipo_str
      {
        { sub }
          {
            \str_if_eq:VnTF \l_@@_mathmlarg_point_mode_str { school }
              {
                \tl_set:Ne \l_@@_mathmlarg_point_kv_tl
                  {
                    arg = "\l_@@_mathmlarg_point_ref_tl",
                    intent = "_ \l_@@_geo_luaset_intent_str -"
                  }
                \@@_luafun_mrow_intent_arg:nn
                  { } { \l_@@_mathmlarg_point_kv_tl }
                \int_compare:nNnTF { \str_count:N \l_@@_geo_luaset_sub_str } >
                  { 1 }
                  {
                    \int_step_inline:nnnn { 1 } { 1 }
                      { \str_count:N \l_@@_geo_luaset_sub_str }
                      {
                        \@@_luafun_mrow_intent_arg:nn
                          { } { intent = ":silent" }
                      }
                    {
                      \l_@@_geo_luaset_letra_str
                      \c_math_subscript_token { \l_@@_geo_luaset_sub_str }
                    }
                  }
                  {
                    {
                      \l_@@_geo_luaset_letra_str
                      \c_math_subscript_token
                        { \MathMLintent { :silent } { \l_@@_geo_luaset_sub_str } }
                    }
                  }
              }
              {
                {
                  \@@_inner_sep:
                  \MathMLarg { \l_@@_mathmlarg_point_ref_tl }
                    {
                      {
                        \l_@@_geo_luaset_letra_str
                        \c_math_subscript_token { \l_@@_geo_luaset_sub_str }
                      }
                    }
                }
              }
          }
%    \end{macrocode}
%
%   With \myhlc|\l_@@_geo_luaset_tipo_str| equal to |prima|, the same
%   distinction applies: \mykey{read-cmd}|=school| tags the letter with a
%   literal |intent| and appends the mark unsilenced, since a prime is not
%   spoken on the base and there is nothing to repeat; \mykey{read-cmd}|=mathcat| tags
%   letter and mark together through a native |\MathMLarg|.
%    \begin{macrocode}
        { prima }
          {
            \str_if_eq:VnTF \l_@@_mathmlarg_point_mode_str { school }
              {
                \tl_set:Ne \l_@@_mathmlarg_point_kv_tl
                  {
                    arg = "\l_@@_mathmlarg_point_ref_tl",
                    intent = "_ \l_@@_geo_luaset_intent_str -"
                  }
                \@@_luafun_mrow_intent_arg:nn
                  { } { \l_@@_mathmlarg_point_kv_tl }
                { \l_@@_geo_luaset_letra_str \l_@@_geo_luaset_sub_str }
              }
              {
                {
                  \@@_inner_sep:
                  \MathMLarg { \l_@@_mathmlarg_point_ref_tl }
                    {
                      {
                        \l_@@_geo_luaset_letra_str
                          \l_@@_geo_luaset_sub_str
                      }
                    }
                }
              }
          }
      }
%    \end{macrocode}
%
%   Otherwise, the same distinction applies to the
%   combined \myhlc|\l_@@_geo_luaset_print_tl|: the shared queue for
%   \mykey{read-cmd}|=school| or when \mykey{silent} is |true|, and a native
%   |\MathMLarg| for \mykey{read-cmd}|=mathcat|.
%   The two mechanisms are never mixed in the same formula.
%    \begin{macrocode}
      {
        \bool_if:NTF \l_@@_mathmlarg_point_silent_bool
          {
            \tl_set:Ne \l_@@_mathmlarg_point_kv_tl
              { arg = "\l_@@_mathmlarg_point_ref_tl", intent = "_-" }
            \@@_luafun_mrow_intent_arg:nn
              {  } % must be empty
              { \l_@@_mathmlarg_point_kv_tl }
            { \l_@@_geo_luaset_print_tl }
          }
          {
            \str_if_eq:VnTF \l_@@_mathmlarg_point_mode_str { school }
              {
                \tl_set:Ne \l_@@_mathmlarg_point_kv_tl
                  {
                    arg = "\l_@@_mathmlarg_point_ref_tl",
                    intent = "_ \l_@@_geo_luaset_intent_str -"
                  }
                \@@_luafun_mrow_intent_arg:nn
                  {  } % must be empty
                  { \l_@@_mathmlarg_point_kv_tl }
                { \l_@@_geo_luaset_print_tl }
              }
              {
                \MathMLarg { \l_@@_mathmlarg_point_ref_tl }
                  { \l_@@_geo_luaset_print_tl }
              }
          }
      }
  }
\cs_generate_variant:Nn \@@_mathmlarg_point:nnn { nnV }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spPoint}}\label{cmd:spPoint}
%
% The user-level command |\spPoint| provides a semantic interface for
% a single geometric point. It delegates argument validation to the
% Lua module \myluamod{}, constructing a valid \mynvmath{MathCAT}
% intent.
%
% \subsubsection{Definition of variables for \cs[no-index]{spPoint}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spPoint_mode_str,
%     \l_@@_spPoint_prefix_tl,
%   }
% The variables \myhlc|\l_@@_spPoint_mode_str| and
% \myhlc|\l_@@_spPoint_prefix_tl| store the values set by the keys
% \mykey[spPoint]{read-cmd} and \mykey[spPoint]{prefix}.
%    \begin{macrocode}
\str_new:N \l_@@_spPoint_mode_str
\tl_new:N  \l_@@_spPoint_prefix_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_spPoint_intent_arg_tl}
% The variable \myhlc|\l_@@_spPoint_intent_arg_tl| stores the |intent| being
% built.
%    \begin{macrocode}
\tl_new:N \l_@@_spPoint_intent_arg_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spPoint}}
%
% \begin{macro}[int]{spPoint-invalid-point}
%   The \enquote{error} |spPoint-invalid-point| is raised when the
%   argument passed in `|#1|' is not a single uppercase letter with an
%   optional subscript or prime modifier.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spPoint-invalid-point }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \spPoint'~\msg_line_context:.
  }
  {
    The~argument~must~be~a~single~uppercase~letter~with~an~optional~subscript~(\_{...})~
    or~prime~(').
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spPoint}}
%
% \begin{macro}[int]{read-cmd, prefix, silent, concept, unknown}
%   We define the \mymeta{keys} \mykey[spPoint]{read-cmd},
%   \mykey[spPoint]{prefix}, \mykey[spPoint]{silent}  and
%   \mykey[spPoint]{concept}.
%    \begin{macrocode}
\keys_define:nn { spintent / spPoint }
  {
    read-cmd .choices:nn = { school , mathcat }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spPoint_mode_str { school  } }
            {2} { \str_set:Nn \l_@@_spPoint_mode_str { mathcat } }
          }
      },
    read-cmd .initial:n = school,
    read-cmd .value_required:n = true,
    read-cmd / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-cmd } { school,~mathcat } { \exp_not:n {#1} },
    prefix   .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spPoint_prefix_tl {#1} },
    prefix   .initial:n = ,
    prefix   .value_required:n = true,
    silent   .bool_set:N = \l_@@_spPoint_silent_bool,
    silent   .initial:n = false,
    silent   .value_required:n = true,
    concept  .bool_set:N = \l_@@_spPoint_concept_bool,
    concept  .initial:n = true,
    concept  .value_required:n = true,
    unknown  .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spPoint }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spPoint}}
%
% \begin{macro}[int]{\@@_spPoint_exec:nnn}
%   The internal function \myhlc|\@@_spPoint_exec:nnn| processes the
%   \mymarg{argument} passed in `|#2|'. It calls
%   \myhlc|\@@_luafun_geo_point_parse_and_set:n|, reporting
%   |spPoint-invalid-point| on error. If \mykey{silent} is |true| or
%   \mykey{concept} is |false|, it sets \myhlc|\l_@@_spPoint_intent_arg_tl| to
%   \myhlc|_($pt)|, or to `\myhlc|_-|' when \mykey{silent} is |true|, whatever
%   \mykey{read-cmd} is. Otherwise it sets it to \myhlc|_punto:prefix($pt)|, with
%   \mykey{prefix} placed after the leading \myhlc|_| when given. Finally it calls
%   \myhlc|\@@_spPoint_mathml:n|.
%
%   Before processing the \mymarg{argument}, it sets the optional \mymeta{keys} passed in `|#1|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spPoint_exec:nnn
  {
    \keys_set:nn { spintent / spPoint } {#1}
    \@@_luafun_geo_point_parse_and_set:n { \exp_not:n {#2} }
    \str_if_eq:VnT \l_@@_geo_luaset_error_str { true }
      {
        \msg_error:nne { spintent } { spPoint-invalid-point } { \exp_not:n {#2} }
      }
    \bool_if:nTF
      {
        \bool_lazy_or_p:nn
          { \l_@@_spPoint_silent_bool }
          { ! \l_@@_spPoint_concept_bool }
      }
      {
        \tl_set:Nn \l_@@_spPoint_intent_arg_tl { _($pt) }
        \bool_if:NT \l_@@_spPoint_silent_bool
          {
            \tl_set:Nn \l_@@_spPoint_intent_arg_tl { _- }
          }
      }
      {
        \tl_set:Ne \l_@@_spPoint_intent_arg_tl
          {
            _
            \tl_if_empty:NF \l_@@_spPoint_prefix_tl
              {
                \tl_use:N \l_@@_spPoint_prefix_tl -
              }
            punto:prefix($pt)
          }
      }
    \@@_spPoint_mathml:n {#2}
    \tl_if_novalue:nF {#3}
      {
        \MathMLintent { :silent } { \c_@@_invisible_sep_int } % prevent "por"
        \tl_set:Nn \l_@@_spcoord_raw_arg_tl {#3}
        \@@_luafun_spcoord_parse_and_set:n { \exp_not:n {#3} }
        \str_if_eq:VnT \l_@@_spcoord_luaset_error_str { true }
          { \msg_error:nne { spintent } { spcoord-invalid-arg } { \exp_not:n {#3} } }
        \@@_spcoord_set_wrap_parens_size:
        \str_if_eq:VnF \l_@@_spcoord_luaset_error_str { true }
          { \@@_spcoord_mathml:N \l_@@_spPoint_silent_bool }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spPoint_mathml:n}
%   The function \myhlc|\@@_spPoint_mathml:n| calls \myhlc|\@@_invisible_sep:|
%   and then wraps \myhlc|\@@_mathmlarg_point:nnn|, which parses `|#1|', tags it
%   |arg="pt"| and silences a subscript or prime, inside a native
%   |\MathMLintent| carrying \myhlc|\l_@@_spPoint_intent_arg_tl| as the concept
%   |intent|. Inside that group it calls \myhlc|\@@_inner_sep:| first.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spPoint_mathml:n
  {
    \@@_invisible_sep:
    \MathMLintent { \l_@@_spPoint_intent_arg_tl }
      {
        \@@_inner_sep:
        \@@_mathmlarg_point:nnn { pt } { spPoint } {#1}
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spPoint}
%   The public command \ics*{spPoint} enforces \emph{math mode}, opens a local
%   group, and delegates execution to \myhlc|\@@_spPoint_exec:nnn|.
%    \begin{macrocode}
\NewDocumentCommand \spPoint { O{} m d() }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_spPoint_exec:nnn {#1} {#2} {#3}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spPoint } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spvec}}\label{cmd:spvec}
%
% The user command |\spvec| supports two types of notation for the
% input \mymarg{argument}: a pair of points, which gets |\overrightarrow|,
% or a one-letter name, which gets |\vec| instead. Both
% notations are detected by the Lua module. An optional
% coordinate, in parentheses, may follow the \mymarg{argument}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spvec}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spvec_mode_str,
%     \l_@@_spvec_prefix_tl,
%   }
% The variables \myhlc|\l_@@_spvec_mode_str| and
% \myhlc|\l_@@_spvec_prefix_tl| store the values set by the keys
% \mykey[spvec]{read-cmd} and \mykey[spvec]{prefix}.
%    \begin{macrocode}
\str_new:N \l_@@_spvec_mode_str
\tl_new:N  \l_@@_spvec_prefix_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spvec_pt_seq,
%     \l_@@_spvec_pt_idx_int,
%     \l_@@_spvec_refs_tl,
%   }
% The variable \myhlc|\l_@@_spvec_pt_seq| stores the points of the
% argument, split at the commas, and \myhlc|\l_@@_spvec_pt_idx_int| counts the
% point being read. The variable \myhlc|\l_@@_spvec_refs_tl| stores the
% reference list |$pts1,$pts2,...| built from them.
%    \begin{macrocode}
\seq_new:N \l_@@_spvec_pt_seq
\int_new:N \l_@@_spvec_pt_idx_int
\tl_new:N  \l_@@_spvec_refs_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spvec_intent_arg_tl,
%     \l_@@_spvec_kv_tl,
%   }
% The variable \myhlc|\l_@@_spvec_intent_arg_tl| stores the |intent| being
% built and \myhlc|\l_@@_spvec_kv_tl| stores the key-value list with the
% attributes |arg| and |intent| of the points.
%    \begin{macrocode}
\tl_new:N \l_@@_spvec_intent_arg_tl
\tl_new:N \l_@@_spvec_kv_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spvec}}
%
% \begin{macro}[int]{spvec-invalid-arg}
%   The \enquote{error} |spvec-invalid-arg| is raised when the
%   argument fails to parse, when a one-letter name carries a prime,
%   or when the point list is not exactly two points.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spvec-invalid-arg }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \spvec'~\msg_line_context:.
  }
  {
    The~argument~must~be~a~lowercase~letter~or~two~points~separated~by~a~comma.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spvec}}
%
% \begin{macro}[int]{silent, concept, autofit, read-cmd, prefix, unknown}
%   We define the \mymeta{keys} \mykey[spvec]{silent},
%   \mykey[spvec]{concept}, \mykey[spvec]{autofit},
%   \mykey[spvec]{read-cmd} and \mykey[spvec]{prefix}.
%    \begin{macrocode}
\keys_define:nn { spintent / spvec }
  {
    silent   .bool_set:N = \l_@@_spvec_silent_bool,
    silent   .initial:n = false,
    silent   .value_required:n = true,
    concept  .bool_set:N = \l_@@_spvec_concept_bool,
    concept  .initial:n = true,
    concept  .value_required:n = true,
    autofit  .bool_set:N = \l_@@_spvec_autofit_bool,
    autofit  .initial:n = false,
    autofit  .value_required:n = true,
    read-cmd .choices:nn = { school , mathcat }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spvec_mode_str { school } }
            {2} { \str_set:Nn \l_@@_spvec_mode_str { mathcat } }
          }
      },
    read-cmd .initial:n = school,
    read-cmd .value_required:n = true,
    read-cmd / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-cmd } { school,~mathcat } { \exp_not:n {##1} },
    prefix   .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spvec_prefix_tl {#1} },
    prefix   .initial:n = { },
    prefix   .value_required:n = true,
    unknown  .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spvec }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spvec}}
%
% \begin{macro}[int]{\@@_spvec_exec:nnn}
%   The internal function \myhlc|\@@_spvec_exec:nnn| processes the
%   \mymarg{argument} passed in `|#2|'. Calls
%   \myhlc|\@@_luafun_geo_parse_and_set:n|, reporting
%   |spvec-invalid-arg| on error, on a proper name with
%   \myhlc|\l_@@_geo_luaset_tipo_str| equal to |prima|, since a one-letter
%   vector cannot carry a prime, or on a point list that is not
%   exactly two points. Otherwise calls \myhlc|\@@_spvec_mathml:n|.
%
%   Before processing the \mymarg{argument}, it sets the optional \mymeta{keys} passed in `|#1|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spvec_exec:nnn
  {
    \keys_set:nn { spintent / spvec } {#1}
    \@@_luafun_geo_parse_and_set:n { \exp_not:n {#2} }
    \str_if_eq:VnT \l_@@_geo_luaset_is_name_str { true }
      {
        \str_if_eq:VnT \l_@@_geo_luaset_tipo_str { prima }
          { \str_set:Nn \l_@@_geo_luaset_error_str { true } }
      }
    \str_if_eq:VnT \l_@@_geo_luaset_is_name_str { false }
      {
        \int_compare:nNnF { \clist_count:N \l_@@_geo_luaset_points_clist } = { 2 }
          { \str_set:Nn \l_@@_geo_luaset_error_str { true } }
      }
    \str_if_eq:VnTF \l_@@_geo_luaset_error_str { true }
      { \msg_error:nne { spintent } { spvec-invalid-arg } { \exp_not:n {#2} } }
      { \@@_spvec_mathml:n {#2} }
    \tl_if_novalue:nF {#3}
      {
        \bool_if:NTF \l_@@_spvec_silent_bool
      { \MathMLintent { :silent } { \equal } }
      { \equal } % vector notation =( , )
        % prevent "intervalo"
        \MathMLintent { :silent } { \c_@@_invisible_sep_int }
        \tl_set:Nn \l_@@_spcoord_raw_arg_tl {#3}
        \@@_luafun_spcoord_parse_and_set:n { \exp_not:n {#3} }
        \str_if_eq:VnT \l_@@_spcoord_luaset_error_str { true }
          { \msg_error:nne { spintent } { spcoord-invalid-arg } { \exp_not:n {#3} } }
        \@@_spcoord_set_wrap_parens_size:
        \str_if_eq:VnF \l_@@_spcoord_luaset_error_str { true }
          { \@@_spcoord_mathml:N \l_@@_spvec_silent_bool }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spvec_mathml:n}
%   The function \myhlc|\@@_spvec_mathml:n| branches on
%   \myhlc|\l_@@_geo_luaset_is_name_str|.
%
%   For a proper name that is a single letter with no prime, the concept |intent| is `\myhlc|_-|'
%   when \mykey{silent} is |true|, whatever \mykey{read-cmd} is, \myhlc|_($pt)| when
%   \mykey{concept} is |false|, and otherwise \myhlc|_vector:prefix($pt)|, with
%   \mykey{prefix} placed after the leading \myhlc|_| when given. With
%   \myhlc|\l_@@_geo_luaset_tipo_str| equal to |sub|, \mykey{read-cmd}|=school|
%   tags |\vec{letra}| with \myhlc|\@@_luafun_mrow_intent_arg:nn| and a literal
%   |kv|, and appends the subscript silenced; \mykey{read-cmd}|=mathcat| tags
%   |\vec{...}| with a native |\MathMLarg| and silences the subscript only under
%   \mykey{silent}. The key \mykey{autofit} inserts a |\vphantom{b}| inside
%   |\vec{...}| to normalize its height. Otherwise, the same |kv|
%   logic wraps |\vec{print\_tl}| as a whole.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spvec_mathml:n
  {
    \str_if_eq:VnTF \l_@@_geo_luaset_is_name_str { true }
      {
        \bool_if:nTF
          {
            \bool_lazy_or_p:nn
              { \l_@@_spvec_silent_bool }
              { ! \l_@@_spvec_concept_bool }
          }
          {
            \tl_set:Nn \l_@@_spvec_intent_arg_tl { _($pt) }
            \bool_if:NT \l_@@_spvec_silent_bool
              { \tl_set:Nn \l_@@_spvec_intent_arg_tl { _- } }
          }
          {
            \tl_set:Ne \l_@@_spvec_intent_arg_tl
              {
                _
                \tl_if_empty:NF \l_@@_spvec_prefix_tl
                  { \tl_use:N \l_@@_spvec_prefix_tl - }
                vector:prefix($pt)
              }
          }
        \str_case:VnF \l_@@_geo_luaset_tipo_str
          {
            { sub }
              {
                \str_if_eq:VnTF \l_@@_spvec_mode_str { school }
                  {
                    \bool_if:NTF \l_@@_spvec_silent_bool
                      {
                        \tl_set:Nn \l_@@_spvec_kv_tl { arg = "pt", intent = "_-" }
                      }
                      {
                        \tl_set:Ne \l_@@_spvec_kv_tl
                          {
                            arg = "pt", intent = "_ \l_@@_geo_luaset_intent_str -"
                          }
                      }
                    \@@_invisible_sep:
                    \MathMLintent { \l_@@_spvec_intent_arg_tl }
                      {
                        \@@_inner_sep:
                        \@@_luafun_mrow_intent_arg:nn
                          { } { \l_@@_spvec_kv_tl }
                        {
                          \vec
                            {
                              \bool_if:NT \l_@@_spvec_autofit_bool
                                { \vphantom{ b } }
                              \l_@@_geo_luaset_letra_str
                            }
                          \c_math_subscript_token
                            {
                              \MathMLintent { :silent }
                                { \l_@@_geo_luaset_sub_str }
                            }
                        }
                      }
                  }
                  {
                    \@@_invisible_sep:
                    \MathMLintent { \l_@@_spvec_intent_arg_tl }
                      {
                        \@@_inner_sep:
                        \vec
                          {
                            \bool_if:NT \l_@@_spvec_autofit_bool
                              { \vphantom{ b } }
                            \MathMLarg{ pt } { \l_@@_geo_luaset_letra_str }
                          }
                        \bool_if:NT \l_@@_spvec_silent_bool
                          { \c_math_subscript_token { \l_@@_geo_luaset_sub_str } }
                      }
                    \bool_if:NF \l_@@_spvec_silent_bool
                      { \c_math_subscript_token { \l_@@_geo_luaset_sub_str } }
                  }
              }
          }
          {
            \bool_if:NTF \l_@@_spvec_silent_bool
              { \tl_set:Nn \l_@@_spvec_kv_tl { arg = "pt", intent = "_-" } }
              {
                \tl_set:Ne \l_@@_spvec_kv_tl
                  { arg = "pt", intent = "_ \l_@@_geo_luaset_intent_str -" }
              }
            \@@_invisible_sep:
            \MathMLintent { \l_@@_spvec_intent_arg_tl }
              {
                \@@_inner_sep:
                \@@_luafun_mrow_intent_arg:nn
                  { } { \l_@@_spvec_kv_tl }
                {
                  \vec
                    {
                      \bool_if:NT \l_@@_spvec_autofit_bool
                        { \vphantom{ b } }
                      \l_@@_geo_luaset_print_tl
                    }
                }
              }
          }
      }
%    \end{macrocode}
%   For a pair of points, \myhlc|\l_@@_geo_luaset_points_clist| is
%   split into \myhlc|\l_@@_spvec_pt_seq| and walked to build
%   \myhlc|\l_@@_spvec_refs_tl|, the |$pts1,$pts2| reference list.
%   The concept |intent| is `\myhlc|_-|' when \mykey{silent} is |true|,
%   \myhlc|_(refs)| when \mykey{concept} is |false| or
%   \myhlc|_vector:prefix(refs)| otherwise, applied with a native |\MathMLintent| wrapping
%   |\overrightarrow|. The sequence is walked again, calling
%   \myhlc|\@@_mathmlarg_point:nnn| once per point.
%    \begin{macrocode}
      {
        \seq_set_from_clist:NN \l_@@_spvec_pt_seq
          \l_@@_geo_luaset_points_clist
        \tl_clear:N \l_@@_spvec_refs_tl
        \int_zero:N \l_@@_spvec_pt_idx_int
        \seq_map_inline:Nn \l_@@_spvec_pt_seq
          {
            \int_incr:N \l_@@_spvec_pt_idx_int
            \tl_if_empty:NF \l_@@_spvec_refs_tl
              { \tl_put_right:Nn \l_@@_spvec_refs_tl { , } }
            \tl_put_right:Ne \l_@@_spvec_refs_tl
              { $ pts \int_use:N \l_@@_spvec_pt_idx_int }
          }
        \bool_if:nTF
          {
            \bool_lazy_or_p:nn
              { \l_@@_spvec_silent_bool }
              { ! \l_@@_spvec_concept_bool }
          }
          {
            \tl_set:Ne \l_@@_spvec_intent_arg_tl
              { _( \l_@@_spvec_refs_tl ) }
            \bool_if:NT \l_@@_spvec_silent_bool
              { \tl_set:Nn \l_@@_spvec_intent_arg_tl { _- } }
          }
          {
            \tl_set:Ne \l_@@_spvec_intent_arg_tl
              {
                _
                \tl_if_empty:NF \l_@@_spvec_prefix_tl
                  { \tl_use:N \l_@@_spvec_prefix_tl - }
                vector:prefix( \l_@@_spvec_refs_tl )
              }
          }
        \@@_invisible_sep:
        \MathMLintent { \l_@@_spvec_intent_arg_tl }
          {
            \@@_inner_sep:
            \overrightarrow
              {
                \int_zero:N \l_@@_spvec_pt_idx_int
                \seq_map_inline:Nn \l_@@_spvec_pt_seq
                  {
                    \int_incr:N \l_@@_spvec_pt_idx_int
                    \@@_mathmlarg_point:nnn
                      { pts \int_use:N \l_@@_spvec_pt_idx_int }
                      { spvec }
                      { ##1 }
                  }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spvec}
%   The public command \ics*{spvec} enforces \emph{math mode}, opens a
%   local group, and delegates execution to \myhlc|\@@_spvec_exec:nnn|.
%    \begin{macrocode}
\NewDocumentCommand \spvec { O{} m d() }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_spvec_exec:nnn {#1} {#2} {#3}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spvec } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{sprecta}}\label{cmd:sprecta}
%
% The user command |\sprecta| supports two types of notation for the
% input \mymarg{argument}: when the argument contains two points separated by
% an optional comma, |\overleftrightarrow| is applied; when the
% argument is a single point without a comma or a lowercase letter, it is
% formatted without any visual decorator. Both notations are
% detected by the Lua module.
%
% \subsubsection{Definition of variables for \cs[no-index]{sprecta}}
%
% \begin{variable}[int]
%   {
%     \l_@@_sprecta_mode_str,
%     \l_@@_sprecta_prefix_tl,
%   }
% The variables \myhlc|\l_@@_sprecta_mode_str| and
% \myhlc|\l_@@_sprecta_prefix_tl| store the values set by the keys
% \mykey[sprecta]{read-cmd} and \mykey[sprecta]{prefix}.
%    \begin{macrocode}
\str_new:N \l_@@_sprecta_mode_str
\tl_new:N  \l_@@_sprecta_prefix_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_sprecta_pt_seq,
%     \l_@@_sprecta_pt_idx_int,
%     \l_@@_sprecta_refs_tl,
%   }
% The variable \myhlc|\l_@@_sprecta_pt_seq| stores the points of the
% argument, split at the commas, and \myhlc|\l_@@_sprecta_pt_idx_int| counts the
% point being read. The variable \myhlc|\l_@@_sprecta_refs_tl| stores the
% reference list |$pts1,$pts2,...| built from them.
%    \begin{macrocode}
\seq_new:N \l_@@_sprecta_pt_seq
\int_new:N \l_@@_sprecta_pt_idx_int
\tl_new:N  \l_@@_sprecta_refs_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_sprecta_intent_arg_tl,
%     \l_@@_sprecta_kv_tl,
%   }
% The variable \myhlc|\l_@@_sprecta_intent_arg_tl| stores the |intent| being
% built and \myhlc|\l_@@_sprecta_kv_tl| stores the key-value list with the
% attributes |arg| and |intent| of the points.
%    \begin{macrocode}
\tl_new:N \l_@@_sprecta_intent_arg_tl
\tl_new:N \l_@@_sprecta_kv_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{sprecta}}
%
% \begin{macro}[int]{sprecta-invalid-points}
%   The \enquote{error} |sprecta-invalid-points| is raised when the
%   argument matches neither the two-point notation nor the
%   name notation.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { sprecta-invalid-points }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \sprecta'~\msg_line_context:.
  }
  {
    The~argument~must~be~a~letter~or~two~points~separated~by~a~comma.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{sprecta}}
%
% \begin{macro}[int]{read-cmd, prefix, silent, concept, unknown}
%   We define the \mymeta{keys} \mykey[sprecta]{read-cmd}, \mykey[sprecta]{prefix},
%   \mykey[sprecta]{silent} and \mykey[sprecta]{concept}.
%    \begin{macrocode}
\keys_define:nn { spintent / sprecta }
  {
    read-cmd .choices:nn = { school , mathcat }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_sprecta_mode_str { school  } }
            {2} { \str_set:Nn \l_@@_sprecta_mode_str { mathcat } }
          }
      },
    read-cmd .initial:n = school,
    read-cmd .value_required:n = true,
    read-cmd / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-cmd } { school,~mathcat } { \exp_not:n {#1} },
    prefix   .code:n =
      { \@@_sanitize_affix:Nn \l_@@_sprecta_prefix_tl {#1} },
    prefix   .initial:n = ,
    prefix   .value_required:n = true,
    silent   .bool_set:N = \l_@@_sprecta_silent_bool,
    silent   .initial:n = false,
    silent   .value_required:n = true,
    concept  .bool_set:N = \l_@@_sprecta_concept_bool,
    concept  .initial:n = true,
    concept  .value_required:n = true,
    unknown  .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { sprecta }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{sprecta}}
%
% \begin{macro}[int]{\@@_sprecta_exec:nn}
%   The internal function \myhlc|\@@_sprecta_exec:nn| processes the
%   \mymarg{argument} passed in `|#2|'. Calls
%   \myhlc|\@@_luafun_geo_parse_and_set:n|, reporting
%   |sprecta-invalid-points| on error, then
%   \myhlc|\@@_sprecta_mathml:n|.
%
%   Before processing the \mymarg{argument}, it sets the optional \mymeta{keys} passed in `|#1|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sprecta_exec:nn
  {
    \keys_set:nn { spintent / sprecta } {#1}
    \@@_luafun_geo_parse_and_set:n { \exp_not:n {#2} }
    \str_if_eq:VnT \l_@@_geo_luaset_error_str { true }
      {
        \msg_error:nne { spintent } { sprecta-invalid-points } { \exp_not:n {#2} }
      }
    \@@_sprecta_mathml:n {#2}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_sprecta_mathml:n}
%   The function \myhlc|\@@_sprecta_mathml:n| branches on
%   \myhlc|\l_@@_geo_luaset_is_name_str|. For a proper name, such as |m| or |l|,
%   the concept |intent| is \myhlc|_($pt)| when \mykey{silent} is |true| \emph{or}
%   \mykey{concept} is |false|, and \myhlc|_recta:prefix($pt)| otherwise, with
%   \mykey{prefix} placed after the leading \myhlc|_| when given; with \mykey{silent}%
%   |=true| and \mykey{read-cmd} not |school| it is `\myhlc|_-|'. With
%   \mykey{read-cmd}|=school|, the |sub| and |prima| cases tag the letter with a
%   literal |kv| that is `\myhlc|_-|' when \mykey{silent} is |true|.
%
%   With \myhlc|\l_@@_geo_luaset_tipo_str| equal to |sub|,
%   \mykey{read-cmd}|=school| tags the base through
%   \myhlc|\@@_luafun_mrow_intent_arg:nn| with a literal |kv|,
%   the subscript appended and silenced; \mykey{read-cmd}|=mathcat| tags base
%   and subscript together through a native |\MathMLarg| instead.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sprecta_mathml:n
  {
    \str_if_eq:VnTF \l_@@_geo_luaset_is_name_str { true }
      {
        \@@_invisible_sep:
        \bool_if:nTF
          {
            \bool_lazy_or_p:nn
              { \l_@@_sprecta_silent_bool }
              { ! \l_@@_sprecta_concept_bool }
          }
          {
            \tl_set:Nn \l_@@_sprecta_intent_arg_tl { _($pt) }
        \bool_if:NT \l_@@_sprecta_silent_bool
          {
            \str_if_eq:VnF \l_@@_sprecta_mode_str { school }
              { \tl_set:Nn \l_@@_sprecta_intent_arg_tl { _- } }
          }
          }
          {
            \tl_set:Ne \l_@@_sprecta_intent_arg_tl
              {
                _
                \tl_if_empty:NF \l_@@_sprecta_prefix_tl
                  { \tl_use:N \l_@@_sprecta_prefix_tl - }
                recta:prefix($pt)
              }
          }
        \str_case:VnF \l_@@_geo_luaset_tipo_str
          {
            { sub }
              {
                \str_if_eq:VnTF \l_@@_sprecta_mode_str { school }
                  {
                    \bool_if:NTF \l_@@_sprecta_silent_bool
                      {
                        \tl_set:Nn \l_@@_sprecta_kv_tl
                          { arg = "pt", intent = "_-" }
                      }
                      {
                        \tl_set:Ne \l_@@_sprecta_kv_tl
                          {
                            arg = "pt", intent = "_ \l_@@_geo_luaset_intent_str -"
                          }
                      }
                    \MathMLintent { \l_@@_sprecta_intent_arg_tl }
                      {
                        \@@_inner_sep:
                        \@@_luafun_mrow_intent_arg:nn
                          { } { \l_@@_sprecta_kv_tl }
                        {
                          \l_@@_geo_luaset_letra_str
                          \c_math_subscript_token
                            {
                              \MathMLintent { :silent }
                                { \l_@@_geo_luaset_sub_str }
                            }
                        }
                      }
                  }
%    \end{macrocode}
%   For \mykey{read-cmd}|=mathcat|, the same |sub| case tags the base and the
%   subscript together with a native |\MathMLarg|, without using the shared
%   queue.
%    \begin{macrocode}
                  {
                    \MathMLintent { \l_@@_sprecta_intent_arg_tl }
                      {
                        {
                          \@@_inner_sep:
                          \MathMLarg { pt }
                            {
                              {
                                \l_@@_geo_luaset_letra_str
                                \c_math_subscript_token
                                  { \l_@@_geo_luaset_sub_str }
                              }
                            }
                        }
                      }
                  }
              }
%    \end{macrocode}
%   With \myhlc|\l_@@_geo_luaset_tipo_str| equal to |prima|, the same
%   distinction applies: \mykey{read-cmd}|=school| tags the base with the
%   literal |kv| and appends the mark, unsilenced, since a prime is not
%   spelled out on the base and there is nothing to repeat; \mykey{read-cmd}|=mathcat|
%   tags base and mark together through a native |\MathMLarg|.
%    \begin{macrocode}
            { prima }
              {
                \str_if_eq:VnTF \l_@@_sprecta_mode_str { school }
                  {
                    \bool_if:NTF \l_@@_sprecta_silent_bool
                      {
                        \tl_set:Nn \l_@@_sprecta_kv_tl
                          { arg = "pt", intent = "_-" }
                      }
                      {
                        \tl_set:Ne \l_@@_sprecta_kv_tl
                          {
                            arg = "pt", intent = "_ \l_@@_geo_luaset_intent_str -"
                          }
                      }
                    \MathMLintent { \l_@@_sprecta_intent_arg_tl }
                      {
                        \@@_inner_sep:
                        \@@_luafun_mrow_intent_arg:nn
                          { } { \l_@@_sprecta_kv_tl }
                        {
                          \l_@@_geo_luaset_letra_str
                            \l_@@_geo_luaset_sub_str
                        }
                      }
                  }
                  {
                    \MathMLintent { \l_@@_sprecta_intent_arg_tl }
                      {
                        {
                          \@@_inner_sep:
                          \MathMLarg { pt }
                            {
                              {
                                \l_@@_geo_luaset_letra_str
                                  \l_@@_geo_luaset_sub_str
                              }
                            }
                        }
                      }
                  }
              }
          }
%    \end{macrocode}
%   Otherwise, the same |kv| logic wraps the combined
%   \myhlc|\l_@@_geo_luaset_print_tl| as a whole: with the shared queue for
%   \mykey{read-cmd}|=school| and with its own |arg| alone for
%   \mykey{read-cmd}|=mathcat|.
%    \begin{macrocode}
          {
            \bool_if:NTF \l_@@_sprecta_silent_bool
              {
                \tl_set:Nn \l_@@_sprecta_kv_tl { arg = "pt", intent = "_-" }
              }
              {
                \str_if_eq:VnTF \l_@@_sprecta_mode_str { school }
                  {
                    \tl_set:Ne \l_@@_sprecta_kv_tl
                      { arg = "pt", intent = "_ \l_@@_geo_luaset_intent_str -" }
                  }
                  {
                    \tl_set:Nn \l_@@_sprecta_kv_tl { arg = "pt" }
                  }
              }
            \@@_luafun_mrow_intent_arg:nn
              { \l_@@_sprecta_intent_arg_tl }
              { \l_@@_sprecta_kv_tl }
            { \l_@@_geo_luaset_print_tl }
          }
      }
%    \end{macrocode}
%   For a list of points, \myhlc|\l_@@_geo_luaset_points_clist| is
%   split into \myhlc|\l_@@_sprecta_pt_seq| and walked to build
%   \myhlc|\l_@@_sprecta_refs_tl|, the |$pt1,$pt2,...| reference list.
%   The concept |intent| is `\myhlc|_-|' when \mykey{silent} is |true|,
%   \myhlc|_(refs)| when \mykey{concept} is |false| or \myhlc|_recta:prefix(refs)|
%   otherwise, applied with a native |\MathMLintent|
%   wrapping |\overleftrightarrow|. The sequence is walked again,
%   calling \myhlc|\@@_mathmlarg_point:nnn| once per point.
%    \begin{macrocode}
      {
        \seq_set_from_clist:NN \l_@@_sprecta_pt_seq
          \l_@@_geo_luaset_points_clist
        \tl_clear:N \l_@@_sprecta_refs_tl
        \int_zero:N \l_@@_sprecta_pt_idx_int
        \seq_map_inline:Nn \l_@@_sprecta_pt_seq
          {
            \int_incr:N \l_@@_sprecta_pt_idx_int
            \tl_if_empty:NF \l_@@_sprecta_refs_tl
              { \tl_put_right:Nn \l_@@_sprecta_refs_tl { , } }
            \tl_put_right:Ne \l_@@_sprecta_refs_tl
              { $ pt \int_use:N \l_@@_sprecta_pt_idx_int }
          }
        \bool_if:nTF
          {
            \bool_lazy_or_p:nn
              { \l_@@_sprecta_silent_bool }
              { ! \l_@@_sprecta_concept_bool }
          }
          {
            \tl_set:Ne \l_@@_sprecta_intent_arg_tl
              { _( \l_@@_sprecta_refs_tl ) }
            \bool_if:NT \l_@@_sprecta_silent_bool
              { \tl_set:Nn \l_@@_sprecta_intent_arg_tl { _- } }
          }
          {
            \tl_set:Ne \l_@@_sprecta_intent_arg_tl
              {
                _
                \tl_if_empty:NF \l_@@_sprecta_prefix_tl
                  { \tl_use:N \l_@@_sprecta_prefix_tl - }
                recta:prefix( \l_@@_sprecta_refs_tl )
              }
          }
        \@@_invisible_sep:
        \MathMLintent { \l_@@_sprecta_intent_arg_tl }
          {
            \@@_inner_sep:
            \overleftrightarrow
              {
                \int_zero:N \l_@@_sprecta_pt_idx_int
                \seq_map_inline:Nn \l_@@_sprecta_pt_seq
                  {
                    \int_incr:N \l_@@_sprecta_pt_idx_int
                    \@@_mathmlarg_point:nnn
                      { pt \int_use:N \l_@@_sprecta_pt_idx_int }
                      { sprecta }
                      { ##1 }
                  }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\sprecta}
%   The public command \ics*{sprecta} enforces \emph{math mode}, opens a local
%   group, and delegates execution to \myhlc|\@@_sprecta_exec:nn|.
%    \begin{macrocode}
\NewDocumentCommand \sprecta { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_sprecta_exec:nn {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \sprecta } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{sprayo}}\label{cmd:sprayo}
%
% The user command |\sprayo| provides a semantic interface for a ray.
% The key \mykey[sprayo]{read-sty} selects the verbalized term,
% |rayo| or |semirrecta|, both rendered identically with |\overrightarrow|.
%
% \subsubsection{Definition of variables for \cs[no-index]{sprayo}}
%
% \begin{variable}[int]
%   {
%     \l_@@_sprayo_mode_str,
%     \l_@@_sprayo_prefix_tl,
%     \l_@@_sprayo_read_sty_str,
%   }
%   The variable \myhlc|\l_@@_sprayo_mode_str| stores the value set by the
%   key \mykey[sprayo]{read-cmd}, the variable
%   \myhlc|\l_@@_sprayo_prefix_tl| the value set by the key
%   \mykey[sprayo]{prefix} and the variable
%   \myhlc|\l_@@_sprayo_read_sty_str| the value set by the key
%   \mykey[sprayo]{read-sty}.
%
%   \begin{important}*
%    The variables defined here are assigned using \myhlc|.choices:nn|
%    or \myhlc|.code:n| within \myhlc|\keys_define:nn|.
%   \end{important}
%    \begin{macrocode}
\str_new:N \l_@@_sprayo_mode_str
\tl_new:N  \l_@@_sprayo_prefix_tl
\str_new:N \l_@@_sprayo_read_sty_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_sprayo_pt_seq,
%     \l_@@_sprayo_pt_idx_int,
%     \l_@@_sprayo_refs_tl,
%   }
% The variable \myhlc|\l_@@_sprayo_pt_seq| stores the points taken from
% \myhlc|\l_@@_geo_luaset_points_clist| by \myhlc|\@@_sprayo_mathml:| and
% \myhlc|\l_@@_sprayo_pt_idx_int| counts the point being read. The variable
% \myhlc|\l_@@_sprayo_refs_tl| stores the reference list |$pts1,$pts2,...|
% built from them.
%    \begin{macrocode}
\seq_new:N \l_@@_sprayo_pt_seq
\int_new:N \l_@@_sprayo_pt_idx_int
\tl_new:N  \l_@@_sprayo_refs_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_sprayo_intent_arg_tl}
% The variable \myhlc|\l_@@_sprayo_intent_arg_tl| stores the |intent| being
% built.
%    \begin{macrocode}
\tl_new:N \l_@@_sprayo_intent_arg_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{sprayo}}
%
% \begin{macro}[int]{sprayo-invalid-points}
%   The \enquote{error} |sprayo-invalid-points| is raised when the argument is
%   not a comma-separated pair of uppercase letters.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { sprayo-invalid-points }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \sprayo'~\msg_line_context:.
  }
  {
    The~argument~must~be~two~points~separated~by~a~comma.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{sprayo}}
%
% \begin{macro}[int]{read-cmd, prefix, silent, concept, read-sty, unknown}
%   We define the \mymeta{keys} \mykey[sprayo]{read-cmd},
%   \mykey[sprayo]{prefix}, \mykey[sprayo]{silent}, \mykey[sprayo]{concept}
%   and \mykey[sprayo]{read-sty}.
%    \begin{macrocode}
\keys_define:nn { spintent / sprayo }
  {
    read-cmd .choices:nn = { school , mathcat }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_sprayo_mode_str { school  } }
            {2} { \str_set:Nn \l_@@_sprayo_mode_str { mathcat } }
          }
      },
    read-cmd .initial:n = school,
    read-cmd .value_required:n = true,
    read-cmd / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-cmd } { school,~mathcat } { \exp_not:n {#1} },
    prefix   .code:n =
      { \@@_sanitize_affix:Nn \l_@@_sprayo_prefix_tl {#1} },
    prefix   .initial:n = ,
    prefix   .value_required:n = true,
    silent   .bool_set:N = \l_@@_sprayo_silent_bool,
    silent   .initial:n = false,
    silent   .value_required:n = true,
    concept  .bool_set:N = \l_@@_sprayo_concept_bool,
    concept  .initial:n = true,
    concept  .value_required:n = true,
    read-sty .choices:nn = { rayo , semirrecta }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_sprayo_read_sty_str { rayo       } }
            {2} { \str_set:Nn \l_@@_sprayo_read_sty_str { semirrecta } }
          }
      },
    read-sty .initial:n = rayo,
    read-sty .value_required:n = true,
    read-sty / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-sty } { rayo,~semirrecta } { \exp_not:n {#1} },
    unknown  .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { sprayo }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{sprayo}}
%
% \begin{macro}[int]{\@@_sprayo_exec:nn}
%   The internal function \myhlc|\@@_sprayo_exec:nn| processes the
%   \mymarg{argument} passed in `|#2|'. It calls
%   \myhlc|\@@_luafun_geo_parse_and_set:n| to process the \mymarg{argument},
%   returning an \enquote{error} if the input is invalid, then calls
%   \myhlc|\@@_sprayo_mathml:|. The command |\sprayo| always takes a list of
%   points, so no |intent| is built here.
%
%   Before processing the \mymarg{argument}, it sets the optional \mymeta{keys} passed in `|#1|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sprayo_exec:nn
  {
    \keys_set:nn { spintent / sprayo } {#1}
    \@@_luafun_geo_parse_and_set:n { \exp_not:n {#2} }
    \bool_if:nT
      {
        \str_if_eq_p:Vn \l_@@_geo_luaset_error_str { true } ||
        \str_if_eq_p:Vn \l_@@_geo_luaset_is_name_str { true }
      }
      {
        \msg_error:nne { spintent } { sprayo-invalid-points } { \exp_not:n {#2} }
      }
    \@@_sprayo_mathml:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_sprayo_mathml:}
%   The function \myhlc|\@@_sprayo_mathml:| splits
%   \myhlc|\l_@@_geo_luaset_points_clist|, which Lua sets with one raw point per
%   element, into \myhlc|\l_@@_sprayo_pt_seq| and walks it once to
%   build \myhlc|\l_@@_sprayo_refs_tl|, the comma-separated
%   |$pts1,$pts2,...| reference list, naming each point by position.
%
%   The concept |intent| is `\myhlc|_-|' when \mykey{silent} is |true|, \myhlc|_(refs)| when
%   \mykey{concept} is |false|, and otherwise
%   \myhlc|_\l_@@_sprayo_read_sty_str|\myhlc|:prefix(refs)|, where the word
%   |rayo| or |semirrecta| comes from \mykey{read-sty}, with \mykey{prefix}
%   placed after the leading \myhlc|_| when given. It is applied with a native
%   |\MathMLintent| wrapping |\overrightarrow|, preceded by
%   \myhlc|\@@_inner_sep:|. The sequence is walked a second time, calling
%   \myhlc|\@@_mathmlarg_point:nnn| once per point with its |pts|$n$ reference
%   name and the values of \mykey[sprayo]{silent} and \mykey[sprayo]{read-cmd}
%   looked up under |sprayo|. This call decides |arg| or the literal |intent|
%   per point, so \mykey{concept} plays no role at that level.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sprayo_mathml:
  {
    \seq_set_from_clist:NN \l_@@_sprayo_pt_seq
      \l_@@_geo_luaset_points_clist
    \tl_clear:N \l_@@_sprayo_refs_tl
    \int_zero:N \l_@@_sprayo_pt_idx_int
    \seq_map_inline:Nn \l_@@_sprayo_pt_seq
      {
        \int_incr:N \l_@@_sprayo_pt_idx_int
        \tl_if_empty:NF \l_@@_sprayo_refs_tl
          { \tl_put_right:Nn \l_@@_sprayo_refs_tl { , } }
        \tl_put_right:Ne \l_@@_sprayo_refs_tl
          { $ pts \int_use:N \l_@@_sprayo_pt_idx_int }
      }
    \bool_if:nTF
      {
        \bool_lazy_or_p:nn
          { \l_@@_sprayo_silent_bool }
          { ! \l_@@_sprayo_concept_bool }
      }
      {
        \tl_set:Ne \l_@@_sprayo_intent_arg_tl
          { _( \l_@@_sprayo_refs_tl ) }
        \bool_if:NT \l_@@_sprayo_silent_bool
          { \tl_set:Nn \l_@@_sprayo_intent_arg_tl { _- } }
      }
      {
        \tl_set:Ne \l_@@_sprayo_intent_arg_tl
          {
            _
            \tl_if_empty:NF \l_@@_sprayo_prefix_tl
              { \tl_use:N \l_@@_sprayo_prefix_tl - }
            \l_@@_sprayo_read_sty_str :prefix( \l_@@_sprayo_refs_tl )
          }
      }
    \@@_invisible_sep:
    \MathMLintent { \l_@@_sprayo_intent_arg_tl }
      {
        \@@_inner_sep:
        \overrightarrow
          {
            \int_zero:N \l_@@_sprayo_pt_idx_int
            \seq_map_inline:Nn \l_@@_sprayo_pt_seq
              {
                \int_incr:N \l_@@_sprayo_pt_idx_int
                \@@_mathmlarg_point:nnn
                  { pts \int_use:N \l_@@_sprayo_pt_idx_int }
                  { sprayo }
                  { ##1 }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\sprayo}
%   The public command \ics*{sprayo} enforces \emph{math mode}, opens a local
%   group, and delegates execution to \myhlc|\@@_sprayo_exec:nn|.
%    \begin{macrocode}
\NewDocumentCommand \sprayo { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_sprayo_exec:nn {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \sprayo } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The commands \cs{spside} and \cs{spmside}}\label{cmd:spside}
%
% The user commands |\spside| and |\spmside| provide a semantic interface for a
% side, segment or trace determined by two points. The key
% \mykey[spside]{read-sty} selects the verbalized term among |lado|, |segmento| and
% |trazo|, and the key \mykey[spside]{read-arg} overrides that term with a
% free-form string. The command |\spmside| also provides the key
% \mykey[spmside]{print-sym}, which controls whether |\operatorname{m}| is
% typeset before the argument.
%
% \subsubsection{Definition of variables for \cs[no-index]{spside} and
%   \cs[no-index]{spmside}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spside_mode_str,
%     \l_@@_spmside_mode_str,
%     \l_@@_spside_prefix_tl,
%     \l_@@_spmside_prefix_tl,
%     \l_@@_spside_read_sty_str,
%     \l_@@_spmside_read_sty_str,
%     \l_@@_spside_read_arg_tl,
%     \l_@@_spmside_read_arg_tl,
%   }
%   Each pair of variables is declared once for both |\spside| and
%   |\spmside| via \myhlc|\clist_map_inline:nn|. The variables \myhlc|\l_@@_spside_mode_str|
%   and \myhlc|\l_@@_spmside_mode_str| store the values set by the key \mykey[spside,spmside]{read-cmd};
%   the variables \myhlc|\l_@@_spside_prefix_tl| and \myhlc|\l_@@_spmside_prefix_tl|
%   store the values set by the key \mykey[spside,spmside]{prefix}.
%
%   The variables \myhlc|\l_@@_spside_read_sty_str| and \myhlc|\l_@@_spmside_read_sty_str|
%   store the value set by the key \mykey[spside,spmside]{read-sty}.
%
%   The variables \myhlc|\l_@@_spside_read_arg_tl| and \myhlc|\l_@@_spmside_read_arg_tl|
%   store the value set by the key \mykey[spside,spmside]{read-arg}.
%
%   \begin{important}*
%     The variables are defined here as they are assigned using \myhlc|.choices:nn|
%     or \myhlc|.code:n| rather than a direct property within \myhlc|\keys_define:nn|.
%   \end{important}
%    \begin{macrocode}
\clist_map_inline:nn { side , mside }
  {
    \str_new:c { l_@@_sp #1 _mode_str     }
    \tl_new:c  { l_@@_sp #1 _prefix_tl    }
    \str_new:c { l_@@_sp #1 _read_sty_str }
    \tl_new:c  { l_@@_sp #1 _read_arg_tl  }
  }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_spside_intent_tl}
% The variable \myhlc|\l_@@_spside_intent_tl| stores the concept word, taken
% from \mykey{read-arg} or \mykey{read-sty}. It is shared by |\spside| and
% |\spmside|.
%    \begin{macrocode}
\tl_new:N \l_@@_spside_intent_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spside_pt_seq,
%     \l_@@_spside_pt_idx_int,
%     \l_@@_spside_refs_tl,
%   }
% The variable \myhlc|\l_@@_spside_pt_seq| stores the points taken from
% \myhlc|\l_@@_geo_luaset_points_clist| and \myhlc|\l_@@_spside_pt_idx_int|
% counts the point being read. The variable \myhlc|\l_@@_spside_refs_tl|
% stores the reference list |$pts1,$pts2,...| built from them. They are
% shared by |\spside| and |\spmside|.
%    \begin{macrocode}
\seq_new:N \l_@@_spside_pt_seq
\int_new:N \l_@@_spside_pt_idx_int
\tl_new:N  \l_@@_spside_refs_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_spside_intent_arg_tl}
% The variable \myhlc|\l_@@_spside_intent_arg_tl| stores the |intent| being
% built. It is shared by |\spside| and |\spmside|.
%    \begin{macrocode}
\tl_new:N \l_@@_spside_intent_arg_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spside}
%   and \cs[no-index]{spmside}}
%
% \begin{macro}[int]{spside-invalid-points, spmside-invalid-points}
%   The \enquote{error}s |spside-invalid-points| and |spmside-invalid-points|
%   are raised when the user includes an invalid \emph{\enquote{point}} in the
%   \mymarg{argument} passed in `|#1|' to |\spside| or |\spmside|.
%    \begin{macrocode}
\clist_map_inline:nn { side , mside }
  {
    \msg_new:nnnn { spintent } { sp #1 -invalid-points }
      {
        Invalid~argument~'##2'~for~'\tl_trim_spaces:n {##1}'~\msg_line_context:.
      }
      {
        The~argument~must~be~two~points~separated~by~a~comma.
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spside} and
%   \cs[no-index]{spmside}}
%
% \begin{macro}[int]
%   {
%     read-cmd,
%     prefix,
%     silent,
%     concept,
%     read-sty,
%     read-arg,
%     print-sym,
%     unknown,
%   }
%   We define the shared \mymeta{keys} \mykey[spside,spmside]{read-cmd},
%   \mykey[spside,spmside]{prefix}, \mykey[spside,spmside]{silent},
%   \mykey[spside,spmside]{concept}, \mykey[spside,spmside]{read-sty},
%   \mykey[spside,spmside]{read-arg} and \mykey[spmside]{print-sym}.
%    \begin{macrocode}
\clist_map_inline:nn { side , mside }
  {
    \keys_define:nn { spintent / sp #1 }
      {
        read-cmd  .choices:nn = { school , mathcat }
          {
            \int_case:nn { \l_keys_choice_int }
              {
                {1} { \str_set:cn { l_@@_sp #1 _mode_str } { school  } }
                {2} { \str_set:cn { l_@@_sp #1 _mode_str } { mathcat } }
              }
          },
        read-cmd  .initial:n = school,
        read-cmd  .value_required:n = true,
        read-cmd / unknown .code:n =
          \msg_error:nneee { spintent } { unknown-choice }
            { read-cmd } { school,~mathcat } { \exp_not:n {##1} },
        prefix    .code:n =
          { \@@_sanitize_affix:cn { l_@@_sp #1 _prefix_tl } {##1} },
        prefix    .initial:n = ,
        prefix    .value_required:n = true,
        silent    .bool_set:c = { l_@@_sp #1 _silent_bool },
        silent    .initial:n = false,
        silent    .value_required:n = true,
        concept   .bool_set:c = { l_@@_sp #1 _concept_bool },
        concept   .initial:n = true,
        concept   .value_required:n = true,
        read-sty  .choices:nn = { lado , segmento , trazo }
          {
            \int_case:nn { \l_keys_choice_int }
              {
                {1} { \str_set:cn { l_@@_sp #1 _read_sty_str } { lado      } }
                {2} { \str_set:cn { l_@@_sp #1 _read_sty_str } { segmento  } }
                {3} { \str_set:cn { l_@@_sp #1 _read_sty_str } { trazo     } }
              }
          },
        read-sty  .initial:n = lado,
        read-sty  .value_required:n = true,
        read-sty / unknown .code:n =
          \msg_error:nneee { spintent } { unknown-choice }
            { read-sty } { lado,~segmento,~trazo } { \exp_not:n {##1} },
        read-arg  .code:n =
          {
            \@@_sanitize_affix:cn { l_@@_sp #1 _read_arg_tl } {##1}
          },
        read-arg  .initial:n = ,
        read-arg  .value_required:n = true,
        print-sym .bool_set:c = { l_@@_sp #1 _print_sym_bool },
        print-sym .initial:n = false,
        print-sym .value_required:n = true,
        unknown   .code:n =
          {
            \str_set:Nn \l_@@_command_name_str { sp #1 }
            \@@_unknown_keys_cmd:n {##1}
          },
      }
  }
%    \end{macrocode}
%   \begin{important}*
%     The key \mykey{print-sym} is defined for both commands for
%     simplicity, but has no effect on |\spside|.
%   \end{important}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spside} and
%   \cs[no-index]{spmside}}
%
% \begin{macro}[int]{\@@_spside_exec:nnn}
%   The internal function \myhlc|\@@_spside_exec:nnn| receives the
%   short name of the command, |side| or |mside|, in `|#1|' and the
%   \mymarg{argument} in `|#3|'. It calls
%   \myhlc|\@@_luafun_geo_parse_and_set:n| to process `|#3|', returning
%   an \enquote{error} if invalid, then builds \myhlc|\l_@@_spside_intent_tl|.
%
%   When \mykey{read-arg} is empty, it is set from \mykey{read-sty},
%   prefixed with `|medida-del-|' for |\spmside|. When \mykey{read-arg}
%   is given, it is used as is, and `|medida-del-|' is prepended only
%   if \mykey{concept} is |true|: with \mykey{concept}|=false|, \mykey{read-arg} is
%   the verbatim escape hatch for a full user-written phrase such as
%   |read-arg={medida de la hipotenusa}|, with gender and article left to
%   the user. No |intent| is built here: it needs the reference list,
%   which \myhlc|\@@_spside_mathml:n| builds after splitting the
%   points.
%
%   Before processing the \mymarg{argument}, it sets the optional \mymeta{keys} passed in `|#2|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spside_exec:nnn
  {
    \keys_set:nn { spintent / sp #1 } {#2}
    \@@_luafun_geo_parse_and_set:n { \exp_not:n {#3} }
    \bool_if:nT
      {
        \str_if_eq_p:Vn \l_@@_geo_luaset_error_str { true } ||
        \str_if_eq_p:Vn \l_@@_geo_luaset_is_name_str { true }
      }
      {
        \msg_error:nnee { spintent } { sp #1 -invalid-points }
          { \c_backslash_str sp #1 } { \exp_not:n {#3} }
      }
    \tl_if_empty:cTF { l_@@_sp #1 _read_arg_tl }
      {
        \tl_set:Ne \l_@@_spside_intent_tl
          { \str_use:c { l_@@_sp #1 _read_sty_str } }
        \str_if_eq:eeT {#1} { mside }
          {
            \tl_put_left:Nn \l_@@_spside_intent_tl { medida-del- }
          }
      }
      {
        \tl_set:Ne \l_@@_spside_intent_tl
          { \tl_use:c { l_@@_sp #1 _read_arg_tl } }
        \bool_if:cT { l_@@_sp #1 _concept_bool }
          {
            \str_if_eq:eeT {#1} { mside }
              {
                \tl_put_left:Nn \l_@@_spside_intent_tl { medida-del- }
              }
          }
      }
    \@@_spside_mathml:n {#1}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spside_mathml:n}
%   The function \myhlc|\@@_spside_mathml:n| receives the short command
%   name in `|#1|'. It splits \myhlc|\l_@@_geo_luaset_points_clist| into
%   \myhlc|\l_@@_spside_pt_seq| and walks it once to build
%   \myhlc|\l_@@_spside_refs_tl|, the comma-separated |$pts1,$pts2,...|
%   reference list, naming each point by position.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spside_mathml:n
  {
    \seq_set_from_clist:NN \l_@@_spside_pt_seq
      \l_@@_geo_luaset_points_clist
    \tl_clear:N \l_@@_spside_refs_tl
    \int_zero:N \l_@@_spside_pt_idx_int
    \seq_map_inline:Nn \l_@@_spside_pt_seq
      {
        \int_incr:N \l_@@_spside_pt_idx_int
        \tl_if_empty:NF \l_@@_spside_refs_tl
          { \tl_put_right:Nn \l_@@_spside_refs_tl { , } }
        \tl_put_right:Ne \l_@@_spside_refs_tl
          { $ pts \int_use:N \l_@@_spside_pt_idx_int }
      }
%    \end{macrocode}
%
%   The concept |intent| is `\myhlc|_-|' when \mykey{silent} is |true|. Otherwise, if
%   \mykey{read-arg} is empty, it is \myhlc|_(refs)| when \mykey{concept} is |false|
%   and \myhlc|_\l_@@_spside_intent_tl|\myhlc|:prefix(refs)| when |true|; if
%   \mykey{read-arg} is given, it is always
%   \myhlc|_\l_@@_spside_intent_tl|\myhlc|:prefix(refs)|. The |:prefix(...)|
%   form is prefixed with \mykey{prefix} when given.
%    \begin{macrocode}
    \bool_if:cTF { l_@@_sp #1 _silent_bool }
      {
        \tl_set:Nn \l_@@_spside_intent_arg_tl { _- }
      }
      {
        \tl_if_empty:cTF { l_@@_sp #1 _read_arg_tl }
          {
            \bool_if:cTF { l_@@_sp #1 _concept_bool }
              {
                \tl_set:Ne \l_@@_spside_intent_arg_tl
                  {
                    _
                    \tl_if_empty:cF { l_@@_sp #1 _prefix_tl }
                      { \tl_use:c { l_@@_sp #1 _prefix_tl } - }
                    \l_@@_spside_intent_tl :prefix( \l_@@_spside_refs_tl )
                  }
              }
              {
                \tl_set:Ne \l_@@_spside_intent_arg_tl
                  { _( \l_@@_spside_refs_tl ) }
              }
          }
          {
            \tl_set:Ne \l_@@_spside_intent_arg_tl
              {
                _
                \tl_if_empty:cF { l_@@_sp #1 _prefix_tl }
                  { \tl_use:c { l_@@_sp #1 _prefix_tl } - }
                \l_@@_spside_intent_tl :prefix( \l_@@_spside_refs_tl )
              }
          }
      }
%    \end{macrocode}
%   It is applied with a native |\MathMLintent|, preceded by
%   \myhlc|\@@_inner_sep:|. Inside, for |#1=side| the points are wrapped in
%   |\overline|; for |#1=mside|, a silent |\operatorname{m}| precedes the
%   |\overline|-wrapped points when \mykey{print-sym} is |true|, and the points
%   are typeset bare, without |\overline|, when it is |false|. In every case the
%   sequence is walked a second time, calling \myhlc|\@@_mathmlarg_point:nnn|
%   once per point with its |pts|$n$ reference name and the values of
%   \mykey[spside,spmside]{silent} and \mykey[spside,spmside]{read-cmd} looked
%   up under \texttt{spside} or \texttt{spmside}. This call decides |arg| or the
%   literal |intent| per point, so \mykey{concept} plays no role at that level.
%    \begin{macrocode}
    \@@_invisible_sep: % (luamml issues #26)
    \MathMLintent { \l_@@_spside_intent_arg_tl }
      {
        \@@_inner_sep:
        \str_if_eq:eeTF {#1} { side }
          {
            \overline { \@@_spside_points_loop:n {#1} }
          }
          {
            \bool_if:cTF { l_@@_sp #1 _print_sym_bool }
              {
                \@@_luafun_mrow_intent_arg:nn
                  { } { intent = ":silent" }
                { \operatorname { m } }
                \overline { \@@_spside_points_loop:n {#1} }
              }
              {
                \@@_spside_points_loop:n {#1}
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spside_points_loop:n}
%   Walks \myhlc|\l_@@_spside_pt_seq| a second time, calling
%   \myhlc|\@@_mathmlarg_point:nnn| once per point. Factored out of
%   \myhlc|\@@_spside_mathml:n| since it is needed identically in three
%   places there: |side|, and |mside| with and without \mykey{print-sym}; it
%   takes the short command name in `|#1|' again since a separate
%   macro does not inherit its caller's own |#1|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spside_points_loop:n
  {
    \int_zero:N \l_@@_spside_pt_idx_int
    \seq_map_inline:Nn \l_@@_spside_pt_seq
      {
        \int_incr:N \l_@@_spside_pt_idx_int
        \@@_mathmlarg_point:nnn
          { pts \int_use:N \l_@@_spside_pt_idx_int }
          { sp #1 }
          { ##1 }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spside, \spmside}
%   Both public commands \ics*{spside} and \ics*{spmside} enforces \emph{math mode},
%   opens a local group, and delegates execution to \myhlc|\@@_spside_exec:nnn|.
%    \begin{macrocode}
\NewDocumentCommand \spside { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_spside_exec:nnn { side } {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spside } }
  }
\NewDocumentCommand \spmside { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_spside_exec:nnn { mside } {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spmside } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The commands \cs{spangle} and \cs{spmangle}}\label{cmd:spangle}
%
% The user commands |\spangle| and |\spmangle| provide a semantic interface for
% angles. The key \mykey[spmangle]{print-sym} chooses between the
% |\angle|/|\rightanglesqr| pair, typeset together with an explicit
% |\operatorname{m}|, and the |\measuredangle|/|\measuredrightangle| pair,
% which already conveys the notion of measure; the key
% \mykey[spangle,spmangle]{recto} chooses the right-angle variant of whichever
% pair is active.
%
% \subsubsection{Definition of variables for \cs[no-index]{spangle} and
%   \cs[no-index]{spmangle}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spangle_mode_str,
%     \l_@@_spmangle_mode_str,
%     \l_@@_spangle_prefix_tl,
%     \l_@@_spmangle_prefix_tl,
%   }
%   Each pair of variables is declared once for both |\spangle| and
%   |\spmangle| via \myhlc|\clist_map_inline:nn|. The variables \myhlc|\l_@@_spangle_mode_str|
%   and \myhlc|\l_@@_spmangle_mode_str| store the values set by the key
%   \mykey[spangle,spmangle]{read-cmd}; the variables \myhlc|\l_@@_spangle_prefix_tl|
%   and \myhlc|\l_@@_spmangle_prefix_tl| store the values set by the key
%   \mykey[spangle,spmangle]{prefix}.
%
%   \begin{important}*
%    The variables defined here are assigned using \myhlc|.choices:nn|
%    or \myhlc|.code:n| within \myhlc|\keys_define:nn|.
%   \end{important}
%    \begin{macrocode}
\clist_map_inline:nn { angle , mangle }
  {
    \str_new:c { l_@@_sp #1 _mode_str  }
    \tl_new:c  { l_@@_sp #1 _prefix_tl }
  }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spangle_intent_tl,
%     \l_@@_spangle_print_sym_now_bool,
%   }
% The variable \myhlc|\l_@@_spangle_intent_tl| stores the concept word,
% |ángulo| or |medida-del-ángulo|, with the suffix |-recto| when \mykey{recto}
% is |true|. The variable \myhlc|\l_@@_spangle_print_sym_now_bool| tells
% whether the symbol is typeset at this moment. Both are shared by
% |\spangle| and |\spmangle|.
%    \begin{macrocode}
\tl_new:N   \l_@@_spangle_intent_tl
\bool_new:N \l_@@_spangle_print_sym_now_bool
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spangle_pt_seq,
%     \l_@@_spangle_pt_idx_int,
%     \l_@@_spangle_refs_tl,
%   }
% The variable \myhlc|\l_@@_spangle_pt_seq| stores the points taken from
% \myhlc|\l_@@_geo_luaset_points_clist| and \myhlc|\l_@@_spangle_pt_idx_int|
% counts the point being read. The variable \myhlc|\l_@@_spangle_refs_tl|
% stores the reference list built from them, in the |tres-puntos| and
% |un-punto| cases of \myhlc|\@@_spangle_mathml:n|.
%    \begin{macrocode}
\seq_new:N \l_@@_spangle_pt_seq
\int_new:N \l_@@_spangle_pt_idx_int
\tl_new:N  \l_@@_spangle_refs_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spangle_intent_arg_tl,
%     \l_@@_spangle_kv_tl,
%   }
% The variable \myhlc|\l_@@_spangle_intent_arg_tl| stores the |intent| being
% built. The variable \myhlc|\l_@@_spangle_kv_tl| stores the key-value list
% passed to \myhlc|\@@_luafun_mrow_intent_arg:nn|.
%    \begin{macrocode}
\tl_new:N \l_@@_spangle_intent_arg_tl
\tl_new:N \l_@@_spangle_kv_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spangle}
%   and \cs[no-index]{spmangle}}
%
% \begin{macro}[int]{spangle-invalid-arg, spmangle-invalid-arg}
%   The \enquote{error}s |spangle-invalid-arg| and |spmangle-invalid-arg| are
%   raised when the user includes an invalid \mymarg{argument} passed in
%   `|#1|' to |\spangle| or |\spmangle|.
%    \begin{macrocode}
\clist_map_inline:nn { angle , mangle }
  {
    \msg_new:nnnn { spintent } { sp #1 -invalid-arg }
      {
        Invalid~argument~'##2'~for~'\tl_trim_spaces:n {##1}'~\msg_line_context:.
      }
      {
        Use~a~point,~a~Greek~letter,~a~number~or~three~points~separated~by~
        commas.
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spangle} and
%   \cs[no-index]{spmangle}}
%
% \begin{macro}[int]
%   {
%     read-cmd,
%     prefix,
%     recto,
%     print-sym,
%     silent,
%     concept,
%     unknown,
%   }
%   We define the shared \mymeta{keys} \mykey[spangle,spmangle]{read-cmd},
%   \mykey[spangle,spmangle]{prefix}, \mykey[spangle,spmangle]{recto},
%   \mykey[spangle,spmangle]{print-sym}, \mykey[spangle,spmangle]{silent}
%   and \mykey[spangle,spmangle]{concept},
%    \begin{macrocode}
\clist_map_inline:nn { angle , mangle }
  {
    \keys_define:nn { spintent / sp #1 }
      {
        read-cmd  .choices:nn = { school , mathcat }
          {
            \int_case:nn { \l_keys_choice_int }
              {
                {1} { \str_set:cn { l_@@_sp #1 _mode_str } { school  } }
                {2} { \str_set:cn { l_@@_sp #1 _mode_str } { mathcat } }
              }
          },
        read-cmd  .initial:n = school,
        read-cmd  .value_required:n = true,
        read-cmd / unknown .code:n =
          \msg_error:nneee { spintent } { unknown-choice }
            { read-cmd } { school,~mathcat } { \exp_not:n {##1} },
        prefix    .code:n =
          { \@@_sanitize_affix:cn { l_@@_sp #1 _prefix_tl } {##1} },
        prefix    .initial:n = ,
        prefix    .value_required:n = true,
        recto     .bool_set:c = { l_@@_sp #1 _recto_bool },
        recto     .initial:n = false,
        recto     .value_required:n = true,
        print-sym .bool_set:c = { l_@@_sp #1 _print_sym_bool },
        print-sym .initial:n = false,
        print-sym .value_required:n = true,
        silent    .bool_set:c = { l_@@_sp #1 _silent_bool },
        silent    .initial:n = false,
        silent    .value_required:n = true,
        concept   .bool_set:c = { l_@@_sp #1 _concept_bool },
        concept   .initial:n = true,
        concept   .value_required:n = true,
        unknown   .code:n =
          {
            \str_set:Nn \l_@@_command_name_str { sp #1 }
            \@@_unknown_keys_cmd:n {##1}
          },
      }
  }
%    \end{macrocode}
%   \begin{important}*
%     The key \mykey{print-sym} is defined for both commands for
%     simplicity, but has no effect on |\spangle|.
%   \end{important}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spangle} and \cs[no-index]{spmangle}}
%
% \begin{macro}[int]{\@@_spangle_sym:n}
%   The internal function \myhlc|\@@_spangle_sym:n| branches on
%   |#1|, \mykey{print-sym} and \mykey{recto} to select one of four
%   symbols: |\angle|/|\rightanglesqr| when \mykey{print-sym} is
%   |true|; |\measuredangle|/|\measuredrightangle| otherwise. When
%   |#1| is |angle|, only \mykey{recto} applies, choosing between
%   |\angle| and |\rightangle|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spangle_sym:n
  {
    \str_if_eq:eeTF {#1} { mangle }
      {
        \bool_if:cTF { l_@@_sp #1 _print_sym_bool }
          {
            \bool_if:cTF { l_@@_sp #1 _recto_bool }
              { \@@_right_angle_sqr: } { \angle }
          }
          {
            \bool_if:cTF { l_@@_sp #1 _recto_bool }
              { \measuredrightangle } { \measuredangle }
          }
      }
      {
        \bool_if:cTF { l_@@_sp #1 _recto_bool }
          { \@@_right_angle_sqr: } { \angle }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spangle_exec:nnn}
%   The internal function \myhlc|\@@_spangle_exec:nnn| receives the short name
%   of the command, |angle| or |mangle|, in `|#1|' and the \mymarg{argument} in
%   `|#3|'. It calls \myhlc|\@@_luafun_geo_angulo_parse_and_set:n|, reporting
%   |spangle-invalid-arg| or |spmangle-invalid-arg| on error. For a proper name
%   that is a single letter, not numeric, it reports the same error if the
%   letter is lowercase, since an angle name takes an uppercase letter, a number
%   or a Greek letter; the length check excludes a Greek letter command such as
%   |\alpha| from this comparison. Then it sets \myhlc|\l_@@_spangle_intent_tl|
%   to |ángulo|, or |medida-del-ángulo| for |\spmangle|, suffixed with |-recto|
%   when \mykey{recto} is |true|, and calls \myhlc|\@@_spangle_mathml:n|.
%
%   Before processing the \mymarg{argument}, it sets the optional \mymeta{keys} passed in `|#2|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spangle_exec:nnn
  {
    \keys_set:nn { spintent / sp #1 } {#2}
    \@@_luafun_geo_angulo_parse_and_set:n { \exp_not:n {#3} }
    \str_if_eq:VnT \l_@@_geo_luaset_error_str { true }
      {
        \msg_error:nnee { spintent } { sp #1 -invalid-arg }
          { \c_backslash_str sp #1 } { \exp_not:n {#3} }
      }
    \str_if_eq:VnT \l_@@_geo_luaset_angulo_case_str { nombre }
      {
        \str_if_eq:VnF \l_@@_geo_luaset_nombre_es_num_str { true }
          {
            \int_compare:nNnT { \str_count:N \l_@@_geo_luaset_letra_str } = { 1 }
              {
                \str_if_eq:eeF { \l_@@_geo_luaset_letra_str }
                  { \str_uppercase:n { \l_@@_geo_luaset_letra_str } }
                  {
                    \msg_error:nnee { spintent } { sp #1 -invalid-arg }
                      { \c_backslash_str sp #1 } { \exp_not:n {#3} }
                  }
              }
          }
      }
    \str_if_eq:eeTF {#1} { mangle }
      {
        \tl_set:Nn \l_@@_spangle_intent_tl { medida-del-ángulo }
      }
      {
        \tl_set:Nn \l_@@_spangle_intent_tl { ángulo }
      }
    \bool_if:cT { l_@@_sp #1 _recto_bool }
      {
        \tl_put_right:Nn \l_@@_spangle_intent_tl { -recto }
      }
    \@@_spangle_mathml:n {#1}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spangle_mathml:n}
%   The function \myhlc|\@@_spangle_mathml:n| branches on
%   \myhlc|\l_@@_geo_luaset_angulo_case_str|.
%
%   For |nombre|, the symbol \myhlc|\@@_spangle_sym:n| is tagged with
%   \myhlc|\@@_luafun_mrow_intent_arg:nn| on its own, with
%   \myhlc|_\l_@@_spangle_intent_tl| alone, without a |$|-reference, or with
%   `\myhlc|_-|' when \mykey{silent} is |true| \emph{or} \mykey{concept} is |false|.
%   With \mykey{silent}|=true|, the name that follows is silenced as a whole.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spangle_mathml:n
  {
    \@@_invisible_sep:
    \str_if_eq:VnTF \l_@@_geo_luaset_angulo_case_str { nombre }
      {
        \bool_if:cTF { l_@@_sp #1 _silent_bool }
          {
            \tl_set:Nn \l_@@_spangle_intent_arg_tl { _- }
          }
          {
            \bool_if:cTF { l_@@_sp #1 _concept_bool }
              {
                \tl_set:Ne \l_@@_spangle_intent_arg_tl
                  {
                    _
                    \tl_if_empty:cF { l_@@_sp #1 _prefix_tl }
                      { \tl_use:c { l_@@_sp #1 _prefix_tl } - }
                    \l_@@_spangle_intent_tl
                  }
              }
              {
                \tl_set:Nn \l_@@_spangle_intent_arg_tl { _- }
              }
          }
        \tl_set:Ne \l_@@_spangle_kv_tl
          { intent = "\l_@@_spangle_intent_arg_tl" }
        \@@_luafun_mrow_intent_arg:nn
          { } { \l_@@_spangle_kv_tl }
        { \@@_spangle_sym:n {#1} }
        \bool_if:cTF { l_@@_sp #1 _silent_bool }
          {
            \@@_luafun_mrow_intent_arg:nn { } { intent = "_-" }
            { \l_@@_geo_luaset_print_tl }
          }
%    \end{macrocode}
%   Otherwise, a numeric name is tagged as a whole, through
%   \myhlc|\@@_luafun_mrow_intent_arg:nn|, with a literal
%   |intent| built from \myhlc|\l_@@_geo_luaset_intent_str|.
%    \begin{macrocode}
          {
            \str_if_eq:VnTF \l_@@_geo_luaset_nombre_es_num_str { true }
              {
                \tl_set:Ne \l_@@_spangle_kv_tl
                  { intent = "_ \l_@@_geo_luaset_intent_str -" }
                \@@_luafun_mrow_intent_arg:nn
                  { } { \l_@@_spangle_kv_tl }
                { \l_@@_geo_luaset_print_tl }
              }
%    \end{macrocode}
%   With \myhlc|\l_@@_geo_luaset_tipo_str| equal to |sub|,
%   \mykey{read-cmd}|=school| tags the letter through the same function, with
%   a literal |intent|, and the subscript is appended and silenced;
%   \mykey{read-cmd}|=mathcat| leaves both letter and subscript untouched, for
%   \mynvmath{MathCAT} to read the whole |<msub>| natively. No |arg|
%   is set in either case, since there is no |$|-reference to resolve.
%    \begin{macrocode}
              {
                \str_case:VnF \l_@@_geo_luaset_tipo_str
                  {
                    { sub }
                      {
                        \str_if_eq:eeTF { \use:c { l_@@_sp #1 _mode_str } }
                          { school }
                          {
                            \tl_set:Ne \l_@@_spangle_kv_tl
                              { intent = "_ \l_@@_geo_luaset_intent_str -" }
                            \@@_luafun_mrow_intent_arg:nn
                              { } { \l_@@_spangle_kv_tl }
                            {
                              \l_@@_geo_luaset_letra_str
                              \c_math_subscript_token
                                {
                                  \MathMLintent { :silent }
                                    { \l_@@_geo_luaset_sub_str }
                                }
                            }
                          }
                          {
                            \l_@@_geo_luaset_letra_str
                            \c_math_subscript_token { \l_@@_geo_luaset_sub_str }
                          }
                      }
%    \end{macrocode}
%   With \myhlc|\l_@@_geo_luaset_tipo_str| equal to |prima|, the prime is always
%   read: it is spoken as part of the literal for \mykey{read-cmd}|=school| and
%   natively for \mykey{read-cmd}|=mathcat|.
%    \begin{macrocode}
                    { prima }
                      {
                        \str_if_eq:eeTF { \use:c { l_@@_sp #1 _mode_str } }
                          { school }
                          {
                            \tl_set:Ne \l_@@_spangle_kv_tl
                              { intent = "_ \l_@@_geo_luaset_intent_str -" }
                            \@@_luafun_mrow_intent_arg:nn
                              { } { \l_@@_spangle_kv_tl }
                            {
                              \l_@@_geo_luaset_letra_str
                                \l_@@_geo_luaset_sub_str
                            }
                          }
                          {
                            \l_@@_geo_luaset_letra_str
                              \l_@@_geo_luaset_sub_str
                          }
                      }
                  }
                  { \l_@@_geo_luaset_print_tl }
              }
          }
      }
%    \end{macrocode}
%   For |tres-puntos| and |un-punto|, with the suffix |-en| added to the
%   concept word in the latter, where a single point reads \emph{\enquote{ángulo en A}},
%   \myhlc|\l_@@_geo_luaset_points_clist| is
%   split into \myhlc|\l_@@_spangle_pt_seq| and walked to build
%   \myhlc|\l_@@_spangle_refs_tl|, the reference list |$pt1,$pt2,...|. A single
%   native |\MathMLintent| wraps the symbol and the points together, with
%   \myhlc|_\l_@@_spangle_intent_tl|\myhlc|:prefix(refs)|, or `\myhlc|_-|' when
%   \mykey{silent} is |true| and \myhlc|_(refs)| when \mykey{concept} is |false|. For
%   |\spmangle| with \mykey{print-sym}|=true|, a silent |\operatorname{m}|
%   precedes \myhlc|\@@_spangle_sym:n| inside the same |\MathMLintent|. The
%   sequence is walked again, calling \myhlc|\@@_mathmlarg_point:nnn| once per
%   point.
%    \begin{macrocode}
      {
        \str_if_eq:VnT \l_@@_geo_luaset_angulo_case_str { un-punto }
          { \tl_put_right:Nn \l_@@_spangle_intent_tl { -en } }
        \seq_set_from_clist:NN \l_@@_spangle_pt_seq
          \l_@@_geo_luaset_points_clist
        \tl_clear:N \l_@@_spangle_refs_tl
        \int_zero:N \l_@@_spangle_pt_idx_int
        \seq_map_inline:Nn \l_@@_spangle_pt_seq
          {
            \int_incr:N \l_@@_spangle_pt_idx_int
            \tl_if_empty:NF \l_@@_spangle_refs_tl
              { \tl_put_right:Nn \l_@@_spangle_refs_tl { , } }
            \tl_put_right:Ne \l_@@_spangle_refs_tl
              { $ pt \int_use:N \l_@@_spangle_pt_idx_int }
          }
        \bool_if:cTF { l_@@_sp #1 _silent_bool }
          {
            \tl_set:Nn \l_@@_spangle_intent_arg_tl { _- }
          }
          {
            \bool_if:cTF { l_@@_sp #1 _concept_bool }
              {
                \tl_set:Ne \l_@@_spangle_intent_arg_tl
                  {
                    _
                    \tl_if_empty:cF { l_@@_sp #1 _prefix_tl }
                      { \tl_use:c { l_@@_sp #1 _prefix_tl } - }
                    \l_@@_spangle_intent_tl :prefix( \l_@@_spangle_refs_tl
                      )
                  }
              }
              {
                \tl_set:Ne \l_@@_spangle_intent_arg_tl
                  { _( \l_@@_spangle_refs_tl ) }
              }
          }
        \bool_set_false:N \l_@@_spangle_print_sym_now_bool
        \str_if_eq:eeT {#1} { mangle }
          {
            \bool_if:cT { l_@@_sp #1 _print_sym_bool }
              { \bool_set_true:N \l_@@_spangle_print_sym_now_bool }
          }
        \MathMLintent { \l_@@_spangle_intent_arg_tl }
          {
            \@@_inner_sep:
            \bool_if:NT \l_@@_spangle_print_sym_now_bool
              {
                \@@_luafun_mrow_intent_arg:nn
                  { } { intent = ":silent" }
                { \operatorname { m } }
              }
            \@@_luafun_mrow_intent_arg:nn
              { } { intent = ":silent" }
            { \@@_spangle_sym:n {#1} }
            \int_zero:N \l_@@_spangle_pt_idx_int
            \seq_map_inline:Nn \l_@@_spangle_pt_seq
              {
                \int_incr:N \l_@@_spangle_pt_idx_int
                \@@_mathmlarg_point:nnn
                  { pt \int_use:N \l_@@_spangle_pt_idx_int }
                  { sp#1 }
                  { ##1 }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spangle, \spmangle}
%   Both public commands \ics*{spangle} and \ics*{spmangle} enforces \emph{math mode},
%   opens a local group, and delegates execution to \myhlc|\@@_spangle_exec:nnn|.
%    \begin{macrocode}
\NewDocumentCommand \spangle { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_spangle_exec:nnn { angle } {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spangle } }
  }
\NewDocumentCommand \spmangle { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_spangle_exec:nnn { mangle } {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spmangle } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The commands \cs{sparc} and \cs{spmarc}}\label{cmd:sparc}
%
% The user commands |\sparc| and |\spmarc| provide a semantic interface for
% arcs. The command |\spmarc| has no \mykey{print-sym} key: |\operatorname{m}|
% is always present, since $\operatorname{m}\widearc{AB}$ is the standard
% notation for the measure of an arc.
%
% \subsubsection{Definition of variables for \cs[no-index]{sparc} and
%   \cs[no-index]{spmarc}}
%
% \begin{variable}[int]
%   {
%     \l_@@_sparc_mode_str,
%     \l_@@_spmarc_mode_str,
%     \l_@@_sparc_prefix_tl,
%     \l_@@_spmarc_prefix_tl,
%   }
%   Each pair of variables is declared once for both |\sparc| and
%   |\spmarc| via \myhlc|\clist_map_inline:nn|. The variables \myhlc|\l_@@_sparc_mode_str|
%   and \myhlc|\l_@@_spmarc_mode_str| store the values set by the key
%   \mykey[sparc,spmarc]{read-cmd}; the variables \myhlc|\l_@@_sparc_prefix_tl|
%   and \myhlc|\l_@@_spmarc_prefix_tl| store the values set by the key
%   \mykey[sparc,spmarc]{prefix}.
%    \begin{macrocode}
\clist_map_inline:nn { arc , marc }
  {
    \str_new:c { l_@@_sp #1 _mode_str  }
    \tl_new:c  { l_@@_sp #1 _prefix_tl }
  }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_sparc_intent_tl}
% The variable \myhlc|\l_@@_sparc_intent_tl| stores the concept word, |arco|
% or |medida-del-arco|. It is shared by |\sparc| and |\spmarc|.
%    \begin{macrocode}
\tl_new:N \l_@@_sparc_intent_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_sparc_pt_seq,
%     \l_@@_sparc_pt_idx_int,
%     \l_@@_sparc_refs_tl,
%   }
% The variable \myhlc|\l_@@_sparc_pt_seq| stores the points taken from
% \myhlc|\l_@@_geo_luaset_points_clist| and \myhlc|\l_@@_sparc_pt_idx_int|
% counts the point being read. The variable \myhlc|\l_@@_sparc_refs_tl|
% stores the reference list |$pt1,$pt2,...| used in the |intent| of the
% concept.
%    \begin{macrocode}
\seq_new:N \l_@@_sparc_pt_seq
\int_new:N \l_@@_sparc_pt_idx_int
\tl_new:N  \l_@@_sparc_refs_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_sparc_intent_arg_tl}
% The variable \myhlc|\l_@@_sparc_intent_arg_tl| stores the |intent| being
% built.
%    \begin{macrocode}
\tl_new:N \l_@@_sparc_intent_arg_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{sparc}
%   and \cs[no-index]{spmarc}}
%
% \begin{macro}[int]{sparc-invalid-points, spmarc-invalid-points}
%   The \enquote{error}s |sparc-invalid-points| and |spmarc-invalid-points| are
%   raised when the user includes an invalid \mymarg{argument} passed in
%   `|#1|' to |\sparc| or |\spmarc|.
%    \begin{macrocode}
\clist_map_inline:nn { arc , marc }
  {
    \msg_new:nnnn { spintent } { sp #1 -invalid-points }
      {
        Invalid~argument~'##2'~for~'\tl_trim_spaces:n {##1}'~\msg_line_context:.
      }
      {
        The~argument~must~be~two~or~three~points~separated~by~commas.
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{sparc} and
%   \cs[no-index]{spmarc}}
%
% \begin{macro}[int]{read-cmd, prefix, silent, concept, unknown}
%   We define the shared \mymeta{keys} \mykey[sparc,spmarc]{read-cmd},
%   \mykey[sparc,spmarc]{prefix}, \mykey[sparc,spmarc]{silent} and
%   \mykey[sparc,spmarc]{concept}.
%    \begin{macrocode}
\clist_map_inline:nn { arc , marc }
  {
    \keys_define:nn { spintent / sp #1 }
      {
        read-cmd .choices:nn = { school , mathcat }
          {
            \int_case:nn { \l_keys_choice_int }
              {
                {1} { \str_set:cn { l_@@_sp #1 _mode_str } { school  } }
                {2} { \str_set:cn { l_@@_sp #1 _mode_str } { mathcat } }
              }
          },
        read-cmd .initial:n = school,
        read-cmd .value_required:n = true,
        read-cmd / unknown .code:n =
          \msg_error:nneee { spintent } { unknown-choice }
            { read-cmd } { school,~mathcat } { \exp_not:n {##1} },
        prefix   .code:n =
          { \@@_sanitize_affix:cn { l_@@_sp #1 _prefix_tl } {##1} },
        prefix   .initial:n = ,
        prefix   .value_required:n = true,
        silent   .bool_set:c = { l_@@_sp #1 _silent_bool },
        silent   .initial:n = false,
        silent   .value_required:n = true,
        concept  .bool_set:c = { l_@@_sp #1 _concept_bool },
        concept  .initial:n = true,
        concept  .value_required:n = true,
        unknown  .code:n =
          {
            \str_set:Nn \l_@@_command_name_str { sp #1 }
            \@@_unknown_keys_cmd:n {##1}
          },
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{sparc} and
%   \cs[no-index]{spmarc}}
%
% \begin{macro}[int]{\@@_sparc_wide_arc:n}
%   The function \myhlc|\@@_sparc_wide_arc:n| is a fallback when the
%   mathematical font used in the document does not provide the command
%   |\widearc|. The definition of this function is a direct adaptation of a
%   response given by Clea F. Rees (\texttt{@cfr}) to a query in the chat room of
%   \href{https://chat.stackexchange.com/transcript/message/69167926#69167926}{TeX-SX}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sparc_wide_arc:n
  {
    \Umathaccent 7 \symoperators "023DC \scan_stop: {#1}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_sparc_print_arc:n}
%   The function \myhlc|\@@_sparc_print_arc:n| applies the command
%   |\widearc| to the argument passed to |\sparc| or
%   |\spmarc|. If the mathematical font set in the document does not
%   provide the command |\widearc|, we will use the built-in function
%   \myhlc|\@@_sparc_wide_arc:n|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sparc_print_arc:n
  {
    \cs_if_exist:NTF \widearc
      { \widearc {#1} }
      { \@@_sparc_wide_arc:n {#1} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_sparc_exec:nnn}
%   The internal function \myhlc|\@@_sparc_exec:nnn| receives the
%   short name of the command, |arc| or |marc|, in `|#1|' and the
%   \mymarg{argument} in `|#3|'. It calls
%   \myhlc|\@@_luafun_geo_parse_and_set:n| to process `|#3|', returning
%   an \enquote{error} if invalid, then sets \myhlc|\l_@@_sparc_intent_tl|
%   to |arco|, or |medida-del-arco| for |\spmarc|. No |intent| is
%   built here: it needs the reference list, which
%   \myhlc|\@@_sparc_mathml:n| builds after splitting the points.
%
%   Before processing the \mymarg{argument}, it sets the optional \mymeta{keys} passed in `|#2|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sparc_exec:nnn
  {
    \keys_set:nn { spintent / sp #1 } {#2}
    \@@_luafun_geo_parse_and_set:n { \exp_not:n {#3} }
    \bool_if:nT
      {
        \str_if_eq_p:Vn \l_@@_geo_luaset_error_str { true } ||
        \str_if_eq_p:Vn \l_@@_geo_luaset_is_name_str { true }
      }
      {
        \msg_error:nnee { spintent } { sp #1 -invalid-points }
          { \c_backslash_str sp #1 } { \exp_not:n {#3} }
      }
    \str_if_eq:eeTF {#1} { marc }
      { \tl_set:Nn \l_@@_sparc_intent_tl { medida-del-arco } }
      { \tl_set:Nn \l_@@_sparc_intent_tl { arco } }
    \@@_sparc_mathml:n {#1}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_sparc_mathml:n}
%   The function \myhlc|\@@_sparc_mathml:n| receives the short command
%   name in `|#1|'. It splits \myhlc|\l_@@_geo_luaset_points_clist| into
%   \myhlc|\l_@@_sparc_pt_seq| and walks it once to build
%   \myhlc|\l_@@_sparc_refs_tl|, the comma-separated |$pt1,$pt2,...|
%   reference list, naming each point by position.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sparc_mathml:n
  {
    \seq_set_from_clist:NN \l_@@_sparc_pt_seq
      \l_@@_geo_luaset_points_clist

    \tl_clear:N \l_@@_sparc_refs_tl
    \int_zero:N \l_@@_sparc_pt_idx_int
    \seq_map_inline:Nn \l_@@_sparc_pt_seq
      {
        \int_incr:N \l_@@_sparc_pt_idx_int
        \tl_if_empty:NF \l_@@_sparc_refs_tl
          { \tl_put_right:Nn \l_@@_sparc_refs_tl { , } }
        \tl_put_right:Ne \l_@@_sparc_refs_tl
          { $ pt \int_use:N \l_@@_sparc_pt_idx_int }
      }
%    \end{macrocode}
%
%   The concept |intent| is `\myhlc|_-|' when \mykey{silent} is |true|,
%   \myhlc|_(refs)| when \mykey{concept} is |false|; otherwise
%   \myhlc|_\l_@@_sparc_intent_tl|\myhlc|:prefix(refs)|, with
%   \mykey{prefix} placed after the leading \myhlc|_| when given.
%    \begin{macrocode}
    \bool_if:cTF { l_@@_sp #1 _silent_bool }
      {
        \tl_set:Nn \l_@@_sparc_intent_arg_tl { _- }
      }
      {
        \bool_if:cTF { l_@@_sp #1 _concept_bool }
          {
            \tl_set:Ne \l_@@_sparc_intent_arg_tl
              {
                _
                \tl_if_empty:cF { l_@@_sp #1 _prefix_tl }
                  { \tl_use:c { l_@@_sp #1 _prefix_tl } - }
                \l_@@_sparc_intent_tl :prefix( \l_@@_sparc_refs_tl )
              }
          }
          {
            \tl_set:Ne \l_@@_sparc_intent_arg_tl
              { _( \l_@@_sparc_refs_tl ) }
          }
      }
%    \end{macrocode}
%   It is applied with a single native |\MathMLintent| wrapping
%   \myhlc|\@@_sparc_print_arc:n| directly, with the same shape as
%   \myhlc|\@@_spside_mathml:n|, preceded by
%   \myhlc|\@@_inner_sep:|, a second sibling so the sole
%   |<mrow>| does not collapse into |<math>|, \mypkg{luamml} issue
%   \href{https://github.com/latex3/luamml/issues/26}{\#26}. A silent
%   |\operatorname{m}| always precedes it for |\spmarc|, since there is no
%   \mykey{print-sym} key here. The sequence is walked a second time,
%   calling \myhlc|\@@_mathmlarg_point:nnn| once per point with its
%   |pt|$n$ reference name, looked up under \texttt{sparc} or \texttt{spmarc}.
%    \begin{macrocode}
    \@@_invisible_sep:
    \MathMLintent { \l_@@_sparc_intent_arg_tl }
      {
        \@@_inner_sep:
        \str_if_eq:eeT {#1} { marc }
          {
            \@@_luafun_mrow_intent_arg:nn
              { } { intent = ":silent" }
            { \operatorname { m } }
          }
        \@@_sparc_print_arc:n
          {
            \int_zero:N \l_@@_sparc_pt_idx_int
            \seq_map_inline:Nn \l_@@_sparc_pt_seq
              {
                \int_incr:N \l_@@_sparc_pt_idx_int
                \@@_mathmlarg_point:nnn
                  { pt \int_use:N \l_@@_sparc_pt_idx_int }
                  { sp #1 }
                  { ##1 }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\sparc, \spmarc}
%   Both public commands \ics*{sparc} and \ics*{spmarc} enforces \emph{math mode},
%   opens a local group, and delegates execution to \myhlc|\@@_sparc_exec:nnn|.
%    \begin{macrocode}
\NewDocumentCommand \sparc { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_sparc_exec:nnn { arc } {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \sparc } }
  }
\NewDocumentCommand \spmarc { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_sparc_exec:nnn { marc } {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spmarc } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spPoly}}\label{cmd:spPoly}
%
% The user command |\spPoly| typesets a polygon defined by its
% vertices. The figure type and its visual symbol are inferred from
% the vertex count: three points produce a triangle, four a square,
% five or more a generic polygon with no symbol. It never sends a
% boolean to Lua: \myhlc|\l_@@_geo_luaset_intent_str|, which holds the full
% |intent| with the figure name.
%
% \subsubsection{Definition of variables for \cs[no-index]{spPoly}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spPoly_mode_str,
%     \l_@@_spPoly_prefix_tl,
%   }
%   The variable \myhlc|\l_@@_spPoly_mode_str| stores the value set by
%   the key \mykey[spPoly]{read-cmd}; the variable
%   \myhlc|\l_@@_spPoly_prefix_tl| stores the value set by the key
%   \mykey[spPoly]{prefix}.
%    \begin{macrocode}
\str_new:N \l_@@_spPoly_mode_str
\tl_new:N  \l_@@_spPoly_prefix_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spPoly_pt_seq,
%     \l_@@_spPoly_refs_tl,
%     \l_@@_spPoly_intent_arg_tl,
%     \l_@@_spPoly_pt_idx_int,
%   }
%   Scratch variables to split \myhlc|\l_@@_geo_luaset_points_clist| and
%   build the |$pt1,$pt2,...| reference list, and to hold the final
%   concept |intent| once built.
%    \begin{macrocode}
\seq_new:N \l_@@_spPoly_pt_seq
\tl_new:N  \l_@@_spPoly_refs_tl
\tl_new:N  \l_@@_spPoly_intent_arg_tl
\int_new:N \l_@@_spPoly_pt_idx_int
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spPoly}}
%
% \begin{macro}[int]{spPoly-invalid-points}
%   The \enquote{error} |spPoly-invalid-points| is raised when the argument is
%   not a comma-separated list of at least three uppercase letters.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spPoly-invalid-points }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \spPoly'~\msg_line_context:.
  }
  {
    The~argument~must~be~a~list~of~at~least~three~points~separated~by~commas.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spPoly}}
%
% \begin{macro}[int]
%   {
%     read-cmd,
%     prefix,
%     print-sym,
%     silent,
%     concept,
%     unknown,
%   }
%   We define the \mymeta{keys} \mykey[spPoly]{read-cmd}, \mykey[spPoly]{prefix},
%  \mykey[spPoly]{print-sym}, \mykey[spPoly]{silent} and \mykey[spPoly]{concept}.
%    \begin{macrocode}
\keys_define:nn { spintent / spPoly }
  {
    read-cmd  .choices:nn = { school , mathcat }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spPoly_mode_str { school  } }
            {2} { \str_set:Nn \l_@@_spPoly_mode_str { mathcat } }
          }
      },
    read-cmd  .initial:n = school,
    read-cmd  .value_required:n = true,
    read-cmd / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-cmd } { school,~mathcat } { \exp_not:n {#1} },
    prefix    .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spPoly_prefix_tl {#1} },
    prefix    .initial:n = ,
    prefix    .value_required:n = true,
    print-sym .bool_set:N = \l_@@_spPoly_print_sym_bool,
    print-sym .initial:n = true,
    print-sym .value_required:n = true,
    silent    .bool_set:N = \l_@@_spPoly_silent_bool,
    silent    .initial:n = false,
    silent    .value_required:n = true,
    concept   .bool_set:N = \l_@@_spPoly_concept_bool,
    concept   .initial:n = true,
    concept   .value_required:n = true,
    unknown   .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spPoly }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spPoly}}
%
% \begin{macro}[int]{\@@_spPoly_sym:}
%   Unchanged: reads \myhlc|\l_@@_geo_luaset_poly_sym_str| and
%   typesets the corresponding symbol.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spPoly_sym:
  {
    \str_case:Vn \l_@@_geo_luaset_poly_sym_str
      {
        { triangle } { \triangle      }
        { square   } { \mdlgwhtsquare }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spPoly_build_refs:}
%   Splits \myhlc|\l_@@_geo_luaset_points_clist| into
%   \myhlc|\l_@@_spPoly_pt_seq| and walks it once to build
%   \myhlc|\l_@@_spPoly_refs_tl|, the comma-separated |$pt1,$pt2,...|
%   reference list, naming each vertex by position.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spPoly_build_refs:
  {
    \seq_set_from_clist:NN \l_@@_spPoly_pt_seq
      \l_@@_geo_luaset_points_clist
    \tl_clear:N \l_@@_spPoly_refs_tl
    \int_zero:N \l_@@_spPoly_pt_idx_int
    \seq_map_inline:Nn \l_@@_spPoly_pt_seq
      {
        \int_incr:N \l_@@_spPoly_pt_idx_int
        \tl_if_empty:NF \l_@@_spPoly_refs_tl
          { \tl_put_right:Nn \l_@@_spPoly_refs_tl { , } }
        \tl_put_right:Ne \l_@@_spPoly_refs_tl
          { $ pt \int_use:N \l_@@_spPoly_pt_idx_int }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spPoly_build_concept:}
%   Builds \myhlc|\l_@@_spPoly_intent_arg_tl|, independently of
%   \mykey{print-sym}: `\myhlc|_-|' when \mykey{silent} is |true|,
%   \myhlc|_(refs)| when \mykey{concept} is |false|; otherwise
%   \myhlc|_\l_@@_geo_luaset_concept_str|\myhlc|:prefix(refs)|, with
%   \mykey{prefix} placed after the leading \myhlc|_| when given. Must be
%   called after \myhlc|\@@_spPoly_build_refs:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spPoly_build_concept:
  {
    \bool_if:NTF \l_@@_spPoly_silent_bool
      {
        \tl_set:Nn \l_@@_spPoly_intent_arg_tl { _- }
      }
      {
        \bool_if:NTF \l_@@_spPoly_concept_bool
          {
            \tl_set:Ne \l_@@_spPoly_intent_arg_tl
              {
                _
                \tl_if_empty:NF \l_@@_spPoly_prefix_tl
                  { \tl_use:N \l_@@_spPoly_prefix_tl - }
                \l_@@_geo_luaset_concept_str :prefix( \l_@@_spPoly_refs_tl
                  )
              }
          }
          {
            \tl_set:Ne \l_@@_spPoly_intent_arg_tl
              { _( \l_@@_spPoly_refs_tl ) }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spPoly_points_loop:}
%   It walks \myhlc|\l_@@_spPoly_pt_seq| a second time, calling
%   \myhlc|\@@_mathmlarg_point:nnn| once per vertex with its |pt|$n$ reference
%   name, looked up under |spPoly|. This call decides |arg| or the literal
%   |intent| per vertex.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spPoly_points_loop:
  {
    \int_zero:N \l_@@_spPoly_pt_idx_int
    \seq_map_inline:Nn \l_@@_spPoly_pt_seq
      {
        \int_incr:N \l_@@_spPoly_pt_idx_int
        \@@_mathmlarg_point:nnn
          { pt \int_use:N \l_@@_spPoly_pt_idx_int }
          { spPoly }
          { ##1 }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spPoly_exec:nn}
%   Calls \myhlc|\@@_luafun_geo_poly_parse_and_set:n|, reports
%   |spPoly-invalid-points| on failure, then delegates to
%   \myhlc|\@@_spPoly_mathml:|. No |intent| is built here: it needs
%   the reference list, which \myhlc|\@@_spPoly_mathml:| builds after
%   splitting the vertices.
%
%   Before processing the \mymarg{argument}, it sets the optional \mymeta{keys} passed in `|#1|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spPoly_exec:nn
  {
    \keys_set:nn { spintent / spPoly } {#1}
    \@@_luafun_geo_poly_parse_and_set:n { \exp_not:n {#2} }
    \str_if_eq:VnT \l_@@_geo_luaset_error_str { true }
      {
        \msg_error:nne { spintent } { spPoly-invalid-points }
          { \exp_not:n {#2} }
      }
    \@@_spPoly_mathml:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spPoly_mathml:}
%   Builds the reference list with \myhlc|\@@_spPoly_build_refs:| and the
%   concept |intent| with \myhlc|\@@_spPoly_build_concept:|, then wraps
%   everything in a single native |\MathMLintent|, preceded by
%   \myhlc|\@@_inner_sep:|, a second sibling so the sole
%   |<mrow>| does not collapse into |<math>|, \mypkg{luamml} issue
%   \href{https://github.com/latex3/luamml/issues/26}{\#26}.
%
%   The keys \mykey{print-sym} and \mykey{concept}/\mykey{silent} are
%   independent. The key \mykey{print-sym} only decides whether the glyph
%   from \myhlc|\@@_spPoly_sym:|, silenced with
%   \myhlc|\@@_luafun_mrow_intent_arg:nn|, is attempted, and the glyph is
%   skipped when \myhlc|\l_@@_geo_luaset_poly_sym_str| is empty, which happens
%   for a pentagon and above because they have no symbol. It does not affect
%   whether the concept word is spoken.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spPoly_mathml:
  {
    \@@_invisible_sep:
    \@@_spPoly_build_refs:
    \@@_spPoly_build_concept:
    \MathMLintent { \l_@@_spPoly_intent_arg_tl }
      {
        \@@_inner_sep:
        \bool_if:NT \l_@@_spPoly_print_sym_bool
          {
            \str_if_empty:NF \l_@@_geo_luaset_poly_sym_str
              {
                \@@_luafun_mrow_intent_arg:nn
                  { } { intent = ":silent" }
                { \@@_spPoly_sym: }
              }
          }
        \@@_spPoly_points_loop:
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spPoly}
%   The public command \ics*{spPoly} enforces \emph{math mode}, opens a local
%   group, and delegates execution to \myhlc|\@@_spPoly_exec:nn|.
%    \begin{macrocode}
\NewDocumentCommand \spPoly { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_spPoly_exec:nn {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spPoly } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The commands \cs{spAfig} and \cs{spPfig}}\label{cmd:spAfig}
%
% The user commands |\spAfig| and |\spPfig| typeset the area and the
% perimeter of a polygonal figure, using the delimited-optional
% argument syntax |d()|. The key \mykey[spAfig,spPfig]{print-sym} is
% never sent to Lua: the symbol type is returned unconditionally in
% \myhlc|\l_@@_geo_luaset_poly_sym_str|, and expl3 decides whether to
% wrap it in |\MathMLintent|\marg{:silent}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spAfig} and
%   \cs[no-index]{spPfig}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spAfig_mode_str,
%     \l_@@_spPfig_mode_str,
%     \l_@@_spAfig_print_sym_str,
%     \l_@@_spPfig_print_sym_str,
%     \l_@@_spAfig_prefix_tl,
%     \l_@@_spPfig_prefix_tl,
%     \l_@@_spAfig_read_arg_tl,
%     \l_@@_spPfig_read_arg_tl,
%   }
%   These variables store the value set by each corresponding key:
%   \mykey[spAfig,spPfig]{read-cmd}, \mykey[spAfig,spPfig]{print-sym},
%   \mykey[spAfig,spPfig]{prefix} and \mykey[spAfig,spPfig]{read-arg},
%   respectively. The value set by \mykey[spAfig,spPfig]{read-arg}
%   overrides the figure name inferred from the vertex count,
%   independently of \mykey[spAfig,spPfig]{print-sym}.
%    \begin{macrocode}
\clist_map_inline:nn { Afig , Pfig }
  {
    \str_new:c { l_@@_sp #1 _mode_str      }
    \str_new:c { l_@@_sp #1 _print_sym_str }
    \tl_new:c  { l_@@_sp #1 _prefix_tl     }
    \tl_new:c  { l_@@_sp #1 _read_arg_tl   }
  }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_geo_final_print_tl,
%     \l_@@_spfig_symbol_tl,
%   }
% These variables are used by \myhlc|\@@_spfig_exec:nnn|. The variable
% \myhlc|\l_@@_geo_final_print_tl| stores the final visual placed under the
% subscript of |\mathcal| and \myhlc|\l_@@_spfig_symbol_tl| stores the symbol
% of the figure, |\triangle| or |\mdlgwhtsquare|, when it is typeset.
%    \begin{macrocode}
\tl_new:N \l_@@_geo_final_print_tl
\tl_new:N \l_@@_spfig_symbol_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_geo_print_sym_str}
% The variable \myhlc|\l_@@_geo_print_sym_str| stores the value of the key
% \mykey[spAfig,spPfig]{print-sym} of the command being executed.
%    \begin{macrocode}
\str_new:N \l_@@_geo_print_sym_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spfig_op_tl,
%     \l_@@_spfig_intent_tl,
%     \l_@@_spfig_desc_tl,
%   }
% The variable \myhlc|\l_@@_spfig_op_tl| stores \emph{«área»} or
% \emph{«perímetro»}. The variable \myhlc|\l_@@_spfig_intent_tl| stores the
% concept word for the vertices case, taken from \mykey{read-arg} or from the
% figure name detected. The variable \myhlc|\l_@@_spfig_desc_tl| stores the
% description of the figure that goes after the operator in the |intent|.
%    \begin{macrocode}
\tl_new:N \l_@@_spfig_op_tl
\tl_new:N \l_@@_spfig_intent_tl
\tl_new:N \l_@@_spfig_desc_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spfig_pt_seq,
%     \l_@@_spfig_pt_idx_int,
%     \l_@@_spfig_refs_tl,
%   }
% The variable \myhlc|\l_@@_spfig_pt_seq| stores the points taken from
% \myhlc|\l_@@_geo_luaset_points_clist| and \myhlc|\l_@@_spfig_pt_idx_int|
% counts the point being read. The variable \myhlc|\l_@@_spfig_refs_tl|
% stores the reference list |$pt1,$pt2,...| built from them.
%    \begin{macrocode}
\seq_new:N \l_@@_spfig_pt_seq
\int_new:N \l_@@_spfig_pt_idx_int
\tl_new:N  \l_@@_spfig_refs_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spfig_intent_arg_tl,
%     \l_@@_geo_temp_tl,
%   }
% The variable \myhlc|\l_@@_spfig_intent_arg_tl| stores the |intent| being
% built and \myhlc|\l_@@_geo_temp_tl| is a scratch copy of
% \myhlc|\l_@@_spfig_desc_tl| from which the connector is removed.
%    \begin{macrocode}
\tl_new:N \l_@@_spfig_intent_arg_tl
\tl_new:N \l_@@_geo_temp_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spAfig}
%   and \cs[no-index]{spPfig}}
%
% \begin{macro}[int]
%   {
%     spAfig-invalid-points, spPfig-invalid-points,
%     spAfig-invalid-arg, spPfig-invalid-arg,
%     spAfig-sym-letter-conflict, spPfig-sym-letter-conflict,
%     spAfig-sym-cardinality-conflict, spPfig-sym-cardinality-conflict,
%     spAfig-read-arg-key, spPfig-read-arg-key,
%   }
%   The ten \enquote{error}s are generated by the same map, using the same
%   |##1|/|##2| doubling convention as |\spside|/|\spmside|.
%   |spAfig-invalid-points| and |spPfig-invalid-points| are currently unused by
%   \myhlc|\@@_spfig_exec:nnn|, which raises only
%   |spAfig-invalid-arg| or |spPfig-invalid-arg| for any parse failure,
%   regardless of whether a comma was present. |spAfig-sym-letter-conflict|,
%   |spPfig-sym-letter-conflict|, |spAfig-sym-cardinality-conflict| and
%   |spPfig-sym-cardinality-conflict| are raised when
%   \mykey[spAfig,spPfig]{print-sym} is forced to a value incompatible
%   with the actual argument.
%    \begin{macrocode}
\clist_map_inline:nn { Afig , Pfig }
  {
    \msg_new:nnnn { spintent } { sp #1 -invalid-points }
      {
        Invalid~argument~'##2'~for~'\tl_trim_spaces:n {##1}'~\msg_line_context:.
      }
      {
        The~argument~must~be~a~list~of~points~separated~by~commas.
      }
    \msg_new:nnnn { spintent } { sp #1 -invalid-arg }
      {
        Invalid~argument~'##2'~for~'\tl_trim_spaces:n {##1}'~\msg_line_context:.
      }
      {
        See~the~manual~for~the~accepted~argument~forms.
      }
    \msg_new:nnnn { spintent } { sp #1 -sym-letter-conflict }
      {
        The~key~'print-sym'~cannot~be~combined~with~a~lettered~argument~in~'##1'~
        \msg_line_context:.
      }
      {
        See~the~documentation~of~'##1'~for~details~on~the~key~'print-sym'.
      }
    \msg_new:nnnn { spintent } { sp #1 -sym-cardinality-conflict }
      {
        The~key~'print-sym'~does~not~match~the~vertex~count~in~'##1'~\msg_line_context:.
      }
      {
        See~the~documentation~of~'##1'~for~details~on~the~key~'print-sym'.
      }
    \msg_new:nnnn { spintent } { sp #1 -read-arg-key }
      {
        The~key~'read-arg'~has~no~effect~with~a~single~argument~in~'##1'~
          \msg_line_context:.
      }
      {
        See~the~documentation~of~'##1'~for~details~on~the~key~'read-arg'.
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spAfig} and \cs[no-index]{spPfig}}
%
% \begin{macro}[int]
%   {
%     read-cmd,
%     prefix,
%     print-sym,
%     read-arg,
%     read-sub,
%     silent,
%     concept,
%     concept*,
%     unknown,
%   }
%   We define the shared \mymeta{keys} \mykey[spAfig,spPfig]{read-cmd},
%   \mykey[spAfig,spPfig]{prefix}, \mykey[spAfig,spPfig]{print-sym},
%   \mykey[spAfig,spPfig]{read-arg}, \mykey[spAfig,spPfig]{read-sub},
%   \mykey[spAfig,spPfig]{silent} and \mykey[spAfig,spPfig]{concept}.
%    \begin{macrocode}
\clist_map_inline:nn { Afig , Pfig }
  {
    \keys_define:nn { spintent / sp #1 }
      {
        read-cmd  .choices:nn = { school , mathcat }
          {
            \int_case:nn { \l_keys_choice_int }
              {
                {1} { \str_set:cn { l_@@_sp #1 _mode_str } { school  } }
                {2} { \str_set:cn { l_@@_sp #1 _mode_str } { mathcat } }
              }
          },
        read-cmd  .initial:n = school,
        read-cmd  .value_required:n = true,
        read-cmd / unknown .code:n =
          \msg_error:nneee { spintent } { unknown-choice }
            { read-cmd } { school,~mathcat } { \exp_not:n {##1} },
        prefix    .code:n =
          { \@@_sanitize_affix:cn { l_@@_sp #1 _prefix_tl } {##1} },
        prefix    .initial:n = ,
        prefix    .value_required:n = true,
        print-sym .choices:nn = { auto , triangle , square , none }
          {
            \int_case:nn { \l_keys_choice_int }
              {
                {1} { \str_set:cn { l_@@_sp #1 _print_sym_str } { auto     } }
                {2} { \str_set:cn { l_@@_sp #1 _print_sym_str } { triangle } }
                {3} { \str_set:cn { l_@@_sp #1 _print_sym_str } { square   } }
                {4} { \str_set:cn { l_@@_sp #1 _print_sym_str } { none     } }
              }
          },
        print-sym .initial:n = auto,
        print-sym .value_required:n = true,
        print-sym / unknown .code:n =
          \msg_error:nneee { spintent } { unknown-choice }
            { print-sym } { auto,~triangle,~square,~none } { \exp_not:n {##1} },
        read-arg  .code:n =
          { \@@_sanitize_affix:cn { l_@@_sp #1 _read_arg_tl } {##1} },
        read-arg  .initial:n = ,
        read-arg  .value_required:n = true,
        read-sub  .bool_set:c = { l_@@_sp #1 _read_sub_bool },
        read-sub  .initial:n = false,
        read-sub  .value_required:n = true,
        silent    .bool_set:c = { l_@@_sp #1 _silent_bool },
        silent    .initial:n = false,
        silent    .value_required:n = true,
        concept   .bool_set:c = { l_@@_sp #1 _concept_bool },
        concept   .initial:n = true,
        concept   .value_required:n = true,
        concept*  .meta:n =
          {
            concept  = false,
            read-arg = { \str_if_eq:eeTF {#1} { Afig } { _área } { _perímetro } },
          },
        concept*  .value_forbidden:n = true,
        unknown   .code:n =
          {
            \str_set:Nn \l_@@_command_name_str { sp #1 }
            \@@_unknown_keys_cmd:n {##1}
          },
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spAfig} and
%   \cs[no-index]{spPfig}}
%
% \begin{macro}[int]{\@@_spfig_sym:}
%   The function \myhlc|\@@_spfig_sym:| reads
%   \myhlc|\l_@@_geo_print_sym_str| and typesets the corresponding
%   symbol: |\triangle| for the value |triangle|, |\mdlgwhtsquare| for
%   the value |square|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfig_sym:
  {
    \str_case:Vn \l_@@_geo_print_sym_str
      {
        { triangle } { \triangle      }
        { square   } { \mdlgwhtsquare }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfig_exec:nnn}
%   Receives the short command name, |Afig| or |Pfig|, in `|#1|' and
%   the \mymarg{argument} in `|#3|'. Calls
%   \myhlc|\@@_luafun_geo_afig_parse_and_set:n| or
%   \myhlc|\@@_luafun_geo_pfig_parse_and_set:n|, reports
%   |spAfig-invalid-arg| or |spPfig-invalid-arg| on failure, and warns with
%   |spAfig-read-arg-key| or |spPfig-read-arg-key| when
%   \mykey[spAfig,spPfig]{read-arg} is given for a bare number/letter
%   argument that has no vertices, where it has no effect and is ignored. Sets
%   \myhlc|\l_@@_spfig_op_tl| to \emph{«área»}/\emph{«perímetro»}, then delegates to
%   \myhlc|\@@_spfig_mathml:n|.
%
%   Before processing the \mymarg{argument}, it sets the optional \mymeta{keys} passed in `|#2|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfig_exec:nnn
  {
    \keys_set:nn { spintent / sp #1 } {#2}
    \str_if_eq:eeTF {#1} { Afig }
      { \@@_luafun_geo_afig_parse_and_set:n { \exp_not:n {#3} } }
      { \@@_luafun_geo_pfig_parse_and_set:n { \exp_not:n {#3} } }
    \str_if_eq:VnT \l_@@_geo_luaset_error_str { true }
      {
        \msg_error:nnee { spintent } { sp #1 -invalid-arg }
          { \c_backslash_str sp #1 } { \exp_not:n {#3} }
      }
    \clist_if_empty:NT \l_@@_geo_luaset_points_clist
      {
        \tl_if_empty:cF { l_@@_sp #1 _read_arg_tl }
          {
            \msg_warning:nne { spintent } { sp #1 -read-arg-key }
              { \c_backslash_str sp #1 }
          }
      }
    \str_if_eq:eeTF {#1} { Afig }
      { \tl_set:Nn \l_@@_spfig_op_tl { área } }
      { \tl_set:Nn \l_@@_spfig_op_tl { perímetro } }
    \str_set:Ne \l_@@_geo_print_sym_str
      { \str_use:c { l_@@_sp #1 _print_sym_str } }
    \@@_spfig_mathml:n {#1}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfig_build_simple:n}
%   Handles the argument-less, table-letter and bare number/letter
%   cases, none of which has vertices. Builds \myhlc|\l_@@_geo_final_print_tl|, the
%   visual placed under |\mathcal|'s subscript, and
%   \myhlc|\l_@@_spfig_desc_tl|, the descriptive part without
%   \emph{«área»}/\emph{«perímetro»}. Receives the short command name in `|#1|'.
%
%   With an empty argument, both are cleared for \mykey{print-sym}|=auto|/
%   |none|; a forced \mykey{print-sym} instead places the symbol alone,
%   with its own name as the description.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfig_build_simple:n
  {
    \tl_if_empty:NTF \l_@@_geo_luaset_print_tl
      {
        \str_case:VnF \l_@@_geo_print_sym_str
          {
            { auto }
              {
                \tl_clear:N \l_@@_geo_final_print_tl
                \tl_clear:N \l_@@_spfig_desc_tl
              }
            { none }
              {
                \tl_clear:N \l_@@_geo_final_print_tl
                \tl_clear:N \l_@@_spfig_desc_tl
              }
          }
          {
            \tl_set:Nn \l_@@_geo_final_print_tl { \@@_spfig_sym: }
            \tl_set:Ne \l_@@_spfig_desc_tl
              {
                \str_if_eq:VnTF \l_@@_geo_print_sym_str { triangle }
                  { triángulo } { cuadrado }
              }
          }
      }
%    \end{macrocode}
%
%   With a bare number/letter, for which
%   \myhlc|\l_@@_geo_luaset_letra_word_str| is empty, the value itself becomes
%   the visual for \mykey{print-sym}|=auto|/|none|, or its own subscript
%   under a forced symbol otherwise; the description is the spoken
%   value, prefixed with «sub-» when \mykey{read-sub} is |true|,
%   further prefixed with the symbol's own name for a forced
%   \mykey{print-sym}.
%    \begin{macrocode}
      {
        \str_if_empty:NTF \l_@@_geo_luaset_letra_word_str
          {
            \str_case:VnF \l_@@_geo_print_sym_str
              {
                { auto }
                  {
                    \tl_set:NV \l_@@_geo_final_print_tl
                      \l_@@_geo_luaset_print_tl
                  }
                { none }
                  {
                    \tl_set:NV \l_@@_geo_final_print_tl
                      \l_@@_geo_luaset_print_tl
                  }
              }
              {
                \tl_set:Nn \l_@@_geo_final_print_tl
                  {
                    \@@_spfig_sym:
                    \c_math_subscript_token { \l_@@_geo_luaset_print_tl }
                  }
              }
            \str_case:VnF \l_@@_geo_print_sym_str
              {
                { auto }
                  {
                    \tl_set:Ne \l_@@_spfig_desc_tl
                      {
                        \bool_if:cT { l_@@_sp #1 _read_sub_bool } { sub - }
                        \l_@@_geo_luaset_sub_spoken_str
                      }
                  }
                { none }
                  {
                    \tl_set:Ne \l_@@_spfig_desc_tl
                      {
                        \bool_if:cT { l_@@_sp #1 _read_sub_bool } { sub - }
                        \l_@@_geo_luaset_sub_spoken_str
                      }
                  }
              }
              {
                \tl_set:Ne \l_@@_spfig_desc_tl
                  {
                    del-
                    \str_if_eq:VnTF \l_@@_geo_print_sym_str { triangle }
                      { triángulo } { cuadrado }
                    -
                    \bool_if:cT { l_@@_sp #1 _read_sub_bool } { sub - }
                    \l_@@_geo_luaset_sub_spoken_str
                  }
              }
          }
%    \end{macrocode}
%   A forced \mykey{print-sym} raises |spAfig-sym-letter-conflict| or
%   |spPfig-sym-letter-conflict| when \myhlc|\l_@@_geo_luaset_letra_word_str|
%   is non-empty, which happens with a table letter, that is |F|, |P|, |B|, |L|
%   or |T|. The visual is the value as given. The description is its own word,
%   connected as \myhlc|\l_@@_geo_luaset_letra_conector_str| requires, with the
%   same spoken subscript as above when present.
%    \begin{macrocode}
          {
            \str_case:VnF \l_@@_geo_print_sym_str
              {
                { auto } { }
                { none } { }
              }
              {
                \msg_error:nne { spintent } { sp #1 -sym-letter-conflict }
                  { \c_backslash_str sp #1 }
              }
            \tl_set:NV \l_@@_geo_final_print_tl \l_@@_geo_luaset_print_tl
            \tl_set:Ne \l_@@_spfig_desc_tl
              {
                \str_if_empty:NF \l_@@_geo_luaset_letra_conector_str
                  { \l_@@_geo_luaset_letra_conector_str - }
                \l_@@_geo_luaset_letra_word_str
                \str_if_empty:NF \l_@@_geo_luaset_sub_spoken_str
                  {
                    -
                    \bool_if:cT { l_@@_sp #1 _read_sub_bool } { sub - }
                    \l_@@_geo_luaset_sub_spoken_str
                  }
              }
          }
      }
%    \end{macrocode}
%   The variable \myhlc|\l_@@_spfig_intent_tl| is always the |word-| shape,
%   without the leading \myhlc|_|:
%   \myhlc|\l_@@_spfig_op_tl| alone when \myhlc|\l_@@_spfig_desc_tl| is
%   empty, both joined by a dash otherwise. \mykey{silent}
%   overrides \myhlc|\l_@@_spfig_intent_arg_tl| with `\myhlc|_-|'; with
%   \mykey{silent}|=false| and \mykey{concept}|=false|, it becomes
%   \myhlc|_|\myhlc|\l_@@_spfig_desc_tl|\myhlc|-|, where the descriptive part
%   is alone, without \emph{«área»}/\emph{«perímetro»} nor, for a table letter, its
%   connector |de-la|/|del|; with \mykey{concept}|=true| it
%   is \myhlc|_|, then \mykey{prefix} when given, and then
%   \myhlc|\l_@@_spfig_intent_tl|.
%    \begin{macrocode}
    \tl_set:Ne \l_@@_spfig_intent_tl
      {
        \tl_if_empty:NTF \l_@@_spfig_desc_tl
          { \l_@@_spfig_op_tl - }
          { \l_@@_spfig_op_tl - \l_@@_spfig_desc_tl - }
      }
    \bool_if:cTF { l_@@_sp #1 _silent_bool }
      { \tl_set:Nn \l_@@_spfig_intent_arg_tl { _- } }
      {
        \bool_if:cTF { l_@@_sp #1 _concept_bool }
          {
            \tl_set:Ne \l_@@_spfig_intent_arg_tl
              {
                _
                \tl_if_empty:cF { l_@@_sp #1 _prefix_tl }
                  { \tl_use:c { l_@@_sp #1 _prefix_tl } - }
                \l_@@_spfig_intent_tl
              }
          }
          {
            \tl_set_eq:NN \l_@@_geo_temp_tl \l_@@_spfig_desc_tl
            \str_if_empty:NF \l_@@_geo_luaset_letra_conector_str
              {
                \tl_replace_once:Nee \l_@@_geo_temp_tl
                  { \l_@@_geo_luaset_letra_conector_str - } { }
              }
            \tl_set:Ne \l_@@_spfig_intent_arg_tl { _ \l_@@_geo_temp_tl - }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfig_build_refs:}
%   Splits \myhlc|\l_@@_geo_luaset_points_clist| into
%   \myhlc|\l_@@_spfig_pt_seq| and walks it once to build
%   \myhlc|\l_@@_spfig_refs_tl|, the comma-separated |$pt1,$pt2,...|
%   reference list, naming each vertex by position.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfig_build_refs:
  {
    \seq_set_from_clist:NN \l_@@_spfig_pt_seq
      \l_@@_geo_luaset_points_clist
    \tl_clear:N \l_@@_spfig_refs_tl
    \int_zero:N \l_@@_spfig_pt_idx_int
    \seq_map_inline:Nn \l_@@_spfig_pt_seq
      {
        \int_incr:N \l_@@_spfig_pt_idx_int
        \tl_if_empty:NF \l_@@_spfig_refs_tl
          { \tl_put_right:Nn \l_@@_spfig_refs_tl { , } }
        \tl_put_right:Ne \l_@@_spfig_refs_tl
          { $ pt \int_use:N \l_@@_spfig_pt_idx_int }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfig_build_concept_vertices:n}
%   Handles the vertices case. Receives the short command name in
%   `|#1|'. Must be called after \myhlc|\@@_spfig_build_refs:|.
%
%   The key \mykey{read-arg}, when given, overrides the auto-detected figure
%   name in \myhlc|\l_@@_spfig_intent_tl|: with \mykey{concept}|=true|
%   it is still prefixed with |área-del-|/|perímetro-del-|, with
%   \mykey{concept}|=false| it is used verbatim. With \mykey{read-arg}
%   empty, \myhlc|\l_@@_spfig_intent_tl| is
%   \myhlc|\l_@@_geo_luaset_concept_str|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfig_build_concept_vertices:n
  {
    \tl_if_empty:cTF { l_@@_sp #1 _read_arg_tl }
      {
        \tl_set:Ne \l_@@_spfig_intent_tl { \l_@@_geo_luaset_concept_str }
      }
      {
        \bool_if:cTF { l_@@_sp #1 _concept_bool }
          {
            \tl_set:Ne \l_@@_spfig_intent_tl
              {
                \l_@@_spfig_op_tl -del-
                \tl_use:c { l_@@_sp #1 _read_arg_tl }
              }
          }
          {
            \tl_set:Ne \l_@@_spfig_intent_tl
              { \tl_use:c { l_@@_sp #1 _read_arg_tl } }
          }
      }
%    \end{macrocode}
%   Determines \myhlc|\l_@@_spfig_symbol_tl|, which is empty when none
%   applies: for \mykey{print-sym}|=auto|, the symbol matching
%   \myhlc|\l_@@_geo_luaset_poly_sym_str|, |\triangle|/
%   |\mdlgwhtsquare|, or none when that is empty; \mykey{print-sym}|=none|
%   always clears it; a forced \mykey{print-sym} requires it to match the
%   actual vertex count, reporting |spAfig-sym-cardinality-conflict| or
%   |spPfig-sym-cardinality-conflict|
%   otherwise.
%    \begin{macrocode}
    \str_case:VnF \l_@@_geo_print_sym_str
      {
        { auto }
          {
            \str_case:Vn \l_@@_geo_luaset_poly_sym_str
              {
                { triangle }
                  { \tl_set:Nn \l_@@_spfig_symbol_tl { \triangle      } }
                { square   }
                  { \tl_set:Nn \l_@@_spfig_symbol_tl { \mdlgwhtsquare } }
              }
            \str_if_empty:NT \l_@@_geo_luaset_poly_sym_str
              { \tl_clear:N \l_@@_spfig_symbol_tl }
          }
        { none }
          { \tl_clear:N \l_@@_spfig_symbol_tl }
      }
      {
        \str_if_eq:VVTF \l_@@_geo_print_sym_str
          \l_@@_geo_luaset_poly_sym_str
          { \tl_set:Nn \l_@@_spfig_symbol_tl { \@@_spfig_sym: } }
          {
            \msg_error:nne { spintent } { sp #1 -sym-cardinality-conflict }
              { \c_backslash_str sp #1 }
          }
      }
%    \end{macrocode}
%   The key \mykey{silent} sets \myhlc|\l_@@_spfig_intent_arg_tl| to `\myhlc|_-|'.
%   Otherwise, with \mykey{read-arg} empty, \mykey{concept} chooses between
%   \myhlc|_\l_@@_spfig_intent_tl|\myhlc|:prefix(refs)|, with \mykey{prefix}
%   placed after the leading \myhlc|_| when given, and \myhlc|_(refs)| alone; with
%   \mykey{read-arg} given, it is always
%   \myhlc|_\l_@@_spfig_intent_tl|\myhlc|:prefix(refs)|.
%    \begin{macrocode}
    \bool_if:cTF { l_@@_sp #1 _silent_bool }
      {
        \tl_set:Nn \l_@@_spfig_intent_arg_tl { _- }
      }
      {
        \tl_if_empty:cTF { l_@@_sp #1 _read_arg_tl }
          {
            \bool_if:cTF { l_@@_sp #1 _concept_bool }
              {
                \tl_set:Ne \l_@@_spfig_intent_arg_tl
                  {
                    _
                    \tl_if_empty:cF { l_@@_sp #1 _prefix_tl }
                      { \tl_use:c { l_@@_sp #1 _prefix_tl } - }
                    \l_@@_spfig_intent_tl :prefix( \l_@@_spfig_refs_tl )
                  }
              }
              {
                \tl_set:Ne \l_@@_spfig_intent_arg_tl
                  { _( \l_@@_spfig_refs_tl ) }
              }
          }
          {
            \tl_set:Ne \l_@@_spfig_intent_arg_tl
              {
                _
                \tl_if_empty:cF { l_@@_sp #1 _prefix_tl }
                  { \tl_use:c { l_@@_sp #1 _prefix_tl } - }
                \l_@@_spfig_intent_tl :prefix( \l_@@_spfig_refs_tl )
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfig_points_loop:n}
%   Walks \myhlc|\l_@@_spfig_pt_seq| a second time, calling
%   \myhlc|\@@_mathmlarg_point:nnn| once per vertex with its |pt|$n$
%   reference name, looked up under \texttt{spAfig} or \texttt{spPfig}. Receives the short command
%   name in `|#1|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfig_points_loop:n
  {
    \int_zero:N \l_@@_spfig_pt_idx_int
    \seq_map_inline:Nn \l_@@_spfig_pt_seq
      {
        \int_incr:N \l_@@_spfig_pt_idx_int
        \@@_mathmlarg_point:nnn
          { pt \int_use:N \l_@@_spfig_pt_idx_int }
          { sp #1 }
          { ##1 }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfig_mathml:n}
%   Receives the short command name in `|#1|'. Branches on whether
%   \myhlc|\l_@@_geo_luaset_points_clist| is empty.
%
%   Without vertices, \myhlc|\@@_spfig_build_simple:n| builds the content. A
%   |\MathMLintent| wraps it only when \mykey{concept} is |true|; with
%   \mykey{concept}|=false| the content is typeset with no |intent|.
%
%   With vertices, \myhlc|\@@_spfig_build_refs:| builds the reference list and
%   \myhlc|\@@_spfig_build_concept_vertices:n| builds the concept |intent|,
%   applied with a single native |\MathMLintent|. Inside,
%   \myhlc|\@@_mathcal:n|\mymarg{A \textnormal{\textcolor{lightgray}{\textbar}} P}
%   is silenced with \myhlc|\@@_luafun_mrow_intent_arg:nn|; the same is done
%   with \myhlc|\l_@@_spfig_symbol_tl| when it is non-empty; the
%   sequence is then walked by \myhlc|\@@_spfig_points_loop:n|. All of this is
%   nested inside a subscript, \myhlc|\c_math_subscript_token|, which the
%   recursive lookup of the shared |mathml_filter| callback reaches through the
%   |sub| field of the noad.
%
%   The function \myhlc|\@@_invisible_sep:| precedes both branches.
%   It adds a second sibling so that the sole |<mrow>| does not collapse into
%   |<math>|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfig_mathml:n
  {
    \@@_invisible_sep:
    \clist_if_empty:NTF \l_@@_geo_luaset_points_clist
      {
        \@@_spfig_build_simple:n {#1}
        \MathMLintent { \l_@@_spfig_intent_arg_tl }
          {
            \@@_inner_sep:
            \str_if_eq:eeTF {#1} { Afig } { \@@_mathcal:n { A } }
              { \@@_mathcal:n { P } }
            \tl_if_empty:NF \l_@@_geo_final_print_tl
              {
                \c_math_subscript_token { \l_@@_geo_final_print_tl }
              }
          }
      }
      {
        \@@_spfig_build_refs:
        \@@_spfig_build_concept_vertices:n {#1}
        \MathMLintent { \l_@@_spfig_intent_arg_tl }
          {
            \@@_inner_sep:
            \@@_luafun_mrow_intent_arg:nn
              { } { intent = ":silent" }
            {
              \str_if_eq:eeTF {#1} { Afig } { \@@_mathcal:n { A } }
                { \@@_mathcal:n { P } }
            }
            \c_math_subscript_token
              {
                \tl_if_empty:NF \l_@@_spfig_symbol_tl
                  {
                    \@@_luafun_mrow_intent_arg:nn
                      { } { intent = ":silent" }
                    { \tl_use:N \l_@@_spfig_symbol_tl }
                  }
                \@@_spfig_points_loop:n {#1}
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spAfig, \spPfig}
%   Both public commands \ics*{spAfig} and \ics*{spPfig} enforce
%   math mode, open a local group, and delegate execution to
%   \myhlc|\@@_spfig_exec:nnn|.
%    \begin{macrocode}
\NewDocumentCommand \spAfig { O{} d() }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \tl_if_novalue:nTF {#2}
            { \@@_spfig_exec:nnn { Afig } {#1} { } }
            { \@@_spfig_exec:nnn { Afig } {#1} {#2} }
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spAfig } }
  }
\NewDocumentCommand \spPfig { O{} d() }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \tl_if_novalue:nTF {#2}
            { \@@_spfig_exec:nnn { Pfig } {#1} { } }
            { \@@_spfig_exec:nnn { Pfig } {#1} {#2} }
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spPfig } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The commands \cs{sprad} and \cs{spdiam}}\label{cmd:sprad}
%
% The user-level commands |\sprad| and |\spdiam| provide a semantic
% interface for the radius and diameter of a circle.
%
% \subsubsection{Definition of variables for \cs[no-index]{sprad} and
%   \cs[no-index]{spdiam}}
%
% \begin{variable}[int]
%   {
%     \l_@@_sp_rad_diam_letra_tl,
%     \l_@@_sp_rad_diam_word_tl,
%     \l_@@_sp_rad_diam_read_arg_full_tl,
%   }
%   The variables \myhlc|\l_@@_sp_rad_diam_letra_tl| and
%   \myhlc|\l_@@_sp_rad_diam_word_tl| are scratch variables used by
%   \myhlc|\@@_sp_rad_diam_mathml:n|. \myhlc|\l_@@_sp_rad_diam_read_arg_full_tl|
%   is used by \myhlc|\@@_sp_rad_diam_read_arg_print:n| to hold the
%   text to voice when \mykey{read-arg} is given.
%    \begin{macrocode}
\tl_new:N \l_@@_sp_rad_diam_letra_tl
\tl_new:N \l_@@_sp_rad_diam_word_tl
\tl_new:N \l_@@_sp_rad_diam_read_arg_full_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_sprad_mode_str,
%     \l_@@_sprad_sub_index_str,
%     \l_@@_sprad_read_arg_tl,
%     \l_@@_spdiam_mode_str,
%     \l_@@_spdiam_sub_index_str,
%     \l_@@_spdiam_read_arg_tl,
%   }
%   Each command has its own copy of the string variables
%   \myhlc|_mode_str| and \myhlc|_sub_index_str| and of the token list
%   variable \myhlc|_read_arg_tl|, which hold the values of the keys
%   \mykey[sprad,spdiam]{read-cmd}, \mykey[sprad,spdiam]{sub-index} and
%   \mykey[sprad,spdiam]{read-arg}.
%    \begin{macrocode}
\clist_map_inline:nn { rad , diam }
  {
    \str_new:c { l_@@_sp #1 _mode_str      }
    \str_new:c { l_@@_sp #1 _sub_index_str }
    \tl_new:c  { l_@@_sp #1 _read_arg_tl   }
  }
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of keys for \cs[no-index]{sprad} and
%   \cs[no-index]{spdiam}}
%
% \begin{macro}[int]
%   {
%     read-cmd,
%     upper-case,
%     sub-index,
%     read-sub,
%     read-arg,
%     concept,
%     silent,
%     print-sym,
%     unknown,
%   }
%   We define the shared \mymeta{keys} \mykey[sprad,spdiam]{read-cmd},
%   \mykey[sprad,spdiam]{upper-case}, \mykey[sprad,spdiam]{sub-index},
%   \mykey[sprad,spdiam]{read-sub}, \mykey[sprad,spdiam]{read-arg},
%   \mykey[sprad,spdiam]{concept}, \mykey[sprad,spdiam]{silent} and
%   \mykey[spmside]{print-sym}.
%    \begin{macrocode}
\clist_map_inline:nn { rad , diam }
  {
    \keys_define:nn { spintent / sp #1 }
      {
        read-cmd   .choices:nn = { school , mathcat }
          {
            \int_case:nn { \l_keys_choice_int }
              {
                {1} { \str_set:cn { l_@@_sp #1 _mode_str } { school  } }
                {2} { \str_set:cn { l_@@_sp #1 _mode_str } { mathcat } }
              }
          },
        read-cmd   .initial:n = school,
        read-cmd   .value_required:n = true,
        read-cmd / unknown .code:n =
          \msg_error:nneee { spintent } { unknown-choice }
            { read-cmd } { school,~mathcat } { \exp_not:n {##1} },
        upper-case .bool_set:c = { l_@@_sp #1 _upper_case_bool },
        upper-case .initial:n = false,
        upper-case .value_required:n = true,
        sub-index  .code:n =
          { \str_set:cn { l_@@_sp #1 _sub_index_str } {##1} },
        sub-index  .initial:n = ,
        sub-index  .value_required:n = true,
        read-sub   .bool_set:c = { l_@@_sp #1 _read_sub_bool },
        read-sub   .initial:n = false,
        read-sub   .value_required:n = true,
        read-arg   .code:n =
          { \@@_sanitize_affix:cn { l_@@_sp #1 _read_arg_tl } {##1} },
        read-arg   .initial:n = ,
        read-arg   .value_required:n = true,
        concept    .bool_set:c = { l_@@_sp #1 _concept_bool },
        concept    .initial:n = true,
        concept    .value_required:n = true,
        silent     .bool_set:c = { l_@@_sp #1 _silent_bool },
        silent     .initial:n = false,
        silent     .value_required:n = true,
        print-sym  .bool_set:c = { l_@@_sp #1 _print_sym_bool },
        print-sym  .initial:n = false,
        print-sym  .value_required:n = true,
        unknown    .code:n =
          {
            \str_set:Nn \l_@@_command_name_str { sp #1 }
            \@@_unknown_keys_cmd:n {##1}
          },
      }
  }
%    \end{macrocode}
%   \begin{important}*
%     The key \mykey{print-sym} is defined for both commands for
%     simplicity, but has no effect on |\sprad|.
%   \end{important}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{sprad} and
%   \cs[no-index]{spdiam}}
%
% \begin{macro}[int]{\@@_sp_rad_diam_mathml:n}
%   The function \myhlc|\@@_sp_rad_diam_mathml:n| sets
%   \myhlc|\l_@@_sp_rad_diam_letra_tl| to the bare letter, |r| or |R| for
%   |\sprad| and |d| or |D| for |\spdiam|, according to \mykey{upper-case}; for
%   |\spdiam| with \mykey{print-sym}|=true|, it silently typesets |\diameter|
%   first. Receives the short command name in `|#1|': |rad| or |diam|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sp_rad_diam_mathml:n
  {
    \bool_if:cTF { l_@@_sp #1 _upper_case_bool }
      {
        \str_if_eq:eeTF {#1} { rad }
          { \tl_set:Nn \l_@@_sp_rad_diam_letra_tl { R } }
          { \tl_set:Nn \l_@@_sp_rad_diam_letra_tl { D } }
      }
      {
        \str_if_eq:eeTF {#1} { rad }
          { \tl_set:Nn \l_@@_sp_rad_diam_letra_tl { r } }
          { \tl_set:Nn \l_@@_sp_rad_diam_letra_tl { d } }
      }
    \@@_invisible_sep:
    \str_if_eq:eeT {#1} { diam }
      {
        \bool_if:NT \l_@@_spdiam_print_sym_bool
          { \MathMLintent { :silent } { \diameter } }
      }
%    \end{macrocode}
%   With \mykey{silent}|=true|, the intent is `\myhlc|_-|': the letter alone with the
%   shared queue when there is no \mykey{sub-index}, or the letter and the
%   subscript together in one native |\MathMLintent|, with the subscript
%   silenced, when there is.
%    \begin{macrocode}
    \bool_if:cTF { l_@@_sp #1 _silent_bool }
      {
        \str_if_empty:cTF { l_@@_sp #1 _sub_index_str }
          {
            \@@_luafun_mrow_intent_arg:nn
              { } { intent = "_-" }
            { \l_@@_sp_rad_diam_letra_tl }
          }
          {
            \MathMLintent { _- }
              {
                \@@_inner_sep:
                \l_@@_sp_rad_diam_letra_tl
                \c_math_subscript_token
                  {
                    \MathMLintent { :silent }
                      { \str_use:c { l_@@_sp #1 _sub_index_str } }
                  }
              }
          }
      }
%    \end{macrocode}
%   Otherwise, with \mykey{concept}|=true|, the default, it verbalizes
%   \emph{\enquote{radio}} or \emph{\enquote{diámetro}}, or the text of
%   \mykey{read-arg} instead of that word when given, followed by the \mykey{sub-index} if
%   present, as |sub-|\mymeta{value} or \mymeta{value} alone according to
%   \mykey{read-sub}; the letter is not read. With no \mykey{sub-index}, the
%   shared queue tags the letter alone. With one, for \mykey{read-cmd}|=school|
%   the letter and the subscript are wrapped together in one native
%   |\MathMLintent| with the subscript silenced, and for
%   \mykey{read-cmd}|=mathcat| the shared queue tags the letter alone and the
%   subscript is left bare.
%    \begin{macrocode}
      {
        \tl_if_empty:cTF { l_@@_sp #1 _read_arg_tl }
          {
            \bool_if:cTF { l_@@_sp #1 _concept_bool }
              {
                \str_if_eq:eeTF {#1} { rad }
                  { \tl_set:Nn \l_@@_sp_rad_diam_word_tl { radio } }
                  { \tl_set:Nn \l_@@_sp_rad_diam_word_tl { diámetro } }
                \str_if_empty:cTF { l_@@_sp #1 _sub_index_str }
                  {
                    \@@_luafun_mrow_intent_arg:nn
                      { } { intent = "_ \l_@@_sp_rad_diam_word_tl :nofix" }
                    { \l_@@_sp_rad_diam_letra_tl }
                  }
                  {
                    \str_if_eq:eeTF
                      { \str_use:c { l_@@_sp #1 _mode_str } } { school }
                      {
                        \MathMLintent
                          {
                            _ \l_@@_sp_rad_diam_word_tl -
                            \bool_if:cT { l_@@_sp #1 _read_sub_bool } { sub- }
                            \str_use:c { l_@@_sp #1 _sub_index_str }
                          }
                          {
                            \@@_inner_sep:
                            \l_@@_sp_rad_diam_letra_tl
                            \c_math_subscript_token
                              {
                                \MathMLintent { :silent }
                                  { \str_use:c { l_@@_sp #1 _sub_index_str } }
                              }
                          }
                      }
                      {
                        \@@_luafun_mrow_intent_arg:nn
                          { } { intent = "_ \l_@@_sp_rad_diam_word_tl :nofix" }
                        { \l_@@_sp_rad_diam_letra_tl }
                        \c_math_subscript_token
                          { \str_use:c { l_@@_sp #1 _sub_index_str } }
                      }
                  }
              }
%    \end{macrocode}
%   With \mykey{concept}|=false|, the letter itself is read, but only when
%   \mykey{read-cmd} is |school|, built in the same way from the letter. With
%   \mykey{sub-index}, the letter and the subscript are wrapped together and the
%   subscript is silenced. With \mykey{read-cmd}|=mathcat|, the intent is left
%   empty and \mynvmath{MathCAT} applies its own rules to the letter and its
%   subscript.
%    \begin{macrocode}
              {
                \str_if_empty:cTF { l_@@_sp #1 _sub_index_str }
                  {
                    \str_if_eq:eeTF
                      { \str_use:c { l_@@_sp #1 _mode_str } } { school }
                      {
                        \@@_luafun_mrow_intent_arg:nn
                          { } { intent = "_ \l_@@_sp_rad_diam_letra_tl -" }
                        { \l_@@_sp_rad_diam_letra_tl }
                      }
                      { \l_@@_sp_rad_diam_letra_tl }
                  }
                  {
                    \str_if_eq:eeTF
                      { \str_use:c { l_@@_sp #1 _mode_str } } { school }
                      {
                        \MathMLintent
                          {
                            _ \l_@@_sp_rad_diam_letra_tl -
                            \bool_if:cT { l_@@_sp #1 _read_sub_bool } { sub- }
                            \str_use:c { l_@@_sp #1 _sub_index_str }
                          }
                          {
                            \@@_inner_sep:
                            \l_@@_sp_rad_diam_letra_tl
                            \c_math_subscript_token
                              {
                                \MathMLintent { :silent }
                                  { \str_use:c { l_@@_sp #1 _sub_index_str } }
                              }
                          }
                      }
                      {
                        \l_@@_sp_rad_diam_letra_tl
                        \c_math_subscript_token
                          { \str_use:c { l_@@_sp #1 _sub_index_str } }
                      }
                  }
              }
          }
          { \@@_sp_rad_diam_read_arg_print:n {#1} }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_sp_rad_diam_read_arg_print:n}
%   With \mykey{read-arg} given, \mykey{sub-index}/\mykey{read-sub}
%   are ignored for the \emph{spoken} content; the subscript, if any,
%   still displays, silenced. With \mykey{concept}|=true|, the
%   default, \myhlc|\l_@@_sp_rad_diam_read_arg_full_tl| becomes
%   \emph{\enquote{radio}}/\emph{\enquote{diámetro}} followed by \mykey{read-arg}'s text; with
%   |false|, \mykey{read-arg}'s text alone. Combined natively,
%   regardless of \mykey{read-cmd}: with no \mykey{sub-index}, the
%   shared queue tags the letter alone; with one, letter and
%   subscript are wrapped together in one native |\MathMLintent|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sp_rad_diam_read_arg_print:n
  {
    \bool_if:cTF { l_@@_sp #1 _concept_bool }
      {
        \str_if_eq:eeTF {#1} { rad }
          {
            \tl_set:Ne \l_@@_sp_rad_diam_read_arg_full_tl
              { radio- \tl_use:c { l_@@_sp #1 _read_arg_tl } }
          }
          {
            \tl_set:Ne \l_@@_sp_rad_diam_read_arg_full_tl
              { diámetro- \tl_use:c { l_@@_sp #1 _read_arg_tl } }
          }
      }
      {
        \tl_set_eq:Nc \l_@@_sp_rad_diam_read_arg_full_tl
          { l_@@_sp #1 _read_arg_tl }
      }
    \str_if_empty:cTF { l_@@_sp #1 _sub_index_str }
      {
        \@@_luafun_mrow_intent_arg:nn
          { } { intent = "_ \l_@@_sp_rad_diam_read_arg_full_tl :nofix" }
        { \l_@@_sp_rad_diam_letra_tl }
      }
      {
        \MathMLintent
          { _ \l_@@_sp_rad_diam_read_arg_full_tl :nofix }
          {
            \@@_inner_sep:
            \l_@@_sp_rad_diam_letra_tl
            \c_math_subscript_token
              {
                \MathMLintent { :silent }
                  { \str_use:c { l_@@_sp #1 _sub_index_str } }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_sp_rad_diam_exec:nn}
%   The function \myhlc|\@@_sp_rad_diam_exec:nn| receives the \emph{short name}
%   of the command passed in `|#1|' and the optional \mymeta{keys} passed in `|#2|'
%   then delegating the rendering to \myhlc|\@@_sp_rad_diam_mathml:n|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_sp_rad_diam_exec:nn
  {
    \keys_set:nn { spintent / sp #1 } {#2}
    \@@_sp_rad_diam_mathml:n {#1}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\sprad, \spdiam}
%   Both public commands \ics*{sprad} and \ics*{spdiam} enforces \emph{math mode},
%   opens a local group, and delegates execution to \myhlc|\@@_sp_rad_diam_exec:nn|.
%    \begin{macrocode}
\NewDocumentCommand \sprad { O{} }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_sp_rad_diam_exec:nn { rad } {#1}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \sprad } }
  }
\NewDocumentCommand \spdiam { O{} }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_sp_rad_diam_exec:nn { diam } {#1}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spdiam } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spcirc}}\label{cmd:spcirc}
%
% The user command |\spcirc| provides a semantic interface for a circle or a
% circumference, using the delimited-optional argument syntax |d()|. The
% argument holds a required center point and an optional radius, separated by a
% comma. The shape of the radius decides whether it is a pure number, a measure,
% number~+~unit, a bare letter or a two-letter segment, and it is processed
% respectively with |\spnum|, |\spunit| and |\spside|/|\spmside|.
%
% \subsubsection{Definition of variables for \cs[no-index]{spcirc}}
%
% \begin{variable}[int]
%   {
%     \l_@@_geo_luaset_circ_radio_type_str,
%     \l_@@_geo_luaset_circ_radio_raw_str,
%   }
%   Shared outputs of \myhlc|\@@_luafun_geo_circ_parse_and_set:n|.
%   The variable \myhlc|\l_@@_geo_luaset_circ_radio_type_str| holds one of
%   |number|, |measure|, |label|, |segment|, or an empty string when
%   no radius was given. \myhlc|\l_@@_geo_luaset_circ_radio_raw_str|
%   holds the corresponding raw text, ready to be forwarded to the
%   subcommand that renders it.
%    \begin{macrocode}
\str_new:N \l_@@_geo_luaset_circ_radio_type_str
\str_new:N \l_@@_geo_luaset_circ_radio_raw_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcirc_mode_str,
%     \l_@@_spcirc_prefix_tl,
%     \l_@@_spcirc_print_sym_str,
%     \l_@@_spcirc_read_sty_str,
%     \l_@@_spcirc_radio_sty_str,
%     \l_@@_spcirc_sub_index_str,
%     \l_@@_spcirc_read_arg_tl,
%   }
% These variables store the values set by the keys. The variable
% \myhlc|\l_@@_spcirc_mode_str| stores \mykey[spcirc]{read-cmd} and
% \myhlc|\l_@@_spcirc_prefix_tl| stores \mykey[spcirc]{prefix}. The variable
% \myhlc|\l_@@_spcirc_print_sym_str| stores \mykey[spcirc]{print-sym}, either
% |usual| or |odot|. The variable \myhlc|\l_@@_spcirc_read_sty_str| stores
% \mykey[spcirc]{read-sty}, either |circun| or |circulo|. The variable
% \myhlc|\l_@@_spcirc_radio_sty_str| stores \mykey[spcirc]{radio-sty}, only
% meaningful when the radius is a segment. The variable
% \myhlc|\l_@@_spcirc_sub_index_str| stores \mykey[spcirc]{sub-index} and
% \myhlc|\l_@@_spcirc_read_arg_tl| stores \mykey[spcirc]{read-arg}.
%    \begin{macrocode}
\str_new:N \l_@@_spcirc_mode_str
\tl_new:N  \l_@@_spcirc_prefix_tl
\str_new:N \l_@@_spcirc_print_sym_str
\str_new:N \l_@@_spcirc_read_sty_str
\str_new:N \l_@@_spcirc_radio_sty_str
\str_new:N \l_@@_spcirc_sub_index_str
\tl_new:N  \l_@@_spcirc_read_arg_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcirc_articulo_str,
%     \l_@@_spcirc_word_tl,
%   }
% The variable \myhlc|\l_@@_spcirc_articulo_str| is set by
% \myhlc|\@@_spcirc_word_and_article:| to |la| or |el|, matching the gender of
% the chosen word. The variable \myhlc|\l_@@_spcirc_word_tl| stores that word,
% |circunferencia| or |círculo|, or the \mykey[spcirc]{read-arg} override;
% it is used by \myhlc|\@@_spcirc_exec:nn|.
%    \begin{macrocode}
\str_new:N \l_@@_spcirc_articulo_str
\tl_new:N  \l_@@_spcirc_word_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcirc_has_arg_bool,
%     \l_@@_spcirc_intent_tl,
%     \l_@@_spcirc_intent_arg_tl,
%     \l_@@_spcirc_connector_tl,
%     \l_@@_spcirc_connector_arg_tl,
%     \l_@@_spcirc_radio_arg_tl,
%   }
%   The variable \myhlc|\l_@@_spcirc_has_arg_bool| stores whether the optional
%   argument |d()| was given. The variable \myhlc|\l_@@_spcirc_intent_tl| stores
%   the word |circunferencia| or |círculo|, followed by the \mykey{sub-index}
%   suffix if present. The variable \myhlc|\l_@@_spcirc_connector_tl| stores
%   |de-centro-en|. The variables \myhlc|\l_@@_spcirc_intent_arg_tl| and
%   \myhlc|\l_@@_spcirc_connector_arg_tl| store the |intent| passed to
%   |\MathMLintent| for the word and for the connector, where the center is
%   referenced as |$pt|. The variable \myhlc|\l_@@_spcirc_radio_arg_tl| stores
%   the |intent| of the radius, spoken on the delimiter |,|.
%    \begin{macrocode}
\bool_new:N \l_@@_spcirc_has_arg_bool
\tl_new:N   \l_@@_spcirc_intent_tl
\tl_new:N   \l_@@_spcirc_intent_arg_tl
\tl_new:N   \l_@@_spcirc_connector_tl
\tl_new:N   \l_@@_spcirc_connector_arg_tl
\tl_new:N   \l_@@_spcirc_radio_arg_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spcirc}}
%
% \begin{macro}[int]{spcirc-invalid-arg}
%   The \enquote{error} |spcirc-invalid-arg| is raised when the argument does
%   not match a single center point, optionally followed by a comma and a valid
%   radius shape.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spcirc-invalid-arg }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \spcirc'~\msg_line_context:.
  }
  {
    The~argument~must~have~the~form~'(center)'~or~'(center,~radius)'.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spcirc}}
%
% \begin{macro}[int]
%   {
%     read-cmd,
%     prefix,
%     print-sym,
%     read-sty,
%     diameter,
%     radio-sty,
%     sub-index,
%     read-sub,
%     read-arg,
%     silent,
%     concept,
%     spside,
%     spmside,
%     unknown,
%   }
%   We define the \mymeta{keys} \mykey[spcirc]{read-cmd},
%   \mykey[spcirc]{prefix}, \mykey[spcirc]{print-sym},
%   \mykey[spcirc]{read-sty}, \mykey[spcirc]{diameter},
%   \mykey[spcirc]{radio-sty}, \mykey[spcirc]{sub-index},
%   \mykey[spcirc]{read-sub}, \mykey[spcirc]{read-arg},
%   \mykey[spcirc]{silent} and \mykey[spcirc]{concept}.
%    \begin{macrocode}
\keys_define:nn { spintent / spcirc }
  {
    read-cmd  .choices:nn = { school , mathcat }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1}
              {
                \str_set:Nn \l_@@_spcirc_mode_str { school }
                \keys_set:nn { spintent / spside  } { read-cmd = school }
                \keys_set:nn { spintent / spmside } { read-cmd = school }
              }
            {2}
              {
                \str_set:Nn \l_@@_spcirc_mode_str { mathcat }
                \keys_set:nn { spintent / spside  } { read-cmd = mathcat }
                \keys_set:nn { spintent / spmside } { read-cmd = mathcat }
              }
          }
      },
    read-cmd  .initial:n = school,
    read-cmd  .value_required:n = true,
    read-cmd / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-cmd } { school,~mathcat } { \exp_not:n {#1} },
    prefix    .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spcirc_prefix_tl {#1} },
    prefix    .initial:n = ,
    prefix    .value_required:n = true,
    print-sym .choices:nn = { usual , odot }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spcirc_print_sym_str { usual } }
            {2} { \str_set:Nn \l_@@_spcirc_print_sym_str { odot  } }
          }
      },
    print-sym .initial:n = odot,
    print-sym .value_required:n = true,
    print-sym / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { print-sym } { usual,~odot } { \exp_not:n {#1} },
    read-sty  .choices:nn = { circun , circulo }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spcirc_read_sty_str { circun } }
            {2} { \str_set:Nn \l_@@_spcirc_read_sty_str { circulo } }
          }
      },
    read-sty  .initial:n = circun,
    read-sty  .value_required:n = true,
    read-sty / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-sty } { circun,~circulo } { \exp_not:n {#1} },
    diameter  .bool_set:N = \l_@@_spcirc_diameter_bool,
    diameter  .initial:n = false,
    diameter  .value_required:n = true,
    radio-sty .choices:nn = { side , mside }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spcirc_radio_sty_str { side  } }
            {2} { \str_set:Nn \l_@@_spcirc_radio_sty_str { mside } }
          }
      },
    radio-sty .initial:n = mside,
    radio-sty .value_required:n = true,
    radio-sty / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { radio-sty } { side,~mside } { \exp_not:n {#1} },
    sub-index .code:n =
      { \str_set:Nn \l_@@_spcirc_sub_index_str {#1} },
    sub-index .initial:n = ,
    sub-index .value_required:n = true,
    read-sub  .bool_set:N = \l_@@_spcirc_read_sub_bool,
    read-sub  .initial:n = false,
    read-sub  .value_required:n = true,
    read-arg  .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spcirc_read_arg_tl {#1} },
    read-arg  .initial:n = ,
    read-arg  .value_required:n = true,
    silent    .bool_set:N = \l_@@_spcirc_silent_bool,
    silent    .initial:n = false,
    silent    .value_required:n = true,
    concept   .bool_set:N = \l_@@_spcirc_concept_bool,
    concept   .initial:n = true,
    concept   .value_required:n = true,
    spside    .meta:nn = { spintent / spside  } {#1},
    spside    .value_required:n = true,
    spmside   .meta:nn = { spintent / spmside } {#1},
    spmside   .value_required:n = true,
    unknown   .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spcirc }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spcirc}}
%
% \begin{macro}[int]{\@@_spcirc_sym:}
%   Typesets |\bigodot| or the plain letter |C|, according to
%   \mykey[spcirc]{print-sym}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcirc_sym:
  {
    \str_if_eq:VnTF \l_@@_spcirc_print_sym_str { odot }
      {
        \group_begin:
          \textstyle\bigodot
        \group_end:
      }
      { C }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcirc_word_and_article:}
%   Sets \myhlc|\l_@@_spcirc_word_tl| to \mykey{read-arg} or
%   |circunferencia|/|círculo| from \mykey{read-sty}, and
%   \myhlc|\l_@@_spcirc_articulo_str| to the matching article, which
%   always follows \mykey{read-sty} even when \mykey{read-arg}
%   overrides the word.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcirc_word_and_article:
  {
    \tl_if_empty:NTF \l_@@_spcirc_read_arg_tl
      {
        \str_if_eq:VnTF \l_@@_spcirc_read_sty_str { circun }
          {
            \tl_set:Nn \l_@@_spcirc_word_tl { circunferencia }
            \str_set:Nn \l_@@_spcirc_articulo_str { la }
          }
          {
            \tl_set:Nn \l_@@_spcirc_word_tl { círculo }
            \str_set:Nn \l_@@_spcirc_articulo_str { el }
          }
      }
      {
        \tl_set:NV \l_@@_spcirc_word_tl \l_@@_spcirc_read_arg_tl
        \str_if_eq:VnTF \l_@@_spcirc_read_sty_str { circun }
          { \str_set:Nn \l_@@_spcirc_articulo_str { la } }
          { \str_set:Nn \l_@@_spcirc_articulo_str { el } }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcirc_radio_print:}
%   Branches on \myhlc|\l_@@_geo_luaset_circ_radio_type_str|,
%   reusing |\spnum|/|\spunit|/|\spside|/|\spmside| directly rather
%   than reimplementing any parsing.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcirc_radio_print:
  {
    \str_case:Vn \l_@@_geo_luaset_circ_radio_type_str
      {
        { number  }
          { \exp_args:Ne \spnum { \l_@@_geo_luaset_circ_radio_raw_str } }
        { measure }
          { \exp_args:Ne \spunit { \l_@@_geo_luaset_circ_radio_raw_str } }
        { label   } { \l_@@_geo_luaset_circ_radio_raw_str }
        { segment }
          {
            \str_if_eq:VnTF \l_@@_spcirc_radio_sty_str { side }
              { \exp_args:Ne \spside  { \l_@@_geo_luaset_circ_radio_raw_str } }
              { \exp_args:Ne \spmside { \l_@@_geo_luaset_circ_radio_raw_str } }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcirc_build_concept:}
%   Builds the words \myhlc|\l_@@_spcirc_intent_tl|, with
%   \myhlc|\@@_spcirc_word_and_article:| and an optional \mykey{sub-index}
%   suffix, and \myhlc|\l_@@_spcirc_connector_tl|, |de-centro-en|. Both are
%   built even when \myhlc|\l_@@_spcirc_has_arg_bool| is |false|, although
%   \myhlc|\@@_spcirc_mathml:| does not use them in that case. The center is not
%   concatenated into them: it is read by reference with
%   \myhlc|\@@_mathmlarg_point:nnn|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcirc_build_concept:
  {
    \@@_spcirc_word_and_article:
    \tl_set:Ne \l_@@_spcirc_intent_tl { \l_@@_spcirc_word_tl }
    \str_if_empty:NF \l_@@_spcirc_sub_index_str
      {
        \tl_put_right:Ne \l_@@_spcirc_intent_tl
          {
            -
            \bool_if:NT \l_@@_spcirc_read_sub_bool { sub - }
            \l_@@_spcirc_sub_index_str
          }
      }
    \tl_set:Nn \l_@@_spcirc_connector_tl { de-centro-en }
%    \end{macrocode}
%
%   The key \mykey{silent}, when |true|, overrides
%   \myhlc|\l_@@_spcirc_intent_arg_tl|, \myhlc|\l_@@_spcirc_connector_arg_tl|
%   and \myhlc|\l_@@_spcirc_radio_arg_tl| with `\myhlc|_-|', regardless of
%   \mykey{read-arg}.
%    \begin{macrocode}
    \bool_if:NTF \l_@@_spcirc_silent_bool
      {
        \tl_set:Nn \l_@@_spcirc_intent_arg_tl     { _- }
        \tl_set:Nn \l_@@_spcirc_connector_arg_tl  { _- }
        \tl_set:Nn \l_@@_spcirc_radio_arg_tl      { _- }
      }
%    \end{macrocode}
%
%   With \mykey{concept}|=true|, \myhlc|\l_@@_spcirc_intent_arg_tl| is
%   \myhlc|_\l_@@_spcirc_intent_tl| alone, with \mykey{prefix} placed after the
%   leading \myhlc|_| when given; \myhlc|\l_@@_spcirc_connector_arg_tl| is
%   \myhlc|_\l_@@_spcirc_connector_tl|\myhlc|:prefix($pt)|; and
%   \myhlc|\l_@@_spcirc_radio_arg_tl| is the literal \myhlc|_y-radio-|, or
%   \myhlc|_y-diámetro-| when \mykey{diameter} is |true|, spoken on the delimiter |,|
%   before the reading of \myhlc|\@@_spcirc_radio_print:|.
%    \begin{macrocode}
      {
        \bool_if:NTF \l_@@_spcirc_concept_bool
          {
            \tl_set:Ne \l_@@_spcirc_intent_arg_tl
              {
                _
                \tl_if_empty:NF \l_@@_spcirc_prefix_tl
                  { \tl_use:N \l_@@_spcirc_prefix_tl - }
                \l_@@_spcirc_intent_tl
              }
            \tl_set:Ne \l_@@_spcirc_connector_arg_tl
              { _ \l_@@_spcirc_connector_tl :prefix( $ pt ) }
            \bool_if:NTF \l_@@_spcirc_diameter_bool
              { \tl_set:Nn \l_@@_spcirc_radio_arg_tl { _y-diámetro- } }
              { \tl_set:Nn \l_@@_spcirc_radio_arg_tl { _y-radio- } }
          }
%    \end{macrocode}
%   With \mykey{concept}|=false| and \mykey{read-arg} empty, the same `\myhlc|_-|' and
%   \myhlc|_($pt)| silencing applies as with \mykey{silent}. With \mykey{read-arg}
%   given, none of the three is silenced; the connector and the words |y-radio|
%   and |y-diámetro| are fixed and \mykey{read-arg} does not change them.
%    \begin{macrocode}
          {
            \tl_if_empty:NTF \l_@@_spcirc_read_arg_tl
              {
                \tl_set:Nn \l_@@_spcirc_intent_arg_tl    { _- }
                \tl_set:Ne \l_@@_spcirc_connector_arg_tl { _( $ pt ) }
                \tl_set:Nn \l_@@_spcirc_radio_arg_tl     { _- }
              }
              {
                \tl_set:Ne \l_@@_spcirc_intent_arg_tl
                  { _ \l_@@_spcirc_intent_tl }
                \tl_set:Ne \l_@@_spcirc_connector_arg_tl
                  { _ \l_@@_spcirc_connector_tl :prefix( $ pt ) }
                \bool_if:NTF \l_@@_spcirc_diameter_bool
                  { \tl_set:Nn \l_@@_spcirc_radio_arg_tl { _y-diámetro- } }
                  { \tl_set:Nn \l_@@_spcirc_radio_arg_tl { _y-radio- } }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcirc_exec:nn}
%   Receives the |d()| argument in `|#2|', which is an explicit empty group when omitted
%   because |\spcirc| converts |-NoValue-| to |{ }| before calling
%   \myhlc|\@@_spcirc_exec:nn|. When the group is empty, the function sets
%   \myhlc|\l_@@_spcirc_has_arg_bool| to |false| and does not call
%   \myhlc|\@@_luafun_geo_circ_parse_and_set:n|. Otherwise it calls that
%   function, reports |spcirc-invalid-arg| on failure and delegates to
%   \myhlc|\@@_spcirc_mathml:|, which calls
%   \myhlc|\@@_spcirc_build_concept:|.
%
%   Before processing the \mymarg{argument}, it sets the optional \mymeta{keys} passed in `|#1|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcirc_exec:nn
  {
    \keys_set:nn { spintent / spcirc } {#1}
    \tl_if_empty:nTF {#2}
      {
        \bool_set_false:N \l_@@_spcirc_has_arg_bool
      }
      {
        \bool_set_true:N \l_@@_spcirc_has_arg_bool
        \@@_luafun_geo_circ_parse_and_set:n { \exp_not:n {#2} }
        \str_if_eq:VnT \l_@@_geo_luaset_error_str { true }
          {
            \msg_error:nne { spintent } { spcirc-invalid-arg } { \exp_not:n {#2} }
          }
      }
    \@@_spcirc_mathml:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcirc_mathml:}
%   Calls \myhlc|\@@_spcirc_build_concept:| and wraps the symbol
%   \myhlc|\l_@@_spcirc_intent_arg_tl| in its own native |\MathMLintent|. This
%   is the whole result when \myhlc|\l_@@_spcirc_has_arg_bool| is |false|,
%   that is, for |\spcirc| with no |(...)|.
%
%   Otherwise, the group |(|center|)|, held in
%   \myhlc|\l_@@_spcirc_connector_arg_tl|, follows as a separate sibling, outside the |\MathMLintent| of the symbol.
%   When a radius is present, the delimiter |,| after the center carries
%   \myhlc|\l_@@_spcirc_radio_arg_tl| and \myhlc|\@@_spcirc_radio_print:|
%   follows as a sibling; with \mykey{silent}|=true|, it is wrapped in a literal
%   `\myhlc|_-|' |\MathMLintent| instead.
%
%   The symbol and, when \mykey{sub-index} is given, its subscript number are
%   silenced separately with \myhlc|\@@_luafun_mrow_intent_arg:nn|, one queue
%   entry per leaf. The delimiter |(| is silenced in the same way but without
%   the usual |{...}| around the character, so that \mypkg{luamml} keeps its
%   math class and produces |<mo>| instead of |<mi>|.
%
%   The final |)|, which follows \myhlc|\@@_spcirc_radio_print:|, is typeset as
%   |\rparen| inside its own native |\MathMLintent{:silent}|, outside the shared
%   queue.
%
%   The center itself is read through \myhlc|\@@_mathmlarg_point:nnn|,
%   referenced as |$pt|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcirc_mathml:
  {
    \@@_invisible_sep:
    \@@_spcirc_build_concept:
    \MathMLintent { \l_@@_spcirc_intent_arg_tl }
      {
        \@@_inner_sep:
        \@@_luafun_mrow_intent_arg:nn
          { } { intent = ":silent" }
        { \@@_spcirc_sym: }
        \str_if_empty:NF \l_@@_spcirc_sub_index_str
          {
            \c_math_subscript_token
              {
                \@@_luafun_mrow_intent_arg:nn
                  { } { intent = ":silent" }
                { \l_@@_spcirc_sub_index_str }
              }
          }
      }
    \bool_if:NT \l_@@_spcirc_has_arg_bool
      {
        \str_if_empty:NTF \l_@@_geo_luaset_circ_radio_type_str
          {
            \MathMLintent { \l_@@_spcirc_connector_arg_tl }
              {
                \@@_inner_sep:
                \@@_luafun_mrow_intent_arg:nn { } { intent = ":silent" } \lparen
                \@@_mathmlarg_point:nnV { pt } { spcirc }
                  \l_@@_geo_luaset_points_clist
                \@@_luafun_mrow_intent_arg:nn { } { intent = ":silent" } \rparen
              }
          }
          {
            \MathMLintent { \l_@@_spcirc_connector_arg_tl }
              {
                \@@_inner_sep:
                \@@_luafun_mrow_intent_arg:nn { } { intent = ":silent" } \lparen
                \@@_mathmlarg_point:nnV { pt } { spcirc }
                  \l_@@_geo_luaset_points_clist
              }
            \MathMLintent { \l_@@_spcirc_radio_arg_tl } { , }
            \bool_if:NTF \l_@@_spcirc_silent_bool
              {
                \MathMLintent { _- }
                  {
                    \@@_inner_sep:
                    \@@_spcirc_radio_print:
                  }
              }
              { \@@_spcirc_radio_print: }
            \MathMLintent { :silent } { \rparen }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spcirc}
%   The public command \ics*{spcirc} enforces math mode, opens a local group,
%   and delegates execution to \myhlc|\@@_spcirc_exec:nn|. An omitted radius
%   forwards an explicit empty group instead of the |-NoValue-| marker.
%    \begin{macrocode}
\NewDocumentCommand \spcirc { O{} d() }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \tl_if_novalue:nTF {#2}
            { \@@_spcirc_exec:nn {#1} { } }
            { \@@_spcirc_exec:nn {#1} {#2} }
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spcirc } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The commands \cs{spAcirc} and \cs{spPcirc}}\label{cmd:spAcirc}
%
% The user commands |\spAcirc| and |\spPcirc| typeset the area and
% the perimeter of a circle or a circumference. The argument follows
% |\spcirc|'s own point-and-radius grammar for a real center, with an
% optional radius; when no comma is present and no valid center is
% found, it falls back to a bare number/letter shortcut, reusing the
% same Lua entry point as |\spAfig|/|\spPfig|. A table letter matched
% by that fallback is rejected, since it has no meaning for a circle.
%
% \subsubsection{Definition of variables for \cs[no-index]{spAcirc} and \cs[no-index]{spPcirc}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spAcirc_mode_str, \l_@@_spPcirc_mode_str,
%     \l_@@_spAcirc_print_sym_str, \l_@@_spPcirc_print_sym_str,
%     \l_@@_spAcirc_read_sty_str, \l_@@_spPcirc_read_sty_str,
%     \l_@@_spAcirc_sub_index_str, \l_@@_spPcirc_sub_index_str,
%     \l_@@_spAcirc_prefix_tl, \l_@@_spPcirc_prefix_tl,
%     \l_@@_spAcirc_read_arg_tl, \l_@@_spPcirc_read_arg_tl,
%   }
%   These variables store the value set by each corresponding key:
%   \mykey[spAcirc,spPcirc]{read-cmd}, \mykey[spAcirc,spPcirc]{print-sym},
%   \mykey[spAcirc,spPcirc]{read-sty},
%   \mykey[spAcirc,spPcirc]{sub-index}, \mykey[spAcirc,spPcirc]{prefix} and
%   \mykey[spAcirc,spPcirc]{read-arg}, respectively.
%    \begin{macrocode}
\clist_map_inline:nn { Acirc , Pcirc }
  {
    \str_new:c { l_@@_sp #1 _mode_str      }
    \str_new:c { l_@@_sp #1 _print_sym_str }
    \str_new:c { l_@@_sp #1 _read_sty_str  }
    \str_new:c { l_@@_sp #1 _sub_index_str }
    \tl_new:c  { l_@@_sp #1 _prefix_tl     }
    \tl_new:c  { l_@@_sp #1 _read_arg_tl   }
  }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_spcircfig_luaset_comma_str}
% The variable \myhlc|\l_@@_spcircfig_luaset_comma_str| is set by
% \myhlc|\@@_luafun_spcircfig_comma:n| and stores |true| when the argument has
% a comma.
%    \begin{macrocode}
\str_new:N \l_@@_spcircfig_luaset_comma_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_spcircfig_short_bool}
% The variable \myhlc|\l_@@_spcircfig_short_bool| stores whether the argument
% was resolved as the short number or letter form, with
% \myhlc|\@@_luafun_geo_afig_parse_and_set:n| or
% \myhlc|\@@_luafun_geo_pfig_parse_and_set:n|, and not as a circle.
%    \begin{macrocode}
\bool_new:N \l_@@_spcircfig_short_bool
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcircfig_op_tl,
%     \l_@@_spcircfig_del_word_tl,
%     \l_@@_spcircfig_intent_tl,
%     \l_@@_spcircfig_intent_arg_tl,
%   }
% The variable \myhlc|\l_@@_spcircfig_op_tl| stores \emph{«área»} or
% \emph{«perímetro»}. The variable \myhlc|\l_@@_spcircfig_del_word_tl|, set by
% \myhlc|\@@_spcircfig_del_word:n|, stores the word that follows the operator.
% The variable \myhlc|\l_@@_spcircfig_intent_tl| stores the spoken phrase:
% that word, an optional \mykey{sub-index} suffix and, when a center is given,
% |-con-centro-en|; the center is read by reference with
% \myhlc|\@@_mathmlarg_point:nnn|. The variable
% \myhlc|\l_@@_spcircfig_intent_arg_tl| stores the |intent| being built.
%    \begin{macrocode}
\tl_new:N \l_@@_spcircfig_op_tl
\tl_new:N \l_@@_spcircfig_del_word_tl
\tl_new:N \l_@@_spcircfig_intent_tl
\tl_new:N \l_@@_spcircfig_intent_arg_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spAcirc} and \cs[no-index]{spPcirc}}
%
% \begin{macro}[int]{spAcirc-invalid-arg, spPcirc-invalid-arg}
%   The \enquote{error}s |spAcirc-invalid-arg| and |spPcirc-invalid-arg| are
%   raised when the argument does not match any of the accepted forms: empty, a
%   center point, optionally followed by a comma and a radius, or a bare
%   number/letter.
%    \begin{macrocode}
\clist_map_inline:nn { Acirc , Pcirc }
  {
    \msg_new:nnnn { spintent } { sp #1 -invalid-arg }
      {
        Invalid~argument~'##2'~for~'\tl_trim_spaces:n {##1}'~\msg_line_context:.
      }
      {
        See~the~manual~for~the~accepted~argument~forms.
      }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spAcirc} and \cs[no-index]{spPcirc}}
%
% \begin{macro}[int]
%   {
%     read-cmd,
%     prefix,
%     print-sym,
%     read-sty,
%     concept,
%     sub-index,
%     read-sub,
%     read-arg,
%     silent,
%     unknown,
%   }
%   We define the shared \mymeta{keys} \mykey[spAcirc,spPcirc]{read-cmd},
%   \mykey[spAcirc,spPcirc]{prefix}, \mykey[spAcirc,spPcirc]{print-sym},
%   \mykey[spAcirc,spPcirc]{read-sty}, \mykey[spAcirc,spPcirc]{concept},
%   \mykey[spAcirc,spPcirc]{sub-index}, \mykey[spAcirc,spPcirc]{read-sub},
%   \mykey[spAcirc,spPcirc]{read-arg} and \mykey[spAcirc,spPcirc]{silent}.
%    \begin{macrocode}
    \keys_define:nn { spintent / sp #1 }
      {
        read-cmd  .choices:nn = { school , mathcat }
          {
            \int_case:nn { \l_keys_choice_int }
              {
                {1} { \str_set:cn { l_@@_sp #1 _mode_str } { school  } }
                {2} { \str_set:cn { l_@@_sp #1 _mode_str } { mathcat } }
              }
          },
        read-cmd  .initial:n = school,
        read-cmd  .value_required:n = true,
        read-cmd / unknown .code:n =
          \msg_error:nneee { spintent } { unknown-choice }
            { read-cmd } { school,~mathcat } { \exp_not:n {##1} },
        prefix    .code:n =
          { \@@_sanitize_affix:cn { l_@@_sp #1 _prefix_tl } {##1} },
        prefix    .initial:n = ,
        prefix    .value_required:n = true,
        print-sym .choices:nn = { usual , odot }
          {
            \int_case:nn { \l_keys_choice_int }
              {
                {1} { \str_set:cn { l_@@_sp #1 _print_sym_str } { usual } }
                {2} { \str_set:cn { l_@@_sp #1 _print_sym_str } { odot  } }
              }
          },
        print-sym .initial:n = odot,
        print-sym .value_required:n = true,
        print-sym / unknown .code:n =
          \msg_error:nneee { spintent } { unknown-choice }
            { print-sym } { usual,~odot } { \exp_not:n {##1} },
        read-sty  .choices:nn = { circun , circulo }
          {
            \int_case:nn { \l_keys_choice_int }
              {
                {1} { \str_set:cn { l_@@_sp #1 _read_sty_str } { circun } }
                {2} { \str_set:cn { l_@@_sp #1 _read_sty_str } { circulo } }
              }
          },
        read-sty  .initial:n = circun,
        read-sty  .value_required:n = true,
        read-sty / unknown .code:n =
          \msg_error:nneee { spintent } { unknown-choice }
            { read-sty } { circun,~circulo } { \exp_not:n {##1} },
        concept   .bool_set:c = { l_@@_sp #1 _concept_bool },
        concept   .initial:n = true,
        concept   .value_required:n = true,
        sub-index .code:n =
          { \str_set:cn { l_@@_sp #1 _sub_index_str } {##1} },
        sub-index .initial:n = ,
        sub-index .value_required:n = true,
        read-sub  .bool_set:c = { l_@@_sp #1 _read_sub_bool },
        read-sub  .initial:n = false,
        read-sub  .value_required:n = true,
        read-arg  .code:n =
          { \@@_sanitize_affix:cn { l_@@_sp #1 _read_arg_tl } {##1} },
        read-arg  .initial:n = ,
        read-arg  .value_required:n = true,
        silent    .bool_set:c = { l_@@_sp #1 _silent_bool },
        silent    .initial:n = false,
        silent    .value_required:n = true,
        unknown   .code:n =
          {
            \str_set:Nn \l_@@_command_name_str { sp #1 }
            \@@_unknown_keys_cmd:n {##1}
          },
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spAcirc} and \cs[no-index]{spPcirc}}
%
% \begin{macro}[int]{\@@_spcircfig_sym:n}
%   Typesets |\bigodot| or the plain letter |C|, according to
%   \mykey[spAcirc,spPcirc]{print-sym}. Receives the short command
%   name in `|#1|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcircfig_sym:n
  {
    \str_if_eq:vnTF { l_@@_sp #1 _print_sym_str } { odot }
      { \bigodot }
      { C }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcircfig_word_and_article:n}
%   Sets \myhlc|\l_@@_spcirc_word_tl| to \mykey{read-arg} or
%   |circunferencia|/|círculo| from \mykey{read-sty}, and
%   \myhlc|\l_@@_spcirc_articulo_str| to the matching article, which
%   always follows \mykey{read-sty} even when \mykey{read-arg}
%   overrides the word. Receives the short command name in `|#1|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcircfig_word_and_article:n
  {
    \str_if_eq:vnTF { l_@@_sp #1 _read_sty_str } { circun }
      { \str_set:Nn \l_@@_spcirc_articulo_str { la } }
      { \str_set:Nn \l_@@_spcirc_articulo_str { el } }
    \tl_if_empty:cTF { l_@@_sp #1 _read_arg_tl }
      {
        \str_if_eq:vnTF { l_@@_sp #1 _read_sty_str } { circun }
          { \tl_set:Nn \l_@@_spcirc_word_tl { circunferencia } }
          { \tl_set:Nn \l_@@_spcirc_word_tl { círculo } }
      }
      {
        \tl_set:Ne \l_@@_spcirc_word_tl
          {
            \tl_use:c { l_@@_sp #1 _read_arg_tl }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcircfig_del_word:n}
%   Sets \myhlc|\l_@@_spcircfig_del_word_tl| to the word of
%   \myhlc|\@@_spcircfig_word_and_article:n|, prefixed with |del-| or |de-la-|.
%   Receives the short command name in `|#1|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcircfig_del_word:n
  {
    \@@_spcircfig_word_and_article:n {#1}
    \str_if_eq:VnTF \l_@@_spcirc_articulo_str { el }
      {
        \tl_set:Ne \l_@@_spcircfig_del_word_tl
          { del- \l_@@_spcirc_word_tl }
      }
      {
        \tl_set:Ne \l_@@_spcircfig_del_word_tl
          { de-la- \l_@@_spcirc_word_tl }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcircfig_build_short:n}
%   Handles the short number or letter case: builds
%   \myhlc|\l_@@_spcircfig_intent_arg_tl| as
%   \myhlc|_\l_@@_spcircfig_op_tl|\myhlc|-|\mymeta{number word}, with
%   \mykey{prefix} placed after the leading \myhlc|_| when given and without
%   |:prefix|, or as `\myhlc|_-|' when \mykey{silent} is |true| or \mykey{concept} is
%   |false|. Receives the short command name in `|#1|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcircfig_build_short:n
  {
    \bool_if:cTF { l_@@_sp #1 _silent_bool }
      {
        \tl_set:Nn \l_@@_spcircfig_intent_arg_tl { _- }
      }
      {
        \bool_if:cTF { l_@@_sp #1 _concept_bool }
          {
            \@@_spcircfig_del_word:n {#1}
            \tl_set:Ne \l_@@_spcircfig_intent_arg_tl
              {
                _
                \tl_if_empty:cF { l_@@_sp #1 _prefix_tl }
                  { \tl_use:c { l_@@_sp #1 _prefix_tl } - }
                \l_@@_spcircfig_op_tl - \l_@@_spcircfig_del_word_tl -
                \bool_if:cT { l_@@_sp #1 _read_sub_bool } { sub- }
                \l_@@_geo_luaset_sub_spoken_str
              }
          }
          {
            \tl_set:Nn \l_@@_spcircfig_intent_arg_tl { _- }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcircfig_finish_intent:nn}
%   Appends an optional \mykey{sub-index} suffix and, when |#2| is non-empty,
%   |-con-centro-en| to \myhlc|\l_@@_spcircfig_intent_tl|. Receives the short
%   command name in `|#1|' and the argument in `|#2|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcircfig_finish_intent:nn
  {
    \str_if_empty:cF { l_@@_sp #1 _sub_index_str }
      {
        \tl_put_right:Ne \l_@@_spcircfig_intent_tl
          {
            -
            \bool_if:cT { l_@@_sp #1 _read_sub_bool } { sub- }
            \str_use:c { l_@@_sp #1 _sub_index_str }
          }
      }
    \tl_if_empty:nF {#2}
      {
        \tl_put_right:Nn \l_@@_spcircfig_intent_tl { -con-centro-en }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcircfig_build_concept:nn}
%   Builds \myhlc|\l_@@_spcircfig_intent_arg_tl|. It is `\myhlc|_-|' when \mykey{silent}
%   is |true|, whatever |#2| is. With \mykey{concept}|=false| and
%   \mykey{read-arg} empty, it is `\myhlc|_-|', or \myhlc|_($pt)| when |#2| is non-empty.
%   Otherwise it is \myhlc|_\l_@@_spcircfig_intent_tl|, with |:prefix($pt)|
%   appended only when |#2| is non-empty and with \mykey{prefix} placed after
%   the leading \myhlc|_| when given. With \mykey{concept}|=true|,
%   \myhlc|\l_@@_spcircfig_intent_tl| is
%   \myhlc|\l_@@_spcircfig_op_tl|\myhlc|-|\myhlc|\l_@@_spcircfig_del_word_tl|,
%   set by \myhlc|\@@_spcircfig_del_word:n|; with \mykey{concept}|=false| and
%   \mykey{read-arg} given, it is \mykey{read-arg} as given, without
%   \myhlc|\l_@@_spcircfig_op_tl|, and the user writes the article if needed. In
%   both cases an optional \mykey{sub-index} suffix and, when |#2| is non-empty,
%   |-con-centro-en| follow. Receives the short command name in `|#1|' and the
%   argument in `|#2|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcircfig_build_concept:nn
  {
    \bool_if:cTF { l_@@_sp #1 _silent_bool }
      {
        \tl_set:Nn \l_@@_spcircfig_intent_arg_tl { _- }
      }
      {
        \bool_if:cTF { l_@@_sp #1 _concept_bool }
          {
            \@@_spcircfig_del_word:n {#1}
            \tl_set:Ne \l_@@_spcircfig_intent_tl
              {
                \l_@@_spcircfig_op_tl - \l_@@_spcircfig_del_word_tl
              }
            \@@_spcircfig_finish_intent:nn {#1} {#2}
            \tl_set:Ne \l_@@_spcircfig_intent_arg_tl
              {
                _
                \tl_if_empty:cF { l_@@_sp #1 _prefix_tl }
                  {
                    \tl_use:c { l_@@_sp #1 _prefix_tl } -
                  }
                \l_@@_spcircfig_intent_tl
                \tl_if_empty:nF {#2} { :prefix ( $ pt ) }
              }
          }
          {
            \tl_if_empty:cTF { l_@@_sp #1 _read_arg_tl }
              {
                \tl_if_empty:nTF {#2}
                  { \tl_set:Nn \l_@@_spcircfig_intent_arg_tl { _- } }
                  { \tl_set:Ne \l_@@_spcircfig_intent_arg_tl { _( $ pt ) } }
              }
              {
                \tl_set:Ne \l_@@_spcircfig_intent_tl
                  { \tl_use:c { l_@@_sp #1 _read_arg_tl } }
                \@@_spcircfig_finish_intent:nn {#1} {#2}
                \tl_set:Ne \l_@@_spcircfig_intent_arg_tl
                  {
                    _
                    \l_@@_spcircfig_intent_tl
                    \tl_if_empty:nF {#2} { :prefix ( $ pt ) }
                  }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcircfig_exec:nnn}
%   Receives the short command name in `|#1|' and the argument in `|#3|'. Sets
%   \myhlc|\l_@@_spcircfig_op_tl| to \emph{«área»} or \emph{«perímetro»}. With |#3| empty, it
%   sets \myhlc|\l_@@_spcircfig_short_bool| to |false| and parses nothing else.
%   With a comma in |#3|, it reports |spAcirc-invalid-arg| or
%   |spPcirc-invalid-arg|. Without a comma, it calls
%   \myhlc|\@@_luafun_geo_circ_parse_and_set:n|; on error, it tries
%   \myhlc|\@@_luafun_geo_afig_parse_and_set:n| or
%   \myhlc|\@@_luafun_geo_pfig_parse_and_set:n| for the short number or letter
%   form, and a table-letter match is an error. It reports |spAcirc-invalid-arg|
%   or |spPcirc-invalid-arg| on failure, and then delegates to
%   \myhlc|\@@_spcircfig_build_short:n| or
%   \myhlc|\@@_spcircfig_build_concept:nn| and to
%   \myhlc|\@@_spcircfig_mathml:nn|.
%
%   \begin{important}*
%     The commands |\spAcirc| and |\spPcirc| take no radius or diameter: the
%     argument names the figure, |A_O|, and the values needed to compute it are
%     written elsewhere in the document.
%   \end{important}
%
%   Before processing the \mymarg{argument}, it sets the optional \mymeta{keys} passed in `|#2|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcircfig_exec:nnn
  {
    \keys_set:nn { spintent / sp #1 } {#2}
    \str_if_eq:eeTF {#1} { Acirc }
      { \tl_set:Nn \l_@@_spcircfig_op_tl { área } }
      { \tl_set:Nn \l_@@_spcircfig_op_tl { perímetro } }
    \tl_if_empty:nTF {#3}
      {
        \bool_set_false:N \l_@@_spcircfig_short_bool
      }
      {
        \bool_set_false:N \l_@@_spcircfig_short_bool
        \@@_luafun_spcircfig_comma:n { \exp_not:n {#3} }
        \str_if_eq:VnTF \l_@@_spcircfig_luaset_comma_str { true }
          {
            \msg_error:nnee { spintent } { sp #1 -invalid-arg }
              { \c_backslash_str sp #1 } { \exp_not:n {#3} }
          }
          {
            \@@_luafun_geo_circ_parse_and_set:n { \exp_not:n {#3} }
            \str_if_eq:VnT \l_@@_geo_luaset_error_str { true }
              {
                \str_if_eq:eeTF {#1} { Acirc }
                  { \@@_luafun_geo_afig_parse_and_set:n { \exp_not:n {#3} } }
                  { \@@_luafun_geo_pfig_parse_and_set:n { \exp_not:n {#3} } }
                \str_if_eq:VnF \l_@@_geo_luaset_error_str { true }
                  {
                    \str_if_empty:NTF \l_@@_geo_luaset_letra_word_str
                      { \bool_set_true:N \l_@@_spcircfig_short_bool }
                      { \str_set:Nn \l_@@_geo_luaset_error_str { true } }
                  }
              }
            \bool_if:NF \l_@@_spcircfig_short_bool
              {
                \str_if_eq:VnT \l_@@_geo_luaset_error_str { true }
                  {
                    \msg_error:nnee { spintent } { sp #1 -invalid-arg }
                      { \c_backslash_str sp #1 } { \exp_not:n {#3} }
                  }
              }
          }
      }
    \bool_if:NTF \l_@@_spcircfig_short_bool
      { \@@_spcircfig_build_short:n {#1} }
      { \@@_spcircfig_build_concept:nn {#1} {#3} }
    \@@_spcircfig_mathml:nn {#1} {#3}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcircfig_mathml:nn}
%   Renders \myhlc|\l_@@_spcircfig_intent_arg_tl|, already built by
%   \myhlc|\@@_spcircfig_exec:nnn|. There are three cases: the short case
%   prints the symbol with the short value as its subscript; an empty |#2|
%   prints the symbol alone; a center prints the symbol, plus the optional
%   \mykey{sub-index} subscript, and the center between parentheses. In every
%   case a single native |\MathMLintent| wraps
%   \myhlc|\@@_mathcal:n|\mymarg{A \textnormal{\textcolor{lightgray}{\textbar}} P}
%   and the whole subscript. The center is read with
%   \myhlc|\@@_mathmlarg_point:nnn|, referenced as |$pt|. Receives the short
%   command name in `|#1|' and the argument in `|#2|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcircfig_mathml:nn
  {
    \@@_invisible_sep:
    \MathMLintent { \l_@@_spcircfig_intent_arg_tl }
      {
        \@@_inner_sep:
        \@@_luafun_mrow_intent_arg:nn
          { } { intent = ":silent" }
        {
          \str_if_eq:eeTF {#1} { Acirc } { \@@_mathcal:n { A } }
            { \@@_mathcal:n { P } }
        }
        \bool_if:NTF \l_@@_spcircfig_short_bool
          {
            \c_math_subscript_token
              {
                \@@_spcircfig_sym:n {#1}
                \c_math_subscript_token { \l_@@_geo_luaset_print_tl }
              }
          }
          {
            \c_math_subscript_token
              {
                \MathMLintent { :silent } { \@@_spcircfig_sym:n {#1} }
                \str_if_empty:cF { l_@@_sp #1 _sub_index_str }
                  {
                    \c_math_subscript_token
                      {
                        \MathMLintent { :silent }
                          { \str_use:c { l_@@_sp #1 _sub_index_str } }
                      }
                  }
                \tl_if_empty:nF {#2}
                  {
                    \MathMLintent { :silent } { \lparen }
                    \@@_mathmlarg_point:nnV { pt } { sp #1 }
                      \l_@@_geo_luaset_points_clist
                    \MathMLintent { :silent } { \rparen }
                  }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spAcirc, \spPcirc}
%   Both public commands \ics*{spAcirc} and \ics*{spPcirc} enforce
%   math mode, open a local group, and delegate execution to
%   \myhlc|\@@_spcircfig_exec:nnn|.
%    \begin{macrocode}
\NewDocumentCommand \spAcirc { O{} d() }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \tl_if_novalue:nTF {#2}
            { \@@_spcircfig_exec:nnn { Acirc } {#1} { } }
            { \@@_spcircfig_exec:nnn { Acirc } {#1} {#2} }
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spAcirc } }
  }
\NewDocumentCommand \spPcirc { O{} d() }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \tl_if_novalue:nTF {#2}
            { \@@_spcircfig_exec:nnn { Pcirc } {#1} { } }
            { \@@_spcircfig_exec:nnn { Pcirc } {#1} {#2} }
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spPcirc } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spVol}}\label{cmd:spVol}
%
% The user-level command |\spVol| provides a semantic interface for
% the volume of a solid, always typesetting |\mathcal{V}|.
%
% \subsubsection{Definition of variables for \cs[no-index]{spVol}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spVol_mode_str,
%     \l_@@_spVol_sub_index_str,
%     \l_@@_spVol_read_arg_tl,
%   }
% The variable \myhlc|\l_@@_spVol_mode_str| stores the value of the key
% \mykey{read-cmd}, |school| or |mathcat|. The variable
% \myhlc|\l_@@_spVol_sub_index_str| stores the raw text of \mykey{sub-index}
% and \myhlc|\l_@@_spVol_read_arg_tl| the sanitized text of \mykey{read-arg}.
%    \begin{macrocode}
\str_new:N \l_@@_spVol_mode_str
\str_new:N \l_@@_spVol_sub_index_str
\tl_new:N  \l_@@_spVol_read_arg_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spVol_word_tl,
%     \l_@@_spVol_suffix_tl,
%     \l_@@_spVol_native_bool,
%   }
% The variable \myhlc|\l_@@_spVol_word_tl| stores the word to voice,
% \emph{«volumen»}, and \myhlc|\l_@@_spVol_suffix_tl| the text attached after
% it. The variable \myhlc|\l_@@_spVol_native_bool| records whether that text
% is combined natively with the subscript, silenced, or left for
% \mynvmath{MathCAT} to read on its own.
%    \begin{macrocode}
\tl_new:N   \l_@@_spVol_word_tl
\tl_new:N   \l_@@_spVol_suffix_tl
\bool_new:N \l_@@_spVol_native_bool
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_spVol_read_arg_full_tl}
% The variable \myhlc|\l_@@_spVol_read_arg_full_tl| stores the text to voice
% when \mykey{read-arg} is given.
%    \begin{macrocode}
\tl_new:N \l_@@_spVol_read_arg_full_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of keys for \cs[no-index]{spVol}}
%
% \begin{macro}[int]
%   {
%     read-cmd,
%     sub-index,
%     read-sub,
%     read-arg,
%     concept,
%     silent,
%     unknown,
%   }
%   We define the \mymeta{keys} \mykey[spVol]{read-cmd},
%   \mykey[spVol]{sub-index}, \mykey[spVol]{read-sub},
%   \mykey[spVol]{read-arg}, \mykey[spVol]{concept} and
%   \mykey[spVol]{silent}.
%    \begin{macrocode}
\keys_define:nn { spintent / spVol }
  {
    read-cmd  .choices:nn = { school , mathcat }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spVol_mode_str { school  } }
            {2} { \str_set:Nn \l_@@_spVol_mode_str { mathcat } }
          }
      },
    read-cmd  .initial:n = school,
    read-cmd  .value_required:n = true,
    read-cmd / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { read-cmd } { school,~mathcat } { \exp_not:n {#1} },
    sub-index .code:n =
      { \str_set:Nn \l_@@_spVol_sub_index_str {#1} },
    sub-index .initial:n = ,
    sub-index .value_required:n = true,
    read-sub  .bool_set:N = \l_@@_spVol_read_sub_bool,
    read-sub  .initial:n = false,
    read-sub  .value_required:n = true,
    read-arg  .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spVol_read_arg_tl {#1} },
    read-arg  .initial:n = ,
    read-arg  .value_required:n = true,
    concept   .bool_set:N = \l_@@_spVol_concept_bool,
    concept   .initial:n = true,
    concept   .value_required:n = true,
    silent    .bool_set:N = \l_@@_spVol_silent_bool,
    silent    .initial:n = false,
    silent    .value_required:n = true,
    unknown   .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spVol }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spVol}}
%
% \begin{macro}[int]{\@@_spVol_mathml:}
%   The function \myhlc|\@@_spVol_mathml:| calls
%   \myhlc|\@@_spVol_silent:| when \mykey{silent} is |true|, or
%   \myhlc|\@@_spVol_verbal:| otherwise.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spVol_mathml:
  {
    \@@_invisible_sep:
    \bool_if:NTF \l_@@_spVol_silent_bool
      { \@@_spVol_silent: }
      { \@@_spVol_verbal: }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spVol_silent:}
%   The function \myhlc|\@@_spVol_silent:| typesets |\mathcal{V}| with the
%   intent `\myhlc|_-|': the symbol alone with the shared queue when there is no
%   \mykey{sub-index}, or the symbol and the subscript together in one native
%   |\MathMLintent|, with the subscript silenced, when there is.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spVol_silent:
  {
    \str_if_empty:NTF \l_@@_spVol_sub_index_str
      {
        \@@_luafun_mrow_intent_arg:nn
          { } { intent = "_-" }
        { \mathcal{V} }
      }
      {
        \MathMLintent { _- }
          {
            \@@_inner_sep:
            \mathcal{V}
            \c_math_subscript_token
              {
                \MathMLintent { :silent }
                  { \l_@@_spVol_sub_index_str }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spVol_verbal:}
%   The function \myhlc|\@@_spVol_verbal:| delegates to
%   \myhlc|\@@_spVol_read_arg_print:| when \mykey{read-arg} is given and,
%   otherwise, with \mykey{concept}|=false|, to \myhlc|\@@_spVol_symbol_print:|.
%   With \mykey{concept}|=true| it sets \myhlc|\l_@@_spVol_word_tl| to \emph{«volumen»}
%   and \myhlc|\l_@@_spVol_suffix_tl| to |sub-|\mymeta{value} or \mymeta{value},
%   according to \mykey{read-sub}; the suffix is combined natively only with
%   \mykey{read-cmd}|=school|, and with \mykey{read-cmd}|=mathcat| it is left to
%   \mynvmath{MathCAT}. It delegates the typesetting to
%   \myhlc|\@@_spVol_attach_subscript:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spVol_verbal:
  {
    \tl_if_empty:NTF \l_@@_spVol_read_arg_tl
      {
        \bool_if:NTF \l_@@_spVol_concept_bool
          {
            \tl_set:Nn \l_@@_spVol_word_tl { volumen }
            \str_if_eq:VnTF \l_@@_spVol_mode_str { school }
              { \bool_set_true:N  \l_@@_spVol_native_bool }
              { \bool_set_false:N \l_@@_spVol_native_bool }
            \tl_set:Ne \l_@@_spVol_suffix_tl
              {
                \bool_if:NT \l_@@_spVol_read_sub_bool { sub- }
                \l_@@_spVol_sub_index_str
              }
            \@@_spVol_attach_subscript:
          }
          { \@@_spVol_symbol_print: }
      }
      { \@@_spVol_read_arg_print: }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spVol_symbol_print:}
%   With \mykey{concept}|=false|, the symbol itself is read, but only when
%   \mykey{read-cmd} is |school|: with no \mykey{sub-index}, the shared queue
%   tags the symbol with the intent \myhlc|_V-|; with one, the symbol and the
%   subscript are wrapped together in one native |\MathMLintent|, with the
%   subscript silenced. With \mykey{read-cmd}|=mathcat|, the intent is left
%   empty and \mynvmath{MathCAT} applies its own rules to the symbol and its
%   subscript.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spVol_symbol_print:
  {
    \str_if_empty:NTF \l_@@_spVol_sub_index_str
      {
        \str_if_eq:VnTF \l_@@_spVol_mode_str { school }
          {
            \@@_luafun_mrow_intent_arg:nn
              { } { intent = "_V-" }
            { \mathcal{V} }
          }
          { \mathcal{V} }
      }
      {
        \str_if_eq:VnTF \l_@@_spVol_mode_str { school }
          {
            \MathMLintent
              {
                _V-
                \bool_if:NT \l_@@_spVol_read_sub_bool { sub- }
                \l_@@_spVol_sub_index_str
              }
              {
                \@@_inner_sep:
                \mathcal{V}
                \c_math_subscript_token
                  {
                    \MathMLintent { :silent }
                      { \l_@@_spVol_sub_index_str }
                  }
              }
          }
          {
            \mathcal{V}
            \c_math_subscript_token
              { \l_@@_spVol_sub_index_str }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spVol_attach_subscript:}
%   The function \myhlc|\@@_spVol_attach_subscript:| typesets
%   |\mathcal{V}|. An empty \mykey{sub-index} tags the symbol alone
%   through the shared queue, and \myhlc|\l_@@_spVol_suffix_tl| is
%   discarded since there is no subscript to attach it to. A given
%   one combines symbol, subscript, and
%   \myhlc|\l_@@_spVol_suffix_tl| in one native |\MathMLintent|,
%   subscript silenced, when \myhlc|\l_@@_spVol_native_bool| is
%   |true|, or tags the symbol alone through the shared queue with the
%   subscript left bare when |false|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spVol_attach_subscript:
  {
    \str_if_empty:NTF \l_@@_spVol_sub_index_str
      {
        \@@_luafun_mrow_intent_arg:nn
          { } { intent = "_ \l_@@_spVol_word_tl :nofix" }
        { \mathcal{V} }
      }
      {
        \bool_if:NTF \l_@@_spVol_native_bool
          {
            \MathMLintent
              { _ \l_@@_spVol_word_tl - \l_@@_spVol_suffix_tl :nofix }
              {
                \@@_inner_sep:
                \mathcal{V}
                \c_math_subscript_token
                  {
                    \MathMLintent { :silent }
                      { \l_@@_spVol_sub_index_str }
                  }
              }
          }
          {
            \@@_luafun_mrow_intent_arg:nn
              { } { intent = "_ \l_@@_spVol_word_tl :nofix" }
            { \mathcal{V} }
            \c_math_subscript_token
              { \l_@@_spVol_sub_index_str }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spVol_read_arg_print:}
%   With \mykey{read-arg} given, \mykey{sub-index}/\mykey{read-sub}
%   are ignored for the \emph{spoken} content; the subscript, if any,
%   still displays, silenced. With \mykey{concept}|=true|,
%   \myhlc|\l_@@_spVol_read_arg_full_tl| becomes \emph{«volumen»} followed by
%   \mykey{read-arg}'s text; with |false|, \mykey{read-arg}'s text
%   alone. Combined natively regardless of \mykey{read-cmd}: with no
%   \mykey{sub-index}, the shared queue tags the symbol alone; with
%   one, symbol and subscript are wrapped together in one native
%   |\MathMLintent|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spVol_read_arg_print:
  {
    \bool_if:NTF \l_@@_spVol_concept_bool
      {
        \tl_set:Ne \l_@@_spVol_read_arg_full_tl
          { volumen- \tl_use:N \l_@@_spVol_read_arg_tl }
      }
      {
        \tl_set_eq:NN \l_@@_spVol_read_arg_full_tl \l_@@_spVol_read_arg_tl
      }
    \str_if_empty:NTF \l_@@_spVol_sub_index_str
      {
        \@@_luafun_mrow_intent_arg:nn
          { } { intent = "_ \l_@@_spVol_read_arg_full_tl :nofix" }
        { \mathcal{V} }
      }
      {
        \MathMLintent
          { _ \l_@@_spVol_read_arg_full_tl :nofix }
          {
            \@@_inner_sep:
            \mathcal{V}
            \c_math_subscript_token
              {
                \MathMLintent { :silent }
                  { \l_@@_spVol_sub_index_str }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spVol}
%   The public command \ics*{spVol} enforces \emph{math mode}, opens a
%   local group, and delegates execution to \myhlc|\@@_spVol_mathml:|.
%    \begin{macrocode}
\NewDocumentCommand \spVol { O{} }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \keys_set:nn { spintent / spVol } {#1}
          \@@_spVol_mathml:
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spVol } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spProm}}\label{cmd:spProm}
%
% The user command |\spProm| typesets its \mymarg{argument} with a bar
% on top, |\bar|, the usual notation for a mean, and reads it
% \emph{«promedio de»} followed by the argument. The argument is not
% parsed: it is typeset as given and read by \mynvmath{MathCAT} with its
% own rules.
%
% \subsubsection{Definition of variables for \cs[no-index]{spProm}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spProm_read_arg_tl,
%     \l_@@_spProm_intent_tl,
%   }
%   The variable \myhlc|\l_@@_spProm_read_arg_tl| holds the normalized text
%   of the \mykey{read-arg} key, and
%   \myhlc|\l_@@_spProm_intent_tl| holds the name of the intent used by
%   \myhlc|\@@_spProm_exec:nn|.
%    \begin{macrocode}
\tl_new:N \l_@@_spProm_read_arg_tl
\tl_new:N \l_@@_spProm_intent_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of keys for \cs[no-index]{spProm}}
%
% \begin{macro}[int]{silent, read-arg, unknown}
%   We define the \mymeta{keys} \mykey[spProm]{silent} and
%   \mykey[spProm]{read-arg}.
%    \begin{macrocode}
\keys_define:nn { spintent / spProm }
  {
    silent   .bool_set:N = \l_@@_spProm_silent_bool,
    silent   .initial:n = false,
    silent   .value_required:n = true,
    read-arg .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spProm_read_arg_tl {#1} },
    read-arg .initial:n = ,
    read-arg .value_required:n = true,
    unknown  .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spProm }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spProm}}
%
% \begin{macro}[int]{\@@_spProm_exec:nn}
%   The function \myhlc|\@@_spProm_exec:nn| receives the optional
%   \mymeta{keys} in `|#1|' and the \mymarg{argument} in `|#2|'. It sets the
%   \mymeta{keys} and calls \myhlc|\@@_spProm_mathml:n|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spProm_exec:nn
  {
    \keys_set:nn { spintent / spProm } {#1}
    \@@_spProm_mathml:n {#2}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spProm_mathml:n}
%   The function \myhlc|\@@_spProm_mathml:n| receives the \mymarg{argument}
%   in `|#1|'. Under \mykey{silent} the whole expression is silenced as one
%   unit with the intent `\myhlc|_-|'.
%
%   Otherwise the \mymarg{argument} is tagged through a native
%   |\MathMLarg|, |a|, inside |\bar|, and the outer |\MathMLintent|
%   reads \myhlc|_promedio-de:prefix($a)|. When \mykey{read-arg} is not
%   empty, its text replaces |promedio-de|. The inner ghost
%   \myhlc|\@@_inner_sep:| keeps the content of the intent from
%   collapsing into a single child; a second one inside the
%   |\MathMLarg| does the same for the argument, whose |arg| attribute
%   would otherwise be lost when the argument is a single compound
%   element, such as a letter with a subscript.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spProm_mathml:n
  {
    \@@_invisible_sep:
    \bool_if:NTF \l_@@_spProm_silent_bool
      {
        \MathMLintent { _- }
          {
            \@@_inner_sep:
            \bar {#1}
          }
      }
      {
        \tl_if_empty:NTF \l_@@_spProm_read_arg_tl
          { \tl_set:Nn \l_@@_spProm_intent_tl { promedio-de } }
          {
            \tl_set_eq:NN \l_@@_spProm_intent_tl \l_@@_spProm_read_arg_tl
          }
        \MathMLintent { _ \l_@@_spProm_intent_tl :prefix($a) }
          {
            \@@_inner_sep:
            \bar { \MathMLarg { a } { \@@_inner_sep: #1 } }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spProm}
%   The public command \ics*{spProm} enforces \emph{math mode}, opens a
%   local group, and delegates execution to \myhlc|\@@_spProm_exec:nn|.
%    \begin{macrocode}
\NewDocumentCommand \spProm { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_spProm_exec:nn {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spProm } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The commands of the \texttt{stat} family}\label{cmd:stat}
%
% The user commands |\spmedia|, |\spmediana|, |\spmoda|, |\spdesv|,
% |\spvarz|, |\sprango|, |\spriq|, |\spmin| and |\spmax| typeset the
% usual symbols of descriptive statistics: |MA| for the arithmetic mean, |MD| for the
% median, |MO| for the mode, |s| for the standard deviation, |s^{2}| for the variance,
% |R| for the range, |RIQ| for the interquartile range, |x_{\min}| for the minimum, and
% |x_{\max}| for the maximum. Each one is read with the name of the concept;
% |\spdesv| and |\spvarz| also read their letter.
%
% \subsubsection{Definition of variables for the \texttt{stat} family}
%
% \begin{variable}[int]
%   {
%     \l_@@_spmedia_sub_index_str,
%     \l_@@_spmedia_read_arg_tl,
%     \l_@@_spmediana_sub_index_str,
%     \l_@@_spmediana_read_arg_tl,
%     \l_@@_spmoda_sub_index_str,
%     \l_@@_spmoda_read_arg_tl,
%     \l_@@_spdesv_sub_index_str,
%     \l_@@_spdesv_read_arg_tl,
%     \l_@@_spvarz_sub_index_str,
%     \l_@@_spvarz_read_arg_tl,
%     \l_@@_sprango_sub_index_str,
%     \l_@@_sprango_read_arg_tl,
%     \l_@@_spriq_sub_index_str,
%     \l_@@_spriq_read_arg_tl,
%     \l_@@_spmin_sub_index_str,
%     \l_@@_spmin_read_arg_tl,
%     \l_@@_spmax_sub_index_str,
%     \l_@@_spmax_read_arg_tl,
%   }
%   Each command of the family has its own copy of the variables
%   \myhlc|_sub_index_str| and \myhlc|_read_arg_tl|, which hold the values of
%   the keys \mykey{sub-index} and \mykey{read-arg}, so each command can be
%   configured separately with |\setspintent|. Since the symbol of |\spmin|
%   and |\spmax| already carries a subscript, they have no \mykey{sub-index}
%   nor \mykey{read-sub} keys; their variable \myhlc|_sub_index_str| exists
%   only because \myhlc|\@@_stat_intent:nnnn| reads it, and always keeps its
%   initial value.
%    \begin{macrocode}
\clist_map_inline:nn
  {
    spmedia , spmediana , spmoda , spdesv , spvarz , sprango , spriq ,
    spmin , spmax
  }
  {
    \str_new:c  { l_@@_ #1 _sub_index_str }
    \tl_new:c   { l_@@_ #1 _read_arg_tl   }
  }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_stat_intent_tl,
%   }
%   The variable \myhlc|\l_@@_stat_intent_tl| holds the name of the intent
%   that \myhlc|\@@_stat_intent:nnnn| builds, until
%   \myhlc|\@@_stat_mathml:nnnn| uses it.
%    \begin{macrocode}
\tl_new:N \l_@@_stat_intent_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of keys for the \texttt{stat} family}
%
% \begin{macro}[int]{silent, sub-index, read-sub, read-arg, unknown}
%   We define the \mymeta{keys}
%   \mykey[spmedia,spmediana,spmoda,spdesv,spvarz,sprango,spriq]{silent},
%   \mykey[spmedia,spmediana,spmoda,spdesv,spvarz,sprango,spriq]{sub-index},
%   \mykey[spmedia,spmediana,spmoda,spdesv,spvarz,sprango,spriq]{read-sub}
%   and \mykey[spmedia,spmediana,spmoda,spdesv,spvarz,sprango,spriq]{read-arg}
%   for the seven commands |\spmedia|, |\spmediana|, |\spmoda|, |\spdesv|,
%   |\spvarz|, |\sprango| and |\spriq|.
%    \begin{macrocode}
\clist_map_inline:nn
  {
    spmedia , spmediana , spmoda , spdesv , spvarz , sprango , spriq
  }
  {
    \keys_define:nn { spintent / #1 }
      {
        silent    .bool_set:c = { l_@@_ #1 _silent_bool },
        silent    .initial:n = false,
        silent    .value_required:n = true,
        sub-index .code:n =
          { \str_set:cn { l_@@_ #1 _sub_index_str } {##1} },
        sub-index .initial:n = ,
        sub-index .value_required:n = true,
        read-sub  .bool_set:c = { l_@@_ #1 _read_sub_bool },
        read-sub  .initial:n = false,
        read-sub  .value_required:n = true,
        read-arg  .code:n =
          { \@@_sanitize_affix:cn { l_@@_ #1 _read_arg_tl } {##1} },
        read-arg  .initial:n = ,
        read-arg  .value_required:n = true,
        unknown   .code:n =
          {
            \str_set:Nn \l_@@_command_name_str {#1}
            \@@_unknown_keys_cmd:n {##1}
          },
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{sigma}
%   The key \mykey[spdesv,spvarz]{sigma} exists only for |\spdesv| and
%   |\spvarz|, and it starts in |false|.
%    \begin{macrocode}
\clist_map_inline:nn { spdesv , spvarz }
  {
    \keys_define:nn { spintent / #1 }
      {
        sigma    .bool_set:c = { l_@@_ #1 _sigma_bool },
        sigma    .initial:n = false,
        sigma    .value_required:n = true,
        read-sub .initial:n = true,
      }
  }
%    \end{macrocode}
%   \begin{important}*
%     For |\spdesv| and |\spvarz|, which read a letter, \mykey{read-sub}
%     starts in |true|, since |s sub x| sounds more natural than |s de x| and it
%     avoids hearing the \mymeta{x} as part of the exponent in
%     |varianza s de x al cuadrado|; for the rest of the commands it
%     starts in |false|.
%   \end{important}
% \end{macro}
%
% \begin{macro}[int]{silent, read-arg, unknown}
%   Since the symbol of |\spmin| and |\spmax| already carries a subscript,
%   we define for them only the \mymeta{keys} \mykey[spmin,spmax]{silent}
%   and \mykey[spmin,spmax]{read-arg}.
%    \begin{macrocode}
\clist_map_inline:nn { spmin , spmax }
  {
    \keys_define:nn { spintent / #1 }
      {
        silent   .bool_set:c = { l_@@_ #1 _silent_bool },
        silent   .initial:n = false,
        silent   .value_required:n = true,
        read-arg .code:n =
          { \@@_sanitize_affix:cn { l_@@_ #1 _read_arg_tl } {##1} },
        read-arg .initial:n = ,
        read-arg .value_required:n = true,
        unknown  .code:n =
          {
            \str_set:Nn \l_@@_command_name_str {#1}
            \@@_unknown_keys_cmd:n {##1}
          },
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for the \texttt{stat} family}
%
% \begin{macro}[int]{\@@_stat_typeset:nnn}
%   The function \myhlc|\@@_stat_typeset:nnn| typesets the symbol. It
%   receives the short name of the command in `|#1|', the symbol in `|#2|'
%   and the exponent in `|#3|', empty when there is none. The symbol is
%   followed by the \mykey{sub-index}, if present, with its own
%   |\MathMLintent| set to |:silent|, and then by the exponent.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_stat_typeset:nnn
  {
    #2
    \str_if_empty:cF { l_@@_ #1 _sub_index_str }
      {
        \c_math_subscript_token
          {
            \MathMLintent { :silent }
              { \str_use:c { l_@@_ #1 _sub_index_str } }
          }
      }
    \tl_if_blank:nF {#3}
      { \c_math_superscript_token {#3} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_stat_intent:nnnn, \@@_stat_intent:nnn, \@@_stat_intent:nn}
%   The function \myhlc|\@@_stat_intent:nnnn| builds the name of the intent of
%   the symbols of the family. It receives the short name of the command in
%   `|#1|', the name of the concept in `|#2|', the name of the letter in
%   `|#3|', empty when the symbol is not a letter, and the spoken form of the
%   exponent in `|#4|', empty when there is none, the last three already
%   written as they go in the intent, with hyphens. It stores the name of the
%   intent in \myhlc|\l_@@_stat_intent_tl|.
%
%   With \mykey{silent}|=true|, the intent is `\myhlc|_-|'. With \mykey{read-arg}
%   given, the intent is \myhlc|_|\mymeta{read-arg}|:nofix| and
%   \mykey{sub-index} and \mykey{read-sub} are ignored for the spoken
%   content; the subscript, if any, still displays, silenced. Otherwise
%   the spoken content is the concept, followed by the letter and, when
%   there is a \mykey{sub-index}, by |de-|\mymeta{value}, or by |sub-|\mymeta{value}
%   when \mykey{read-sub} is |true|, which is its default for |\spdesv| and
%   |\spvarz|, and last by the exponent. With no
%   \mykey{sub-index} the intent ends in |:nofix|.
%
%   The functions \myhlc|\@@_stat_intent:nnn| and
%   \myhlc|\@@_stat_intent:nn| are the short forms for the symbols with a
%   letter but no exponent, and for those with neither a letter nor an
%   exponent. They give the empty values to the full form, so no command
%   has to.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_stat_intent:nnnn
  {
    \tl_set:Ne \l_@@_stat_intent_tl
      {
        \bool_if:cTF { l_@@_ #1 _silent_bool }
          { _- }
          {
            \tl_if_empty:cTF { l_@@_ #1 _read_arg_tl }
              {
                _ #2
                \tl_if_blank:nF {#3} { - #3 }
                \str_if_empty:cTF { l_@@_ #1 _sub_index_str }
                  {
                    \tl_if_blank:nF {#4} { - #4 }
                    :nofix
                  }
                  {
                    -
                    \bool_if:cTF { l_@@_ #1 _read_sub_bool } { sub } { de }
                    - \str_use:c { l_@@_ #1 _sub_index_str }
                    \tl_if_blank:nF {#4} { - #4 }
                  }
              }
              { _ \tl_use:c { l_@@_ #1 _read_arg_tl } :nofix }
          }
      }
  }
\cs_new_protected:Nn \@@_stat_intent:nnn
  {
    \@@_stat_intent:nnnn {#1} {#2} {#3} { }
  }
\cs_new_protected:Nn \@@_stat_intent:nn
  {
    \@@_stat_intent:nnnn {#1} {#2} { } { }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_stat_mathml:nnnn}
%   The function \myhlc|\@@_stat_mathml:nnnn| receives the short name of the
%   command in `|#1|', the symbol in `|#2|', the exponent in `|#3|', empty
%   when there is none, and the name of the intent in `|#4|', as stored in
%   \myhlc|\l_@@_stat_intent_tl| by \myhlc|\@@_stat_intent:nnnn|. The
%   symbol, with its subscript and exponent, is always typeset by
%   \myhlc|\@@_stat_typeset:nnn| inside one native |\MathMLintent|,
%   preceded by the inner ghost \myhlc|\@@_inner_sep:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_stat_mathml:nnnn
  {
    \@@_invisible_sep:
    \MathMLintent {#4}
      {
        \@@_inner_sep:
        \@@_stat_typeset:nnn {#1} {#2} {#3}
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_stat_exec:nnn, \@@_stat_simple:nnn, \@@_stat_letter:nnnn}
%   The function \myhlc|\@@_stat_exec:nnn| holds what all the commands of
%   the family share. It receives the short name of the command in `|#1|',
%   the optional \mymeta{keys} in `|#2|' and the code that prints the symbol
%   in `|#3|'. It enforces \emph{math mode}, opens a local group, sets the
%   \mymeta{keys} and only then runs `|#3|', so that the code can depend on
%   the \mymeta{keys} already set, as \mykey{sigma} does.
%
%   The function \myhlc|\@@_stat_simple:nnn| is the short form of
%   \myhlc|\@@_stat_mathml:nnnn| for the symbols with neither a letter nor
%   an exponent. It receives the short name in `|#1|', the symbol in `|#2|'
%   and the name of the concept in `|#3|'.
%
%   The function \myhlc|\@@_stat_letter:nnnn| is the short form for the
%   symbols with a letter but no exponent. It receives the same three
%   arguments and the name of the letter in `|#4|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_stat_exec:nnn
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \keys_set:nn { spintent / #1 } {#2}
          #3
        \group_end:
      }
      { \msg_error:nne { spintent } { need-math-mode } { \exp_not:c {#1} } }
  }
\cs_new_protected:Nn \@@_stat_simple:nnn
  {
    \@@_stat_intent:nn {#1} {#3}
    \@@_stat_mathml:nnnn {#1} {#2} { } { \l_@@_stat_intent_tl }
  }
\cs_new_protected:Nn \@@_stat_letter:nnnn
  {
    \@@_stat_intent:nnn {#1} {#3} {#4}
    \@@_stat_mathml:nnnn {#1} {#2} { } { \l_@@_stat_intent_tl }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spmedia, \spmediana, \spmoda}
%   The public commands \ics*{spmedia}, \ics*{spmediana} and \ics*{spmoda} delegate
%   execution to \myhlc|\@@_stat_exec:nnn|, which prints the symbols |MA|,
%   |MD| and |MO| through \myhlc|\@@_stat_simple:nnn| with the concepts
%   |media-aritmética|, |mediana| and |moda|.
%    \begin{macrocode}
\NewDocumentCommand \spmedia { O{} }
  {
    \@@_stat_exec:nnn { spmedia } {#1}
      { \@@_stat_simple:nnn { spmedia } { \mathrm{MA} } { media-aritmética } }
  }
\NewDocumentCommand \spmediana { O{} }
  {
    \@@_stat_exec:nnn { spmediana } {#1}
      { \@@_stat_simple:nnn { spmediana } { \mathrm{MD} } { mediana } }
  }
\NewDocumentCommand \spmoda { O{} }
  {
    \@@_stat_exec:nnn { spmoda } {#1}
      { \@@_stat_simple:nnn { spmoda } { \mathrm{MO} } { moda } }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spdesv, \spvarz}
%   The public commands \ics*{spdesv} and \ics*{spvarz} work the same way, but
%   \ics*{spdesv} calls \myhlc|\@@_stat_letter:nnnn| and \ics*{spvarz} calls
%   \myhlc|\@@_stat_mathml:nnnn| directly. The symbol is |s|, or |\sigma|
%   when \mykey{sigma} is |true|, and its name is part of the spoken
%   content; |\spvarz| adds the exponent |2|, spoken |al-cuadrado|. The
%   concepts are |desviación-estándar| and |varianza|.
%    \begin{macrocode}
\NewDocumentCommand \spdesv { O{} }
  {
    \@@_stat_exec:nnn { spdesv } {#1}
      {
        \bool_if:NTF \l_@@_spdesv_sigma_bool
          {
            \@@_stat_letter:nnnn { spdesv } { \sigma }
              { desviación-estándar } { sigma }
          }
          {
            \@@_stat_letter:nnnn { spdesv } { s }
              { desviación-estándar } { s }
          }
      }
  }
\NewDocumentCommand \spvarz { O{} }
  {
    \@@_stat_exec:nnn { spvarz } {#1}
      {
        \bool_if:NTF \l_@@_spvarz_sigma_bool
          {
            \@@_stat_intent:nnnn { spvarz } { varianza } { sigma }
              { al-cuadrado }
            \@@_stat_mathml:nnnn { spvarz } { \sigma } { 2 }
              { \l_@@_stat_intent_tl }
          }
          {
            \@@_stat_intent:nnnn { spvarz } { varianza } { s }
              { al-cuadrado }
            \@@_stat_mathml:nnnn { spvarz } { s } { 2 }
              { \l_@@_stat_intent_tl }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\sprango, \spriq, \spmin, \spmax}
%   The public commands \ics*{sprango}, \ics*{spriq}, \ics*{spmin} and \ics*{spmax} work
%   like the first three, with the symbols |R|, |RIQ|, |x_{\min}| and
%   |x_{\max}| and the concepts |rango|, |rango-intercuartil|, |mínimo| and
%   |máximo|.
%    \begin{macrocode}
\NewDocumentCommand \sprango { O{} }
  {
    \@@_stat_exec:nnn { sprango } {#1}
      { \@@_stat_simple:nnn { sprango } { R } { rango } }
  }
\NewDocumentCommand \spriq { O{} }
  {
    \@@_stat_exec:nnn { spriq } {#1}
      { \@@_stat_simple:nnn { spriq } { \mathrm{RIQ} } { rango-intercuartil } }
  }
\NewDocumentCommand \spmin { O{} }
  {
    \@@_stat_exec:nnn { spmin } {#1}
      {
        \@@_stat_simple:nnn { spmin }
          { x \c_math_subscript_token { \min } } { mínimo }
      }
  }
\NewDocumentCommand \spmax { O{} }
  {
    \@@_stat_exec:nnn { spmax } {#1}
      {
        \@@_stat_simple:nnn { spmax }
          { x \c_math_subscript_token { \max } } { máximo }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The commands of the \texttt{stat\_pos} family}\label{cmd:stat-pos}
%
% The user commands |\spcuartil|, |\spdecil| and |\spPerc| typeset the
% measures of position: |Q_{k}| for quartiles, |D_{k}| for deciles, and |P_{k}|
% for percentiles. The index |k| is the \mymarg{argument}: an integer from
% |1| to |3|, |1| to |9| and |1| to |99| respectively. Each one is read
% with the name of the measure and the index, for example,
% \emph{«cuartil 1»}.
%
% \subsubsection{Definition of variables for the \texttt{stat\_pos} family}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcuartil_read_arg_tl,
%     \l_@@_spdecil_read_arg_tl,
%     \l_@@_spPerc_read_arg_tl,
%   }
%   Each command of the family has its own copy of the variable
%   \myhlc|_read_arg_tl|, so each command can be configured separately with
%   |\setspintent|.
%    \begin{macrocode}
\clist_map_inline:nn { spcuartil , spdecil , spPerc }
  {
    \tl_new:c { l_@@_ #1 _read_arg_tl }
  }
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_stat_pos_error_bool,
%     \l_@@_stat_pos_value_int,
%   }
%   The variable \myhlc|\l_@@_stat_pos_error_bool| is set to |true| by
%   \myhlc|\@@_stat_pos_check:nnn| when the index is rejected, so that
%   nothing is typeset. The variable \myhlc|\l_@@_stat_pos_value_int| holds
%   the accepted index without leading zeros.
%    \begin{macrocode}
\bool_new:N \l_@@_stat_pos_error_bool
\int_new:N  \l_@@_stat_pos_value_int
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for the \texttt{stat\_pos} family}
%
% \begin{macro}[int]{stat-pos-invalid-index}
%   The \enquote{error} |stat-pos-invalid-index| is raised when the index passed
%   to |\spcuartil|, |\spdecil| or |\spPerc| is not an integer without sign
%   between |1| and the maximum of the measure.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { stat-pos-invalid-index }
  {
    Invalid~index~'#2'~for~'\tl_trim_spaces:n {#1}'~\msg_line_context:.
  }
  {
    Only~an~integer~without~sign~between~'1'~and~'#3'~is~accepted.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for the \texttt{stat\_pos} family}
%
% \begin{macro}[int]{silent, read-arg, unknown}
%   We define the \mymeta{keys} \mykey[spcuartil,spdecil,spPerc]{silent}
%   and \mykey[spcuartil,spdecil,spPerc]{read-arg} for the three commands.
%    \begin{macrocode}
\clist_map_inline:nn { spcuartil , spdecil , spPerc }
  {
    \keys_define:nn { spintent / #1 }
      {
        silent   .bool_set:c = { l_@@_ #1 _silent_bool },
        silent   .initial:n = false,
        silent   .value_required:n = true,
        read-arg .code:n =
          { \@@_sanitize_affix:cn { l_@@_ #1 _read_arg_tl } {##1} },
        read-arg .initial:n = ,
        read-arg .value_required:n = true,
        unknown  .code:n =
          {
            \str_set:Nn \l_@@_command_name_str {#1}
            \@@_unknown_keys_cmd:n {##1}
          },
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for the \texttt{stat\_pos} family}
%
% \begin{macro}[int]{\@@_stat_pos_check:nnn}
%   The function \myhlc|\@@_stat_pos_check:nnn| receives the short name of the
%   command in `|#1|', the maximum of the index in `|#2|' and the index
%   in `|#3|'. It calls \myhlc|\@@_luafun_clean_split_and_set:n| on the
%   index and accepts it only if it is an integer without sign between
%   |1| and `|#2|'. Otherwise it raises |stat-pos-invalid-index|, whatever the
%   value of \mykey{silent}, and sets \myhlc|\l_@@_stat_pos_error_bool| to |true|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_stat_pos_check:nnn
  {
    \@@_luafun_clean_split_and_set:n { \exp_not:n {#3} }
    \bool_set_true:N \l_@@_stat_pos_error_bool
    \bool_lazy_and:nnT
      { \str_if_eq_p:Vn \l_@@_luaset_has_integer_str { true } }
      { \tl_if_empty_p:N \l_@@_luaset_sign_tl }
      {
        \bool_lazy_and:nnT
          { \int_compare_p:nNn { \l_@@_luaset_only_part_int_str } > { 0 } }
          { \int_compare_p:nNn { \l_@@_luaset_only_part_int_str } < { #2 + 1 } }
          { \bool_set_false:N \l_@@_stat_pos_error_bool }
      }
    \bool_if:NT \l_@@_stat_pos_error_bool
      {
        \msg_error:nneee { spintent } { stat-pos-invalid-index }
          { \token_to_str:c {#1} } { \exp_not:n {#3} } {#2}
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_stat_pos_typeset:nn}
%   The function \myhlc|\@@_stat_pos_typeset:nn| typesets the symbol in `|#1|'
%   with the index in `|#2|' as a subscript, with its own |\MathMLintent|
%   set to |:silent|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_stat_pos_typeset:nn
  {
    #1
    \c_math_subscript_token { \MathMLintent { :silent } {#2} }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_stat_pos_mathml:nnnn, \@@_stat_pos_mathml:nnnV}
%   The function \myhlc|\@@_stat_pos_mathml:nnnn| receives the short name of the
%   command in `|#1|', the symbol in `|#2|', the name of the measure in
%   `|#3|' and the index in `|#4|'. The symbol with its index is always
%   typeset by \myhlc|\@@_stat_pos_typeset:nn| inside one native
%   |\MathMLintent|, preceded by the inner ghost \myhlc|\@@_inner_sep:|.
%
%   With \mykey{silent}|=true|, the intent is `\myhlc|_-|'. With \mykey{read-arg}
%   given, the intent is \myhlc|_|\mymeta{read-arg}|:nofix|. Otherwise the
%   spoken content is the name of the measure followed by the index, as in
%   \myhlc|_cuartil-1:nofix|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_stat_pos_mathml:nnnn
  {
    \@@_invisible_sep:
    \bool_if:cTF { l_@@_ #1 _silent_bool }
      {
        \MathMLintent { _- }
          {
            \@@_inner_sep:
            \@@_stat_pos_typeset:nn {#2} {#4}
          }
      }
      {
        \tl_if_empty:cTF { l_@@_ #1 _read_arg_tl }
          {
            \MathMLintent { _ #3 - #4 :nofix }
              {
                \@@_inner_sep:
                \@@_stat_pos_typeset:nn {#2} {#4}
              }
          }
          {
            \MathMLintent { _ \tl_use:c { l_@@_ #1 _read_arg_tl } :nofix }
              {
                \@@_inner_sep:
                \@@_stat_pos_typeset:nn {#2} {#4}
              }
          }
      }
  }
\cs_generate_variant:Nn \@@_stat_pos_mathml:nnnn { nnnV }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_stat_pos_exec:nnnnn}
%   The function \myhlc|\@@_stat_pos_exec:nnnnn| receives the short name of the
%   command in `|#1|', the symbol in `|#2|', the name of the measure in
%   `|#3|', the maximum of the index in `|#4|' and the index itself in
%   `|#5|'. It validates the index with \myhlc|\@@_stat_pos_check:nnn| and, if
%   it is accepted, stores its value in \myhlc|\l_@@_stat_pos_value_int|, so
%   |025| is printed and read as |25|, and prints it through
%   \myhlc|\@@_stat_pos_mathml:nnnn|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_stat_pos_exec:nnnnn
  {
    \@@_stat_pos_check:nnn {#1} {#4} {#5}
    \bool_if:NF \l_@@_stat_pos_error_bool
      {
        \int_set:NV \l_@@_stat_pos_value_int \l_@@_luaset_only_part_int_str
        \@@_stat_pos_mathml:nnnV {#1} {#2} {#3} \l_@@_stat_pos_value_int
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spcuartil, \spdecil, \spPerc}
%   The public commands \ics*{spcuartil}, \ics*{spdecil} and \ics*{spPerc} enforce
%   \emph{math mode}, open a local group, set the \mymeta{keys} and
%   delegate execution to \myhlc|\@@_stat_pos_exec:nnnnn| with the symbols
%   |Q|, |D| and |P|, the measures |cuartil|, |decil| and |percentil| and
%   the maxima |3|, |9| and |99|.
%    \begin{macrocode}
\NewDocumentCommand \spcuartil { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \keys_set:nn { spintent / spcuartil } {#1}
          \@@_stat_pos_exec:nnnnn { spcuartil } { Q } { cuartil } { 3 } {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spcuartil } }
  }
\NewDocumentCommand \spdecil { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \keys_set:nn { spintent / spdecil } {#1}
          \@@_stat_pos_exec:nnnnn { spdecil } { D } { decil } { 9 } {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spdecil } }
  }
\NewDocumentCommand \spPerc { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \keys_set:nn { spintent / spPerc } {#1}
          \@@_stat_pos_exec:nnnnn { spPerc } { P } { percentil } { 99 } {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spPerc } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spFrec}}\label{cmd:spFrec}
%
% The user command |\spFrec| typesets the usual symbols of the frequencies
% of a table of data: |f| for the absolute frequency, |F| for the accumulated absolute,
% |f_{r}| for the relative, |F_{r}| for the accumulated relative, \myhlc|f_{r}\%| for the
% percentage, and \myhlc|F_{r}\%| for the accumulated percentage. The symbol is
% the \mymarg{argument}, written exactly as it is printed, and it is read
% with the name of the frequency, for example, \emph{«frecuencia absoluta
% acumulada»}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spFrec}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spFrec_sub_index_str,
%     \l_@@_spFrec_read_arg_tl,
%   }
%   The string variable \myhlc|\l_@@_spFrec_sub_index_str| and the token
%   list variable \myhlc|\l_@@_spFrec_read_arg_tl| hold the values of the
%   keys \mykey[spFrec]{sub-index} and \mykey[spFrec]{read-arg}.
%    \begin{macrocode}
\str_new:N \l_@@_spFrec_sub_index_str
\tl_new:N  \l_@@_spFrec_read_arg_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spFrec_symbol_str,
%     \l_@@_spFrec_concept_tl,
%   }
%   The string variable \myhlc|\l_@@_spFrec_symbol_str| holds the
%   \mymarg{argument} without spaces, and the token list variable
%   \myhlc|\l_@@_spFrec_concept_tl| the name of the frequency, empty while
%   the symbol is not in the table.
%    \begin{macrocode}
\str_new:N \l_@@_spFrec_symbol_str
\tl_new:N  \l_@@_spFrec_concept_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spFrec}}
%
% \begin{macro}[int]{spFrec-invalid-symbol, spFrec-invalid-sub-index}
%   The \enquote{error} |spFrec-invalid-symbol| is raised when the symbol is
%   not in the table and the key \mykey[spFrec]{read-arg} was not given.
%   The \enquote{error} |spFrec-invalid-sub-index| is raised when the key
%   \mykey[spFrec]{sub-index} is given with a symbol other than |f| or |F|.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spFrec-invalid-symbol }
  {
    Invalid~symbol~'#1'~for~'\token_to_str:N \spFrec'~\msg_line_context:.
  }
  {
    Use~'f',~'F',~'f_{r}',~'F_{r}',~'f_{r}\iow_char:N \%'~or~'F_{r}\iow_char:N \%',~or~
    give~the~key~'read-arg'.
  }
\msg_new:nnnn { spintent } { spFrec-invalid-sub-index }
  {
    Invalid~key~'sub-index'~for~the~symbol~'#1'~\msg_line_context:.
  }
  {
    Only~the~symbols~'f'~and~'F'~accept~the~key~'sub-index'.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spFrec}}
%
% \begin{macro}[int]{silent, sub-index, read-sub, read-arg, unknown}
%   We define the \mymeta{keys} \mykey[spFrec]{silent},
%   \mykey[spFrec]{sub-index}, \mykey[spFrec]{read-sub} and
%   \mykey[spFrec]{read-arg}, the third one starting in |true|.
%    \begin{macrocode}
\keys_define:nn { spintent / spFrec }
  {
    silent    .bool_set:N = \l_@@_spFrec_silent_bool,
    silent    .initial:n = false,
    silent    .value_required:n = true,
    sub-index .code:n =
      { \str_set:Nn \l_@@_spFrec_sub_index_str {#1} },
    sub-index .initial:n = ,
    sub-index .value_required:n = true,
    read-sub  .bool_set:N = \l_@@_spFrec_read_sub_bool,
    read-sub  .initial:n = true,
    read-sub  .value_required:n = true,
    read-arg  .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spFrec_read_arg_tl {#1} },
    read-arg  .initial:n = ,
    read-arg  .value_required:n = true,
    unknown   .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spFrec }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spFrec}}
%
% \begin{macro}[int]{\@@_spFrec_exec:n}
%   The function \myhlc|\@@_spFrec_exec:n| receives the symbol in `|#1|'.
%   It removes the spaces and looks the result up in the table, setting
%   \myhlc|\l_@@_spFrec_concept_tl| to |frecuencia-absoluta|,
%   |frecuencia-absoluta-acumulada|, |frecuencia-relativa|,
%   |frecuencia-relativa-acumulada|, |frecuencia-porcentual| or
%   |frecuencia-porcentual-acumulada|.
%
%   If the symbol is not in the table, it is rejected with
%   |spFrec-invalid-symbol|, unless the key \mykey[spFrec]{read-arg} was
%   given: then the symbol is printed as it was written and read as
%   \mykey{read-arg} says. A \mykey{sub-index} is accepted only with the
%   symbols |f| and |F|; with any other symbol, even with
%   \mykey{read-arg}, it is rejected with |spFrec-invalid-sub-index|. The
%   \mymarg{argument} is rejected whatever the value of the key
%   \mykey{silent}. When it is accepted, the function delegates the
%   printing to \myhlc|\@@_stat_simple:nnn|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spFrec_exec:n
  {
    \str_set:Ne \l_@@_spFrec_symbol_str { \tl_to_str:n {#1} }
    \str_remove_all:Nn \l_@@_spFrec_symbol_str { ~ }
    \tl_clear:N \l_@@_spFrec_concept_tl
    \str_case:Vn \l_@@_spFrec_symbol_str
      {
        { f }
          { \tl_set:Nn \l_@@_spFrec_concept_tl { frecuencia-absoluta } }
        { F }
          { \tl_set:Nn \l_@@_spFrec_concept_tl { frecuencia-absoluta-acumulada } }
        { f_{r} }
          { \tl_set:Nn \l_@@_spFrec_concept_tl { frecuencia-relativa } }
        { F_{r} }
          { \tl_set:Nn \l_@@_spFrec_concept_tl { frecuencia-relativa-acumulada } }
        { f_{r}\% }
          { \tl_set:Nn \l_@@_spFrec_concept_tl { frecuencia-porcentual } }
        { F_{r}\% }
          {
            \tl_set:Nn \l_@@_spFrec_concept_tl { frecuencia-porcentual-acumulada }
          }
      }
    \bool_lazy_and:nnTF
      { \tl_if_empty_p:N \l_@@_spFrec_concept_tl }
      { \tl_if_empty_p:N \l_@@_spFrec_read_arg_tl }
      {
        \msg_error:nnn { spintent } { spFrec-invalid-symbol } {#1}
      }
      {
        \bool_lazy_and:nnTF
          { \bool_not_p:n { \str_if_empty_p:N \l_@@_spFrec_sub_index_str } }
          {
            \bool_not_p:n
              {
                \bool_lazy_or_p:nn
                  { \str_if_eq_p:Vn \l_@@_spFrec_symbol_str { f } }
                  { \str_if_eq_p:Vn \l_@@_spFrec_symbol_str { F } }
              }
          }
          {
            \msg_error:nnn { spintent } { spFrec-invalid-sub-index } {#1}
          }
          {
            \@@_stat_simple:nnn { spFrec } {#1}
              { \l_@@_spFrec_concept_tl }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spFrec}
%   The public command \ics*{spFrec} enforces \emph{math mode}, opens a local
%   group, sets the \mymeta{keys} and delegates execution to
%   \myhlc|\@@_spFrec_exec:n|.
%    \begin{macrocode}
\NewDocumentCommand \spFrec { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \keys_set:nn { spintent / spFrec } {#1}
          \@@_spFrec_exec:n {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spFrec } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spcase}}\label{cmd:spcase}
%
% The user command |\spcase| typesets the symbols of the cases of a
% probability: |C_{f}| for the favorable cases, |C_{p}| for the possible ones, and |C_{t}| for the total.
% The letter is the \mymarg{argument}, and the symbol is read with the name
% of the cases, for example, \emph{«casos favorables»}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spcase}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcase_sub_index_str,
%     \l_@@_spcase_read_arg_tl,
%   }
%   The token list variable \myhlc|\l_@@_spcase_read_arg_tl| holds the value
%   of the key \mykey[spcase]{read-arg}. This command has no
%   \mykey{sub-index} key, so the string variable
%   \myhlc|\l_@@_spcase_sub_index_str| exists only because
%   \myhlc|\@@_stat_intent:nnnn| reads it, and always keeps its initial
%   value.
%    \begin{macrocode}
\str_new:N \l_@@_spcase_sub_index_str
\tl_new:N  \l_@@_spcase_read_arg_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcase_letter_str,
%     \l_@@_spcase_concept_tl,
%   }
%   The string variable \myhlc|\l_@@_spcase_letter_str| holds the
%   \mymarg{argument} without spaces, and the token list variable
%   \myhlc|\l_@@_spcase_concept_tl| the name of the cases, empty while the
%   letter is not in the table.
%    \begin{macrocode}
\str_new:N \l_@@_spcase_letter_str
\tl_new:N  \l_@@_spcase_concept_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spcase}}
%
% \begin{macro}[int]{spcase-invalid-letter}
%   The \enquote{error} |spcase-invalid-letter| is raised when the
%   \mymarg{argument} is not one of the letters |f|, |p| or |t|.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spcase-invalid-letter }
  {
    Invalid~letter~'#1'~for~'\token_to_str:N \spcase'~\msg_line_context:.
  }
  {
    The~letter~must~be~'f',~'p'~or~'t'.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spcase}}
%
% \begin{macro}[int]{silent, read-arg, unknown}
%   We define the \mymeta{keys} \mykey[spcase]{silent} and
%   \mykey[spcase]{read-arg}.
%    \begin{macrocode}
\keys_define:nn { spintent / spcase }
  {
    silent   .bool_set:N = \l_@@_spcase_silent_bool,
    silent   .initial:n = false,
    silent   .value_required:n = true,
    read-arg .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spcase_read_arg_tl {#1} },
    read-arg .initial:n = ,
    read-arg .value_required:n = true,
    unknown  .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spcase }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spcase}}
%
% \begin{macro}[int]{\@@_spcase_exec:n}
%   The function \myhlc|\@@_spcase_exec:n| receives the letter in `|#1|'.
%   It removes the spaces and looks the result up in the table, setting
%   \myhlc|\l_@@_spcase_concept_tl| to |casos-favorables|, |casos-posibles| or
%   |casos-totales|. If the letter is not in the table, it is rejected with
%   |spcase-invalid-letter| whatever the value of the keys
%   \mykey[spcase]{silent} and \mykey[spcase]{read-arg}. Otherwise the
%   function delegates the printing of the symbol, with the letter as its
%   subscript, to \myhlc|\@@_stat_simple:nnn|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcase_exec:n
  {
    \str_set:Ne \l_@@_spcase_letter_str { \tl_to_str:n {#1} }
    \str_remove_all:Nn \l_@@_spcase_letter_str { ~ }
    \tl_clear:N \l_@@_spcase_concept_tl
    \str_case:Vn \l_@@_spcase_letter_str
      {
        { f } { \tl_set:Nn \l_@@_spcase_concept_tl { casos-favorables } }
        { p } { \tl_set:Nn \l_@@_spcase_concept_tl { casos-posibles } }
        { t } { \tl_set:Nn \l_@@_spcase_concept_tl { casos-totales } }
      }
    \tl_if_empty:NTF \l_@@_spcase_concept_tl
      { \msg_error:nnn { spintent } { spcase-invalid-letter } {#1} }
      {
        \@@_stat_simple:nnn { spcase }
          { C \c_math_subscript_token { \str_use:N \l_@@_spcase_letter_str } }
          { \l_@@_spcase_concept_tl }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spcase}
%   The public command \ics*{spcase} enforces \emph{math mode}, opens a local
%   group, sets the \mymeta{keys} and delegates execution to
%   \myhlc|\@@_spcase_exec:n|.
%    \begin{macrocode}
\NewDocumentCommand \spcase { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \keys_set:nn { spintent / spcase } {#1}
          \@@_spcase_exec:n {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spcase } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spOmega}}\label{cmd:spOmega}
%
% The user command |\spOmega| typesets the symbol |\Omega| of the sample
% space, or |\#\Omega|, which is its cardinality, with the key \mykey[spOmega]{card}.
% It is read \emph{«espacio muestral»} or \emph{«cardinalidad del espacio
% muestral»}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spOmega}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spOmega_sub_index_str,
%     \l_@@_spOmega_read_arg_tl,
%     \l_@@_spOmega_read_sub_bool,
%   }
%   The string variable \myhlc|\l_@@_spOmega_sub_index_str| and the token
%   list variable \myhlc|\l_@@_spOmega_read_arg_tl| hold the values of the
%   keys \mykey[spOmega]{sub-index} and \mykey[spOmega]{read-arg}. This
%   command has no \mykey{read-sub} key, so the boolean variable
%   \myhlc|\l_@@_spOmega_read_sub_bool| starts as |true| and the subscript
%   is always spoken with \emph{«sub»}.
%    \begin{macrocode}
\str_new:N       \l_@@_spOmega_sub_index_str
\tl_new:N        \l_@@_spOmega_read_arg_tl
\bool_new:N      \l_@@_spOmega_read_sub_bool
\bool_set_true:N \l_@@_spOmega_read_sub_bool
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of keys for \cs[no-index]{spOmega}}
%
% \begin{macro}[int]{silent, sub-index, read-arg, card, unknown}
%   We define the \mymeta{keys} \mykey[spOmega]{silent},
%   \mykey[spOmega]{sub-index}, \mykey[spOmega]{read-arg} and
%   \mykey[spOmega]{card}, the last one starting in |false|.
%    \begin{macrocode}
\keys_define:nn { spintent / spOmega }
  {
    silent    .bool_set:N = \l_@@_spOmega_silent_bool,
    silent    .initial:n = false,
    silent    .value_required:n = true,
    sub-index .code:n =
      { \str_set:Nn \l_@@_spOmega_sub_index_str {#1} },
    sub-index .initial:n = ,
    sub-index .value_required:n = true,
    read-arg  .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spOmega_read_arg_tl {#1} },
    read-arg  .initial:n = ,
    read-arg  .value_required:n = true,
    card      .bool_set:N = \l_@@_spOmega_card_bool,
    card      .initial:n = false,
    card      .value_required:n = true,
    unknown   .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spOmega }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spOmega}}
%
% \begin{macro}{\spOmega}
%   The public command \ics*{spOmega} enforces \emph{math mode}, opens a local
%   group, sets the \mymeta{keys} and delegates execution to
%   \myhlc|\@@_stat_simple:nnn|, with the symbol |\Omega| and the concept
%   |espacio-muestral|, or the symbol |\#\Omega| and the concept
%   |cardinalidad-del-espacio-muestral| when \mykey[spOmega]{card} is |true|.
%    \begin{macrocode}
\NewDocumentCommand \spOmega { O{} }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \keys_set:nn { spintent / spOmega } {#1}
          \bool_if:NTF \l_@@_spOmega_card_bool
            {
              \@@_stat_simple:nnn { spOmega } { \# \Omega }
                { cardinalidad-del-espacio-muestral }
            }
            {
              \@@_stat_simple:nnn { spOmega } { \Omega }
                { espacio-muestral }
            }
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spOmega } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spPval}}\label{cmd:spPval}
%
% The user command |\spPval| typesets the value of a probability as a fraction
% |a|/|b|, using |\dfrac| or |\frac| according to \mykey[spPval]{use-dfrac}, read
% natively as \emph{«a de b»}, without parsing in Lua.
%
% \subsubsection{Definition of keys for \cs[no-index]{spPval}}
%
% \begin{macro}[int]{silent, use-dfrac, unknown}
%   We define the \mymeta{keys} \mykey[spPval]{silent} and
%   \mykey[spPval]{use-dfrac}.
%    \begin{macrocode}
\keys_define:nn { spintent / spPval }
  {
    silent    .bool_set:N = \l_@@_spPval_silent_bool,
    silent    .initial:n = false,
    silent    .value_required:n = true,
    use-dfrac .bool_set:N = \l_@@_spPval_use_dfrac_bool,
    use-dfrac .initial:n = false,
    use-dfrac .value_required:n = true,
    unknown   .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spPval }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spPval}}
%
% \begin{macro}[int]{\@@_spPval_exec:nnn}
%   The function \myhlc|\@@_spPval_exec:nnn| receives the optional
%   \mymeta{keys} in `|#1|', the numerator in `|#2|' and the denominator in
%   `|#3|'. It sets the \mymeta{keys} and calls
%   \myhlc|\@@_spPval_mathml:nn|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spPval_exec:nnn
  {
    \keys_set:nn { spintent / spPval } {#1}
    \@@_spPval_mathml:nn {#2} {#3}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spPval_mathml:nn}
%   The function \myhlc|\@@_spPval_mathml:nn| receives the numerator in
%   `|#1|' and the denominator in `|#2|'. Under \mykey{silent}, the
%   fraction, |\frac| or |\dfrac| per \mykey{use-dfrac}, is silenced as one
%   unit. Otherwise numerator and denominator are each tagged through a
%   native |\MathMLarg|, as |num| and |denom|, inside the fraction, and the outer
%   |\MathMLintent| reads \myhlc|_de:infix($num,$denom)|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spPval_mathml:nn
  {
    \@@_invisible_sep:
    \bool_if:NTF \l_@@_spPval_silent_bool
      {
        \MathMLintent { _- }
          {
            \@@_inner_sep:
            \bool_if:NTF \l_@@_spPval_use_dfrac_bool
              { \dfrac {#1} {#2} }
              { \frac  {#1} {#2} }
          }
      }
      {
        \MathMLintent { _de:infix($num,$denom) }
          {
            \@@_inner_sep:
            \bool_if:NTF \l_@@_spPval_use_dfrac_bool
              {
                \dfrac { \MathMLarg { num } {#1} } { \MathMLarg { denom } {#2} }
              }
              { \frac  { \MathMLarg { num } {#1} } { \MathMLarg { denom } {#2} } }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spPval}
%   The public command \ics*{spPval} enforces \emph{math mode}, opens a local
%   group and delegates execution to \myhlc|\@@_spPval_exec:nnn|.
%    \begin{macrocode}
\NewDocumentCommand \spPval { O{} m m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \@@_spPval_exec:nnn {#1} {#2} {#3}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spPval } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spProb}}\label{cmd:spProb}
%
% The user command |\spProb| typesets the probability of an event as
% |P(|\mymarg{event}|)|. The \mymarg{argument} is written as it is printed:
% a single event, or two events joined by exactly one of |\cap|, |\cup|,
% |\mid| or the character `\texttt{\textbar}', which are read \emph{«probabilidad de
% A»}, \emph{«probabilidad de A y B»}, \emph{«probabilidad de A o B»} and
% \emph{«probabilidad de A dado B»}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spProb}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spProb_luaset_error_str,
%     \l_@@_spProb_luaset_op_str,
%     \l_@@_spProb_luaset_a_str,
%     \l_@@_spProb_luaset_b_str,
%   }
%   Variables set by the function \myhlc|\@@_luafun_spProb_parse_and_set:n|.
%   The string variable \myhlc|\l_@@_spProb_luaset_error_str| is |true| when
%   the \mymarg{argument} is invalid. The string variable
%   \myhlc|\l_@@_spProb_luaset_op_str| is |none|, |cap|, |cup| or |mid|, and
%   \myhlc|\l_@@_spProb_luaset_a_str| and \myhlc|\l_@@_spProb_luaset_b_str|
%   hold the event before and after the operator; the second one is empty
%   when there is no operator.
%    \begin{macrocode}
\str_new:N \l_@@_spProb_luaset_error_str
\str_new:N \l_@@_spProb_luaset_op_str
\str_new:N \l_@@_spProb_luaset_a_str
\str_new:N \l_@@_spProb_luaset_b_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_spProb_read_arg_tl}
%   The token list variable \myhlc|\l_@@_spProb_read_arg_tl| stores the key
%   \mykey[spProb]{read-arg}.
%    \begin{macrocode}
\tl_new:N \l_@@_spProb_read_arg_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]{\l_@@_spProb_nested_bool}
%   The boolean variable \myhlc|\l_@@_spProb_nested_bool| makes the
%   redefinition of |\cap|, |\cup| and |\mid| happen only once when the
%   groups are nested, for example, a |\spcomp| inside |\spProb|, so that the
%   saved meanings are the original ones.
%    \begin{macrocode}
\bool_new:N \l_@@_spProb_nested_bool
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spProb}}
%
% \begin{macro}[int]{spProb-invalid-arg}
%   The \enquote{error} |spProb-invalid-arg| is raised when the
%   \mymarg{argument} is empty, has unbalanced braces or parentheses, has an
%   empty event, or has more than one operator.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spProb-invalid-arg }
  {
    Invalid~argument~'#1'~for~'\token_to_str:N \spProb'~\msg_line_context:.
  }
  {
    Use~one~event,~or~two~joined~by~one~of~'\token_to_str:N \cap',~
    '\token_to_str:N \cup',~'\token_to_str:N \mid'~or~'|'~(outside~braces~
    and~parentheses).
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spProb}}
%
% \begin{macro}[int]{silent, read-arg, unknown}
%   We define the \mymeta{keys} \mykey[spProb]{silent} and
%   \mykey[spProb]{read-arg}.
%    \begin{macrocode}
\keys_define:nn { spintent / spProb }
  {
    silent   .bool_set:N = \l_@@_spProb_silent_bool,
    silent   .initial:n = false,
    silent   .value_required:n = true,
    read-arg .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spProb_read_arg_tl {#1} },
    read-arg .initial:n = ,
    read-arg .value_required:n = true,
    unknown  .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spProb }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spProb}}
%
% \begin{macro}[int]{\@@_spProb_cap:, \@@_spProb_cup:, \@@_spProb_mid:, \@@_spProb_nested_ops:, \@@_spProb_event:N}
%   The functions \myhlc|\@@_spProb_cap:|, \myhlc|\@@_spProb_cup:| and
%   \myhlc|\@@_spProb_mid:| wrap the saved meaning of |\cap|, |\cup| and
%   |\mid|, each in its own |\MathMLintent|, which is \myhlc|_y-|, \myhlc|_o| and \myhlc|_dado| respectively, while keeping
%   its math class and therefore its spacing.
%   The function \myhlc|\@@_spProb_nested_ops:| saves the three meanings and
%   redefines, locally, |\cap|, |\cup| and |\mid| as these functions. The function
%   \myhlc|\@@_spProb_event:N| typesets the event stored in its argument
%   inside a group with these definitions, so the operators nested in
%   braces or parentheses are also read; the operators at the top level never
%   go through it, they are printed by \myhlc|\@@_spProb_mathml:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spProb_cap:
  {
    \mathbin { \MathMLintent { _y- } { \@@_spProb_cap_orig: } }
  }
\cs_new_protected:Nn \@@_spProb_cup:
  {
    \mathbin { \MathMLintent { _o } { \@@_spProb_cup_orig: } }
  }
\cs_new_protected:Nn \@@_spProb_mid:
  {
    \mathrel { \MathMLintent { _dado } { \@@_spProb_mid_orig: } }
  }
\cs_new_protected:Nn \@@_spProb_nested_ops:
  {
    \bool_if:NF \l_@@_spProb_nested_bool
      {
        \bool_set_true:N \l_@@_spProb_nested_bool
        \cs_set_eq:NN \@@_spProb_cap_orig: \cap
        \cs_set_eq:NN \@@_spProb_cup_orig: \cup
        \cs_set_eq:NN \@@_spProb_mid_orig: \mid
        \cs_set_eq:NN \cap \@@_spProb_cap:
        \cs_set_eq:NN \cup \@@_spProb_cup:
        \cs_set_eq:NN \mid \@@_spProb_mid:
      }
  }
\cs_new_protected:Nn \@@_spProb_event:N
  {
    \group_begin:
      \@@_spProb_nested_ops:
      \str_use:N #1
    \group_end:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spProb_content:}
%   The function \myhlc|\@@_spProb_content:| typesets the event, or the two
%   events and the operator, without any annotation.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spProb_content:
  {
    \str_use:N \l_@@_spProb_luaset_a_str
    \str_case:Vn \l_@@_spProb_luaset_op_str
      {
        { cap } { \cap \str_use:N \l_@@_spProb_luaset_b_str }
        { cup } { \cup \str_use:N \l_@@_spProb_luaset_b_str }
        { mid } { \mid \str_use:N \l_@@_spProb_luaset_b_str }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spProb_mathml:}
%   The function \myhlc|\@@_spProb_mathml:| prints |P(|\mymeta{content}|)|
%   inside one native |\MathMLintent|, preceded by the outer ghost
%   \myhlc|\@@_invisible_sep:| and by the inner ghost
%   \myhlc|\@@_inner_sep:|.
%
%   With \mykey{silent}|=true|, the intent is `\myhlc|_-|'. With \mykey{read-arg}
%   given, the intent is \myhlc|_|\mymeta{read-arg}|:nofix|. Otherwise the
%   intent is \myhlc|_probabilidad-de:prefix($a)| with the single event tagged as
%   |a|; with an operator, the intent is \myhlc|_probabilidad-de:prefix($p)|, where
%   |p| is the group of the two events, which carries its own intent
%   \myhlc|_y-:infix($a,$b)|, \myhlc|_o:infix($a,$b)| or \myhlc|_dado:infix($a,$b)| according to
%   the operator, with the two events tagged as |a| and |b|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spProb_mathml:
  {
    \@@_invisible_sep:
    \bool_if:NTF \l_@@_spProb_silent_bool
      {
        \MathMLintent { _- }
          {
            \@@_inner_sep:
            P \lparen \@@_spProb_content: \rparen
          }
      }
      {
        \tl_if_empty:NTF \l_@@_spProb_read_arg_tl
          {
            \str_if_eq:VnTF \l_@@_spProb_luaset_op_str { none }
              {
                \MathMLintent { _probabilidad-de:prefix($a) }
                  {
                    \@@_inner_sep:
                    P
                    \lparen
                      \MathMLarg { a }
                        {
                          \@@_inner_sep: \@@_spProb_event:N
                            \l_@@_spProb_luaset_a_str
                        }
                    \rparen
                  }
              }
              {
                \MathMLintent { _probabilidad-de:prefix($p) }
                  {
                    \@@_inner_sep:
                    P
                    \lparen
                      \MathMLarg { p }
                        {
                          \MathMLintent
                            {
                              _
                              \str_case:Vn \l_@@_spProb_luaset_op_str
                                { { cap } { y- } { cup } { o } { mid } { dado } }
                              :infix($a,$b)
                            }
                            {
                              \@@_inner_sep:
                              \MathMLarg { a }
                                {
                                  \@@_inner_sep: \@@_spProb_event:N
                                    \l_@@_spProb_luaset_a_str
                                }
                              \str_case:Vn \l_@@_spProb_luaset_op_str
                                { { cap } { \cap } { cup } { \cup } { mid } { \mid } }
                              \MathMLarg { b }
                                {
                                  \@@_inner_sep: \@@_spProb_event:N
                                    \l_@@_spProb_luaset_b_str
                                }
                            }
                        }
                    \rparen
                  }
              }
          }
          {
            \MathMLintent { _ \tl_use:N \l_@@_spProb_read_arg_tl :nofix }
              {
                \@@_inner_sep:
                P \lparen \@@_spProb_content: \rparen
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spProb_exec:nn}
%   The function \myhlc|\@@_spProb_exec:nn| sets the \mymeta{keys} in
%   `|#1|' and calls \myhlc|\@@_luafun_spProb_parse_and_set:n| with the
%   \mymarg{argument} in `|#2|'. If the Lua module reports an error, it is
%   rejected with |spProb-invalid-arg| whatever the value of the key
%   \mykey{silent}, and nothing is printed; otherwise it delegates the
%   printing to \myhlc|\@@_spProb_mathml:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spProb_exec:nn
  {
    \keys_set:nn { spintent / spProb } {#1}
    \@@_luafun_spProb_parse_and_set:n { \exp_not:n {#2} }
    \str_if_eq:VnTF \l_@@_spProb_luaset_error_str { true }
      { \msg_error:nnn { spintent } { spProb-invalid-arg } {#2} }
      { \@@_spProb_mathml: }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spProb}
%   The public command \ics*{spProb} enforces \emph{math mode}, opens a local
%   group and delegates execution to \myhlc|\@@_spProb_exec:nn|.
%    \begin{macrocode}
\NewDocumentCommand \spProb { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \@@_spProb_exec:nn {#1} {#2}
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spProb } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spcomp}}\label{cmd:spcomp}
%
% The user command |\spcomp| typesets the complement of an event, as
% \mymeta{event}|^{c}| or, with the key \mykey[spcomp]{bar}, as
% |\overline{|\mymeta{event}|}|, and reads it \emph{«complemento de»} the
% event.
%
% \subsubsection{Definition of variables for \cs[no-index]{spcomp}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcomp_read_arg_tl,
%     \l_@@_spcomp_wrap_parens_str,
%   }
% The variable \myhlc|\l_@@_spcomp_read_arg_tl| stores the key
% \mykey[spcomp]{read-arg} and \myhlc|\l_@@_spcomp_wrap_parens_str| the key
% \mykey[spcomp]{wrap-parens}.
%    \begin{macrocode}
\tl_new:N  \l_@@_spcomp_read_arg_tl
\str_new:N \l_@@_spcomp_wrap_parens_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcomp_luaset_compound_str,
%     \l_@@_spcomp_compound_bool,
%     \l_@@_spcomp_do_paren_bool,
%   }
% The function \myhlc|\@@_luafun_spcomp_compound:n| decides whether the event
% contains |\cap|, |\cup|, |\setminus| or |\mid|, whatever the keys are, and
% leaves the answer in \myhlc|\l_@@_spcomp_luaset_compound_str|. The boolean
% variable \myhlc|\l_@@_spcomp_compound_bool| is true in that case. The
% boolean variable \myhlc|\l_@@_spcomp_do_paren_bool| tells whether the event
% is printed in parentheses once the key and the event have been examined.
%    \begin{macrocode}
\str_new:N  \l_@@_spcomp_luaset_compound_str
\bool_new:N \l_@@_spcomp_compound_bool
\bool_new:N \l_@@_spcomp_do_paren_bool
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spcomp}}
%
% \begin{macro}[int]{spcomp-empty-arg}
%   The \enquote{error} |spcomp-empty-arg| is raised when the argument of
%   |\spcomp| is missing or \emph{blank}.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spcomp-empty-arg }
  {
    Empty~argument~for~'\token_to_str:N \spcomp'~\msg_line_context:.
  }
  {
    The~event~is~required.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spcomp}}
%
% \begin{macro}[int]{silent, bar, wrap-parens, read-arg, unknown}
%   We define the \mymeta{keys} \mykey[spcomp]{silent}, \mykey[spcomp]{bar},
%   \mykey[spcomp]{wrap-parens} and \mykey[spcomp]{read-arg}.
%    \begin{macrocode}
\keys_define:nn { spintent / spcomp }
  {
    silent      .bool_set:N = \l_@@_spcomp_silent_bool,
    silent      .initial:n = false,
    silent      .value_required:n = true,
    bar         .bool_set:N = \l_@@_spcomp_bar_bool,
    bar         .initial:n = false,
    bar         .value_required:n = true,
    wrap-parens .choices:nn = { auto , true , false }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spcomp_wrap_parens_str { auto } }
            {2} { \str_set:Nn \l_@@_spcomp_wrap_parens_str { true } }
            {3} { \str_set:Nn \l_@@_spcomp_wrap_parens_str { false } }
          }
      },
    wrap-parens .initial:n = auto,
    wrap-parens .value_required:n = true,
    wrap-parens / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { wrap-parens } { auto,~true,~false } { \exp_not:n {#1} },
    read-arg    .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spcomp_read_arg_tl {#1} },
    read-arg    .initial:n = ,
    read-arg    .value_required:n = true,
    unknown     .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spcomp }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spcomp}}
%
% \begin{macro}[int]{\@@_spcomp_set_paren:n, \@@_spcomp_event:n}
%   The function \myhlc|\@@_spcomp_set_paren:n| sets
%   \myhlc|\l_@@_spcomp_compound_bool| and
%   \myhlc|\l_@@_spcomp_do_paren_bool|. The first one is true when Lua
%   reports that the event contains |\cap|, |\cup|, |\setminus| or |\mid|. The second one
%   follows the key \mykey[spcomp]{wrap-parens}: with |auto| it is true when the
%   event is compound, and it is always false when the key
%   \mykey[spcomp]{bar} is set, since the overline already covers the event. The
%   function
%   \myhlc|\@@_spcomp_event:n| typesets the event in a group where the
%   operators nested in it are read as in \myhlc|\@@_spProb_event:N|, and
%   in parentheses when \myhlc|\l_@@_spcomp_do_paren_bool| is true. The
%   parentheses belong to the event, so that they are inside the
%   |\MathMLarg| and are read.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcomp_set_paren:n
  {
    \bool_set_false:N \l_@@_spcomp_do_paren_bool
    \bool_set_false:N \l_@@_spcomp_compound_bool
    \@@_luafun_spcomp_compound:n { \exp_not:n {#1} }
    \str_if_eq:VnT \l_@@_spcomp_luaset_compound_str { true }
      { \bool_set_true:N \l_@@_spcomp_compound_bool }
    \bool_if:NF \l_@@_spcomp_bar_bool
      {
        \str_case:Vn \l_@@_spcomp_wrap_parens_str
          {
            { true } { \bool_set_true:N \l_@@_spcomp_do_paren_bool }
            { auto }
              {
                \bool_if:NT \l_@@_spcomp_compound_bool
                  { \bool_set_true:N \l_@@_spcomp_do_paren_bool }
              }
          }
      }
  }
\cs_new_protected:Nn \@@_spcomp_event:n
  {
    \group_begin:
      \@@_spProb_nested_ops:
      \bool_if:NTF \l_@@_spcomp_do_paren_bool
        { \lparen #1 \rparen }
        {#1}
    \group_end:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcomp_typeset:n}
%   The function \myhlc|\@@_spcomp_typeset:n| typesets its argument as the
%   complement of the event, with the exponent |c| or with an overline
%   according to the key \mykey[spcomp]{bar}.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcomp_typeset:n
  {
    \bool_if:NTF \l_@@_spcomp_bar_bool
      { \overline {#1} }
      { {#1} ^ { c } }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcomp_exec:nn}
%   The function \myhlc|\@@_spcomp_exec:nn| sets the \mymeta{keys} in
%   `|#1|', calls \myhlc|\@@_spcomp_set_paren:n| with the event in `|#2|'
%   and then \myhlc|\@@_spcomp_mathml:n|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcomp_exec:nn
  {
    \keys_set:nn { spintent / spcomp } {#1}
    \@@_spcomp_set_paren:n {#2}
    \@@_spcomp_mathml:n {#2}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcomp_mathml:n}
%   The function \myhlc|\@@_spcomp_mathml:n| prints the complement of the
%   event `|#1|' inside one native |\MathMLintent|, preceded by the outer
%   ghost \myhlc|\@@_invisible_sep:| and by the inner ghost
%   \myhlc|\@@_inner_sep:|. With \mykey{silent}%
%   |=true| the intent is `\myhlc|_-|'; with \mykey{read-arg} given it is
%   \myhlc|_|\mymeta{read-arg}|:nofix|; otherwise it is
%   \myhlc|_complemento-de:prefix($a)| with the event tagged as |a|; when the
%   key \mykey[spcomp]{bar} is set and the event is compound, the intent is
%   \myhlc|_complemento-de-la-expresión:prefix($a)|, because the overline is not
%   read and the reading would not tell what the complement covers.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcomp_mathml:n
  {
    \@@_invisible_sep:
    \bool_if:NTF \l_@@_spcomp_silent_bool
      {
        \MathMLintent { _- }
          {
            \@@_inner_sep:
            \@@_spcomp_typeset:n { \@@_spcomp_event:n {#1} }
          }
      }
      {
        \tl_if_empty:NTF \l_@@_spcomp_read_arg_tl
          {
            \MathMLintent
              {
                \bool_lazy_and:nnTF
                  { \bool_if_p:N \l_@@_spcomp_bar_bool }
                  { \bool_if_p:N \l_@@_spcomp_compound_bool }
                  { _complemento-de-la-expresión:prefix($a) }
                  { _complemento-de:prefix($a) }
              }
              {
                \@@_inner_sep:
                \@@_spcomp_typeset:n
                  {
                    \MathMLarg { a }
                      { \@@_inner_sep: \@@_spcomp_event:n {#1} }
                  }
              }
          }
          {
            \MathMLintent { _ \tl_use:N \l_@@_spcomp_read_arg_tl :nofix }
              {
                \@@_inner_sep:
                \@@_spcomp_typeset:n { \@@_spcomp_event:n {#1} }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spcomp}
%   The public command \ics*{spcomp} enforces \emph{math mode}, opens a local
%   group and delegates execution to \myhlc|\@@_spcomp_exec:nn|.
%    \begin{macrocode}
\NewDocumentCommand \spcomp { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \tl_if_blank:nTF {#2}
            { \msg_error:nn { spintent } { spcomp-empty-arg } }
            { \@@_spcomp_exec:nn {#1} {#2} }
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spcomp } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spcomb}}\label{cmd:spcomb}
%
% The user command |\spcomb| typesets the counting symbols of
% combinatorics: combinations, variations, arrangements and permutations,
% with or without repetition, and circular permutations. The keys
% \mykey[spcomb]{kind}, \mykey[spcomb]{repeat} and \mykey[spcomb]{circular} choose
% the symbol and the reading; the key \mykey[spcomb]{print-sty} chooses between
% the binomial coefficient and the letter form.
%
% \subsubsection{Definition of variables for \cs[no-index]{spcomb}}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcomb_luaset_single_str,
%     \l_@@_spcomb_luaset_raw_n_str,
%     \l_@@_spcomb_luaset_raw_k_str,
%   }
% These variables are set by the function \myhlc|\@@_luafun_spcomb_split:n|,
% which splits the argument of the command at its first comma of top level,
% ignoring the commas inside braces, parentheses or brackets. The variable
% \myhlc|\l_@@_spcomb_luaset_raw_n_str| stores what precedes the comma and
% \myhlc|\l_@@_spcomb_luaset_raw_k_str| what follows it. The variable
% \myhlc|\l_@@_spcomb_luaset_single_str| is |true| when the argument has no
% such comma.
%    \begin{macrocode}
\str_new:N \l_@@_spcomb_luaset_single_str
\str_new:N \l_@@_spcomb_luaset_raw_n_str
\str_new:N \l_@@_spcomb_luaset_raw_k_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcomb_luaset_error_str,
%     \l_@@_spcomb_luaset_n_str,
%     \l_@@_spcomb_luaset_k_str,
%     \l_@@_spcomb_luaset_list_tl,
%   }
% These variables are set by the function
% \myhlc|\@@_luafun_spcomb_prepare:nnnn|. The variable
% \myhlc|\l_@@_spcomb_luaset_error_str| is |true| when the arguments are
% invalid. The variables \myhlc|\l_@@_spcomb_luaset_n_str| and
% \myhlc|\l_@@_spcomb_luaset_k_str| store the arguments without the
% surrounding spaces. In the |list| mode, \myhlc|\l_@@_spcomb_luaset_list_tl|
% stores the multiplicities, each one in its own braces.
%    \begin{macrocode}
\str_new:N \l_@@_spcomb_luaset_error_str
\str_new:N \l_@@_spcomb_luaset_n_str
\str_new:N \l_@@_spcomb_luaset_k_str
\tl_new:N  \l_@@_spcomb_luaset_list_tl
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcomb_read_arg_tl,
%     \l_@@_spcomb_kind_str,
%     \l_@@_spcomb_print_sty_str,
%   }
%   The token list variable \myhlc|\l_@@_spcomb_read_arg_tl| stores the key
%   \mykey[spcomb]{read-arg}, and the string variables store the keys
%   \mykey[spcomb]{kind} and \mykey[spcomb]{print-sty}.
%    \begin{macrocode}
\tl_new:N  \l_@@_spcomb_read_arg_tl
\str_new:N \l_@@_spcomb_kind_str
\str_new:N \l_@@_spcomb_print_sty_str
%    \end{macrocode}
% \end{variable}
%
% \begin{variable}[int]
%   {
%     \l_@@_spcomb_letter_tl,
%     \l_@@_spcomb_concept_tl,
%     \l_@@_spcomb_mode_str,
%     \l_@@_spcomb_problem_str,
%     \l_@@_spcomb_use_binom_bool,
%     \l_@@_spcomb_list_seq,
%     \l_@@_spcomb_last_tl,
%   }
%   Variables computed from the keys. The token list variable
%   \myhlc|\l_@@_spcomb_letter_tl| holds the letters of the symbol: |C|,
%   |V|, |A| or |P|, followed by |R| and/or |C|; and
%   \myhlc|\l_@@_spcomb_concept_tl| the name of the concept that is read,
%   before the final |-de|. The string variable
%   \myhlc|\l_@@_spcomb_mode_str| is |one|, |pair| or |list| according to
%   the arguments the symbol takes, and \myhlc|\l_@@_spcomb_problem_str|
%   describes an incompatible combination of keys, it is empty when there is
%   none. The boolean variable \myhlc|\l_@@_spcomb_use_binom_bool| is true
%   when the symbol is printed with |\binom|. The sequence
%   \myhlc|\l_@@_spcomb_list_seq| holds the multiplicities to print, and the
%   token list \myhlc|\l_@@_spcomb_last_tl| the last one.
%    \begin{macrocode}
\tl_new:N   \l_@@_spcomb_letter_tl
\tl_new:N   \l_@@_spcomb_concept_tl
\str_new:N  \l_@@_spcomb_mode_str
\str_new:N  \l_@@_spcomb_problem_str
\bool_new:N \l_@@_spcomb_use_binom_bool
\seq_new:N  \l_@@_spcomb_list_seq
\tl_new:N   \l_@@_spcomb_last_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spcomb}}
%
% \begin{macro}[int]
%   {
%     spcomb-invalid-keys,
%     spcomb-missing-arg,
%     spcomb-extra-arg,
%     spcomb-invalid-arg,
%   }
%   The \enquote{error} |spcomb-invalid-keys| is raised for a combination of
%   keys that does not exist: \mykey{circular} without
%   \mykey{kind}|=permutation|, \mykey{circular} with \mykey{repeat}, or \mykey{print-sty}|=binom|
%   for anything but combinations without repetition. The \enquote{error}
%   |spcomb-missing-arg| is raised when |k| is missing and the symbol needs it.
%   The \enquote{error} |spcomb-extra-arg| is raised when |k| is given and the
%   symbol does not take it. The \enquote{error} |spcomb-invalid-arg| is raised
%   when the arguments are empty, when the list of multiplicities has fewer than
%   two elements or an empty one, or when integers are not possible, because |k| is greater
%   than |n| or the multiplicities add more than |n|.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spcomb-invalid-keys }
  {
    Incompatible~keys~for~'\token_to_str:N \spcomb':~'#1'~\msg_line_context:.
  }
  {
    'circular'~needs~'kind=permutation'~and~'repeat=false';~'print-sty=binom'~needs~
    'kind=combination'~and~'repeat=false'.
  }
\msg_new:nnnn { spintent } { spcomb-missing-arg }
  {
    Missing~'k'~(after~the~comma)~for~'\token_to_str:N \spcomb'~\msg_line_context:.
  }
  {
    This~kind~of~symbol~needs~'k'~or~the~list~of~repetitions~after~the~
    comma:~'\token_to_str:N \spcomb {n,k}'.
  }
\msg_new:nnnn { spintent } { spcomb-extra-arg }
  {
    Unexpected~comma~and~'k'~for~'\token_to_str:N \spcomb'~\msg_line_context:.
  }
  {
    Circular~permutations~take~only~'n':~'\token_to_str:N \spcomb {n}'.
  }
\msg_new:nnnn { spintent } { spcomb-invalid-arg }
  {
    Invalid~arguments~'#1'~and~'#2'~for~'\token_to_str:N \spcomb'~\msg_line_context:.
  }
  {
    Use~'n',~'n,k'~or~'n,a,b,...'~(at~least~two~repetitions,~none~empty);~if~
    integers,~'k~<=~n'~and~'a+b+...~<=~n'.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spcomb}}
%
% \begin{macro}[int]{silent, kind, repeat, circular, print-sty, read-arg, unknown}
%   We define the \mymeta{keys} \mykey[spcomb]{silent},
%   \mykey[spcomb]{kind}, \mykey[spcomb]{repeat}, \mykey[spcomb]{circular},
%   \mykey[spcomb]{print-sty} and \mykey[spcomb]{read-arg}.
%    \begin{macrocode}
\keys_define:nn { spintent / spcomb }
  {
    silent    .bool_set:N = \l_@@_spcomb_silent_bool,
    silent    .initial:n = false,
    silent    .value_required:n = true,
    repeat    .bool_set:N = \l_@@_spcomb_repeat_bool,
    repeat    .initial:n = false,
    repeat    .value_required:n = true,
    circular  .bool_set:N = \l_@@_spcomb_circular_bool,
    circular  .initial:n = false,
    circular  .value_required:n = true,
    kind      .choices:nn =
      { combination , variation , arrangement , permutation }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spcomb_kind_str { combination } }
            {2} { \str_set:Nn \l_@@_spcomb_kind_str { variation } }
            {3} { \str_set:Nn \l_@@_spcomb_kind_str { arrangement } }
            {4} { \str_set:Nn \l_@@_spcomb_kind_str { permutation } }
          }
      },
    kind      .initial:n = combination,
    kind      .value_required:n = true,
    kind / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { kind } { combination,~variation,~arrangement,~permutation }
        { \exp_not:n {#1} },
    print-sty .choices:nn = { auto , binom , letter }
      {
        \int_case:nn { \l_keys_choice_int }
          {
            {1} { \str_set:Nn \l_@@_spcomb_print_sty_str { auto } }
            {2} { \str_set:Nn \l_@@_spcomb_print_sty_str { binom } }
            {3} { \str_set:Nn \l_@@_spcomb_print_sty_str { letter } }
          }
      },
    print-sty .initial:n = auto,
    print-sty .value_required:n = true,
    print-sty / unknown .code:n =
      \msg_error:nneee { spintent } { unknown-choice }
        { print-sty } { auto,~binom,~letter } { \exp_not:n {#1} },
    read-arg  .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spcomb_read_arg_tl {#1} },
    read-arg  .initial:n = ,
    read-arg  .value_required:n = true,
    unknown   .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spcomb }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spcomb}}
%
% \begin{macro}[int]{\@@_spcomb_setup:}
%   The function \myhlc|\@@_spcomb_setup:| computes the letters and the
%   concept from the keys, decides whether |\binom| is used, and leaves in
%   \myhlc|\l_@@_spcomb_problem_str| the description of an incompatible
%   combination of keys, if any.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcomb_setup:
  {
    \str_clear:N \l_@@_spcomb_problem_str
    \str_case:Vn \l_@@_spcomb_kind_str
      {
        { combination }
          {
            \tl_set:Nn \l_@@_spcomb_letter_tl  { C }
            \tl_set:Nn \l_@@_spcomb_concept_tl { combinación }
          }
        { variation }
          {
            \tl_set:Nn \l_@@_spcomb_letter_tl  { V }
            \tl_set:Nn \l_@@_spcomb_concept_tl { variación }
          }
        { arrangement }
          {
            \tl_set:Nn \l_@@_spcomb_letter_tl  { A }
            \tl_set:Nn \l_@@_spcomb_concept_tl { arreglo }
          }
        { permutation }
          {
            \tl_set:Nn \l_@@_spcomb_letter_tl  { P }
            \tl_set:Nn \l_@@_spcomb_concept_tl { permutación }
          }
      }
    \bool_if:NT \l_@@_spcomb_circular_bool
      {
        \bool_lazy_and:nnTF
          { \str_if_eq_p:Vn \l_@@_spcomb_kind_str { permutation } }
          { ! \bool_if_p:N \l_@@_spcomb_repeat_bool }
          {
            \tl_put_right:Nn \l_@@_spcomb_letter_tl  { C }
            \tl_put_right:Nn \l_@@_spcomb_concept_tl { -circular }
          }
          {
            \str_set:Nn \l_@@_spcomb_problem_str
              { 'circular'~requires~'kind=permutation'~and~'repeat=false' }
          }
      }
    \bool_if:NT \l_@@_spcomb_repeat_bool
      {
        \tl_put_right:Nn \l_@@_spcomb_letter_tl  { R }
        \tl_put_right:Nn \l_@@_spcomb_concept_tl { -con-repetición }
      }
    \bool_set_false:N \l_@@_spcomb_use_binom_bool
    \bool_lazy_and:nnTF
      { \str_if_eq_p:Vn \l_@@_spcomb_kind_str { combination } }
      { ! \bool_if_p:N \l_@@_spcomb_repeat_bool }
      {
        \str_if_eq:VnF \l_@@_spcomb_print_sty_str { letter }
          { \bool_set_true:N \l_@@_spcomb_use_binom_bool }
      }
      {
        \str_if_eq:VnT \l_@@_spcomb_print_sty_str { binom }
          {
            \str_if_empty:NT \l_@@_spcomb_problem_str
              {
                \str_set:Nn \l_@@_spcomb_problem_str
                  { 'print-sty=binom'~requires~'kind=combination'~and~'repeat=false' }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]
%   {
%     \@@_spcomb_arg:nn,
%     \@@_spcomb_arg_ann:nn,
%     \@@_spcomb_arg_plain:nn,
%     \@@_spcomb_sep:,
%     \@@_spcomb_sep_ann:,
%     \@@_spcomb_sep_plain:,
%     \@@_spcomb_last:n,
%     \@@_spcomb_last_ann:n,
%     \@@_spcomb_last_plain:n,
%   }
%   The functions \myhlc|\@@_spcomb_arg:nn|, \myhlc|\@@_spcomb_sep:| and
%   \myhlc|\@@_spcomb_last:n| are the places where the symbol is annotated.
%   They are set locally to the annotated versions, which use an |\MathMLarg| with the
%   inner ghost, a silent comma, and a last multiplicity that carries the
%   intent \myhlc|_y-:prefix($l)| so that it reads \emph{\enquote{y 4}}, or to the plain
%   ones, the content alone and a plain comma, so that the same function
%   \myhlc|\@@_spcomb_symbol:| prints the symbol in both cases.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcomb_arg_ann:nn
  {
    \MathMLarg {#1} { \@@_inner_sep: #2 }
  }
\cs_new_protected:Nn \@@_spcomb_arg_plain:nn
  {
    #2
  }
\cs_new_eq:NN \@@_spcomb_arg:nn \@@_spcomb_arg_plain:nn
\cs_new_protected:Nn \@@_spcomb_sep_ann:
  {
    \MathMLintent { :silent } { , }
  }
\cs_new_protected:Nn \@@_spcomb_sep_plain:
  {
    ,
  }
\cs_new_eq:NN \@@_spcomb_sep: \@@_spcomb_sep_plain:
\cs_new_protected:Nn \@@_spcomb_last_ann:n
  {
    \MathMLintent { _y-:prefix($l) }
      { \@@_inner_sep: \MathMLarg { l } { \@@_inner_sep: #1 } }
  }
\cs_new_protected:Nn \@@_spcomb_last_plain:n
  {
    #1
  }
\cs_new_eq:NN \@@_spcomb_last:n \@@_spcomb_last_plain:n
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcomb_list:}
%   The function \myhlc|\@@_spcomb_list:| typesets the multiplicities
%   separated by commas; the last one reads with a \emph{\enquote{y}} before it,
%   \emph{\enquote{3 3 y 4}}, through the intent \myhlc|_y-:prefix($l)|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcomb_list:
  {
    \seq_clear:N \l_@@_spcomb_list_seq
    \tl_map_inline:Nn \l_@@_spcomb_luaset_list_tl
      { \seq_put_right:Nn \l_@@_spcomb_list_seq {##1} }
    \seq_pop_right:NN \l_@@_spcomb_list_seq \l_@@_spcomb_last_tl
    \seq_use:Nn \l_@@_spcomb_list_seq { \@@_spcomb_sep: }
    \@@_spcomb_sep:
    \exp_args:NV \@@_spcomb_last:n \l_@@_spcomb_last_tl
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcomb_symbol:}
%   The function \myhlc|\@@_spcomb_symbol:| typesets the symbol: |\binom|
%   with |n| and |k|, or the letters with |k| as the subscript and |n| as the
%   superscript. With one argument |n| is the subscript, and with a list of
%   multiplicities they form the superscript. The letters are grouped so the
%   scripts belong to all of them.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcomb_symbol:
  {
    \bool_if:NTF \l_@@_spcomb_use_binom_bool
      {
        \int_compare:nNnTF { \mathstyle } < { 2 } { \Bigl } { \bigl } \lparen
        \genfrac { } { } { 0pt } { }
          {
            \@@_spcomb_arg:nn { n }
              { \str_use:N \l_@@_spcomb_luaset_n_str }
          }
          {
            \@@_spcomb_arg:nn { k }
              { \str_use:N \l_@@_spcomb_luaset_k_str }
          }
        \int_compare:nNnTF { \mathstyle } < { 2 } { \Bigr } { \bigr } \rparen
      }
      {
        { \tl_use:N \l_@@_spcomb_letter_tl }
        \str_case:Vn \l_@@_spcomb_mode_str
          {
            { pair }
              {
                \c_math_subscript_token
                  {
                    \@@_spcomb_arg:nn { k }
                      { \str_use:N \l_@@_spcomb_luaset_k_str }
                  }
                ^
                  {
                    \@@_spcomb_arg:nn { n }
                      { \str_use:N \l_@@_spcomb_luaset_n_str }
                  }
              }
            { one }
              {
                \c_math_subscript_token
                  {
                    \@@_spcomb_arg:nn { n }
                      { \str_use:N \l_@@_spcomb_luaset_n_str }
                  }
              }
            { list }
              {
                \c_math_subscript_token
                  {
                    \@@_spcomb_arg:nn { n }
                      { \str_use:N \l_@@_spcomb_luaset_n_str }
                  }
                ^
                  {
                    \@@_spcomb_arg:nn { m } { \@@_spcomb_list: }
                  }
              }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcomb_mathml:}
%   The function \myhlc|\@@_spcomb_mathml:| prints the symbol inside one native
%   |\MathMLintent|, preceded by the outer ghost \myhlc|\@@_invisible_sep:|
%   and by the inner ghost \myhlc|\@@_inner_sep:|.
%
%   With \mykey{silent}|=true| the intent is `\myhlc|_-|'. With \mykey{read-arg}
%   given it is \myhlc|_|\mymeta{read-arg}|:nofix|. Otherwise the intent of the
%   whole symbol is the concept followed by |-de|: with |one| argument it is
%   \myhlc|_|\mymeta{concept}|-de:prefix($c)|, where |c| wraps the symbol with the
%   intent \myhlc|_elementos:postfix($n)|; with |pair| it is
%   \myhlc|_|\mymeta{concept}|-de:prefix($c)|, where |c| is the symbol, which
%   carries its own intent \myhlc|_elementos-tomados-de-a:infix($n,$k)|, except for the binomial
%   coefficient, whose single intent is \myhlc|_sobre:infix($n,$k)|; with |list| it is
%   \myhlc|_|\mymeta{concept}|-de:prefix($c)|, where |c| is the symbol, which
%   carries the intent \myhlc|_elementos-con-repeticiones:infix($n,$m)|, and |m| is the
%   group of the multiplicities, typeset by \myhlc|\@@_spcomb_list:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcomb_mathml:
  {
    \@@_invisible_sep:
    \bool_if:NTF \l_@@_spcomb_silent_bool
      {
        \cs_set_eq:NN \@@_spcomb_arg:nn \@@_spcomb_arg_plain:nn
        \cs_set_eq:NN \@@_spcomb_sep: \@@_spcomb_sep_plain:
        \cs_set_eq:NN \@@_spcomb_last:n \@@_spcomb_last_plain:n
        \MathMLintent { _- }
          { \@@_inner_sep: \@@_spcomb_symbol: }
      }
      {
        \tl_if_empty:NTF \l_@@_spcomb_read_arg_tl
          {
            \cs_set_eq:NN \@@_spcomb_arg:nn \@@_spcomb_arg_ann:nn
            \cs_set_eq:NN \@@_spcomb_sep: \@@_spcomb_sep_ann:
            \cs_set_eq:NN \@@_spcomb_last:n \@@_spcomb_last_ann:n
            \str_case:Vn \l_@@_spcomb_mode_str
              {
                { one }
                  {
                    \MathMLintent
                      { _ \tl_use:N \l_@@_spcomb_concept_tl -de:prefix($c) }
                      {
                        \@@_inner_sep:
                        \MathMLarg { c }
                          {
                            \MathMLintent { _elementos:postfix($n) }
                              { \@@_inner_sep: \@@_spcomb_symbol: }
                          }
                      }
                  }
                { pair }
                  {
                    \bool_if:NTF \l_@@_spcomb_use_binom_bool
                      {
                        \MathMLintent { _sobre:infix($n,$k) }
                          { \@@_inner_sep: \@@_spcomb_symbol: }
                      }
                      {
                        \MathMLintent
                          { _ \tl_use:N \l_@@_spcomb_concept_tl -de:prefix($c) }
                          {
                            \@@_inner_sep:
                            \MathMLarg { c }
                              {
                                \MathMLintent { _elementos-tomados-de-a:infix($n,$k) }
                                  { \@@_inner_sep: \@@_spcomb_symbol: }
                              }
                          }
                      }
                  }
                { list }
                  {
                    \MathMLintent
                      { _ \tl_use:N \l_@@_spcomb_concept_tl -de:prefix($c) }
                      {
                        \@@_inner_sep:
                        \MathMLarg { c }
                          {
                            \MathMLintent { _elementos-con-repeticiones:infix($n,$m) }
                              { \@@_inner_sep: \@@_spcomb_symbol: }
                          }
                      }
                  }
              }
          }
          {
            \cs_set_eq:NN \@@_spcomb_arg:nn \@@_spcomb_arg_plain:nn
            \cs_set_eq:NN \@@_spcomb_sep: \@@_spcomb_sep_plain:
            \cs_set_eq:NN \@@_spcomb_last:n \@@_spcomb_last_plain:n
            \MathMLintent { _ \tl_use:N \l_@@_spcomb_read_arg_tl :nofix }
              { \@@_inner_sep: \@@_spcomb_symbol: }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcomb_exec:nn}
%   The function \myhlc|\@@_spcomb_exec:nn| sets the \mymeta{keys} in `|#1|',
%   rejects an incompatible combination of keys, asks Lua to split the
%   argument `|#2|' at its first comma, decides the mode from the keys and
%   from the variable \myhlc|\l_@@_spcomb_luaset_single_str|, and calls
%   \myhlc|\@@_luafun_spcomb_prepare:nnnn| with the pieces. Whatever the
%   value of \mykey{silent}, an invalid case is rejected with its error
%   message and nothing is printed; otherwise the printing is delegated to
%   \myhlc|\@@_spcomb_mathml:|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcomb_exec:nn
  {
    \keys_set:nn { spintent / spcomb } {#1}
    \@@_spcomb_setup:
    \str_if_empty:NTF \l_@@_spcomb_problem_str
      {
        \@@_luafun_spcomb_split:n { \exp_not:n {#2} }
        \bool_if:NTF \l_@@_spcomb_circular_bool
          {
            \str_if_eq:VnTF \l_@@_spcomb_luaset_single_str { true }
              { \str_set:Nn \l_@@_spcomb_mode_str { one } }
              { \str_set:Nn \l_@@_spcomb_mode_str { extra } }
          }
          {
            \bool_lazy_and:nnTF
              { \str_if_eq_p:Vn \l_@@_spcomb_kind_str { permutation } }
              { \bool_if_p:N \l_@@_spcomb_repeat_bool }
              {
                \str_if_eq:VnTF \l_@@_spcomb_luaset_single_str { true }
                  { \str_set:Nn \l_@@_spcomb_mode_str { missing } }
                  { \str_set:Nn \l_@@_spcomb_mode_str { list } }
              }
              {
                \str_if_eq:VnTF \l_@@_spcomb_luaset_single_str { true }
                  {
                    \str_if_eq:VnTF \l_@@_spcomb_kind_str { permutation }
                      { \str_set:Nn \l_@@_spcomb_mode_str { one } }
                      { \str_set:Nn \l_@@_spcomb_mode_str { missing } }
                  }
                  { \str_set:Nn \l_@@_spcomb_mode_str { pair } }
              }
          }
        \str_case:VnF \l_@@_spcomb_mode_str
          {
            { extra }
              { \msg_error:nn { spintent } { spcomb-extra-arg } }
            { missing }
              { \msg_error:nn { spintent } { spcomb-missing-arg } }
          }
          {
            \@@_luafun_spcomb_prepare:nnnn
              { \l_@@_spcomb_mode_str }
              { \l_@@_spcomb_luaset_raw_n_str }
              { \l_@@_spcomb_luaset_raw_k_str }
              {
                \bool_if:NTF \l_@@_spcomb_repeat_bool
                  { false } { true }
              }
            \str_if_eq:VnTF \l_@@_spcomb_luaset_error_str { true }
              {
                \msg_error:nnee { spintent } { spcomb-invalid-arg }
                  { \l_@@_spcomb_luaset_raw_n_str }
                  { \l_@@_spcomb_luaset_raw_k_str }
              }
              { \@@_spcomb_mathml: }
          }
      }
      {
        \msg_error:nne { spintent } { spcomb-invalid-keys }
          { \l_@@_spcomb_problem_str }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spcomb_begin:nn}
%   The function \myhlc|\@@_spcomb_begin:nn| opens a local group, activates
%   the scope attribute and delegates execution to
%   \myhlc|\@@_spcomb_exec:nn|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spcomb_begin:nn
  {
    \group_begin:
      \attribute \l_@@_scope_attr_int = 1
      \@@_spcomb_exec:nn {#1} {#2}
    \group_end:
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spcomb}
%   The public command \ics*{spcomb} enforces \emph{math mode} and delegates
%   execution to \myhlc|\@@_spcomb_begin:nn|, which opens a local group. The
%   mandatory argument `|#2|' is |n| or |n,k|, and Lua splits it.
%    \begin{macrocode}
\NewDocumentCommand \spcomb { O{} m }
  {
    \mode_if_math:TF
      { \@@_spcomb_begin:nn {#1} {#2} }
      { \msg_error:nnn { spintent } { need-math-mode } { \spcomb } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{The command \cs{spfact}}\label{cmd:spfact}
%
% The user command |\spfact| typesets the factorial |n!| and reads it
% \emph{«n factorial»}. It has the keys \mykey[spfact]{silent} and
% \mykey[spfact]{read-arg}.
%
% \subsubsection{Definition of variables for \cs[no-index]{spfact}}
%
% \begin{variable}[int]{\l_@@_spfact_read_arg_tl}
%   The token list variable \myhlc|\l_@@_spfact_read_arg_tl| stores the key
%   \mykey[spfact]{read-arg}.
%    \begin{macrocode}
\tl_new:N \l_@@_spfact_read_arg_tl
%    \end{macrocode}
% \end{variable}
%
% \subsubsection{Definition of error messages for \cs[no-index]{spfact}}
%
% \begin{macro}[int]{spfact-empty-arg}
%   The \enquote{error} |spfact-empty-arg| is raised when the argument of
%   |\spfact| is missing or \emph{blank}.
%    \begin{macrocode}
\msg_new:nnnn { spintent } { spfact-empty-arg }
  {
    Empty~argument~for~'\token_to_str:N \spfact'~\msg_line_context:.
  }
  {
    The~number~n~is~required.
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Definition of keys for \cs[no-index]{spfact}}
%
% \begin{macro}[int]{silent, read-arg, unknown}
%   We define the \mymeta{keys} \mykey[spfact]{silent} and \mykey[spfact]{read-arg}.
%    \begin{macrocode}
\keys_define:nn { spintent / spfact }
  {
    silent   .bool_set:N = \l_@@_spfact_silent_bool,
    silent   .initial:n = false,
    silent   .value_required:n = true,
    read-arg .code:n =
      { \@@_sanitize_affix:Nn \l_@@_spfact_read_arg_tl {#1} },
    read-arg .initial:n = ,
    read-arg .value_required:n = true,
    unknown  .code:n =
      {
        \str_set:Nn \l_@@_command_name_str { spfact }
        \@@_unknown_keys_cmd:n {#1}
      },
  }
%    \end{macrocode}
% \end{macro}
%
% \subsubsection{Internal functions for \cs[no-index]{spfact}}
%
% \begin{macro}[int]{\@@_spfact_exec:nn}
%   The function \myhlc|\@@_spfact_exec:nn| sets the \mymeta{keys} in `|#1|'
%   and calls \myhlc|\@@_spfact_mathml:n| with the argument in `|#2|'.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfact_exec:nn
  {
    \keys_set:nn { spintent / spfact } {#1}
    \@@_spfact_mathml:n {#2}
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}[int]{\@@_spfact_mathml:n}
%   The function \myhlc|\@@_spfact_mathml:n| prints the factorial of `|#1|'
%   inside one native |\MathMLintent|, preceded by the outer ghost
%   \myhlc|\@@_invisible_sep:| and followed by the inner ghost
%   \myhlc|\@@_inner_sep:|. With \mykey{silent}|=true| the intent is `\myhlc|_-|';
%   with \mykey{read-arg} given it is \myhlc|_|\mymeta{read-arg}|:nofix|;
%   otherwise it is \myhlc|_factorial:postfix($n)|.
%    \begin{macrocode}
\cs_new_protected:Nn \@@_spfact_mathml:n
  {
    \@@_invisible_sep:
    \bool_if:NTF \l_@@_spfact_silent_bool
      { \MathMLintent { _- } { \@@_inner_sep: #1 \mathexclam } }
      {
        \tl_if_empty:NTF \l_@@_spfact_read_arg_tl
          {
            \MathMLintent { _factorial:postfix($n) }
              {
                \@@_inner_sep:
                \MathMLarg { n } { \@@_inner_sep: #1 }
                \mathexclam
              }
          }
          {
            \MathMLintent { _ \tl_use:N \l_@@_spfact_read_arg_tl :nofix }
              { \@@_inner_sep: #1 \mathexclam }
          }
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\spfact}
%   The public command \ics*{spfact} enforces \emph{math mode}, opens a local
%   group and delegates execution to \myhlc|\@@_spfact_exec:nn|.
%    \begin{macrocode}
\NewDocumentCommand \spfact { O{} m }
  {
    \mode_if_math:TF
      {
        \group_begin:
          \attribute \l_@@_scope_attr_int = 1
          \tl_if_blank:nTF {#2}
            { \msg_error:nn { spintent } { spfact-empty-arg } }
            { \@@_spfact_exec:nn {#1} {#2} }
        \group_end:
      }
      { \msg_error:nnn { spintent } { need-math-mode } { \spfact } }
  }
%    \end{macrocode}
% \end{macro}
%
% \subsection{Finish package}
%
% Finish package implementation.
%
%    \begin{macrocode}
\file_input_stop:
%</package>
%    \end{macrocode}
% \Finale
% \endinput
% \iffalse
%<*spintentlua>
% \fi
--[[
     Lua module spintent.lua for spanish-mathml-intent package - v1.4 [2026-11-02]
--]]

-- ÍNDICE DE SECCIONES (cada una empieza con su título en mayúsculas)
--   CACHÉ, LPEG Y HERRAMIENTAS GLOBALES
--   ANOTACIÓN INTENT/ARG Y SEPARADOR INVISIBLE
--   FUENTES: \mathcal CLÁSICO Y \right_angle_sqr
--   MOTOR CENTRAL Y NÚMEROS (\spnum y \spunit)
--   UNIDADES FÍSICAS Y CIENTÍFICAS (\spunit)
--   DIVISAS Y MONEDAS (\spmoney)
--   ABREVIATURAS Y ORDINALES (\spshort)
--   SIGLOS Y NÚMEROS ROMANOS (\spsiglo)
--   FECHA Y HORA (\spdate, \sptime)
--   MATEMÁTICAS: MCM Y MCD (\spmcm, \spmcd)
--   SISTEMA SEXAGESIMAL (\spang)
--   MATEMÁTICAS: MÚLTIPLOS Y DIVISORES (\spnM, \spnD)
--   NÚMEROS MIXTOS (\spmQ)
--   GEOMETRÍA PLANA
--   FUNCIONES: \spfun, \spdom, \sprec, \spcod

-- CACHÉ, LPEG Y HERRAMIENTAS GLOBALES

local tonumber   = tonumber
local tostring   = tostring
local math_floor = math.floor
local math_sqrt  = math.sqrt
local math_min   = math.min
local t_sort     = table.sort
local s_format   = string.format
local t_concat   = table.concat
local t_unpack   = table.unpack or unpack
local s_match    = string.match
local s_gmatch   = string.gmatch
local s_sub      = string.sub
local s_gsub     = string.gsub
local s_lower    = string.lower
local s_upper    = string.upper
local s_byte     = string.byte
local d_todirect = node.direct.todirect
local d_tonode   = node.direct.tonode
local d_traverse = node.direct.traverse_glyph
local d_getfield = node.direct.getfield
local d_new      = node.direct.new
local d_setfield = node.direct.setfield
local token_set_macro = token.set_macro

-- Todas las variables que Lua entrega a TeX comparten el prefijo l__spintent_
local function spintent_set_macro(name, value)
    token_set_macro("l__spintent_" .. name, value)
end

-- Valor booleano en la forma que TeX espera
local function spintent_bool_str(cond)
    return cond and "true" or "false"
end

local function spintent_set_bool(name, cond)
    spintent_set_macro(name, spintent_bool_str(cond))
end

local document_cctab = luatexbase.registernumber("c_document_cctab")

local lpeg_base = lpeg or require("lpeg")
local P, R, S, Cs, Ct, Cg, C, Cc =
    lpeg_base.P,
    lpeg_base.R,
    lpeg_base.S,
    lpeg_base.Cs,
    lpeg_base.Ct,
    lpeg_base.Cg,
    lpeg_base.C,
    lpeg_base.Cc

local spintent_spshort_V  = lpeg_base.V
local spintent_digit = R"09"

local spintent_math_space =
    P"\\," + P"\\-" + P"\\;" + P"\\:" + P"\\!" +
    P"\\>" + P"\\quad" + P"\\qquad" + P"~" +
    P"\\ " + P"\\thinspace" + P"\\nobreakspace"

local spintent_discard_space = (S" \t\r\n" + spintent_math_space) / ""
local spintent_opt_spaces = spintent_discard_space^0

-- Trim de espacios centralizado en LPeg (sin backtracking)
local spintent_ws = S" \t\r\n"
local spintent_trim_pattern =
    spintent_ws^0 * C((P(1) - spintent_ws^0 * P(-1))^0) * spintent_ws^0
local function spintent_trim(str)
    return spintent_trim_pattern:match(str)
end

-- Combina trim + minúsculas para los casos que no necesitan conservar
-- el case original después.
local function spintent_normalize_key(str)
    return str and s_lower(spintent_trim(str)) or ""
end

-- Eliminación de espacios matemáticos para cálculos
local spintent_num_chunk = Cs(
    spintent_digit * (spintent_opt_spaces * spintent_digit)^0
)

local spintent_num_chunk_keep = Cs(
    spintent_digit * (
        ((S" \t\r\n" + spintent_math_space)^0 * #spintent_digit) * spintent_digit
    )^0
)

-- luaotfload workaround (https://github.com/latex3/luaotfload/pull/342)
do
    local original = luatexbase.remove_from_callback(
        "luaotfload.patch_font", "luaotfload.rewrite_fontname"
    )
    if original then
        luatexbase.add_to_callback(
            "luaotfload.patch_font", function(tfmdata, specification)
                if type(specification) == "string" then
                    return original(tfmdata, specification)
                else
                    tfmdata.name = specification.name
                end
            end, "luaotfload.rewrite_fontname"
        )
    end
end

-- Registro de las funciones para expl3

local function register_tex_cmd(name, func, args)
    name = "__spintent_" .. name .. ":" .. string.rep("n", #args)
    local scanners = {}
    for i = 1, #args do
        local scan_type = (args[i] == "string" and "scan_argument") or "scan_" .. args[i]
        scanners[i] = token[scan_type]
    end

    local scanning_func
    if #scanners == 0 then
        scanning_func = func
    elseif #scanners == 1 then
        local s1 = scanners[1]
        scanning_func = function() func(s1()) end
    elseif #scanners == 2 then
        local s1, s2 = scanners[1], scanners[2]
        scanning_func = function() func(s1(), s2()) end
    else
        scanning_func = function()
            local values = {}
            for i = 1, #scanners do values[i] = scanners[i]() end
            func(t_unpack(values))
        end
    end

    local index = luatexbase.new_luafunction(name)
    lua.get_functions_table()[index] = scanning_func
    token.set_lua(name, index, "global", "protected")
end

-- ANOTACIÓN INTENT/ARG Y SEPARADOR INVISIBLE

do
-- spintent: anotación de MathML con 'intent'/'arg' sin token
-- fantasma. Aplica cuando el contenido es un noad simple o
-- compuesto (mi, msub, msup, radical, fraction) cuyo 'nucleus'
-- es alcanzable directamente en la lista de nivel superior, o
-- anidado dentro de un accent/contenedor con múltiples items
-- (el callback recursa para encontrarlo).

local spintent_sub_mlist_t = node.id("sub_mlist")

local spintent_pending_mrow_intent_queue = {}

-- El segundo argumento es una lista estilo Lua (key = val), evaluada
-- con load(); mismo mecanismo que \luamml_annotate:en usa para su propio
-- core=/mathml=.
register_tex_cmd("luafun_mrow_intent_arg", function(concept_intent, kv_string)
    local opts = load('return {' .. kv_string .. '}')()

    spintent_pending_mrow_intent_queue[#spintent_pending_mrow_intent_queue + 1] = {
        concept_intent = (concept_intent ~= "") and concept_intent or nil,
        arg_name       = opts.arg,
        child_intent   = opts.intent,
    }
end, { "string", "string" })

-- Numero de atributo asignado por el propio kernel de LuaTeX (sin
-- riesgo de colision con otro paquete).
local spintent_scope_attr = luatexbase.new_attribute("spintent_scope")
spintent_set_macro("luaset_scope_attr_str", tostring(spintent_scope_attr))

-- Reclama 'target' con la siguiente entrada de la cola. Solo si
-- tiene spintent_scope_attr=1 (tipografiado dentro de un comando
-- de spintent) y todavia no tiene mathml_filter propio.
local function spintent_claim_target(target, properties)
    if node.has_attribute(target, spintent_scope_attr) ~= 1 then return end
    local existing = properties[target]
    if existing and existing.mathml_filter then return end

    local entry = table.remove(spintent_pending_mrow_intent_queue, 1)

    local concept_intent, arg_name, child_intent
    if type(entry) == "table" then
        concept_intent = entry.concept_intent
        arg_name       = entry.arg_name
        child_intent   = entry.child_intent
    else
        concept_intent = entry
    end

    local p = properties[target] or {}
    p.mathml_filter = function(result, core)
        if arg_name then result.arg = arg_name end
        if child_intent then result.intent = child_intent end
        if result[0] == "mrow" then
            if concept_intent then result.intent = concept_intent end
            return result, core
        end
        if concept_intent then
            return { [0] = "mrow", intent = concept_intent, result }, nil
        end
        return result, core
    end
    properties[target] = p
end

-- Recursa dentro de cualquier sub_mlist encontrado en .nucleus O EN
-- .sub (subíndice) -- \c_math_subscript_token pone el contenido ahí,
-- no en .nucleus, así que sin esto la cola nunca alcanza nada
-- dentro de un subíndice.
local function spintent_try_annotate(n, properties)
    if #spintent_pending_mrow_intent_queue == 0 then return end

    if n.nucleus then
        local nucleus = n.nucleus
        if nucleus.id == spintent_sub_mlist_t and nucleus.head then
            for inner in node.traverse(nucleus.head) do
                spintent_try_annotate(inner, properties)
                if #spintent_pending_mrow_intent_queue == 0 then return end
            end
        else
            spintent_claim_target(nucleus, properties)
        end
    end

    if #spintent_pending_mrow_intent_queue == 0 then return end

    if n.sub then
        local sub = n.sub
        if sub.id == spintent_sub_mlist_t and sub.head then
            for inner in node.traverse(sub.head) do
                spintent_try_annotate(inner, properties)
                if #spintent_pending_mrow_intent_queue == 0 then return end
            end
        else
            spintent_claim_target(sub, properties)
        end
    end
end

luatexbase.add_to_callback("pre_mlist_to_hlist_filter", function(mlist)
    if #spintent_pending_mrow_intent_queue == 0 then return true end
    local properties = node.get_properties_table()
    for n in node.traverse(mlist) do
        if #spintent_pending_mrow_intent_queue == 0 then break end
        spintent_try_annotate(n, properties)
    end
    -- Cualquier entrada sobrante (un comando pidio mas anotaciones de
    -- las que encontro hojas libres) se descarta aca -- si no, se
    -- queda esperando y puede terminar aplicandose, sin aviso, a una
    -- formula completamente distinta mas adelante en el documento.
    if #spintent_pending_mrow_intent_queue > 0 then
        spintent_pending_mrow_intent_queue = {}
    end
    return true
end, "spintent.wrap_mrow_intent")

luatexbase.declare_callback_rule("pre_mlist_to_hlist_filter",
    "spintent.wrap_mrow_intent", "before", "luamml.to_mathml")

-- Recibe "true"/"false" desde expl3, una sola vez, justo despues de
-- cargar el modulo -- activa/desactiva la marca sptmp="inner"/"outer"
-- en las dos funciones fantasma.
local spintent_debug_ghost = false

register_tex_cmd("luafun_set_debug_ghost", function(state)
    spintent_debug_ghost = (state == "true")
end, { "string" })

-- Reemplaza \c__spintent_invisible_sep_int cuando se usa junto a
-- \MathMLintent nativo (necesita un segundo hermano para no
-- colapsar). Se auto-anota con su propio mathml_filter al
-- crearse -- por eso la recursión de wrap_mrow_intent_arg lo
-- reconoce como "ya reclamado" y lo salta, en vez de confundirlo
-- con un objetivo libre de la cola. A diferencia del fantasma
-- exterior, este NO se deduplica: puede hacer falta más de una
-- vez por fórmula (una por cada \MathMLintent anidado).
register_tex_cmd("luafun_inner_sep", function()
    local kernel = node.new("math_char")
    kernel.char  = 0x2063
    kernel.fam   = 0
    local noad   = node.new("noad")
    noad.subtype = 0
    noad.nucleus = kernel

    local properties = node.get_properties_table()
    local p = properties[kernel] or {}
    p.mathml_filter = function(result, core)
        result.intent = ":silent"
        if spintent_debug_ghost then result.sptmp = "inner" end
        return result, core
    end
    properties[kernel] = p

    node.write(noad)
end, {})

-- \__spintent_point_sep: separador entre puntos consecutivos (AOB, ABC...).
-- Escribe <mo intent=":silent" mathvariant="normal">U+2063</mo>.
-- Tiene que ser <mo>: MathCAT (desde 1d864232, en 0.7.7) fusiona letras
-- <mi> seguidas en una sola palabra antes de leer el intent (heurística
-- "c o s"), aunque tengan arg; un <mi> silencioso entre ellas cuenta
-- como una letra más y no lo evita.
-- El noad sigue siendo ord (el PDF no cambia). Por eso no basta con
-- renombrar en mathml_filter: luamml (noad_to_table) devuelve a <mi>
-- todo <mo> de un noad ord. Con mathml_table, sin mathml_core, luamml
-- usa el elemento tal cual (como luamml-array). :nodes enlaza el glifo
-- con el elemento en el modo mathml-SE. El mathml_filter queda solo
-- como marca de "ya reclamado" para spintent_claim_target.
-- No se deduplica: va uno entre cada par de puntos. Sin marca sptmp: es
-- contenido real de la formula, spmmli-tool no debe quitarlo.
register_tex_cmd("luafun_point_sep", function()
    local kernel = node.new("math_char")
    kernel.char  = 0x2063
    kernel.fam   = 0
    local noad   = node.new("noad")
    noad.subtype = 0
    noad.nucleus = kernel

    local properties = node.get_properties_table()
    local p = properties[kernel] or {}
    p.mathml_table = {
        [0]          = "mo",
        "\u{2063}",
        intent       = ":silent",
        mathvariant  = "normal",
        [":nodes"]   = { kernel },
    }
    p.mathml_filter = function(result, core) return result, core end
    properties[kernel] = p

    node.write(noad)
end, {})

-- \__spintent_invisible_sep: (fantasma exterior, <mi intent=":silent">).
-- Necesario cuando el resultado anotado es el único elemento de
-- nivel superior de la fórmula: sin un segundo hermano, to_math
-- renombra el <mrow> resultante directamente a <math intent=...>
-- en vez de dejarlo como hijo (luamml issue #26).
local spintent_ghost_injected = false

register_tex_cmd("luafun_invisible_sep", function()
    if spintent_ghost_injected then return end
    spintent_ghost_injected = true

    local kernel = node.new("math_char")
    kernel.char  = 0x2063
    kernel.fam   = 0
    local noad   = node.new("noad")
    noad.subtype = 0   -- noad_ord. Forzar op (1) da <mo> correcto
                       -- pero agrega espaciado real de operador al
                       -- PDF (rspace visible). Se acepta <mi> sin
                       -- efecto lateral -- intent=':silent' hace
                       -- que la etiqueta (mi/mo) sea irrelevante
                       -- para cualquier lector.
    noad.nucleus = kernel

    local properties = node.get_properties_table()
    local p = properties[kernel] or {}
    p.mathml_filter = function(result, core)
        result.intent = ":silent"
        if spintent_debug_ghost then result.sptmp = "outer" end
        return result, core
    end
    properties[kernel] = p

    node.write(noad)
end, {})

-- Marca el fantasma exterior como ya colocado, sin escribirlo: lo usa
-- \spmQ para que \spnZ no lo escriba dentro de la fraccion y los
-- comandos siguientes de la formula se comporten como antes.
register_tex_cmd("luafun_invisible_sep_mark", function()
    spintent_ghost_injected = true
end, {})

luatexbase.add_to_callback("pre_mlist_to_hlist_filter", function()
    spintent_ghost_injected = false
    return true
end, "spintent.reset_ghost")
end

-- FUENTES: \mathcal CLÁSICO Y \right_angle_sqr

do
local function spintent_find_free_family()
    for i = 15, 1, -1 do
        local ok, id = pcall(tex.getfontoffamily, i)
        if ok and (not id or id == 0) then return i end
    end
    return nil
end

local function spintent_load_scaled(filename, size, features)
    return luaotfload.define_font {
        lookup = "file",
        name = filename,
        size = size,
        features = { normal = features },
    }
end

local function spintent_load_family(fontfile, features, fam)
    features = table.merged(features, {
        mode = "base",
        script = "math",
        language = "dflt",
    })

    local main_id = tex.getfontoffamily(0)
    local main_fd = main_id and font.getfont(main_id)
    if not main_fd then
        luatexbase.module_error("spintent", "familia matematica 0 sin fuente \z
            -- llama a \\setmathfont antes de \\begin{document}")
        return false
    end
    local cur_size = main_fd.size

    local text_id = spintent_load_scaled(fontfile, cur_size, features)
    if not text_id then return false end
    local mc = font.getfont(text_id).MathConstants
    local script_pct    = (mc and mc.ScriptPercentScaleDown or 70) / 100
    local scriptscr_pct = (mc and mc.ScriptScriptPercentScaleDown or 50) / 100
    local script_id       = spintent_load_scaled(fontfile, math_floor(cur_size * script_pct), features)
    local scriptscript_id = spintent_load_scaled(fontfile, math_floor(cur_size * scriptscr_pct), features)
    if not script_id or not scriptscript_id then return false end

    tex.definefont(true, "MathFBtmpText",      text_id)
    tex.definefont(true, "MathFBtmpScript",    script_id)
    tex.definefont(true, "MathFBtmpScriptScr", scriptscript_id)

    tex.runtoks(function()
        tex.sprint(document_cctab, s_format(
            "\\global\\textfont%d=\\MathFBtmpText\z
            \\global\\scriptfont%d=\\MathFBtmpScript\z
            \\global\\scriptscriptfont%d=\\MathFBtmpScriptScr",
            fam, fam, fam))
    end)
    return true
end

local spintent_cal_slots = {
  A=0x1D49C, B=0x212C,  C=0x1D49E, D=0x1D49F, E=0x2130,  F=0x2131,  G=0x1D4A2,
  H=0x210B,  I=0x2110,  J=0x1D4A5, K=0x1D4A6, L=0x2112,  M=0x2133,  N=0x1D4A9,
  O=0x1D4AA, P=0x1D4AB, Q=0x1D4AC, R=0x211B,  S=0x1D4AE, T=0x1D4AF, U=0x1D4B0,
  V=0x1D4B1, W=0x1D4B2, X=0x1D4B3, Y=0x1D4B4, Z=0x1D4B5,
}

-- spintent_init_math_fallbacks, diferida a \AtBeginDocument.
local spintent_cal_fam, spintent_cal_fam_ok
local spintent_sym_fam, spintent_sym_fam_ok

local function spintent_classic_mathcal(letters)
    if not spintent_cal_fam_ok then tex.sprint(document_cctab, letters); return end
    local parts = {}
    for i = 1, #letters do
        local ch = letters:sub(i,i)
        local cp = spintent_cal_slots[ch]
        parts[#parts+1] = cp and s_format([[\Umathchar 0 %d "%X ]], spintent_cal_fam, cp) or ch
    end
    tex.sprint(document_cctab, parts)
end
register_tex_cmd("luafun_classic_mathcal", spintent_classic_mathcal, {"string"})

local function spintent_right_angle_sqr()
    if not spintent_sym_fam_ok then return end
    tex.sprint(document_cctab, s_format([[\Umathchar 0 %d "299C ]], spintent_sym_fam))
end
register_tex_cmd("luafun_right_angle_sqr", spintent_right_angle_sqr, {})

-- Se llama desde \AtBeginDocument en spanish-mathml-intent.sty, no al cargar el modulo
-- Si no hay fuente matematica cargada, degrada con un aviso..
local function spintent_init_math_fallbacks()
    spintent_cal_fam = spintent_find_free_family()
    spintent_cal_fam_ok = spintent_cal_fam and spintent_load_family("NewCMMath-Regular.otf", {}, spintent_cal_fam)
    spintent_set_bool("luaset_cal_fam_ok_str", spintent_cal_fam_ok)

    spintent_sym_fam = spintent_find_free_family()
    spintent_sym_fam_ok = spintent_sym_fam and spintent_load_family("NewCMMath-Regular.otf", {}, spintent_sym_fam)
    spintent_set_bool("luaset_sym_fam_ok_str", spintent_sym_fam_ok)
end
register_tex_cmd("luafun_init_math_fallbacks", spintent_init_math_fallbacks, {})
end

-- Locales compartidos entre secciones; se asignan dentro de sus bloques do … end
local spintent_number_pattern, spintent_custom_spunit_aliases, spintent_sanitize_for_screen_reader

-- MOTOR CENTRAL Y NÚMEROS (\spnum y \spunit)

do
local spintent_num_sign = S "+-"
local spintent_num_decimal = S ".," + P "{.}" + P "{,}"
local spintent_num_semi = P ";"
local spintent_forbidden_in_extra = spintent_num_decimal + spintent_num_semi
spintent_custom_spunit_aliases = {}

local spintent_extra_char = P(1) - spintent_forbidden_in_extra

spintent_number_pattern = Ct(
    Cg(spintent_num_sign^-1, "sign")
    * spintent_opt_spaces
    * Cg(spintent_num_chunk_keep^-1, "integer")
    * (
        spintent_opt_spaces
        * Cg(C(spintent_num_decimal), "decimal")
        * spintent_opt_spaces
        * Cg(spintent_num_chunk_keep, "fraction")
    )^-1
    * (
        spintent_opt_spaces
        * spintent_num_semi
        * spintent_opt_spaces
        * Cg(spintent_num_chunk_keep, "period")
    )^-1
    * (
        spintent_opt_spaces
        * S"eE"
        * spintent_opt_spaces
        * Cg(Cs(spintent_num_sign^-1 * spintent_num_chunk), "exponent")
    )^-1
    * spintent_opt_spaces
    * Cg(Cs(spintent_extra_char^0), "extra")
    * P(-1)
)

local function spintent_rae_format_digits(str_num, reverse)
    if not str_num or str_num == "" then return "" end

    str_num = tostring(str_num)
    local len = #str_num -- Los números en Lua son 1 byte por carácter (ASCII)
    if len <= 4 then return str_num end

    local chunks = {}
    if reverse then
        for i = 1, len, 3 do
            chunks[#chunks + 1] = s_sub(str_num, i, i + 2)
        end
    else
        local first = len % 3
        if first == 0 then first = 3 end
        chunks[#chunks + 1] = s_sub(str_num, 1, first)

        for i = first + 1, len, 3 do
            chunks[#chunks + 1] = s_sub(str_num, i, i + 2)
        end
    end

    return t_concat(chunks, "\\,")
end

local spintent_strip_math_spaces = Cs((spintent_discard_space + P(1)) ^ 0)

register_tex_cmd("luafun_clean_split_and_set", function(raw_string)
    raw_string = spintent_trim(raw_string)

    local result = spintent_number_pattern:match(raw_string)

    -- Prevención de creación de tabla si falla el match (nil or {})
    local r_sign = (result and result.sign) or ""
    local r_int  = (result and result.integer) or ""
    local r_frac = (result and result.fraction) or ""

    local r_int_clean  = spintent_strip_math_spaces:match(r_int)
    local r_frac_clean = spintent_strip_math_spaces:match(r_frac)

    local r_dec    = (result and result.decimal) or ""
    local r_period = (result and result.period) or ""
    local r_extra  = (result and result.extra)
    local r_exp    = (result and result.exponent) or ""

    local has_exponent = spintent_bool_str(r_exp ~= "")
    local above = ""
    local below = ""
    local unit_status = "valid"
    local has_units = "false"
    local denom_has_numeric = "false"

    if r_extra then
        local extra = spintent_trim(r_extra)

        if spintent_custom_spunit_aliases[extra] then
            extra = spintent_custom_spunit_aliases[extra]
        end

        if extra ~= "" then
            has_units = "true"
            local parts = {}
            for part in s_gmatch(extra, "[^/]+") do
                parts[#parts + 1] = part
            end

            if #parts > 2 or s_match(extra, "/%s*/") then
                unit_status = "multislash"
            else
                above = parts[1] or ""
                below = parts[2] or ""

                if below ~= "" and s_match(below, "^%s*%d") then
                    denom_has_numeric = "true"
                end
            end
        end
    end

    local is_million_clean = "false"
    if r_int_clean ~= "" then
        local int_value = tonumber(r_int_clean)
        if int_value and int_value > 999999 and (int_value % 1000000 == 0) then
            is_million_clean = "true"
        end
    end

    local has_dec = r_dec ~= ""
    local has_per = r_period ~= ""

    local dec_and_per      = "false"
    local not_dec_and_per  = "false"
    local not_dec_not_per  = "false"

    if has_dec and has_per then
        dec_and_per = "true"
    elseif not has_dec and has_per then
        not_dec_and_per = "true"
    elseif not has_dec and not has_per then
        not_dec_not_per = "true"
    end

    local has_integer = "false"

    if r_int_clean ~= "" and r_dec == "" and r_frac_clean == "" and r_period == "" and has_exponent == "false" and has_units == "false" then
        has_integer = "true"
    end

    spintent_set_macro("luaset_part_int_tl", r_int)
    spintent_set_macro("luaset_part_dec_tl", r_frac)
    spintent_set_macro("luaset_sign_tl", r_sign)
    spintent_set_macro("luaset_dec_sep_tl", r_dec)
    spintent_set_macro("luaset_part_period_tl", r_period)
    spintent_set_macro("luaset_only_part_int_str", r_int_clean)
    spintent_set_macro("luaset_only_part_dec_str", r_frac_clean)
    spintent_set_macro("luaset_only_part_period_str", (result and result.period) or "")
    spintent_set_macro("luaset_format_part_int_str", spintent_rae_format_digits(r_int_clean, false))
    spintent_set_macro("luaset_format_part_dec_str", spintent_rae_format_digits(r_frac_clean, true))
    spintent_set_macro("luaset_millons_str", is_million_clean)
    spintent_set_macro("luaset_decimal_period_str", dec_and_per)
    spintent_set_macro("luaset_not_decimal_period_str", not_dec_and_per)
    spintent_set_macro("luaset_not_decimal_not_period_str", not_dec_not_per)
    spintent_set_macro("luaset_has_exponent_str", has_exponent)
    spintent_set_macro("luaset_exponent_tl", r_exp)
    spintent_set_macro("luaset_has_units_str", has_units)
    spintent_set_macro("luaset_status_str", unit_status)
    spintent_set_macro("luaset_arg_above_tl", above)
    spintent_set_macro("luaset_arg_below_tl", below)
    spintent_set_macro("luaset_denom_has_number_str", denom_has_numeric)
    spintent_set_macro("luaset_has_integer_str", has_integer)

    if r_frac_clean ~= "" and s_match(r_frac_clean, "^0+$") then
        spintent_set_macro("luaset_dec_is_all_zeros_str", "true")
    else
        spintent_set_macro("luaset_dec_is_all_zeros_str", "false")
    end
end, { "string" })
end

-- UNIDADES FÍSICAS Y CIENTÍFICAS (\spunit)

do
local spintent_units = {
    ["°K"]   = "°K",
    ["A"]    = "A",    ["B"]    = "B",     ["Bq"]   = "Bq",   ["C"]    = "C",
    ["Da"]   = "Da",   ["F"]    = "F",     ["Gy"]   = "Gy",   ["H"]    = "H",
    ["Hz"]   = "Hz",   ["J"]    = "J",     ["K"]    = "K",    ["l"]    = "l",
    ["L"]    = "L",    ["ℓ"]    = "ℓ",     ["N"]    = "N",    ["Np"]   = "Np",
    ["Pa"]   = "Pa",   ["S"]    = "S",     ["Sv"]   = "Sv",   ["T"]    = "T",
    ["V"]    = "V",    ["W"]    = "W",     ["Wb"]   = "Wb",   ["a"]    = "a",
    ["atm"]  = "atm",  ["au"]   = "au",    ["b"]    = "b",    ["bar"]  = "bar",
    ["cal"]  = "cal",  ["cd"]   = "cd",    ["d"]    = "d",    ["dB"]   = "dB",
    ["dyn"]  = "dyn",  ["eV"]   = "eV",    ["erg"]  = "erg",  ["g"]    = "g",
    ["h"]    = "h",    ["ha"]   = "ha",    ["kat"]  = "kat",  ["kg"]   = "kg",
    ["km"]   = "km",   ["lm"]   = "lm",    ["lx"]   = "lx",   ["m"]    = "m",
    ["cm"]   = "cm",   ["min"]  = "min",   ["mm"]   = "mm",   ["mol"]  = "mol",
    ["pc"]   = "pc",   ["rad"]  = "rad",   ["rpm"]  = "rpm",  ["s"]    = "s",
    ["sr"]   = "sr",   ["t"]    = "t",     ["u"]    = "u",    ["w"]    = "w",
    ["y"]    = "y",    ["°"]    = "°",     ["°C"]   = "°C",   ["°F"]   = "°F",
    ["Ω"]    = "Ω",    ["℧"]    = "℧",     ["′"]    = "′",    ["″"]    = "″",
    ["Å"]    = "Å",    ["µ"]    = "µ",
    ["in"]  = "in",    ["pulgada"]  = "in",   ["pulgadas"]  = "in",
    ["ft"]  = "ft",    ["pie"]      = "ft",   ["pies"]    = "ft",
    ["yd"]  = "yd",    ["yarda"]    = "yd",   ["yardas"]  = "yd",
    ["mi"]  = "mi",    ["milla"]    = "mi",   ["millas"]  = "mi",
    ["lb"]  = "lb",    ["libra"]    = "lb",   ["libras"]  = "lb",
    ["oz"]  = "oz",    ["onza"]     = "oz",   ["onzas"]   = "oz",
    ["gal"] = "gal",   ["galon"]    = "gal",  ["galones"] = "gal",
    ["hora"]     = "h",   ["horas"]      = "h",
    ["dia"]      = "d",   ["dias"]       = "d",
    ["caloria"]  = "cal", ["calorias"]   = "cal",
    ["faradio"]  = "F",   ["culombio"]   = "C",
    ["bit"]      = "b",   ["byte"]       = "B",
    ["bytes"]    = "B",   ["fahrenheit"] = "°F",
    ["kelvin"]   = "K",   ["Kelvin"]   = "K",
    ["ohmio"]    = "Ω",   ["ohm"]      = "Ω",   ["Omega"]    = "Ω",
    ["amperio"]  = "A",   ["ampere"]   = "A",
    ["voltio"]   = "V",   ["volt"]     = "V",
    ["vatio"]    = "W",   ["watt"]     = "W",
    ["julio"]    = "J",   ["joule"]    = "J",
    ["newton"]   = "N",
    ["pascal"]   = "Pa",
    ["hercio"]   = "Hz",  ["hertz"]    = "Hz",
    ["metro"]    = "m",   ["metros"]   = "m",
    ["segundo"]  = "s",   ["segundos"] = "s",
    ["litro"]    = "l",   ["l-litro"]  = "l",
    ["gramo"]    = "g",   ["gramos"]   = "g",
    ["angstrom"] = "Å",
    ["celsius"]  = "°C",  ["degC"]     = "°C",  ["℃"]        = "°C",
    ["degF"]     = "°F",  ["℉"]        = "°F",  ["micro"]    = "µ",
    ["minmin"]   = "′",   ["segseg"]   = "″",   ["u-masa"]   = "u",
    ["mho"]      = "℧",
    ["㎡"]   = "m^2",      ["㎢"]   = "km^2",     ["㎠"]   = "cm^2",     ["㎜²"]  = "mm^2",
    ["㎥"]   = "m^3",      ["㎦"]   = "km^3",     ["㎤"]   = "cm^3",     ["㎜³"]  = "mm^3",
    ["㎐"]   = "Hz",       ["㎑"]   = "kHz",      ["㎒"]   = "MHz",      ["㎓"]   = "GHz",      ["㎔"]   = "THz",
    ["㎽"]   = "mW",       ["㎾"]   = "kW",       ["㎿"]   = "MW",       ["㎴"]   = "V",        ["㎵"]   = "kV",
    ["㎶"]   = "µV",       ["㎷"]   = "mV",       ["㎸"]   = "kV",       ["㎀"]   = "pA",       ["㎁"]   = "nA",
    ["㎂"]   = "µA",       ["㎃"]   = "mA",       ["㎄"]   = "kA",       ["㎫"]   = "MPa",      ["㎬"]   = "GPa",
    ["㎺"]   = "pW",       ["㎻"]   = "nW",       ["㎼"]   = "µW",
    ["㎍"]   = "µg",       ["㎎"]   = "mg",       ["㎏"]   = "kg",
    ["㎜"]   = "mm",       ["㎝"]   = "cm",       ["㎞"]   = "km",       ["㎛"]   = "µm",       ["㎕"]   = "µL",
    ["㎖"]   = "mL",       ["㎗"]   = "dL",       ["㎘"]   = "kL",       ["㎧"]   = "m/s",
    ["㎨"]   = "m/s^2",    ["㎭"]   = "rad",      ["㎮"]   = "rad/s",    ["㎯"]   = "rad/s^2"
}

local spintent_unit_compact_spoken_names = {
    ["㎡"]     = "metro-cuadrado",
    ["㎥"]     = "metro-cúbico",
    ["㎢"]     = "kilómetro-cuadrado",
    ["㎟"]     = "milímetro-cuadrado",
    ["㎠"]     = "centímetro-cuadrado",
    ["㎣"]     = "milímetro-cúbico",
    ["㎤"]     = "centímetro-cúbico",
    ["㎦"]     = "kilómetro-cúbico",
    ["㎧"]     = "metros-por-segundo",
    ["㎨"]     = "metros-por-segundo-al-cuadrado",
    ["㎐"]     = "hercio",
    ["℃"]     = "grados-celsius",
    ["℉"]     = "grados-fahrenheit",
    ["'"]      = "minutos",
    ["''"]     = "segundos",
    ['"']      = "segundos",
    ["minmin"] = "minutos",
    ["segseg"] = "segundos",
    ["′"]      = "minutos",
    ["″"]      = "segundos",
}

local spintent_custom_spunit_spoken_names = {}

local spintent_normalizations = {
    ["K"] = "K", ["Ω"] = "Ω", ["ℓ"] = "\\ell", ["μ"] = "µ", ["°K"] = "°K",
    ["'"]  = "′", ["''"] = "″", ['"']  = "″", ["minmin"] = "′", ["segseg"] = "″"
}

local spintent_tex_accents = {
    ["\\'a"] = "á", ["\\'e"] = "é", ["\\'i"] = "í", ["\\'o"] = "ó", ["\\'u"] = "ú",
    ["\\'A"] = "Á", ["\\'E"] = "É", ["\\'I"] = "Í", ["\\'O"] = "Ó", ["\\'U"] = "Ú",
    ["\\\"u"] = "ü", ["\\\"U"] = "Ü", ["\\~n"] = "ñ", ["\\~N"] = "Ñ"
}

function spintent_sanitize_for_screen_reader(raw_string)
    if not raw_string then return "" end
    local clean = s_gsub(raw_string, "[{}]", "")
    clean = s_gsub(clean, "\\[\'~\x22].", spintent_tex_accents)
    clean = s_gsub(clean, "%s+", "-")
    return clean
end

register_tex_cmd("luafun_define_custom_unit", function(unit_symbol, spoken_name)
    if spintent_units[unit_symbol] or spintent_custom_spunit_spoken_names[unit_symbol] or spintent_custom_spunit_aliases[unit_symbol] then
        spintent_set_macro("spunit_luaset_status_str", "duplicate")
        return
    end

    -- Sanitizamos y aglutinamos para MathCAT
    local clean_spoken_name = spintent_sanitize_for_screen_reader(spoken_name)

    -- El prefijo _ marca un intent propio, no reconocido por MathCAT
    spintent_custom_spunit_spoken_names[unit_symbol] = "_" .. clean_spoken_name
    spintent_units[unit_symbol] = unit_symbol

    spintent_set_macro("spunit_luaset_status_str", "success")
end, { "string", "string" })

-- °, ′, ″ son los tres símbolos sexagesimales reconocidos -- chequeo
-- compartido por las dos ramas de spintent_spunit_lookup.
local function spintent_is_sexagesimal_symbol(s)
    return s == "°" or s == "′" or s == "″"
end

-- Busca una unidad con exponente opcional ("m", "m^2", "㎡"). Devuelve
-- nil si no existe; si existe, { canonical, read, exp, sexagesimal } con
-- 'exp' vacío cuando la unidad no lleva exponente.
local function spintent_spunit_lookup(raw_unit_name)
    raw_unit_name = s_gsub(raw_unit_name, "%s+", "")
    local clean_exp_format = s_gsub(raw_unit_name, "[{}]", "")

    local search_name = spintent_normalizations[clean_exp_format] or spintent_normalizations[raw_unit_name] or raw_unit_name
    local canonical = spintent_units[search_name] or spintent_units[clean_exp_format]

    if canonical then
        local base_clean = s_gsub(canonical, "%^%-?%d+", "")
        local spoken = spintent_custom_spunit_spoken_names[base_clean]
            or spintent_unit_compact_spoken_names[raw_unit_name]
            or spintent_unit_compact_spoken_names[clean_exp_format]
            or spintent_unit_compact_spoken_names[search_name]
            or ":unit"

        local base, exp = s_match(canonical, "([^%^]+)%^(%-?%d+)")
        if not (base and exp) then
            base, exp = canonical, ""
        end
        return {
            canonical   = base,
            read        = spoken,
            exp         = exp,
            sexagesimal = spintent_is_sexagesimal_symbol(canonical),
        }
    end

    local literal_base, literal_exp = s_match(clean_exp_format, "([^%^]+)%^(%-?%d+)")
    if literal_base and literal_exp then
        local canonical_base = spintent_units[spintent_normalizations[literal_base] or literal_base]
        if canonical_base then
            return {
                canonical   = canonical_base,
                read        = spintent_custom_spunit_spoken_names[canonical_base] or ":unit",
                exp         = literal_exp,
                sexagesimal = spintent_is_sexagesimal_symbol(canonical_base),
            }
        end
    end
    return nil
end

-- \spunit: parte un producto de unidades ("u1*u2^n*...") en sus términos
-- y busca cada uno. Para el término <i> deja en
--   l__spintent_spunit_luaset_<which>_<i>_{canonical,read}_str y _exp_tl
-- sus datos, y en l__spintent_spunit_luaset_<which>_{n,found,sexa}_str el
-- número de términos, si todos existen y si es una única unidad
-- sexagesimal. 'which' es "above" o "below".
register_tex_cmd("luafun_spunit_parse_product", function(which, product)
    local prefix = "spunit_luaset_" .. which .. "_"
    local n, all_found, sexagesimal = 0, true, false
    for term in s_gmatch(product .. "*", "([^*]*)%*") do
        n = n + 1
        local info = spintent_spunit_lookup(term)
        if info then
            spintent_set_macro(prefix .. n .. "_canonical_str", info.canonical)
            spintent_set_macro(prefix .. n .. "_read_str", info.read)
            spintent_set_macro(prefix .. n .. "_exp_tl", info.exp)
            sexagesimal = info.sexagesimal
        else
            all_found = false
        end
    end
    spintent_set_macro(prefix .. "n_str", tostring(n))
    spintent_set_bool(prefix .. "found_str", all_found)
    spintent_set_bool(prefix .. "sexa_str", all_found and n == 1 and sexagesimal)
end, { "string", "string" })

register_tex_cmd("luafun_define_spunit_alias", function(alias_name, unit_expression)
    alias_name = s_gsub(alias_name, "%s+", "")
    unit_expression = spintent_trim(unit_expression)
    if spintent_units[alias_name] or spintent_normalizations[alias_name] or spintent_custom_spunit_aliases[alias_name] then
        spintent_set_macro("spunit_luaset_status_str", "duplicate")
        return
    end
    local is_valid = true
    local _, slash_count = s_gsub(unit_expression, "/", "")
    if slash_count > 1 then is_valid = false end

    if s_match(unit_expression, "[^%a°ΩµÅℓ′″℧%*%/%^%-%d%s%'%\x22{}]") then is_valid = false end
    if is_valid then
        for unit_base in s_gmatch(unit_expression, "[%a°ΩµÅℓ′″℧%'%\x22]+") do
            if not (spintent_units[unit_base] or spintent_normalizations[unit_base]) then is_valid = false; break end
        end
    end
    if not is_valid then
        spintent_set_macro("spunit_luaset_status_str", "invalid-expr")
    else
        spintent_custom_spunit_aliases[alias_name] = unit_expression
        spintent_set_macro("spunit_luaset_status_str", "success")
    end
end, { "string", "string" })
end

-- DIVISAS Y MONEDAS (\spmoney)

do
local spintent_internal_currencies = {
    ["$"]        = "$",         ["€"]        = "€",         ["£"]        = "£",         ["¥"]        = "¥",
    ["¢"]        = "¢",         ["₩"]        = "₩",         ["₪"]        = "₪",         ["₹"]        = "₹",
    ["₽"]        = "₽",         ["₺"]        = "₺",         ["₴"]        = "₴",
    ["dolar"]    = "$",         ["dolares"]  = "$",         ["usd"]      = "$",
    ["peso"]     = "$",         ["pesos"]    = "$",         ["clp"]      = "$",         ["mxn"]      = "$",
    ["euro"]     = "€",         ["euros"]    = "€",         ["eur"]      = "€",
    ["libra"]    = "£",         ["libras"]   = "£",         ["gbp"]      = "£",
    ["yen"]      = "¥",         ["yenes"]    = "¥",         ["yuan"]     = "¥",         ["yuanes"]   = "¥",
    ["jpy"]      = "¥",         ["cny"]      = "¥",         ["centavo"]  = "¢",         ["centavos"] = "¢",
    ["won"]      = "₩",         ["wons"]     = "₩",         ["krw"]      = "₩",
    ["shekel"]   = "₪",         ["shekels"]  = "₪",         ["sequel"]   = "₪",         ["sequeles"] = "₪",
    ["ils"]      = "₪",         ["rupia"]    = "₹",         ["rupias"]   = "₹",         ["inr"]      = "₹",
    ["rublo"]    = "₽",         ["rublos"]   = "₽",         ["rub"]      = "₽",
    ["lira"]     = "₺",         ["liras"]    = "₺",         ["try"]      = "₺",
    ["grivna"]   = "₴",         ["grivnas"]  = "₴",         ["uah"]      = "₴",
}

local spintent_currency_grammatical_dict = {
    ["$"]        = { sing = "peso",                  plur = "pesos",                  conde = "de-pesos" },
    ["peso"]     = { sing = "peso",                  plur = "pesos",                  conde = "de-pesos" },
    ["pesos"]    = { sing = "peso",                  plur = "pesos",                  conde = "de-pesos" },
    ["clp"]      = { sing = "peso-chileno",          plur = "pesos-chilenos",          conde = "de-pesos-chilenos" },
    ["mxn"]      = { sing = "peso-mexicano",         plur = "pesos-mexicanos",         conde = "de-pesos-mexicanos" },
    ["dolar"]    = { sing = "dólar",                 plur = "dólares",                conde = "de-dólares" },
    ["dolares"]  = { sing = "dólar",                 plur = "dólares",                conde = "de-dólares" },
    ["usd"]      = { sing = "dólar-estadounidense",  plur = "dólares-estadounidenses", conde = "de-dólares-estadounidenses" },
    ["euro"]     = { sing = "euro",                  plur = "euros",                  conde = "de-euros" },
    ["euros"]    = { sing = "euro",                  plur = "euros",                  conde = "de-euros" },
    ["eur"]      = { sing = "euro",                  plur = "euros",                  conde = "de-euros" },
    ["libra"]    = { sing = "libra",                 plur = "libras",                 conde = "de-libras" },
    ["libras"]   = { sing = "libra",                 plur = "libras",                 conde = "de-libras" },
    ["gbp"]      = { sing = "libra",                 plur = "libras",                 conde = "de-libras" },
    ["yen"]      = { sing = "yen",                   plur = "yenes",                  conde = "de-yenes" },
    ["yenes"]    = { sing = "yen",                   plur = "yenes",                  conde = "de-yenes" },
    ["jpy"]      = { sing = "yen",                   plur = "yenes",                  conde = "de-yenes" },
    ["yuan"]     = { sing = "yuan",                  plur = "yuanes",                 conde = "de-yuanes" },
    ["yuanes"]   = { sing = "yuan",                  plur = "yuanes",                 conde = "de-yuanes" },
    ["cny"]      = { sing = "yuan",                  plur = "yuanes",                 conde = "de-yuanes" },
    ["won"]      = { sing = "won",                   plur = "wons",                   conde = "de-wons" },
    ["krw"]      = { sing = "won",                   plur = "wons",                   conde = "de-wons" },
    ["shekel"]   = { sing = "séquel",                plur = "séqueles",               conde = "de-séqueles" },
    ["ils"]      = { sing = "séquel",                plur = "séqueles",               conde = "de-séqueles" },
    ["rupia"]    = { sing = "rupia",                 plur = "rupias",                 conde = "de-rupias" },
    ["inr"]      = { sing = "rupia",                 plur = "rupias",                 conde = "de-rupias" },
    ["rublo"]    = { sing = "rublo",                 plur = "rublos",                 conde = "de-rublos" },
    ["rub"]      = { sing = "rublo",                 plur = "rublos",                 conde = "de-rublos" },
    ["lira"]     = { sing = "lira",                  plur = "liras",                  conde = "de-liras" },
    ["try"]      = { sing = "lira",                  plur = "liras",                  conde = "de-liras" },
    ["grivna"]   = { sing = "grivna",                plur = "grivnas",                conde = "de-grivnas" },
    ["uah"]      = { sing = "grivna",                plur = "grivnas",                conde = "de-grivnas" },
    ["€"]        = { sing = "euro",                  plur = "euros",                  conde = "de-euros" },
    ["£"]        = { sing = "libra",                 plur = "libras",                 conde = "de-libras" },
    ["¥"]        = { sing = "yen",                   plur = "yenes",                  conde = "de-yenes" },
    ["₩"]        = { sing = "won",                   plur = "wons",                   conde = "de-wons" },
    ["₪"]        = { sing = "séquel",                plur = "séqueles",               conde = "de-séqueles" },
    ["₹"]        = { sing = "rupia",                 plur = "rupias",                 conde = "de-rupias" },
    ["₽"]        = { sing = "rublo",                 plur = "rublos",                 conde = "de-rublos" },
    ["₺"]        = { sing = "lira",                  plur = "liras",                  conde = "de-liras" },
    ["₴"]        = { sing = "grivna",                plur = "grivnas",                conde = "de-grivnas" },
    ["¢"]        = { sing = "centavo",               plur = "centavos",               conde = "de-centavos" },
    ["centavo"]  = { sing = "centavo",               plur = "centavos",               conde = "de-centavos" },
    ["centavos"] = { sing = "centavo",               plur = "centavos",               conde = "de-centavos" },
}

local spintent_currency_subunits_matrix = {
    ["pesos"]                   = { sing = "centavo", plur = "centavos" },
    ["pesos-chilenos"]          = nil,
    ["pesos-colombianos"]       = nil,
    ["pesos-mexicanos"]         = { sing = "centavo", plur = "centavos" },
    ["dólares"]                 = { sing = "centavo", plur = "centavos" },
    ["dólares-estadounidenses"] = { sing = "centavo", plur = "centavos" },
    ["euros"]                   = { sing = "céntimo", plur = "céntimos" },
    ["libras"]                  = { sing = "penique", plur = "peniques" },
    ["yenes"]                   = nil,
    ["yuanes"]                  = { sing = "fen",     plur = "fen" },
    ["wons"]                    = nil,
    ["séqueles"]                = { sing = "ágora",   plur = "agorot" },
    ["rupias"]                  = { sing = "paisa",   plur = "paisas" },
    ["rublos"]                  = { sing = "kopek",   plur = "kopeks" },
    ["liras"]                   = { sing = "kuruş",   plur = "kuruş" },
    ["grivnas"]                 = { sing = "kopek",   plur = "kopeks" }
}

local spintent_currency_spoken_names = {
    ["$"]        = "pesos",                    ["peso"]    = "pesos",                    ["pesos"]   = "pesos",
    ["clp"]      = "pesos-chilenos",           ["mxn"]     = "pesos-mexicanos",
    ["dolar"]    = "dólares-estadounidenses",  ["dolares"] = "dólares-estadounidenses",  ["usd"]     = "dólares-estadounidenses",
    ["euro"]     = "euros",                    ["euros"]   = "euros",                    ["eur"]     = "euros",
    ["libra"]    = "libras",                   ["libras"]  = "libras",                   ["gbp"]     = "libras",
    ["yen"]      = "yenes",                    ["yenes"]   = "yenes",                    ["jpy"]     = "yenes",
    ["yuan"]     = "yuanes",                   ["yuanes"]  = "yuanes",                   ["cny"]     = "yuanes",
    ["centavo"]  = "centavos",                 ["centavos"]= "centavos",
    ["won"]      = "wons",                     ["krw"]     = "wons",
    ["shekel"]   = "séqueles",                 ["ils"]     = "séqueles",
    ["rupia"]    = "rupias",                   ["inr"]     = "rupias",
    ["rublo"]    = "rublos",                   ["rub"]     = "rublos",
    ["lira"]     = "liras",                    ["try"]     = "liras",
    ["grivna"]   = "grivnas",                  ["uah"]     = "grivnas",
    ["€"]        = "euros",
    ["£"]        = "libras",
    ["¥"]        = "yenes",
    ["¢"]        = "centavos",
    ["₩"]        = "wons",
    ["₪"]        = "séqueles",
    ["₹"]        = "rupias",
    ["₽"]        = "rublos",
    ["₺"]        = "liras",
    ["₴"]        = "grivnas",
}

-- Clave de búsqueda de una moneda: sin espacios en los extremos, \$ igual
-- que $, sin acentos y en minúsculas
local spintent_money_accents = {
    ["á"] = "a", ["é"] = "e", ["í"] = "i", ["ó"] = "o", ["ú"] = "u", ["ü"] = "u",
    ["Á"] = "a", ["É"] = "e", ["Í"] = "i", ["Ó"] = "o", ["Ú"] = "u", ["Ü"] = "u",
}

local function spintent_money_key(str)
    local key = spintent_trim(str)
    key = s_gsub(key, "^\\%$", "$")
    key = s_gsub(key, "[\xC3][\x80-\xBF]", spintent_money_accents)
    return s_lower(key)
end

-- Códigos ISO 4217 que se imprimen como código cuando se escriben en mayúsculas
local spintent_money_iso_codes = {
    clp = true, mxn = true, usd = true, eur = true, gbp = true, jpy = true, cny = true,
    krw = true, ils = true, inr = true, rub = true, ["try"] = true, uah = true,
}

-- Escritura válida de una moneda: sin mayúsculas (símbolos y alias) o un
-- código ISO escrito todo en mayúsculas; cualquier otra mezcla no es válida
local function spintent_money_valid_case(raw, clean)
    local trimmed = spintent_trim(raw)
    if not (s_match(trimmed, "%u") or s_match(trimmed, "\xC3[\x80-\x9E]")) then
        return true
    end
    return spintent_money_iso_codes[clean] and trimmed == s_upper(trimmed)
end

register_tex_cmd("luafun_spmoney_lookup_data", function(currency_name)
    local trimmed = spintent_trim(currency_name)
    local clean = spintent_money_key(currency_name)
    local resolved_symbol = spintent_internal_currencies[clean] or "$"

    local gram_entry = spintent_currency_grammatical_dict[clean] or spintent_currency_grammatical_dict[resolved_symbol]
        or { sing = "peso", plur = "pesos", conde = "de-pesos" }

    local position = "pre"
    if resolved_symbol == "€" or resolved_symbol == "¢" then position = "post" end

    if spintent_money_iso_codes[clean] and trimmed == s_upper(trimmed) then
        spintent_set_macro("spmoney_luaset_print_iso_str", "true")
    else
        spintent_set_macro("spmoney_luaset_print_iso_str", "false")
    end

    local sub_db = spintent_currency_subunits_matrix[gram_entry.plur]

    spintent_set_macro("spmoney_luaset_symbol_str", resolved_symbol)
    spintent_set_macro("spmoney_luaset_position_str", position)
    spintent_set_macro("spmoney_luaset_sing_str", gram_entry.sing)
    spintent_set_macro("spmoney_luaset_plur_str", gram_entry.plur)
    spintent_set_macro("spmoney_luaset_conde_str", gram_entry.conde)

    if sub_db then
        spintent_set_macro("spmoney_luaset_subsing_str", sub_db.sing)
        spintent_set_macro("spmoney_luaset_subplur_str", sub_db.plur)
    else
        spintent_set_macro("spmoney_luaset_subsing_str", "")
        spintent_set_macro("spmoney_luaset_subplur_str", "")
    end
end, { "string" })

register_tex_cmd("luafun_spmoney_normalize_key", function(raw_input)
    local clean = spintent_money_key(raw_input)
    local resolved_symbol = spintent_internal_currencies[clean]
    local resolved_spoken = spintent_currency_spoken_names[clean]
        or (resolved_symbol and spintent_currency_spoken_names[resolved_symbol])

    if resolved_spoken and spintent_money_valid_case(raw_input, clean) then
        spintent_set_macro("spmoney_luaset_currency_arg_str", resolved_spoken)
        spintent_set_macro("spmoney_luaset_status_str", "found")
    else
        spintent_set_macro("spmoney_luaset_currency_arg_str", raw_input)
        spintent_set_macro("spmoney_luaset_status_str", "notfound")
    end
end, { "string" })

-- Dígitos, signos, espacios, separadores numéricos y espacios matemáticos
local spintent_money_num_item = spintent_math_space + S"0123456789 \t\r\n+-.,{}:;"

-- Extrae solo la moneda (borra la parte numérica)
local spintent_money_curr_only = Cs(((spintent_money_num_item / "") + P(1))^0)

-- Extrae solo el número matemático puro (conserva los espacios matemáticos)
local spintent_money_num_only = Cs((spintent_money_num_item + (P(1) / ""))^0)

register_tex_cmd("luafun_spmoney_prepare_input", function(raw_input)
    local curr_part = spintent_money_curr_only:match(raw_input)
    local num_part  = spintent_money_num_only:match(raw_input)

    spintent_set_macro("spmoney_luaset_extracted_curr_str", curr_part)
    spintent_set_macro("spmoney_luaset_extracted_num_tl", num_part)
end, { "string" })
end

-- ABREVIATURAS Y ORDINALES (\spshort)

do
local spintent_spshort_dict = {
  ["dr.a"]   = { expand = "doctora",   layout_type = "superscript", base = "Dr.", suffix = "a" },
  ["dr.as"]  = { expand = "doctoras",  layout_type = "superscript", base = "Dr.", suffix = "as" },
  ["prof.a"] = { expand = "profesora", layout_type = "superscript", base = "Prof.", suffix = "a" },
  ["d.a"]    = { expand = "doña",      layout_type = "superscript", base = "D.", suffix = "a" },
  ["m.a"]    = { expand = "maría",     layout_type = "superscript", base = "M.", suffix = "a" },
  ["n.o"]    = { expand = "número",    layout_type = "superscript", base = "N.", suffix = "o" },
  ["n.os"]   = { expand = "números",   layout_type = "superscript", base = "N.", suffix = "os" },
  ["c.ia"]   = { expand = "compañía",  layout_type = "superscript", base = "C.", suffix = "ia" },

  ["dr.ª"] = { expand = "doctora", layout_type = "superscript", base = "Dr.", suffix = "a" },
  ["d.ª"]  = { expand = "doña",    layout_type = "superscript", base = "D.", suffix = "a" },
  ["n.º"]  = { expand = "número",  layout_type = "superscript", base = "N.", suffix = "o" },

  ["pág."] = { expand = "página",   layout_type = "linear_regular", output = "pág." },
  ["pag."] = { expand = "página",   layout_type = "linear_regular", output = "pág." },
  ["vol."] = { expand = "volumen",  layout_type = "linear_regular", output = "vol." },
  ["etc."] = { expand = "etcétera", layout_type = "linear_regular", output = "etc." },

  ["et al."]    = { expand = "y otros",      layout_type = "linear_regular", output = "et\u{00A0}al." },
  ["et. al."]   = { expand = "y otros",      layout_type = "linear_regular", output = "et\u{00A0}al." },
  ["ibíd."]     = { expand = "ibídem",       layout_type = "linear_regular", output = "ibíd." },
  ["ibid."]     = { expand = "ibídem",       layout_type = "linear_regular", output = "ibíd." },
  ["op. cit."]  = { expand = "obra citada",  layout_type = "linear_regular", output = "op.\u{00A0}cit." },
  ["op.cit."]   = { expand = "obra citada",  layout_type = "linear_regular", output = "op.\u{00A0}cit." },
  ["loc. cit."] = { expand = "lugar citado", layout_type = "linear_regular", output = "loc.\u{00A0}cit." },
  ["loc.cit."]  = { expand = "lugar citado", layout_type = "linear_regular", output = "loc.\u{00A0}cit." },
  ["v. gr."]    = { expand = "verbigracia",  layout_type = "linear_regular", output = "v.\u{00A0}gr." },
  ["v.gr."]     = { expand = "verbigracia",  layout_type = "linear_regular", output = "v.\u{00A0}gr." },
  ["i. e."]     = { expand = "esto es",      layout_type = "linear_regular", output = "i.\u{00A0}e." },
  ["i.e."]      = { expand = "esto es",      layout_type = "linear_regular", output = "i.\u{00A0}e." },
  ["e. g."]     = { expand = "por ejemplo",  layout_type = "linear_regular", output = "e.\u{00A0}g." },
  ["e.g."]      = { expand = "por ejemplo",  layout_type = "linear_regular", output = "e.\u{00A0}g." },
  ["p. ej."]    = { expand = "por ejemplo",  layout_type = "linear_regular", output = "p.\u{00A0}ej." },
  ["p.ej."]     = { expand = "por ejemplo",  layout_type = "linear_regular", output = "p.\u{00A0}ej." },

  ["sr."]    = { expand = "señor",       layout_type = "linear_regular", output = "Sr." },
  ["sra."]   = { expand = "señora",      layout_type = "linear_regular", output = "Sra." },
  ["srta."]  = { expand = "señorita",    layout_type = "linear_regular", output = "Srta." },
  ["dr."]    = { expand = "doctor",      layout_type = "linear_regular", output = "Dr." },
  ["dra."]   = { expand = "doctora",     layout_type = "linear_regular", output = "Dra." },
  ["dres."]  = { expand = "doctores",    layout_type = "linear_regular", output = "Dres." },
  ["dras."]  = { expand = "doctoras",    layout_type = "linear_regular", output = "Dras." },
  ["prof."]  = { expand = "profesor",    layout_type = "linear_regular", output = "Prof." },
  ["profa."] = { expand = "profesora",   layout_type = "linear_regular", output = "Profa." },
  ["profs."] = { expand = "profesores",  layout_type = "linear_regular", output = "Profs." },
  ["ing."]   = { expand = "ingeniero",   layout_type = "linear_regular", output = "Ing." },
  ["ings."]  = { expand = "ingenieros",  layout_type = "linear_regular", output = "Ings." },
  ["lic."]   = { expand = "licenciado",  layout_type = "linear_regular", output = "Lic." },
  ["v. b."]  = { expand = "visto bueno", layout_type = "linear_regular", output = "V.\u{00A0}B." },
  ["v.b."]   = { expand = "visto bueno", layout_type = "linear_regular", output = "V.\u{00A0}B." },

  ["s. a."]    = { expand = "sociedad anónima",      layout_type = "linear_caps",    output = "S.\u{00A0}A." },
  ["s.a."]     = { expand = "sociedad anónima",      layout_type = "linear_caps",    output = "S.\u{00A0}A." },
  ["ee. uu."]  = { expand = "estados unidos",        layout_type = "linear_caps",    output = "EE.\u{00A0}UU." },
  ["ee.uu."]   = { expand = "estados unidos",        layout_type = "linear_caps",    output = "EE.\u{00A0}UU." },
  ["dd. hh."]  = { expand = "derechos humanos",      layout_type = "linear_caps",    output = "DD.\u{00A0}HH." },
  ["dd.hh."]   = { expand = "derechos humanos",      layout_type = "linear_caps",    output = "DD.\u{00A0}HH." },
  ["jj. oo."]  = { expand = "juegos olímpicos",      layout_type = "linear_caps",    output = "JJ.\u{00A0}OO." },
  ["jj.oo."]   = { expand = "juegos olímpicos",      layout_type = "linear_caps",    output = "JJ.\u{00A0}OO." },
  ["ff. aa."]  = { expand = "fuerzas armadas",       layout_type = "linear_caps",    output = "FF.\u{00A0}AA." },
  ["ff.aa."]   = { expand = "fuerzas armadas",       layout_type = "linear_caps",    output = "FF.\u{00A0}AA." },
  ["rr. ee."]  = { expand = "relaciones exteriores", layout_type = "linear_caps",    output = "RR.\u{00A0}EE." },
  ["rr.ee."]   = { expand = "relaciones exteriores", layout_type = "linear_caps",    output = "RR.\u{00A0}EE." },
  ["cc. aa."]  = { expand = "comunidades autónomas", layout_type = "linear_caps",    output = "CC.\u{00A0}AA." },
  ["cc.aa."]   = { expand = "comunidades autónomas", layout_type = "linear_caps",    output = "CC.\u{00A0}AA." },
  ["p. d."]    = { expand = "posdata",               layout_type = "linear_caps",    output = "P.\u{00A0}D." },
  ["p.d."]     = { expand = "posdata",               layout_type = "linear_caps",    output = "P.\u{00A0}D." },

-- Solo por compatibilidad (no deben llevar punto acorde a la RAE)
  ["d. n. i."] = { expand = "documento nacional de identidad", layout_type = "linear_caps", output = "D.\u{00A0}N.\u{00A0}I." },
  ["d.n.i."]   = { expand = "documento nacional de identidad", layout_type = "linear_caps", output = "D.\u{00A0}N.\u{00A0}I." },
  ["r. u. t."] = { expand = "rol único tributario", layout_type = "linear_caps", output = "R.\u{00A0}U.\u{00A0}T." },
  ["r.u.t."]   = { expand = "rol único tributario", layout_type = "linear_caps", output = "R.\u{00A0}U.\u{00A0}T." },
  ["r. u. n."] = { expand = "rol único nacional", layout_type = "linear_caps", output = "R.\u{00A0}U.\u{00A0}N." },
  ["r.u.n."]   = { expand = "rol único nacional", layout_type = "linear_caps", output = "R.\u{00A0}U.\u{00A0}N." },

  ["a. c."]    = { expand = "antes de Cristo",          layout_type = "linear_mixed",   output = "a.\u{00A0}C." },
  ["a.c."]     = { expand = "antes de Cristo",          layout_type = "linear_mixed",   output = "a.\u{00A0}C." },
  ["d. c."]    = { expand = "después de Cristo",        layout_type = "linear_mixed",   output = "d.\u{00A0}C." },
  ["d.c."]     = { expand = "después de Cristo",        layout_type = "linear_mixed",   output = "d.\u{00A0}C." },

  ["dni"]      = { expand = "documento nacional de identidad",     layout_type = "small_caps_pure", output = "DNI" },
  ["run"]      = { expand = "rol único nacional",                  layout_type = "small_caps_pure", output = "RUN" },
  ["rut"]      = { expand = "rol único tributario",                layout_type = "small_caps_pure", output = "RUT" },
  ["onu"]      = { expand = "organización de las naciones unidas", layout_type = "small_caps_pure", output = "ONU" },
  ["rae"]      = { expand = "real academia española",             layout_type = "small_caps_pure", output = "RAE" },
  ["ong"]      = { expand = "organización no gubernamental",      layout_type = "small_caps_pure", output = "ONG" },
  ["ongs"]     = { expand = "organizaciones no gubernamentales",  layout_type = "small_caps_pure", output = "ONGs" },
  ["urss"]     = { expand = "unión de repúblicas socialistas soviéticas", layout_type = "small_caps_pure", output = "URSS" },
  ["oea"]      = { expand = "organización de los estados americanos", layout_type = "small_caps_pure", output = "OEA" },
  ["oms"]      = { expand = "organización mundial de la salud",    layout_type = "small_caps_pure", output = "OMS" },
  ["fmi"]      = { expand = "fondo monetario internacional",      layout_type = "small_caps_pure", output = "FMI" },
  ["bid"]      = { expand = "banco interamericano de desarrollo", layout_type = "small_caps_pure", output = "BID" },
  ["otan"]     = { expand = "organización del tratado del atlántico norte", layout_type = "small_caps_pure", output = "OTAN" },
  ["unam"]     = { expand = "universidad nacional autónoma de méxico", layout_type = "small_caps_pure", output = "UNAM" },
  ["pib"]      = { expand = "producto interno bruto",             layout_type = "small_caps_pure", output = "PIB" },
  ["ue"]       = { expand = "unión europea",                       layout_type = "small_caps_pure", output = "UE" },
  ["eau"]      = { expand = "emiratos árabes unidos",              layout_type = "small_caps_pure", output = "EAU" },
  ["ru"]       = { expand = "reino unido",                         layout_type = "small_caps_pure", output = "RU" },
  ["rca"]      = { expand = "república centroafricana",            layout_type = "small_caps_pure", output = "RCA" },
  ["rdc"]      = { expand = "república democrática del congo",     layout_type = "small_caps_pure", output = "RDC" },
  ["rfa"]      = { expand = "república federal alemana",           layout_type = "small_caps_pure", output = "RFA" },
  ["rda"]      = { expand = "república democrática alemana",       layout_type = "small_caps_pure", output = "RDA" },

  ["unicef"]   = { expand = "fondo de las naciones unidas para la infancia", layout_type = "acronym_long",   output = "Unicef" },
  ["unesco"]   = { expand = "organización de las naciones unidas para la educación, la ciencia y la cultura", layout_type = "acronym_long",   output = "Unesco" },
  ["mercosur"] = { expand = "mercado común del sur",               layout_type = "acronym_long",   output = "Mercosur" },
  ["cepal"]    = { expand = "comisión económica para américa latina", layout_type = "acronym_long",  output = "Cepal" },
  ["celac"]    = { expand = "comunidad de estados latinoamericanos", layout_type = "acronym_long",  output = "Celac" },
  ["unasur"]   = { expand = "unión de naciones suramericanas",     layout_type = "acronym_long",   output = "Unasur" },
  ["acnur"]    = { expand = "alto comisionado de las naciones unidas", layout_type = "acronym_long",  output = "Acnur" },
  ["mineduc"]  = { expand = "ministerio de educación",            layout_type = "acronym_long",   output = "Mineduc" },
  ["senadis"]  = { expand = "servicio nacional de la discapacidad", layout_type = "acronym_long",  output = "Senadis" },
  ["conicet"]  = { expand = "consejo nacional de investigaciones", layout_type = "acronym_long",   output = "Conicet" }
}

local spintent_spshort_ord_suffixes = {
  ["a"]  = "ª",
  ["o"]  = "º",
  ["er"] = "er",
  ["os"] = "os",
  ["as"] = "as"
}

local spintent_ord_units = {
  [1] = "primer", [2] = "segund", [3] = "tercer", [4] = "cuart", [5] = "quint",
  [6] = "sext",   [7] = "séptim", [8] = "octav",  [9] = "noven"
}

local spintent_ord_tens = {
  [1] = "décim",       [2] = "vigésim",     [3] = "trigésim",    [4] = "cuadragésim",
  [5] = "quincuagésim",[6] = "sexagésim",   [7] = "septuagésim", [8] = "octogésim",
  [9] = "nonagésim"
}

local function spintent_get_semantic_ordinal(val, suffix)
    if val < 1 or val > 99 then
        return val .. (spintent_spshort_ord_suffixes[suffix] or suffix)
    end

    local end_char = (suffix == "a" or suffix == "as") and "a" or "o"
    local plural = (suffix == "as" or suffix == "os") and "s" or ""

    if suffix == "er" then
        end_char = "o"
        plural = ""
    end

    local ten_val = math_floor(val / 10)
    local unit_val = val % 10

    if val == 11 and suffix ~= "er" then return "undécim" .. end_char .. plural end
    if val == 12 then return "duodécim" .. end_char .. plural end

    local result = ""
    if ten_val > 0 then
        result = spintent_ord_tens[ten_val] .. end_char .. plural
    end

    if unit_val > 0 then
        local unit_str = spintent_ord_units[unit_val]

        if unit_val == 1 and suffix ~= "er" then unit_str = "primer" end
        if unit_val == 3 and suffix ~= "er" then unit_str = "tercer" end

        local unit_end = (suffix == "er") and "" or (end_char .. plural)
        local full_unit = unit_str .. unit_end

        if ten_val > 0 then
            result = result .. " " .. full_unit
        else
            result = full_unit
        end
    end

    return result
end

local spintent_spshort_digits       = spintent_digit^1
local spintent_spshort_dot          = P(".")
local spintent_spshort_raw_suffix   = C(P("er") + P("os") + P("as") + P("a") + P("o"))
                                    + (P("ª") / "a") + (P("º") / "o")
local spintent_spshort_illegal_suff = C((R("az") + R("AZ"))^1)

local spintent_spshort_ord_grammar = P({
  "Entry",
  Entry  = #spintent_digit * spintent_spshort_V("Main"),
  Main   = C(spintent_spshort_digits) * spintent_spshort_dot * (spintent_spshort_raw_suffix + spintent_spshort_illegal_suff * Cg(Cc(true)))
})

local function spintent_spshort_execute_analysis(raw_input)
    local clean_input = spintent_normalize_key(raw_input)

    local dict_match = spintent_spshort_dict[clean_input]
    if not dict_match then
        if s_sub(clean_input, -1) == "." then
            dict_match = spintent_spshort_dict[s_sub(clean_input, 1, -2)]
        else
            dict_match = spintent_spshort_dict[clean_input .. "."]
        end
    end

    if dict_match then
        spintent_set_macro("spshort_luaset_status_str", "success")
        spintent_set_macro("spshort_luaset_layout_str", dict_match.layout_type)
        spintent_set_macro("spshort_luaset_expand_str", dict_match.expand)

        if dict_match.layout_type == "superscript" then
            spintent_set_macro("spshort_luaset_base_str", dict_match.base)
            spintent_set_macro("spshort_luaset_suffix_str", dict_match.suffix)
        else
            spintent_set_macro("spshort_luaset_output_str", dict_match.output)
        end
        return
    end

    local ordinal_input = clean_input
    if s_sub(ordinal_input, -1) == "." and not s_match(ordinal_input, "%d+%.%a+$") and not s_match(ordinal_input, "^%d+%.") then
        ordinal_input = s_sub(ordinal_input, 1, -2)
    end

    local num_part, suffix_part, is_illegal = spintent_spshort_ord_grammar:match(ordinal_input)

    if num_part and not is_illegal and spintent_spshort_ord_suffixes[suffix_part] then

        if suffix_part == "er" then
            local val = tonumber(num_part)
            local last_digit = val % 10
            if last_digit ~= 1 and last_digit ~= 3 then
                spintent_set_macro("spshort_luaset_status_str", "error")
                return
            end
        end

        local semantic_read = spintent_get_semantic_ordinal(tonumber(num_part), suffix_part)

        spintent_set_macro("spshort_luaset_status_str", "success")
        spintent_set_macro("spshort_luaset_layout_str", "superscript")
        spintent_set_macro("spshort_luaset_expand_str", semantic_read)
        spintent_set_macro("spshort_luaset_base_str", num_part .. ".")
        spintent_set_macro("spshort_luaset_suffix_str", spintent_spshort_ord_suffixes[suffix_part])
        return
    end

    if s_match(raw_input, "^%a+$") then
        spintent_set_macro("spshort_luaset_status_str", "fallback")
        spintent_set_macro("spshort_luaset_layout_str", "none")
        spintent_set_macro("spshort_luaset_expand_str", raw_input)
        spintent_set_macro("spshort_luaset_output_str", raw_input)
    else
        spintent_set_macro("spshort_luaset_status_str", "error")
    end
end

register_tex_cmd("luafun_spshort_process", function(raw_input)
    spintent_spshort_execute_analysis(raw_input)
end, { "string" })
end

-- SIGLOS Y NÚMEROS ROMANOS (\spsiglo)

do
local spintent_arab_to_roman_map = {
  {1000, "m"}, {900, "cm"}, {500, "d"}, {400, "cd"},
  {100, "c"}, {90, "xc"}, {50, "l"}, {40, "xl"},
  {10, "x"}, {9, "ix"}, {5, "v"}, {4, "iv"}, {1, "i"}
}

local spintent_roman_to_arab_byte_map = {
  [105] = 1,   -- 'i'
  [118] = 5,   -- 'v'
  [120] = 10,  -- 'x'
  [108] = 50,  -- 'l'
  [99]  = 100, -- 'c'
  [100] = 500, -- 'd'
  [109] = 1000 -- 'm'
}

local function spintent_arabic_to_roman(num)
    local result = {}
    for i = 1, #spintent_arab_to_roman_map do
        local pair = spintent_arab_to_roman_map[i]
        while num >= pair[1] do
            result[#result + 1] = pair[2]
            num = num - pair[1]
        end
    end
    return t_concat(result)
end

local function spintent_roman_to_arabic(str_roman)
    local total = 0
    local i = 1
    local len = #str_roman

    while i <= len do
        local v1 = spintent_roman_to_arab_byte_map[s_byte(str_roman, i)] or 0

        if i + 1 <= len then
            local v2 = spintent_roman_to_arab_byte_map[s_byte(str_roman, i + 1)] or 0

            if v1 < v2 then
                total = total + (v2 - v1)
                i = i + 2
            else
                total = total + v1
                i = i + 1
            end
        else
            total = total + v1
            i = i + 1
        end
    end
    return total
end

register_tex_cmd("luafun_spsiglo_parse", function(raw_siglo_input)
    local clean = s_lower(s_gsub(raw_siglo_input, "%s+", ""))
    local arabic_val = nil
    local roman_val = nil
    local is_error = false

    if s_match(clean, "^%d+$") then
        arabic_val = tonumber(clean)
        if arabic_val > 0 and arabic_val < 4000 then
            roman_val = spintent_arabic_to_roman(arabic_val)
        else
            is_error = true
        end
    elseif s_match(clean, "^[ivxlcdm]+$") and clean ~= "" then
        roman_val = clean
        arabic_val = spintent_roman_to_arabic(clean)

        if arabic_val >= 4000 or spintent_arabic_to_roman(arabic_val) ~= clean then
            is_error = true
        end
    else
        is_error = true
    end

    if is_error then
        spintent_set_macro("spsiglo_luaset_error_str", "true")
        spintent_set_macro("spsiglo_luaset_arabic_str", "")
        spintent_set_macro("spsiglo_luaset_roman_min_str", "")
    else
        spintent_set_macro("spsiglo_luaset_error_str", "false")
        spintent_set_macro("spsiglo_luaset_arabic_str", tostring(arabic_val))
        spintent_set_macro("spsiglo_luaset_roman_min_str", roman_val)
    end
end, { "string" })
end

-- FECHA Y HORA (\spdate, \sptime)

do
local spintent_date_sep = C(S"/-")

-- Patrón neutro: 3 partes numéricas divididas por 2 separadores
local spintent_date_pattern = Ct(
    Cg(spintent_num_chunk, "p1") * Cg(spintent_date_sep, "sep1") * Cg(spintent_num_chunk, "p2") * Cg(spintent_date_sep, "sep2") * Cg(spintent_num_chunk, "p3") * P(-1)
)

-- Función auxiliar: Validar que el día y mes existan (¡incluyendo bisiestos!)
local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}

local function spintent_is_valid_date(d, m, y)
    if m < 1 or m > 12 then return false end
    if d < 1 then return false end

    -- Ajuste para febrero en años bisiestos
    if m == 2 then
        local is_leap = (y % 4 == 0 and y % 100 ~= 0) or (y % 400 == 0)
        if is_leap then
            return d <= 29
        end
    end
    return d <= days_in_month[m]
end

register_tex_cmd("luafun_spdate_parse", function(raw_date_input)
    if not raw_date_input then return end
    local clean_str = s_gsub(raw_date_input, "%s+", "")
    local result = spintent_date_pattern:match(clean_str)

    -- Si no encaja el patrón, o si el usuario mezcla separadores (ej. 2024-01/02) -> error
    if not result or result.sep1 ~= result.sep2 then
        spintent_set_macro("spdate_luaset_error_str", "true")
        return
    end

    local p1, p2, p3 = result.p1, result.p2, result.p3
    local sep = result.sep1
    local year, month, day

    -- Como spintent_num_chunk elimina espacios, las longitudes son exactas
    local p1_len = #p1
    local p3_len = #p3

    -- Detección del Año
    if p1_len == 4 and p3_len <= 2 then
        year, month, day = p1, p2, p3
    elseif p3_len == 4 and p1_len <= 2 then
        day, month, year = p1, p2, p3
    else
        -- Si ninguno tiene 4 dígitos (ej. 12-10-81) o ambos lo tienen, hay ambigüedad
        spintent_set_macro("spdate_luaset_error_str", "true")
        return
    end

    local num_y, num_m, num_d = tonumber(year), tonumber(month), tonumber(day)

    -- Validación de calendario real
    if not (num_y and num_m and num_d) or not spintent_is_valid_date(num_d, num_m, num_y) then
        spintent_set_macro("spdate_luaset_error_str", "true")
        return
    end

    -- Reglas de renderizado visual
    local output_str
    if p1_len == 4 and sep == "/" then
        -- Único caso a corregir: YYYY/MM/DD se invierte a DD/MM/YYYY
        output_str = day .. "/" .. month .. "/" .. year
    else
        -- Para el resto, se devuelve EXACTAMENTE el mismo formato que ingresó
        output_str = p1 .. sep .. p2 .. sep .. p3
    end

    -- Si sobrevivió a todo, pasamos las variables limpias a TeX
    spintent_set_macro("spdate_luaset_error_str", "false")
    spintent_set_macro("spdate_luaset_day_str", day)
    spintent_set_macro("spdate_luaset_month_str", month)
    spintent_set_macro("spdate_luaset_year_str", year)
    spintent_set_macro("spdate_luaset_output_str", output_str)
end, { "string" })

local spintent_time_pattern = Ct(
    Cg(spintent_num_chunk, "h") * P":" * Cg(spintent_num_chunk, "m") * (P":" * Cg(spintent_num_chunk, "s"))^-1 * P(-1)
)

register_tex_cmd("luafun_sptime_parse", function(raw_time_input)
    if not raw_time_input then return end
    local clean_str = s_gsub(raw_time_input, "%s+", "")
    local result = spintent_time_pattern:match(clean_str)

    if not result then
        spintent_set_macro("sptime_luaset_error_str", "true")
        return
    end

    local h = tonumber(result.h)
    local m = tonumber(result.m)
    local s = result.s and tonumber(result.s) or nil

    if not h or not m or h >= 24 or m >= 60 or (s and s >= 60) then
        spintent_set_macro("sptime_luaset_error_str", "true")
        return
    end

    spintent_set_macro("sptime_luaset_error_str", "false")
    spintent_set_macro("sptime_luaset_base_str", result.h .. ":" .. result.m)

    -- Solo es "fracción especial" si la hora es < 13 y los minutos son 15 o 30
    if h < 13 and (m == 15 or m == 30) then
        spintent_set_macro("sptime_luaset_is_fraction_str", "true")
    else
        spintent_set_macro("sptime_luaset_is_fraction_str", "false")
    end

    if result.s then
        spintent_set_macro("sptime_luaset_has_seconds_str", "true")
        spintent_set_macro("sptime_luaset_seconds_str", result.s)
    else
        spintent_set_macro("sptime_luaset_has_seconds_str", "false")
        spintent_set_macro("sptime_luaset_seconds_str", "")
    end
end, { "string" })
end

-- MATEMÁTICAS: MCM Y MCD (\spmcm, \spmcd)

do
local function spintent_gcd_algorithm(val_a, val_b)
    while val_b ~= 0 do val_a, val_b = val_b, val_a % val_b end
    return val_a
end

local function spintent_lcm_algorithm(val_a, val_b)
    if val_a == 0 or val_b == 0 then return 0 end
    return math_floor((val_a * val_b) / spintent_gcd_algorithm(val_a, val_b))
end

local function spintent_execute_mcm_mcd_result(raw_csv_list, tl_out, operation_fn)
    local numbers = {}

    for item in s_gmatch(raw_csv_list, "([^,]+)") do
        local clean_item = spintent_trim(item)
        local result = spintent_number_pattern:match(clean_item) or {}

        local r_int  = result.integer
        local r_sign = result.sign
        local r_dec  = result.decimal
        local r_per  = result.period
        local r_ext  = result.extra

        local es_natural = r_int and (not r_sign or r_sign == "")
            and (not r_dec or r_dec == "") and (not r_per or r_per == "")
            and (not r_ext or s_gsub(r_ext, "%s+", "") == "")

        if es_natural then
            numbers[#numbers + 1] = tonumber(r_int)
        else
            return
        end
    end
    if #numbers == 0 then return end
    local final_result = numbers[1]
    for i = 2, #numbers do final_result = operation_fn(final_result, numbers[i]) end
    token_set_macro(tl_out, s_format("%d", final_result))
end

register_tex_cmd("luafun_calculate_mcd", function(raw_csv_list)
    spintent_execute_mcm_mcd_result(raw_csv_list, "l__spintent_spmcd_luaset_mcd_value_tl", spintent_gcd_algorithm)
end, { "string" })

register_tex_cmd("luafun_calculate_mcm", function(raw_csv_list)
    spintent_execute_mcm_mcd_result(raw_csv_list, "l__spintent_spmcm_luaset_mcm_value_tl", spintent_lcm_algorithm)
end, { "string" })

-- Clasifica CADA argumento de la lista (sin abortar en el primero no
-- numerico, a diferencia de spintent_execute_mcm_mcd_result)
register_tex_cmd("luafun_mc_classify_args", function(raw_csv_list)
    local types = {}
    local all_numeric = true
    for item in s_gmatch(raw_csv_list, "([^,]+)") do
        local clean_item = spintent_trim(item)
        local result = spintent_number_pattern:match(clean_item) or {}

        local es_natural = result.integer and (not result.sign or result.sign == "")
            and (not result.decimal or result.decimal == "") and (not result.period or result.period == "")
            and (not result.extra or s_gsub(result.extra, "%s+", "") == "")

        if es_natural then
            types[#types + 1] = "number"
        else
            types[#types + 1] = "other"
            all_numeric = false
        end
    end
    spintent_set_macro("mc_luaset_types_clist", t_concat(types, ","))
    spintent_set_bool("mc_luaset_all_numeric_str", all_numeric)
end, { "string" })

-- Clasifica un solo argumento (mismo chequeo que
-- spintent_execute_mcm_mcd_result) -- para \spnD/\spnM. Un cero se
-- marca como error: el argumento debe ser un entero positivo.
register_tex_cmd("luafun_spnmd_classify", function(raw_arg)
    local clean_item = spintent_trim(raw_arg)
    local result = spintent_number_pattern:match(clean_item) or {}

    local es_natural = result.integer and (not result.sign or result.sign == "")
        and (not result.decimal or result.decimal == "") and (not result.period or result.period == "")
        and (not result.extra or s_gsub(result.extra, "%s+", "") == "")

    local es_cero = es_natural and s_match(result.integer, "^0+$") ~= nil

    spintent_set_bool("spnmd_luaset_is_number_str", es_natural and not es_cero)
    spintent_set_bool("spnM_spnD_luaset_error_str", es_cero)
end, { "string" })
end

-- SISTEMA SEXAGESIMAL (\spang)

do
-- Los minutos y los segundos son opcionales: "45", "45:30" y "45:30:15"
-- son válidos; una entrada vacía no.
local spintent_angle_sexag_pattern = Ct(
    -P(-1) * Cg(spintent_num_chunk^-1, "a")
    * (P ":" * Cg(spintent_num_chunk^-1, "b") * (P ":" * Cg(spintent_num_chunk^-1, "c"))^-1)^-1
    * P(-1)
)

register_tex_cmd("luafun_spang_parse", function(raw_sexag_str)
    raw_sexag_str = s_gsub(raw_sexag_str, "%s+", "")
    local result = spintent_angle_sexag_pattern:match(raw_sexag_str)

    if not result then
        spintent_set_macro("spang_luaset_error_str", "true")
        return
    end

    spintent_set_macro("spang_luaset_error_str", "false")
    spintent_set_macro("spang_luaset_grado_str", result.a or "")
    spintent_set_macro("spang_luaset_minuto_str", result.b or "")
    spintent_set_macro("spang_luaset_segundo_str", result.c or "")
end, { "string" })
end

-- MATEMÁTICAS: MÚLTIPLOS Y DIVISORES (\spnM, \spnD)

do
local function spintent_calculate_multiples(n_val, count)
    local results = {}
    for i = 1, count do
        results[#results + 1] = tostring(n_val * i)
    end
    return t_concat(results, ", ")
end

local function spintent_calculate_divisors(n_val, limit)
    local divisors = {}
    -- Algoritmo optimizado de raíz cuadrada para divisores rápidos
    for i = 1, math_floor(math_sqrt(n_val)) do
        if n_val % i == 0 then
            divisors[#divisors + 1] = i
            -- Si el cociente es diferente, agregarlo también
            local quotient = n_val // i
            if i ~= quotient then
                divisors[#divisors + 1] = quotient
            end
        end
    end
    -- Ordenar los divisores de menor a mayor
    t_sort(divisors)

    local results = {}
    local actual_count = #divisors
    local count_to_show = (limit > 0) and math_min(limit, actual_count) or actual_count

    for i = 1, count_to_show do
        results[#results + 1] = tostring(divisors[i])
    end

    local is_truncated = spintent_bool_str(count_to_show < actual_count)
    return t_concat(results, ", "), is_truncated
end

local function spintent_execute_spnM_spnD_result(raw_n, raw_limit, is_multiple)
    spintent_set_macro("spnM_spnD_luaset_error_str", "false")

    local num_n = tonumber(raw_n)
    if not num_n or num_n < 1 then
        spintent_set_macro("spnM_spnD_luaset_error_str", "true")
        return
    end

    local clean_limit = spintent_normalize_key(raw_limit)
    local num_limit = tonumber(clean_limit) or 0
    if clean_limit == "all" or clean_limit == "" then
        num_limit = 0
    end

    if is_multiple then
        if num_limit <= 0 then num_limit = 5 end -- 5 múltiplos por defecto
        local result_str = spintent_calculate_multiples(num_n, num_limit)

        spintent_set_macro("spnM_luaset_result_tl", result_str)
        spintent_set_macro("spnM_luaset_is_truncated_str", "true") -- Siempre infinitos
    else
        local result_str, is_truncated = spintent_calculate_divisors(num_n, num_limit)

        spintent_set_macro("spnD_luaset_result_tl", result_str)
        spintent_set_macro("spnD_luaset_is_truncated_str", is_truncated)
    end
end

register_tex_cmd("luafun_calcular_nM", function(raw_n, raw_limit)
    spintent_execute_spnM_spnD_result(raw_n, raw_limit, true)
end, { "string", "string" })

register_tex_cmd("luafun_calcular_nD", function(raw_n, raw_limit)
    spintent_execute_spnM_spnD_result(raw_n, raw_limit, false)
end, { "string", "string" })

-- Diccionario simbolo/comando -> lectura para \spnewread/\spread

local spintent_spread_dict = {}

register_tex_cmd("luafun_spread_new", function(sym, reading)
    if spintent_spread_dict[sym] then
        spintent_set_macro("spread_luaset_status_str", "duplicate")
    else
        spintent_spread_dict[sym] = reading
        spintent_set_macro("spread_luaset_status_str", "success")
    end
end, { "string", "string" })

register_tex_cmd("luafun_spread_lookup", function(sym)
    local reading = spintent_spread_dict[sym]
    if reading then
        spintent_set_macro("spread_luaset_status_str", "found")
        spintent_set_macro("spread_luaset_reading_str", reading)
    else
        spintent_set_macro("spread_luaset_status_str", "notfound")
    end
end, { "string" })
end

-- NÚMEROS MIXTOS (\spmQ); ver https://tex.stackexchange.com/q/764828

do
local spintent_font_cache = {}

-- Tabla constante: nunca cambia entre llamadas, se construye una sola vez
-- en vez de en cada invocación de \spmQ.
local spintent_mathstyle_names = {
    [0] = "display",      [1] = "display",
    [2] = "text",         [3] = "text",
    [4] = "script",       [5] = "script",
    [6] = "scriptscript", [7] = "scriptscript",
}

local function spintent_get_scaled_font(id, factor)
    local key = tostring(id) .. ":" .. tostring(factor)
    if spintent_font_cache[key] then return spintent_font_cache[key] end

    local old_font = font.getfont(id)
    if not old_font or not old_font.specification then return id end

    local specification = old_font.specification
    specification.size = math_floor(old_font.size * factor)

    local new_id = luaotfload.define_font(specification)
    spintent_font_cache[key] = new_id
    return new_id
end

local function spintent_build_char_noads(str)
    local head, tail
    for i = 1, #str do
        local kernel = node.new("math_char")
        kernel.char  = str:byte(i)
        kernel.fam   = 0
        local n      = node.new("noad")
        n.subtype    = 0
        n.nucleus    = kernel
        if not head then head = n tail = n
        else tail.next = n tail = n end
    end
    return head
end

local function spintent_build_submlist_noad(str)
    local mlist  = node.new("sub_mlist")
    mlist.head   = spintent_build_char_noads(str)
    local noad   = node.new("noad")
    noad.subtype = 0
    noad.nucleus = mlist
    return noad
end

local function spintent_medir_frac(dt, cmd)
    tex.runtoks(function()
        tex.sprint(document_cctab,
            s_format([[\setbox0\hbox{$\%sstyle\%s{0}{0}$}]], dt, cmd)
        )
    end)
    local h  = tex.box[0].height
    local dp = tex.box[0].depth
    tex.box[0] = nil
    return h, dp
end

register_tex_cmd("luafun_spmQ_scale_int", function(entero, frac_cmd)
    local display_type = (frac_cmd == "dfrac") and "display" or
        (spintent_mathstyle_names[tonumber(tex.mathstyle)] or "text")

    -- 1. Medir fracción
    local frac_height, frac_depth = spintent_medir_frac(display_type, frac_cmd)
    local ht_frac_real = frac_height + frac_depth

    -- 2. Construir y medir la hlist del entero UNA sola vez, antes de
    --    escalarla.
    local h_esc = node.mlist_to_hlist(spintent_build_submlist_noad(entero), display_type, false)
    local ht_entero = h_esc.height + h_esc.depth

    -- 3. Factor
    local factor = (ht_entero == 0) and 1.0 or (ht_frac_real / ht_entero)

    -- 4. Escalado + ancho + glyphs físicos en una sola pasada node.direct

    local ancho = 0
    local glyph_nodes = {}

    for n in d_traverse(d_todirect(h_esc.list)) do
        local ch = d_getfield(n, "char")
        if ch >= 48 and ch <= 57 then
            local new_id = spintent_get_scaled_font(d_getfield(n, "font"), factor)
            d_setfield(n, "font", new_id)
            local f = font.getfont(new_id)
            if f and f.characters then
                local c = f.characters[ch]
                if c then
                    d_setfield(n, "width",  c.width)
                    d_setfield(n, "height", c.height)
                    d_setfield(n, "depth",  c.depth)
                end
            end
            ancho = ancho + d_getfield(n, "width")
            glyph_nodes[#glyph_nodes + 1] = d_tonode(n)
        else
            ancho = ancho + d_getfield(n, "width")
        end
    end

    h_esc.width  = ancho
    h_esc.height = frac_height
    h_esc.depth  = frac_depth
    h_esc.shift  = math_floor(frac_depth + 0.5)

    -- 5. sub_box con mathml_filter usando node.direct
    local sub = d_new("sub_box")
    d_setfield(sub, "head", d_todirect(h_esc))

    local noad = d_new("noad")
    d_setfield(noad, "subtype", 0)
    d_setfield(noad, "nucleus", sub)

    -- 6. Devolvemos a userdata para interactuar con las properties de luamml y node.write
    local sub_user = d_tonode(sub)
    local noad_user = d_tonode(noad)

    local mml_mn = {
        [0]         = 'mn',
        entero,
        [':nodes']  = glyph_nodes,
        [':actual'] = entero,
    }

    local properties = node.get_properties_table()
    local p = properties[sub_user] or {}
    p.mathml_filter = function(_mml, _core)
        return mml_mn, mml_mn
    end
    properties[sub_user] = p

    -- 7. Inyectar el noad con luamml deshabilitado temporalmente.
    --    node.write aquí solo deposita el noad en la lista matemática
    --    actual; el callback de luamml se dispara al CERRAR el grupo
    --    matemático (al final de $ o \]). Por tanto deshabilitar el
    --    flag aquí NO suprime el procesamiento del noad — luamml lo
    --    verá igual cuando procese la lista completa.
    --    Lo que sí suprime es cualquier callback intermedio espurio
    --    que pudiera dispararse durante el node.write mismo.
    node.write(noad_user)

end, {"string", "string"})
end

-- Locales compartidos con la sección FUNCIONES; se asignan dentro del bloque de GEOMETRÍA PLANA
local spintent_geo_primas, spintent_geo_normalize_primes, spintent_geo_build_body_nombre, spintent_geo_build_visual_nombre

-- GEOMETRÍA PLANA

do
local spintent_geo_cont_sub = C((1 - P"}")^1)
spintent_geo_primas   = C(P"'"^1)

local spintent_geo_punto = Ct(
    C(R"AZ") *
    (
        P"_{" * spintent_geo_cont_sub * P"}" * Cc"sub"
        + spintent_geo_primas             * Cc"prima"
        + Cc""                            * Cc""
    )
)

-- No usa spintent_opt_spaces: su "/ ''" (captura de sustitución),
-- usada dentro de un Ct externo, no se descarta — se inserta como
-- elemento string "" suelto en la tabla, duplicando el conteo real
-- de puntos.
local spintent_geo_ws     = S" \t\r\n"
local spintent_geo_ws_opt = spintent_geo_ws^0
local spintent_geo_sep    = spintent_geo_ws_opt * P"," * spintent_geo_ws_opt

local spintent_geo_grammar =
    Ct( spintent_geo_punto * (spintent_geo_sep * spintent_geo_punto)^0 * P(-1) )

-- Sin anclaje final: se re-chequea la cardinalidad exacta después
-- (\spPoly, \spAfig, \spPfig).
local spintent_geo_poly_grammar =
    Ct( spintent_geo_punto * (spintent_geo_sep * spintent_geo_punto)^0 )

-- Puntos concatenados sin separador (p.ej. "AB", "ABC", "A_{1}B_{2}").
-- Misma forma de tabla de salida que spintent_geo_grammar/poly_grammar,
-- así que spintent_geo_build_intent/build_visual la consumen sin
-- cambios. Usada como alternativa a la lista separada por comas en
-- \sprecta, \sprayo, \spside, \spmside, \sparc, \spmarc, \spPoly,
-- \spAfig, \spPfig.
local spintent_geo_puntos_concat = Ct( spintent_geo_punto^1 ) * P(-1)

-- Gramática para nombre propio de recta (mayúsculas Y minúsculas).
-- IMPORTANTE: se prueba SIEMPRE antes que spintent_geo_puntos_concat
-- en luafun_geo_parse_and_set, para que una sola letra sin coma
-- ("A") siga interpretándose como nombre de recta en \sprecta (no
-- como lista de 1 punto).
local spintent_geo_nombre_recta = Ct(
    C(R"AZ" + R"az") *
    (
        P"_{" * spintent_geo_cont_sub * P"}" * Cc"sub"
        + spintent_geo_primas             * Cc"prima"
        + Cc""                            * Cc""
    )
) * P(-1)

local spintent_geo_num_a_texto = {
    ["0"] = "cero",  ["1"] = "uno",   ["2"] = "dos",
    ["3"] = "tres",  ["4"] = "cuatro",["5"] = "cinco",
    ["6"] = "seis",  ["7"] = "siete", ["8"] = "ocho",
    ["9"] = "nueve",
}

local spintent_geo_prima_prefijo = {
    "prima", "doble-prima", "triple-prima", "cuádruple-prima"
}

-- Normaliza Unicode (′″‴) y comandos (^{\prime}, ^{\dprime}, ...) a
-- comillas rectas, único formato que spintent_geo_primas reconoce.
function spintent_geo_normalize_primes(str)
    str = s_gsub(str, "‴", "'''")
    str = s_gsub(str, "″", "''")
    str = s_gsub(str, "′", "'")

    -- Los espacios que TeX deja tras un comando (\prime }) no cuentan.
    str = s_gsub(str, "%^%{([^%}]*)%}", function(inner)
        local compact = (s_gsub(inner, "%s+", ""))
        if compact == "\\prime" then return "'" end
        if compact == "\\prime\\prime" then return "''" end
        if compact == "\\prime\\prime\\prime" then return "'''" end
        if compact == "\\dprime" then return "''" end
        if compact == "\\trprime" then return "'''" end
        return "^{" .. inner .. "}"
    end)

    return str
end

-- cmd == "" (silent en modo school) envuelve el resultado en
-- guiones bajos ("_...-") para que MathCAT lo trate como literal por
-- default-text y no aplique sus reglas de mayúsculas a un <mi> suelto.
local function spintent_geo_build_intent(cmd, puntos)
    local partes = {}
    if cmd ~= "" then
        partes[#partes + 1] = cmd
    end
    for _, pt in ipairs(puntos) do
        local letra, mod, tipo = pt[1], pt[2], pt[3]
        partes[#partes + 1] = letra
        if tipo == "sub" then
            partes[#partes + 1] = "sub"
            partes[#partes + 1] = spintent_geo_num_a_texto[mod] or mod
        elseif tipo == "prima" then
            local n = #mod
            partes[#partes + 1] = spintent_geo_prima_prefijo[n] or (n .. "-prima")
        end
    end
    local resultado = t_concat(partes, "-")
    if cmd == "" then
        resultado = "_" .. resultado .. "-"
    end
    return resultado
end

-- Constructores "puros": solo el cuerpo (puntos convertidos a texto),
-- sin concepto ni envoltorio "_...-" — usados por las funciones que ya
-- no reciben "cmd"/"read_arg" desde expl3 (se antepone/envuelve del
-- lado expl3, después de la llamada).
local function spintent_geo_build_body(puntos)
    return spintent_geo_build_intent("", puntos):match("^_(.-)-$")
end

function spintent_geo_build_body_nombre(nombre)
    return spintent_geo_build_body({ nombre })
end

-- Un solo punto {letra, contenido, tipo}: usada tanto para el texto
-- visual (TeX) como para reconstruir el crudo sin combinar de
-- "points_clist" -- el mismo formato sirve para las dos cosas, ya que
-- para "prima" el contenido ya trae las comillas ("'", "''", ...)
-- listas para pegar, sin necesidad de reconstruirlas caracter por
-- caracter.
function spintent_geo_build_visual_nombre(nombre)
    local letra, mod, tipo = nombre[1], nombre[2], nombre[3]
    if tipo == "sub" then
        return letra .. "_{" .. mod .. "}"
    elseif tipo == "prima" then
        return letra .. mod
    else
        return letra
    end
end

local function spintent_geo_build_visual(puntos)
    local partes = {}
    for _, pt in ipairs(puntos) do
        partes[#partes + 1] = spintent_geo_build_visual_nombre(pt)
    end
    return t_concat(partes)
end

-- Arma "l__spintent_geo_luaset_points_clist" (la lista de puntos SIN
-- combinar, separada por comas) a partir de una tabla 'puntos', para
-- que expl3 pueda referenciar cada uno por separado (__spintent_mathmlarg_point:nnn).
local function spintent_geo_set_points_clist(puntos)
    local raw_parts = {}
    for i, punto in ipairs(puntos) do
        raw_parts[i] = spintent_geo_build_visual_nombre(punto)
    end
    spintent_set_macro("geo_luaset_points_clist", t_concat(raw_parts, ","))
end

-- Punto: \spPoint
local spintent_geo_single_point_grammar = spintent_geo_punto * P(-1)

register_tex_cmd("luafun_geo_point_parse_and_set",
    function(csv)
    csv = spintent_trim(csv)
    csv = spintent_geo_normalize_primes(csv)

    local punto = spintent_geo_single_point_grammar:match(csv)
    if not punto then
        spintent_set_macro("geo_luaset_error_str",  "true")
        spintent_set_macro("geo_luaset_intent_str", "")
        spintent_set_macro("geo_luaset_print_tl",   "")
        spintent_set_macro("geo_luaset_letra_str",  "")
        spintent_set_macro("geo_luaset_sub_str",    "")
        spintent_set_macro("geo_luaset_tipo_str",   "")
        return
    end

    spintent_set_macro("geo_luaset_error_str", "false")
    spintent_set_macro("geo_luaset_intent_str",
                    spintent_geo_build_body_nombre(punto))
    spintent_set_macro("geo_luaset_print_tl",
                    spintent_geo_build_visual_nombre(punto))

    -- letra/sub/tipo por separado: así \__spintent_mathmlarg_point:nnn puede
    -- silenciar el subíndice VISUAL aparte de la base; pegado en una sola
    -- cadena quedaría sin proteger y la cola compartida podría
    -- asignárselo a otro comando (\spPoint{A_{1}}\spPoint{B}).
    spintent_set_macro("geo_luaset_letra_str", punto[1])
    spintent_set_macro("geo_luaset_sub_str",   punto[2] or "")
    spintent_set_macro("geo_luaset_tipo_str",  punto[3] or "")
end, { "string" })

-- Recta, rayo, segmento y arco
-- Orden de intentos en la rama sin coma: nombre de recta primero
-- (preserva la semántica de \sprecta con una sola letra), luego
-- puntos concatenados sin separador, luego error.
local function spintent_geo_set_error_recta()
    spintent_set_macro("geo_luaset_error_str",    "true")
    spintent_set_macro("geo_luaset_intent_str",   "")
    spintent_set_macro("geo_luaset_print_tl",     "")
    spintent_set_macro("geo_luaset_is_name_str",  "false")
    spintent_set_macro("geo_luaset_points_clist", "")
    spintent_set_macro("geo_luaset_letra_str",    "")
    spintent_set_macro("geo_luaset_sub_str",      "")
    spintent_set_macro("geo_luaset_tipo_str",     "")
end

register_tex_cmd("luafun_geo_parse_and_set", function(csv)
    csv = spintent_trim(csv)
    csv = spintent_geo_normalize_primes(csv)

    local tiene_coma = s_match(csv, ",")

    if tiene_coma then
        local puntos = spintent_geo_grammar:match(csv)
        if not puntos or #puntos == 0 then
            spintent_geo_set_error_recta()
            return
        end
        spintent_set_macro("geo_luaset_error_str",    "false")
        spintent_set_macro("geo_luaset_is_name_str",  "false")
        spintent_set_macro("geo_luaset_intent_str",
                        spintent_geo_build_body(puntos))
        spintent_set_macro("geo_luaset_print_tl",
                        spintent_geo_build_visual(puntos))
        spintent_geo_set_points_clist(puntos)
        return
    end

    local nombre = spintent_geo_nombre_recta:match(csv)
    if nombre then
        spintent_set_macro("geo_luaset_error_str",    "false")
        spintent_set_macro("geo_luaset_is_name_str",  "true")
        spintent_set_macro("geo_luaset_intent_str",
                        spintent_geo_build_body_nombre(nombre))
        spintent_set_macro("geo_luaset_print_tl",
                        spintent_geo_build_visual_nombre(nombre))
        spintent_set_macro("geo_luaset_points_clist", "") -- nombre = 1 solo punto, no aplica
        -- letra/sub/tipo por separado -- mismo motivo que en
        -- luafun_geo_point_parse_and_set: permite silenciar el
        -- subíndice VISUAL aparte de la base.
        spintent_set_macro("geo_luaset_letra_str", nombre[1])
        spintent_set_macro("geo_luaset_sub_str",   nombre[2] or "")
        spintent_set_macro("geo_luaset_tipo_str",  nombre[3] or "")
        return
    end

    local puntos = spintent_geo_puntos_concat:match(csv)
    if puntos and #puntos > 0 then
        spintent_set_macro("geo_luaset_error_str",    "false")
        spintent_set_macro("geo_luaset_is_name_str",  "false")
        spintent_set_macro("geo_luaset_intent_str",
                        spintent_geo_build_body(puntos))
        spintent_set_macro("geo_luaset_print_tl",
                        spintent_geo_build_visual(puntos))
        spintent_geo_set_points_clist(puntos)
        return
    end

    spintent_geo_set_error_recta()
end, { "string" })

-- Ángulo: \spangle, \spmangle
local spintent_geo_angulo_un_punto = spintent_geo_punto * P(-1)

local function spintent_geo_build_body_angulo(csv)
    local tiene_coma = s_match(csv, ",")

    if tiene_coma then
        local puntos = spintent_geo_grammar:match(csv)
        if not puntos or #puntos == 0 then
            return nil, nil, nil
        end
        local cuerpo = spintent_geo_build_body(puntos)
        local visual = spintent_geo_build_visual(puntos)
        spintent_geo_set_points_clist(puntos)
        return cuerpo, visual, "tres-puntos"
    end

    local un_punto = spintent_geo_angulo_un_punto:match(csv)
    if un_punto then
        local base = spintent_geo_build_body_nombre(un_punto)
        local cuerpo = "en-" .. base
        local visual = spintent_geo_build_visual_nombre(un_punto)
        spintent_geo_set_points_clist({ un_punto })
        return cuerpo, visual, "un-punto"
    end

    -- Concatenado sin coma ("AOB"): reutiliza la misma
    -- gramática ya compartida con el resto de la familia geo.
    local puntos_concat = spintent_geo_puntos_concat:match(csv)
    if puntos_concat and #puntos_concat >= 2 then
        local cuerpo = spintent_geo_build_body(puntos_concat)
        local visual = spintent_geo_build_visual(puntos_concat)
        spintent_geo_set_points_clist(puntos_concat)
        return cuerpo, visual, "tres-puntos"
    end

    -- Si llegó hasta aquí con un "_" en el texto, es un intento
    -- fallido de notación de punto (p.ej. "A_1" sin llaves) — Lua no
    -- sabe leer nada con "_" salvo el patrón letra+"_{...}" ya
    -- descartado arriba. Se rechaza en vez de aceptarlo como nombre.
    if s_match(csv, "_") then
        return nil, nil, nil
    end

    local nombre = spintent_trim(csv)
    if nombre == "" then return nil, nil, nil end
    local es_numero = s_match(nombre, "^%d+$") ~= nil
    local cuerpo = es_numero and nombre or spintent_sanitize_for_screen_reader(nombre)
    spintent_set_macro("geo_luaset_points_clist", "")
    spintent_set_bool("geo_luaset_nombre_es_num_str", es_numero)
    return cuerpo, nombre, "nombre"
end

register_tex_cmd("luafun_geo_angulo_parse_and_set",
    function(csv)
    csv = spintent_trim(csv)
    csv = spintent_geo_normalize_primes(csv)

    local cuerpo, visual, caso = spintent_geo_build_body_angulo(csv)
    if not visual then
        spintent_set_macro("geo_luaset_error_str",       "true")
        spintent_set_macro("geo_luaset_intent_str",      "")
        spintent_set_macro("geo_luaset_print_tl",        "")
        spintent_set_macro("geo_luaset_angulo_case_str", "")
        spintent_set_macro("geo_luaset_points_clist",    "")
        return
    end

    spintent_set_macro("geo_luaset_error_str", "false")
    spintent_set_macro("geo_luaset_angulo_case_str", caso)
    spintent_set_macro("geo_luaset_intent_str", cuerpo)
    spintent_set_macro("geo_luaset_print_tl", visual)
end, { "string" })

-- Polígono: \spPoly, \spAfig, \spPfig
local spintent_geo_poly_npts = {
    [3] = { cmd = "triángulo",  sym = "triangle" },
    [4] = { cmd = "cuadrado",   sym = "square"   },
    [5] = { cmd = "pentágono",  sym = ""         },
    [6] = { cmd = "hexágono",   sym = ""         },
    [7] = { cmd = "heptágono",  sym = ""         },
    [8] = { cmd = "octágono",   sym = ""         },
}

-- Parsea el argumento de \spPoly: lista de vértices separados por
-- coma, o concatenados sin separador (mínimo 3). Establece:
--   l__spintent_geo_luaset_error_str    -- "true"/"false"
--   l__spintent_geo_luaset_poly_sym_str -- "triangle"/"square"/"" (según cantidad de vértices)
--   l__spintent_geo_luaset_concept_str  -- nombre del polígono ("triángulo", "cuadrado", ...)
--   l__spintent_geo_luaset_print_tl     -- vértices concatenados, visual TeX ("ABC")
--   l__spintent_geo_luaset_points_clist   -- vértices sin combinar, separados por coma ("A,B,C")
register_tex_cmd("luafun_geo_poly_parse_and_set",
    function(csv)
    csv = spintent_trim(csv)
    csv = spintent_geo_normalize_primes(csv)

    local tiene_coma = s_match(csv, ",")
    local puntos
    if tiene_coma then
        puntos = spintent_geo_poly_grammar:match(csv)
    else
        puntos = spintent_geo_puntos_concat:match(csv)
    end

    if not puntos or #puntos < 3 then
        spintent_set_macro("geo_luaset_error_str", "true")
        spintent_set_macro("geo_luaset_print_tl", "")
        spintent_set_macro("geo_luaset_poly_sym_str", "")
        spintent_set_macro("geo_luaset_points_clist","")
        return
    end

    local info     = spintent_geo_poly_npts[#puntos]
    local cmd_base = info and info.cmd or "polígono"
    local sym      = info and info.sym or ""

    spintent_set_macro("geo_luaset_error_str",      "false")
    spintent_set_macro("geo_luaset_poly_sym_str",   sym)
    spintent_set_macro("geo_luaset_concept_str",    cmd_base)
    spintent_set_macro("geo_luaset_print_tl", spintent_geo_build_visual(puntos))
    spintent_geo_set_points_clist(puntos)
end, { "string" })

-- Tabla de letras válidas para el argumento único de \spAfig/\spPfig
-- (p.ej. "F_{1}", "P", "B_{2}"). Cada letra lleva su palabra y el
-- conector gramatical correcto: sustantivos usan "de-la"/"del" según
-- género; adjetivos usan conector vacío (van pegados directamente
-- tras "área"/"perímetro").
local spintent_geo_fig_letras = {
    F = { palabra = "figura",   conector = "de-la"  },
    P = { palabra = "polígono", conector = "del"    },
    B = { palabra = "basal",    conector = ""       },
    L = { palabra = "lateral",  conector = ""       },
    T = { palabra = "total",    conector = ""       },
}

-- Letra de la tabla, con subíndice opcional (dígitos u otra letra).
local spintent_geo_fig_letra_sola = Ct(
    C(S"FPBLT") *
    (
        P"_{" * spintent_geo_cont_sub * P"}" * Cc"sub"
        + Cc"" * Cc""
    )
) * P(-1)

-- Dígito(s) sueltos, o una sola letra suelta (sin prefijo de tabla) —
-- se convierten directamente en el subíndice de \mathcal{A} (o del
-- símbolo, si print-sym!=none).
local spintent_geo_fig_sub_simple = (C(R"09"^1) + C(R"az")) * P(-1)

-- Devuelve: ok, visual, palabra, conector, sub_spoken.
local function spintent_geo_fig_parse_arg(csv)
    local con_letra = spintent_geo_fig_letra_sola:match(csv)
    if con_letra then
        local letra, sub, tipo = con_letra[1], con_letra[2], con_letra[3]
        local info = spintent_geo_fig_letras[letra]
        if tipo == "sub" then
            local visual = letra .. "_{" .. sub .. "}"
            local sub_spoken = spintent_geo_num_a_texto[sub] or sub
            return true, visual, info.palabra, info.conector, sub_spoken
        else
            return true, letra, info.palabra, info.conector, ""
        end
    end

    local solo = spintent_geo_fig_sub_simple:match(csv)
    if solo then
        local sub_spoken = spintent_geo_num_a_texto[solo] or solo
        return true, solo, "", "", sub_spoken
    end

    return false
end

-- Arma y establece las variables de salida para el caso de vértices
-- reales (lista de al menos 3 puntos), sin importar si vinieron
-- separados por comas o concatenados sin separador. Siempre usa el
-- nombre inferido por cardinalidad; expl3 arma el intent completo por
-- su cuenta (via __spintent_mathmlarg_point:nnn, punto por punto).
local function spintent_geo_fig_set_vertices(op, puntos)
    local info    = spintent_geo_poly_npts[#puntos]
    local cmd_fig = info and info.cmd or "polígono"
    local sym     = (info and info.sym) or ""
    local concepto = op .. "-del-" .. cmd_fig

    spintent_set_macro("geo_luaset_error_str",         "false")
    spintent_set_macro("geo_luaset_poly_sym_str",      sym)
    spintent_set_macro("geo_luaset_concept_str",       concepto)
    spintent_set_macro("geo_luaset_print_tl", spintent_geo_build_visual(puntos))
    spintent_set_macro("geo_luaset_letra_word_str",    "")
    spintent_set_macro("geo_luaset_letra_conector_str","")
    spintent_set_macro("geo_luaset_sub_spoken_str",    "")
    spintent_geo_set_points_clist(puntos)
end

-- Reset compartido por los tres casos que no traen vértices reales
-- (argumento vacío, error, y letra sola de la tabla F/P/B/L/T): ningún
-- campo de vértices aplica, solo cambian error/concept/print/letra.
local function spintent_geo_fig_reset(error_val, concept_val, print_val, palabra, conector, sub_spoken)
    spintent_set_macro("geo_luaset_error_str",          error_val)
    spintent_set_macro("geo_luaset_poly_sym_str",       "")
    spintent_set_macro("geo_luaset_concept_str",        concept_val)
    spintent_set_macro("geo_luaset_print_tl",           print_val)
    spintent_set_macro("geo_luaset_letra_word_str",     palabra)
    spintent_set_macro("geo_luaset_letra_conector_str", conector)
    spintent_set_macro("geo_luaset_sub_spoken_str",     sub_spoken)
    spintent_set_macro("geo_luaset_points_clist",       "")
end

-- l__spintent_geo_luaset_concept_str lleva solo el nombre del
-- concepto ("área-del-triángulo", sin vértices) — a diferencia del
-- resto de la familia, aquí solo se conoce tras el parseo (por la
-- cardinalidad), no antes.
--
-- Orden de intentos sin coma: gramática de argumento único (número/
-- letra/tabla) primero; si falla, puntos concatenados como lista de
-- vértices (mínimo 3); si también falla, error.
local function spintent_geo_fig_parse_and_set(op, csv)
    csv = spintent_trim(csv)
    csv = spintent_geo_normalize_primes(csv)

    if csv == "" then
        spintent_geo_fig_reset("false", op, "", "", "", "")
        return
    end

    local tiene_coma = s_match(csv, ",")

    if tiene_coma then
        local puntos = spintent_geo_poly_grammar:match(csv)
        if not puntos or #puntos < 3 then
            spintent_geo_fig_reset("true", "", "", "", "", "")
            return
        end
        spintent_geo_fig_set_vertices(op, puntos)
        return
    end

    local ok, visual, palabra, conector, sub_spoken = spintent_geo_fig_parse_arg(csv)
    if ok then
        spintent_geo_fig_reset("false", op, visual, palabra, conector, sub_spoken)
        return
    end

    local puntos = spintent_geo_puntos_concat:match(csv)
    if puntos and #puntos >= 3 then
        spintent_geo_fig_set_vertices(op, puntos)
        return
    end

    spintent_geo_fig_reset("true", "", "", "", "", "")
end

register_tex_cmd("luafun_geo_afig_parse_and_set",
    function(csv)
    spintent_geo_fig_parse_and_set("área", csv)
end, { "string" })

register_tex_cmd("luafun_geo_pfig_parse_and_set",
    function(csv)
    spintent_geo_fig_parse_and_set("perímetro", csv)
end, { "string" })

-- Círculo y circunferencia: \spcirc
--
-- El radio se clasifica por forma (nunca por llave): número (entero o
-- decimal, con coma o punto), medida (número+unidad, delegada a
-- spnum/spunit desde expl3), letra suelta, o segmento de dos letras
-- (side/mside según radio-sty, decidido en expl3). Lua solo clasifica
-- y extrae; no arma intent de medida (eso es trabajo ya existente de
-- \spnum/\spunit en expl3).
local spintent_geo_single_point = spintent_geo_punto * P(-1)

-- "number": entero o decimal. "2.5 cm" no coincide (tiene unidad) y
-- sigue siendo "measure".
local spintent_geo_radio_number  = C(R"09"^1 * (S".," * R"09"^1)^-1) * P(-1)
local spintent_geo_radio_label   = C(R"az" + R"AZ") * P(-1)
local spintent_geo_radio_segment = Ct( spintent_geo_punto * spintent_geo_punto ) * P(-1)

-- "measure": empieza con dígitos y tiene algo más después (unidad),
-- ya sea con espacio o pegado. No se valida la unidad aquí — eso
-- queda para spunit en expl3.
local spintent_geo_radio_measure = C(R"09"^1) * S" "^0 * (1 - P(-1))^1 * P(-1)

register_tex_cmd("luafun_geo_circ_parse_and_set",
    function(csv)
    csv = spintent_trim(csv)
    csv = spintent_geo_normalize_primes(csv)

    local coma_pos = s_match(csv, "()%,")
    local centro_str = coma_pos and csv:sub(1, coma_pos - 1) or csv
    local radio_str  = coma_pos and spintent_trim(csv:sub(coma_pos + 1)) or ""

    local centro = spintent_geo_single_point:match(spintent_trim(centro_str))
    if not centro then
        spintent_set_macro("geo_luaset_error_str",      "true")
        spintent_set_macro("geo_luaset_print_tl",       "")
        spintent_set_macro("geo_luaset_circ_radio_type_str", "")
        spintent_set_macro("geo_luaset_circ_radio_raw_str",  "")
        spintent_set_macro("geo_luaset_points_clist",   "")
        return
    end

    spintent_set_macro("geo_luaset_error_str", "false")
    spintent_set_macro("geo_luaset_print_tl",
                    spintent_geo_build_visual_nombre(centro))
    spintent_geo_set_points_clist({ centro })

    if radio_str == "" then
        spintent_set_macro("geo_luaset_circ_radio_type_str", "")
        spintent_set_macro("geo_luaset_circ_radio_raw_str",  "")
        return
    end

    local seg_puntos = spintent_geo_radio_segment:match(radio_str)
    if seg_puntos then
        spintent_set_macro("geo_luaset_circ_radio_type_str", "segment")
        spintent_set_macro("geo_luaset_circ_radio_raw_str",
                        spintent_geo_build_visual(seg_puntos))
        return
    end

    local numero = spintent_geo_radio_number:match(radio_str)
    if numero then
        spintent_set_macro("geo_luaset_circ_radio_type_str", "number")
        spintent_set_macro("geo_luaset_circ_radio_raw_str",  numero)
        return
    end

    local etiqueta = spintent_geo_radio_label:match(radio_str)
    if etiqueta then
        spintent_set_macro("geo_luaset_circ_radio_type_str", "label")
        spintent_set_macro("geo_luaset_circ_radio_raw_str",  etiqueta)
        return
    end

    if spintent_geo_radio_measure:match(radio_str) then
        spintent_set_macro("geo_luaset_circ_radio_type_str", "measure")
        spintent_set_macro("geo_luaset_circ_radio_raw_str",  radio_str)
        return
    end

    spintent_set_macro("geo_luaset_error_str",           "true")
    spintent_set_macro("geo_luaset_circ_radio_type_str", "")
    spintent_set_macro("geo_luaset_circ_radio_raw_str",  "")
end, { "string" })

-- Coordenadas

-- \spcoord (modo school): separa "#1,#2" o "#1;#2", clasifica cada
-- mitad (numero/crudo), y distingue frac/dfrac/ninguno para el
-- tamano del parentesis -- \mathstyle se lee directo en expl3,
-- no hace falta reportarlo desde aca.
--
-- Reutiliza spintent_number_pattern (la gramatica LPeg de
-- luafun_clean_split_and_set) solo como clasificador -- sin llamar a
-- esa funcion completa ni disparar sus propios token_set_macro de
-- unidades/millones/exponente, que no aplican aca.

-- Altura "alta" de un valor: busca \dfrac, \frac o \spmQ en CUALQUIER
-- posicion (como \spinterval), pero ignora lo que quede dentro de un
-- subindice o superindice (x_{\frac{1}{2}} no agranda el parentesis).
-- Devuelve "dfrac", "frac" o "none".
local function spintent_spcoord_tall(value)
    local v = value
    v = v:gsub("[_^]%s*%b{}", "")
    v = v:gsub("[_^]%s*\\%a+", "")
    v = v:gsub("[_^]%s*[^%s{\\]", "")
    if s_match(v, "\\dfrac%f[%A]") then return "dfrac" end
    if s_match(v, "\\spmQ%s*%[[^%]]*use%-dfrac%s*=%s*true") then return "dfrac" end
    if s_match(v, "\\frac%f[%A]") or s_match(v, "\\spmQ%f[%A]") then return "frac" end
    return "none"
end

local function spintent_spcoord_classify_base(part)
    local sign, rest = "", part
    local first = s_sub(part, 1, 1)
    if first == "+" or first == "-" then
        sign = first
        rest = spintent_trim(s_sub(part, 2))
    end

    local tall = spintent_spcoord_tall(rest)
    if tall ~= "none" then
        return sign, "raw", rest, tall
    end

    local num_result = spintent_number_pattern:match(rest)
    if num_result
        and (num_result.extra or "") == ""
        and ((num_result.integer or "") ~= "" or (num_result.fraction or "") ~= "") then
        return sign, "number", rest, "none"
    end

    return sign, "raw", rest, "none"
end

-- Un valor "raw" que es un solo nodo con subíndice, potencia o raíz
-- (x_{1}, a^{2}, \sqrt{2}) pierde el arg de \MathMLarg en luamml: se
-- avisa con una quinta salida para que TeX le agregue un segundo hijo.
local function spintent_spcoord_classify(part)
    local sign, type_, value, has_frac = spintent_spcoord_classify_base(part)
    local comp = "false"
    if type_ == "raw"
        and not s_match(value, "^\\d?frac%f[%A]")
        and (s_match(value, "[_^]") or s_match(value, "\\sqrt")) then
        comp = "true"
    end
    return sign, type_, value, has_frac, comp
end

-- profundidad final de llaves y posiciones de ";" y "," a profundidad 0
local function spintent_interval_scan(inner)
    local depth, semis, commas = 0, {}, {}
    local i, n = 1, #inner
    while i <= n do
        local c = s_sub(inner, i, i)
        if c == "\\" then
            i = i + 2
        else
            if c == "{" then
                depth = depth + 1
            elseif c == "}" then
                depth = depth - 1
            elseif depth == 0 then
                if c == ";" then
                    semis[#semis + 1] = i
                elseif c == "," then
                    commas[#commas + 1] = i
                end
            end
            i = i + 1
        end
    end
    return depth, semis, commas
end

local function spintent_spcoord_set_error()
    spintent_set_macro("spcoord_luaset_error_str", "true")
    spintent_set_macro("spcoord_luaset_sep_str",   "")
    spintent_set_macro("spcoord_luaset_x_sign_str",     "")
    spintent_set_macro("spcoord_luaset_x_type_str",     "")
    spintent_set_macro("spcoord_luaset_x_value_str",    "")
    spintent_set_macro("spcoord_luaset_x_has_frac_str", "")
    spintent_set_macro("spcoord_luaset_x_composite_str", "")
    spintent_set_macro("spcoord_luaset_y_sign_str",     "")
    spintent_set_macro("spcoord_luaset_y_type_str",     "")
    spintent_set_macro("spcoord_luaset_y_value_str",    "")
    spintent_set_macro("spcoord_luaset_y_has_frac_str", "")
    spintent_set_macro("spcoord_luaset_y_composite_str", "")
end

register_tex_cmd("luafun_spcoord_parse_and_set", function(raw_content)
    raw_content = spintent_trim(raw_content)

    -- el separador es un ";" o, si no hay, una unica "," a profundidad
    -- de llaves 0 (a_{1,2}, b es valido); con ";" las comas son decimales
    local depth, semis, commas = spintent_interval_scan(raw_content)
    local pos, sep_char
    if depth ~= 0 then
        spintent_spcoord_set_error()
        return
    elseif #semis > 0 then
        if #semis > 1 then
            spintent_spcoord_set_error()
            return
        end
        pos, sep_char = semis[1], ";"
    elseif #commas == 1 then
        pos, sep_char = commas[1], ","
    else
        spintent_spcoord_set_error()
        return
    end

    local x_raw = spintent_trim(s_sub(raw_content, 1, pos - 1))
    local y_raw = spintent_trim(s_sub(raw_content, pos + 1))

    if x_raw == "" or y_raw == "" then
        spintent_spcoord_set_error()
        return
    end

    local x_sign, x_type, x_value, x_has_frac, x_comp = spintent_spcoord_classify(x_raw)
    local y_sign, y_type, y_value, y_has_frac, y_comp = spintent_spcoord_classify(y_raw)

    spintent_set_macro("spcoord_luaset_error_str", "false")
    spintent_set_macro("spcoord_luaset_sep_str",   sep_char)

    spintent_set_macro("spcoord_luaset_x_sign_str",     x_sign)
    spintent_set_macro("spcoord_luaset_x_type_str",     x_type)
    spintent_set_macro("spcoord_luaset_x_value_str",    x_value)
    spintent_set_macro("spcoord_luaset_x_has_frac_str", x_has_frac)
    spintent_set_macro("spcoord_luaset_x_composite_str", x_comp)

    spintent_set_macro("spcoord_luaset_y_sign_str",     y_sign)
    spintent_set_macro("spcoord_luaset_y_type_str",     y_type)
    spintent_set_macro("spcoord_luaset_y_value_str",    y_value)
    spintent_set_macro("spcoord_luaset_y_has_frac_str", y_has_frac)
    spintent_set_macro("spcoord_luaset_y_composite_str", y_comp)
end, { "string" })

-- Intervalos

-- \spinterval: delimitador ( [ ] a la izquierda y ) ] [ a la derecha; el
-- separador es un ";" o, si no hay, una única "," a profundidad de llaves 0.

local spintent_interval_left_open  = { ["("] = true,  ["["] = false, ["]"] = true }
local spintent_interval_right_open = { [")"] = true,  ["]"] = false, ["["] = true }

local spintent_interval_keys_end = { "sign", "type", "value", "inf", "tall", "composite", "integer", "short", "braced" }
local spintent_interval_keys_all = { "left_sym", "right_sym", "type", "sep", "tall" }

local function spintent_interval_set_error(code)
    spintent_set_macro("spinterval_luaset_error_str",      "true")
    spintent_set_macro("spinterval_luaset_error_code_str", code)
    for _, k in ipairs(spintent_interval_keys_all) do
        spintent_set_macro("spinterval_luaset_" .. k .. "_str", "")
    end
    for _, p in ipairs({ "a", "b" }) do
        for _, k in ipairs(spintent_interval_keys_end) do
            spintent_set_macro("spinterval_luaset_" .. p .. "_" .. k .. "_str", "")
        end
    end
end

local function spintent_interval_infinity(part)
    local compact = s_gsub(part, "%s+", "")
    if compact == "\\infty" or compact == "+\\infty" then
        return "pos"
    elseif compact == "-\\infty" then
        return "neg"
    end
    return "none"
end

local function spintent_interval_tall(value)
    if s_match(value, "\\dfrac") then
        return "dfrac"
    elseif s_match(value, "\\frac") then
        return "frac"
    end
    return "none"
end

register_tex_cmd("luafun_spinterval_parse_and_set", function(raw_content)
    raw_content = spintent_trim(raw_content)

    local left  = s_sub(raw_content, 1, 1)
    local right = s_sub(raw_content, -1)
    local left_open, right_open = spintent_interval_left_open[left], spintent_interval_right_open[right]
    if #raw_content < 2 or left_open == nil or right_open == nil then
        spintent_interval_set_error("delim")
        return
    end

    local inner = spintent_trim(s_sub(raw_content, 2, -2))
    if inner == "" then
        spintent_interval_set_error("vacio")
        return
    end

    local depth, semis, commas = spintent_interval_scan(inner)
    if depth ~= 0 then
        spintent_interval_set_error("llaves")
        return
    end

    local pos, sep_char
    if #semis > 0 then
        if #semis > 1 then
            spintent_interval_set_error("ambiguo")
            return
        end
        pos, sep_char = semis[1], ";"
    elseif #commas == 1 then
        pos, sep_char = commas[1], ","
    elseif #commas == 0 then
        spintent_interval_set_error("sep")
        return
    else
        spintent_interval_set_error("ambiguo")
        return
    end

    local a_raw = spintent_trim(s_sub(inner, 1, pos - 1))
    local b_raw = spintent_trim(s_sub(inner, pos + 1))
    if a_raw == "" or b_raw == "" then
        spintent_interval_set_error("vacio")
        return
    end

    local a_inf = spintent_interval_infinity(a_raw)
    local b_inf = spintent_interval_infinity(b_raw)
    if a_inf == "pos" or b_inf == "neg" then
        spintent_interval_set_error("inf-orden")
        return
    end
    if (a_inf == "neg" and not left_open) or (b_inf == "pos" and not right_open) then
        spintent_interval_set_error("inf-cerrado")
        return
    end

    local a_sign, a_type, a_value, _, a_comp = spintent_spcoord_classify(a_raw)
    local b_sign, b_type, b_value, _, b_comp = spintent_spcoord_classify(b_raw)
    if a_inf ~= "none" then a_type = "infinity" end
    if b_inf ~= "none" then b_type = "infinity" end

    -- is_integer: solo dígitos; is_short: además a lo más tres dígitos
    local function is_integer(v) return spintent_bool_str(s_match(v, "^%d+$")) end
    local function is_short(v) return spintent_bool_str(s_match(v, "^%d%d?%d?$")) end

    local a_tall = spintent_interval_tall(a_value)
    local b_tall = spintent_interval_tall(b_value)
    local tall = "none"
    if a_tall == "dfrac" or b_tall == "dfrac" then
        tall = "dfrac"
    elseif a_tall == "frac" or b_tall == "frac" then
        tall = "frac"
    end

    local kind
    if left_open and right_open then
        kind = "open"
    elseif (not left_open) and (not right_open) then
        kind = "closed"
    elseif left_open then
        kind = "open-closed"
    else
        kind = "closed-open"
    end

    spintent_set_macro("spinterval_luaset_error_str",      "false")
    spintent_set_macro("spinterval_luaset_error_code_str", "")
    spintent_set_macro("spinterval_luaset_left_sym_str",   left)
    spintent_set_macro("spinterval_luaset_right_sym_str",  right)
    spintent_set_macro("spinterval_luaset_type_str",       kind)
    spintent_set_macro("spinterval_luaset_sep_str",        sep_char)
    spintent_set_macro("spinterval_luaset_tall_str",       tall)

    spintent_set_macro("spinterval_luaset_a_sign_str",      a_sign)
    spintent_set_macro("spinterval_luaset_a_type_str",      a_type)
    spintent_set_macro("spinterval_luaset_a_value_str",     a_value)
    spintent_set_macro("spinterval_luaset_a_inf_str",       a_inf)
    spintent_set_macro("spinterval_luaset_a_tall_str",      a_tall)
    spintent_set_macro("spinterval_luaset_a_composite_str", a_comp)
    spintent_set_macro("spinterval_luaset_a_integer_str",   is_integer(a_value))
    spintent_set_macro("spinterval_luaset_a_short_str",     is_short(a_value))
    spintent_set_macro("spinterval_luaset_a_braced_str",    (s_gsub(a_value, ",", "{,}")))

    spintent_set_macro("spinterval_luaset_b_sign_str",      b_sign)
    spintent_set_macro("spinterval_luaset_b_type_str",      b_type)
    spintent_set_macro("spinterval_luaset_b_value_str",     b_value)
    spintent_set_macro("spinterval_luaset_b_inf_str",       b_inf)
    spintent_set_macro("spinterval_luaset_b_tall_str",      b_tall)
    spintent_set_macro("spinterval_luaset_b_composite_str", b_comp)
    spintent_set_macro("spinterval_luaset_b_integer_str",   is_integer(b_value))
    spintent_set_macro("spinterval_luaset_b_short_str",     is_short(b_value))
    spintent_set_macro("spinterval_luaset_b_braced_str",    (s_gsub(b_value, ",", "{,}")))
end, { "string" })

-- \spProb: separa  A \cap B,  A \cup B  o  A \mid B  (también A | B).
-- Solo cuenta el nivel superior: lo que está entre llaves, paréntesis
-- o corchetes (también \left...\right) no se examina.
local spintent_prob_words = { cap = "cap", cup = "cup", mid = "mid", vert = "mid" }

local function spintent_prob_set_error()
    spintent_set_macro("spProb_luaset_error_str", "true")
    spintent_set_macro("spProb_luaset_op_str",    "")
    spintent_set_macro("spProb_luaset_a_str",     "")
    spintent_set_macro("spProb_luaset_b_str",     "")
end

register_tex_cmd("luafun_spProb_parse_and_set", function(raw_content)
    raw_content = spintent_trim(raw_content)
    local depth, ops = 0, {}
    local i, n = 1, #raw_content
    while i <= n do
        local c = s_sub(raw_content, i, i)
        if c == "\\" then
            local word = s_match(raw_content, "^\\(%a+)", i)
            if word == "left" or word == "right" then
                local j = i + 1 + #word
                while s_sub(raw_content, j, j) == " " do j = j + 1 end
                local d = s_sub(raw_content, j, j)
                if word == "left" and (d == "(" or d == "[") then
                    depth = depth + 1
                elseif word == "right" and (d == ")" or d == "]") then
                    depth = depth - 1
                end
                i = j + 1
            elseif word and depth == 0 and spintent_prob_words[word] then
                ops[#ops + 1] = { i, 1 + #word, spintent_prob_words[word] }
                i = i + 1 + #word
            elseif word then
                i = i + 1 + #word
            else
                local d = s_sub(raw_content, i + 1, i + 1)
                if d == "{" then
                    depth = depth + 1
                elseif d == "}" then
                    depth = depth - 1
                end
                i = i + 2
            end
        else
            if c == "{" or c == "(" or c == "[" then
                depth = depth + 1
            elseif c == "}" or c == ")" or c == "]" then
                depth = depth - 1
            elseif c == "|" and depth == 0 then
                ops[#ops + 1] = { i, 1, "mid" }
            end
            if depth < 0 then
                spintent_prob_set_error()
                return
            end
            i = i + 1
        end
    end
    if depth ~= 0 or raw_content == "" or #ops > 1 then
        spintent_prob_set_error()
        return
    end

    local a_raw, b_raw, op = raw_content, "", "none"
    if #ops == 1 then
        op = ops[1][3]
        a_raw = spintent_trim(s_sub(raw_content, 1, ops[1][1] - 1))
        b_raw = spintent_trim(s_sub(raw_content, ops[1][1] + ops[1][2]))
        if a_raw == "" or b_raw == "" then
            spintent_prob_set_error()
            return
        end
    end

    spintent_set_macro("spProb_luaset_error_str", "false")
    spintent_set_macro("spProb_luaset_op_str",    op)
    spintent_set_macro("spProb_luaset_a_str",     a_raw)
    spintent_set_macro("spProb_luaset_b_str",     b_raw)
end, { "string" })

-- \spAcirc, \spPcirc: una coma en el argumento nunca es válida.
register_tex_cmd("luafun_spcircfig_comma", function(raw)
    spintent_set_bool("spcircfig_luaset_comma_str", string.find(raw, ",", 1, true))
end, { "string" })

-- \spcomp: un evento es compuesto si contiene \cap, \cup, \setminus
-- o \mid (a cualquier profundidad de llaves).
local spintent_compound_ops = { cap = true, cup = true, setminus = true, mid = true }

register_tex_cmd("luafun_spcomp_compound", function(raw)
    local compound, i, n = "false", 1, #raw
    while i <= n do
        if s_sub(raw, i, i) == "\\" then
            local word = s_match(raw, "^%a+", i + 1)
            if word then
                if spintent_compound_ops[word] then compound = "true"; break end
                i = i + 1 + #word
            else
                i = i + 2
            end
        else
            i = i + 1
        end
    end
    spintent_set_macro("spcomp_luaset_compound_str", compound)
end, { "string" })

-- \spabs: decide si se lee el cierre del valor absoluto. Cuenta los
-- tokens del argumento sin espacios y sin un signo - o + inicial
-- (primero -, luego +); una secuencia de control, un grupo entre
-- llaves y cualquier otro carácter son un token cada uno. Hay cierre
-- si hay más de un token.
register_tex_cmd("luafun_spabs_close", function(raw)
    local i, n, count = 1, #raw, 0
    local function skip_spaces()
        while s_sub(raw, i, i) == " " do i = i + 1 end
    end
    skip_spaces()
    for _, sign in ipairs({ "-", "+" }) do
        if s_sub(raw, i, i) == sign then
            i = i + 1
            skip_spaces()
        end
    end
    while i <= n do
        local c = s_sub(raw, i, i)
        if c == " " then
            i = i + 1
        else
            count = count + 1
            if c == "\\" then
                local word = s_match(raw, "^%a+", i + 1)
                i = word and (i + 1 + #word) or (i + 2)
            elseif c == "{" then
                local depth = 0
                repeat
                    local d = s_sub(raw, i, i)
                    if d == "\\" then
                        i = i + 1
                    elseif d == "{" then
                        depth = depth + 1
                    elseif d == "}" then
                        depth = depth - 1
                    end
                    i = i + 1
                until depth == 0 or i > n
            else
                i = i + 1
            end
        end
    end
    spintent_set_bool("spabs_luaset_close_str", count > 1)
end, { "string" })

-- \spcomb: combinatoria. El argumento único es n o n,k. La función
-- luafun_spcomb_split lo parte en la primera coma de nivel superior
-- y luafun_spcomb_prepare valida y deja listos n y k según el modo
-- que decide expl3:
--   pair  n y k (k <= n si ambos son enteros y check_le es "true")
--   list  n y una lista de multiplicidades separadas por comas
--         (permutaciones con repetición); queda como {a}{b}{c}
--   one   solo n
-- Las comas que quedan dentro de llaves, paréntesis o corchetes
-- (también \left...\right) no separan elementos. Si todo es entero,
-- la suma de las multiplicidades no puede pasar de n.
local function spintent_comb_set_error()
    spintent_set_macro("spcomb_luaset_error_str", "true")
    spintent_set_macro("spcomb_luaset_n_str",     "")
    spintent_set_macro("spcomb_luaset_k_str",     "")
    spintent_set_macro("spcomb_luaset_list_tl",   "")
end

local function spintent_comb_split(raw)
    local items, depth, start = {}, 0, 1
    local i, n = 1, #raw
    while i <= n do
        local c = s_sub(raw, i, i)
        if c == "\\" then
            local word = s_match(raw, "^\\(%a+)", i)
            if word == "left" or word == "right" then
                local j = i + 1 + #word
                while s_sub(raw, j, j) == " " do j = j + 1 end
                local d = s_sub(raw, j, j)
                if word == "left" and (d == "(" or d == "[") then
                    depth = depth + 1
                elseif word == "right" and (d == ")" or d == "]") then
                    depth = depth - 1
                end
                i = j + 1
            elseif word then
                i = i + 1 + #word
            else
                local d = s_sub(raw, i + 1, i + 1)
                if d == "{" then
                    depth = depth + 1
                elseif d == "}" then
                    depth = depth - 1
                end
                i = i + 2
            end
        else
            if c == "{" or c == "(" or c == "[" then
                depth = depth + 1
            elseif c == "}" or c == ")" or c == "]" then
                depth = depth - 1
            elseif c == "," and depth == 0 then
                items[#items + 1] = s_sub(raw, start, i - 1)
                start = i + 1
            end
            if depth < 0 then return nil end
            i = i + 1
        end
    end
    if depth ~= 0 then return nil end
    items[#items + 1] = s_sub(raw, start)
    return items
end

register_tex_cmd("luafun_spcomb_split", function(raw)
    local items = spintent_comb_split(raw)
    local raw_n, raw_k, single = raw, "", "true"
    if items and #items >= 2 then
        raw_n, raw_k, single = items[1], t_concat(items, ",", 2), "false"
    end
    spintent_set_macro("spcomb_luaset_raw_n_str",  raw_n)
    spintent_set_macro("spcomb_luaset_raw_k_str",  raw_k)
    spintent_set_macro("spcomb_luaset_single_str", single)
end, { "string" })

register_tex_cmd("luafun_spcomb_prepare", function(mode, raw_n, raw_k, check_le)
    local n_s = spintent_trim(raw_n)
    local k_s = spintent_trim(raw_k)
    if n_s == "" then return spintent_comb_set_error() end
    local list = ""
    if mode == "pair" then
        if k_s == "" then return spintent_comb_set_error() end
        if check_le == "true" and s_match(n_s, "^%d+$") and s_match(k_s, "^%d+$")
                and tonumber(k_s) > tonumber(n_s) then
            return spintent_comb_set_error()
        end
    elseif mode == "list" then
        local items = spintent_comb_split(k_s)
        if not items or #items < 2 then return spintent_comb_set_error() end
        local numeric = s_match(n_s, "^%d+$") ~= nil
        local sum, parts = 0, {}
        for _, item in ipairs(items) do
            item = spintent_trim(item)
            if item == "" then return spintent_comb_set_error() end
            if numeric and s_match(item, "^%d+$") then
                sum = sum + tonumber(item)
            else
                numeric = false
            end
            parts[#parts + 1] = "{" .. item .. "}"
        end
        if numeric and sum > tonumber(n_s) then return spintent_comb_set_error() end
        list = t_concat(parts)
    end
    spintent_set_macro("spcomb_luaset_error_str", "false")
    spintent_set_macro("spcomb_luaset_n_str",     n_s)
    spintent_set_macro("spcomb_luaset_k_str",     k_s)
    spintent_set_macro("spcomb_luaset_list_tl",   list)
end, { "string", "string", "string", "string" })
end

-- FUNCIONES: \spfun, \spdom, \sprec, \spcod

do
--
-- \spfun reconoce  nombre(arg, arg, ...)  donde nombre es una letra
-- (mayúscula o minúscula) con subíndice entre llaves (letras o
-- dígitos) o con primas, igual que un punto de la geometría, y con
-- el exponente ^{-1} opcional de la función inversa (que no se
-- combina con primas). Solo la inversa puede ir sin paréntesis. Los
-- argumentos se separan por las comas de nivel superior: las que
-- quedan dentro de llaves, paréntesis o corchetes no cuentan.
-- Deja el nombre en las mismas variables que usa la familia geo
-- (letra/sub/tipo/intent/print) y cada argumento en
-- l__spintent_spfun_luaset_arg_<i>_tl. Un argumento que es a su vez
-- una llamada (f(g(x)), f(g^{-1}(y))) se analiza de forma recursiva y
-- queda marcado "call"; expl3 vuelve a llamar a esta función con él.
--
-- \spdom, \sprec y \spcod reciben solo el nombre (con la misma
-- gramática) y lo dejan en las mismas variables.
local spintent_fun_alnum = R"09" + R"az" + R"AZ"

local spintent_fun_head = Ct(
    C(R"az" + R"AZ") *
    (
        P"_{" * C(spintent_fun_alnum^1) * P"}" * Cc"sub"
        + spintent_geo_primas               * Cc"prima"
        + Cc""                              * Cc""
    )
) * lpeg_base.Cp()

-- Nombre + exponente ^{-1} opcional. Devuelve nombre, inv (booleano) y
-- el resto del texto; nil si no empieza con un nombre válido o si la
-- inversa va junto a primas. Los espacios dentro del exponente y antes
-- de él no cuentan.
local function spintent_fun_parse_name(raw)
    local nombre, pos = spintent_fun_head:match(raw)
    if not nombre then return nil end
    local rest = s_sub(raw, pos)
    local inv = false
    local after = s_match(rest, "^%s*%^%s*%{%s*%-%s*1%s*%}(.*)$")
    if after then
        if nombre[3] == "prima" then return nil end
        inv, rest = true, after
    end
    return nombre, inv, rest
end

-- Separa por las comas de nivel superior. Devuelve nil si los
-- delimitadores no quedan balanceados.
local function spintent_fun_split_args(inner)
    local args, depth, start = {}, 0, 1
    local i, n = 1, #inner
    while i <= n do
        local c = s_sub(inner, i, i)
        if c == "\\" then
            i = i + 2
        else
            if c == "{" or c == "(" or c == "[" then
                depth = depth + 1
            elseif c == "}" or c == ")" or c == "]" then
                depth = depth - 1
                if depth < 0 then return nil end
            elseif c == "," and depth == 0 then
                args[#args + 1] = s_sub(inner, start, i - 1)
                start = i + 1
            end
            i = i + 1
        end
    end
    if depth ~= 0 then return nil end
    args[#args + 1] = s_sub(inner, start)
    return args
end

-- Un argumento "simple" es un único nodo (letra, número o comando
-- como \alpha): lleva su arg directo. Cualquier otro (2x, x+1,
-- x_{1}, \frac{1}{2}, \sqrt{2}) necesita el separador interno para
-- que luamml le conserve el arg.
local function spintent_fun_is_simple(arg)
    return s_match(arg, "^%w$") ~= nil
        or s_match(arg, "^%d+$") ~= nil
        or s_match(arg, "^\\%a+$") ~= nil
end

-- ¿El argumento es, entero, una llamada nombre(...) o nombre^{-1}?
-- Se comprueba solo la forma; su validez se analiza aparte.
local function spintent_fun_is_call(arg)
    local nombre, inv, rest = spintent_fun_parse_name(arg)
    if not nombre then return false end
    local inner = s_match(rest, "^%s*%((.*)%)%s*$")
    if inner then
        return spintent_fun_split_args(inner) ~= nil
    end
    return inv and s_match(rest, "^%s*$") ~= nil
end

local spintent_fun_analyze

-- Clasifica una lista de argumentos en crudo: devuelve { texto, tipo }
-- por argumento ("true" un solo nodo, "false" expresión, "call" otra
-- llamada ya validada de forma recursiva) o nil si alguno es inválido.
local function spintent_fun_classify_args(args)
    for i = 1, #args do
        local a = spintent_trim(args[i])
        if a == "" then return nil end
        local kind
        if spintent_fun_is_simple(a) then
            kind = "true"
        elseif spintent_fun_is_call(a) then
            if not spintent_fun_analyze(a) then return nil end
            kind = "call"
        else
            kind = "false"
        end
        args[i] = { a, kind }
    end
    return args
end

-- Analiza una llamada completa. Devuelve nombre, inv y la lista de
-- argumentos { texto, tipo }; nil si algo no es válido en cualquier
-- nivel.
spintent_fun_analyze = function(raw)
    local nombre, inv, rest = spintent_fun_parse_name(raw)
    if not nombre then return nil end
    local args
    local inner = s_match(rest, "^%s*%((.*)%)%s*$")
    if inner then
        args = spintent_fun_split_args(inner)
    elseif inv and s_match(rest, "^%s*$") then
        args = {}
    end
    if not args then return nil end
    args = spintent_fun_classify_args(args)
    if not args then return nil end
    return nombre, inv, args
end

local function spintent_fun_set_error()
    spintent_set_macro("spfun_luaset_error_str", "true")
    spintent_set_macro("spfun_luaset_nargs_str", "0")
    spintent_set_macro("spfun_luaset_inv_str",   "false")
    spintent_set_macro("geo_luaset_intent_str",  "")
    spintent_set_macro("geo_luaset_print_tl",    "")
    spintent_set_macro("geo_luaset_letra_str",   "")
    spintent_set_macro("geo_luaset_sub_str",     "")
    spintent_set_macro("geo_luaset_tipo_str",    "")
end

-- Deja el nombre en las variables compartidas. Con la inversa, el
-- intent literal termina en "-inversa" (f-inversa, f-sub-uno-inversa).
local function spintent_fun_set_name(nombre, inv)
    local intent = spintent_geo_build_body_nombre(nombre)
    local visual = spintent_geo_build_visual_nombre(nombre)
    if inv then
        intent = intent .. "-inversa"
        visual = visual .. "^{-1}"
    end
    spintent_set_macro("geo_luaset_intent_str", intent)
    spintent_set_macro("geo_luaset_print_tl",   visual)
    spintent_set_macro("geo_luaset_letra_str",  nombre[1])
    spintent_set_macro("geo_luaset_sub_str",    nombre[2] or "")
    spintent_set_macro("geo_luaset_tipo_str",   nombre[3] or "")
    spintent_set_bool("spfun_luaset_inv_str",  inv)
end

register_tex_cmd("luafun_spfun_parse_and_set", function(raw)
    raw = spintent_trim(raw)
    raw = spintent_geo_normalize_primes(raw)

    local nombre, inv, args = spintent_fun_analyze(raw)
    if not nombre then
        spintent_fun_set_error()
        return
    end

    spintent_set_macro("spfun_luaset_error_str", "false")
    spintent_set_macro("spfun_luaset_nargs_str", tostring(#args))
    for i = 1, #args do
        spintent_set_macro("spfun_luaset_arg_" .. i .. "_tl", args[i][1])
        spintent_set_macro("spfun_luaset_simple_" .. i .. "_str", args[i][2])
    end
    spintent_fun_set_name(nombre, inv)
end, { "string" })

register_tex_cmd("luafun_spdom_parse_and_set", function(raw)
    raw = spintent_trim(raw)
    raw = spintent_geo_normalize_primes(raw)

    local nombre, inv, rest = spintent_fun_parse_name(raw)
    if not nombre or not s_match(rest, "^%s*$") then
        spintent_fun_set_error()
        spintent_set_macro("spdom_luaset_error_str", "true")
        return
    end
    spintent_set_macro("spdom_luaset_error_str", "false")
    spintent_fun_set_name(nombre, inv)
end, { "string" })

-- \spfuncomp: composición lineal f\circ g, (f\circ g)(x)
--
-- Formas: f\circ g\circ ...   |   (f\circ g)   |   (f\circ g)(args)
-- Cada función es un nombre como en \spfun (letra con subíndice,
-- primas o ^{-1}); hay al menos dos. Los argumentos se analizan
-- igual que en \spfun (simple / expresión / llamada).

-- Índice del paréntesis que cierra al de la posición i, o nil.
local function spintent_fun_match_paren(str, i)
    local depth = 0
    local n = #str
    while i <= n do
        local c = s_sub(str, i, i)
        if c == "\\" then
            i = i + 2
        else
            if c == "{" or c == "(" or c == "[" then
                depth = depth + 1
            elseif c == "}" or c == ")" or c == "]" then
                depth = depth - 1
                if depth == 0 then return i end
                if depth < 0 then return nil end
            end
            i = i + 1
        end
    end
    return nil
end

-- Parte por los \circ (o ∘) de nivel superior.
local function spintent_fun_split_circ(str)
    local parts, depth, start = {}, 0, 1
    local i, n = 1, #str
    while i <= n do
        local c = s_sub(str, i, i)
        if c == "\\" then
            if depth == 0 and s_sub(str, i, i + 4) == "\\circ"
                and not s_match(s_sub(str, i + 5, i + 5), "%a") then
                parts[#parts + 1] = s_sub(str, start, i - 1)
                i = i + 5
                start = i
            else
                i = i + 2
            end
        elseif depth == 0 and s_sub(str, i, i + 2) == "∘" then
            parts[#parts + 1] = s_sub(str, start, i - 1)
            i = i + 3
            start = i
        else
            if c == "{" or c == "(" or c == "[" then
                depth = depth + 1
            elseif c == "}" or c == ")" or c == "]" then
                depth = depth - 1
                if depth < 0 then return nil end
            end
            i = i + 1
        end
    end
    if depth ~= 0 then return nil end
    parts[#parts + 1] = s_sub(str, start)
    return parts
end

-- Lista de funciones { intent, visual } separadas por \circ; al menos
-- dos y cada una un nombre completo. nil si algo no es válido.
local function spintent_fun_comp_funs(str)
    local parts = spintent_fun_split_circ(str)
    if not parts or #parts < 2 then return nil end
    local funs = {}
    for i = 1, #parts do
        local nombre, inv, rest = spintent_fun_parse_name(spintent_trim(parts[i]))
        if not nombre or not s_match(rest, "^%s*$") then return nil end
        local intent = spintent_geo_build_body_nombre(nombre)
        local visual = spintent_geo_build_visual_nombre(nombre)
        if inv then
            intent = intent .. "-inversa"
            visual = visual .. "^{-1}"
        end
        funs[i] = { intent, visual, nombre[1], nombre[2] or "", nombre[3] or "", inv }
    end
    return funs
end

local function spintent_fun_comp_error()
    spintent_set_macro("spfuncomp_luaset_error_str", "true")
end

register_tex_cmd("luafun_spfuncomp_parse_and_set", function(raw)
    raw = spintent_trim(raw)
    raw = spintent_geo_normalize_primes(raw)

    local funs, args, paren
    if s_sub(raw, 1, 1) == "(" then
        local close = spintent_fun_match_paren(raw, 1)
        if not close then spintent_fun_comp_error() return end
        paren = true
        funs = spintent_fun_comp_funs(s_sub(raw, 2, close - 1))
        local rest = spintent_trim(s_sub(raw, close + 1))
        if rest ~= "" then
            if s_sub(rest, 1, 1) ~= "(" then spintent_fun_comp_error() return end
            local c2 = spintent_fun_match_paren(rest, 1)
            if c2 ~= #rest then spintent_fun_comp_error() return end
            args = spintent_fun_split_args(s_sub(rest, 2, c2 - 1))
            if not args then spintent_fun_comp_error() return end
            args = spintent_fun_classify_args(args)
            if not args then spintent_fun_comp_error() return end
        end
    else
        paren = false
        funs = spintent_fun_comp_funs(raw)
    end
    if not funs then spintent_fun_comp_error() return end

    spintent_set_macro("spfuncomp_luaset_error_str", "false")
    spintent_set_macro("spfuncomp_luaset_n_str", tostring(#funs))
    spintent_set_bool("spfuncomp_luaset_paren_str", paren)
    spintent_set_bool("spfuncomp_luaset_apply_str", args)
    for i = 1, #funs do
        spintent_set_macro("spfuncomp_luaset_intent_" .. i .. "_str", funs[i][1])
        spintent_set_macro("spfuncomp_luaset_print_" .. i .. "_tl",  funs[i][2])
        spintent_set_macro("spfuncomp_luaset_letra_" .. i .. "_str", funs[i][3])
        spintent_set_macro("spfuncomp_luaset_sub_" .. i .. "_str",   funs[i][4])
        spintent_set_macro("spfuncomp_luaset_tipo_" .. i .. "_str",  funs[i][5])
        spintent_set_bool("spfuncomp_luaset_inv_" .. i .. "_str",   funs[i][6])
    end
    if args then
        spintent_set_macro("spfun_luaset_nargs_str", tostring(#args))
        for i = 1, #args do
            spintent_set_macro("spfun_luaset_arg_" .. i .. "_tl", args[i][1])
            spintent_set_macro("spfun_luaset_simple_" .. i .. "_str", args[i][2])
        end
    end
end, { "string" })
end
% \iffalse
%</spintentlua>
% \fi
%
% \iffalse
%<@@=>
%<*spitool>
% \fi
#!/usr/bin/env texlua

-- Must be the first line of any texlua script: LuaTeX doesn't add the
-- kpse-aware require() searcher until this has run, and other things
-- are half-broken without it too.
kpse.set_program_name("luatex")

local SPITOOL_VERSION = "1.4"
local lfs = require("lfs")

-- OS detection and Quoting helper (POSIX vs Windows)
local is_windows = package.config:sub(1, 1) == '\\'

local function quote_arg(v)
    if is_windows then
        if v:find("%%") then
            io.stderr:write("[WARNING] argument contains '%': "
                .. v .. " -- cmd.exe may expand it as an environment variable.\n")
        end
        return '"' .. v:gsub('"', '\\"') .. '"'
    else
        return "'" .. v:gsub("'", "'\\''") .. "'"
    end
end

-- Helper: Check executable presence in system PATH
local function has_cmd(cmd)
    local null_dev = is_windows and "NUL" or "/dev/null"
    local check = is_windows and ("where " .. cmd .. " > " .. null_dev .. " 2>&1")
                             or ("command -v " .. cmd .. " > " .. null_dev .. " 2>&1")
    local res = os.execute(check)
    return (res == true or res == 0)
end

-- Input filename safety validation (prevents command & TeX injection)
local function is_safe_filename(filename)
    if filename:match("[%s\"'%%&|<>^`$%{%}%[%]!#~]") then
        return false
    end
    return true
end

-- File name without its directory part (TeX's \jobname never has one)
local function file_name_only(path)
    return path:match("([^/\\]+)$") or path
end

-- Config & CLI Parser State
local config = {
    path = nil,
    strict = false,
    overwrite = false,
    check_only = false,
    engine = nil,
    help = false,
    version = false
}
local input_files = {}

-- Set when any validation or compilation step fails (drives the exit code)
local had_failure = false

-- True only for regular files (directories are rejected)
local function is_regular_file(path)
    local attr = lfs.attributes(path)
    return attr ~= nil and attr.mode == "file"
end

-- Smart file path resolution (allows omitting extensions)
local function resolve_input_file(input)
    if is_regular_file(input) then
        return input
    end

    local candidates
    if config.check_only then
        candidates = { input .. ".pdf" }
    else
        candidates = {
            input .. ".tex",
            input .. ".ltx",
            input .. ".pdf",
            input .. "-luamml-mathml.html",
            input .. ".html"
        }
    end

    for _, candidate in ipairs(candidates) do
        if is_regular_file(candidate) then
            return candidate
        end
    end

    return nil
end

-- Check if output file exists and enforce --overwrite protection
local function check_output_permission(output_path, overwrite_flag)
    local check = io.open(output_path, "r")
    if check then
        check:close()
        if not overwrite_flag then
            io.stderr:write("Error: output file '" .. output_path .. "' already exists. Use -o or --overwrite to replace it.\n")
            return false
        end
    end
    return true
end

-- Docopt-style CLI parser
local i = 1
local parse_flags = true

while i <= #arg do
    local current = arg[i]

    if parse_flags and current == "--" then
        parse_flags = false

    elseif parse_flags and (current == "--help" or current == "-h") then
        config.help = true

    elseif parse_flags and (current == "--version" or current == "-v") then
        config.version = true

    elseif parse_flags and (current == "--strict" or current == "-s") then
        config.strict = true

    elseif parse_flags and (current == "--overwrite" or current == "-o") then
        config.overwrite = true

    elseif parse_flags and (current == "--check-only" or current == "-c") then
        config.check_only = true

    elseif parse_flags and (current == "--path" or current == "-p") then
        if not arg[i+1] then
            io.stderr:write("Error: option '" .. current .. "' requires a value\n")
            os.exit(1)
        end
        i = i + 1
        config.path = arg[i]

    elseif parse_flags and (current:match("^%-%-path=(.*)") or current:match("^%-p=(.*)")) then
        config.path = current:match("^%-%-path=(.*)") or current:match("^%-p=(.*)")

    elseif parse_flags and (current == "--engine" or current == "-e") then
        if not arg[i+1] then
            io.stderr:write("Error: option '" .. current .. "' requires a value\n")
            os.exit(1)
        end
        i = i + 1
        config.engine = arg[i]

    elseif parse_flags and (current:match("^%-%-engine=(.*)") or current:match("^%-e=(.*)")) then
        config.engine = current:match("^%-%-engine=(.*)") or current:match("^%-e=(.*)")

    else
        if parse_flags and current:sub(1,1) == "-" then
            io.stderr:write("Error: unrecognized option '" .. current .. "'\n")
            os.exit(1)
        else
            table.insert(input_files, current)
        end
    end

    i = i + 1
end

-- Zero arguments check: show help if running from interactive terminal (TTY)
if #arg == 0 then
    local ffi = require("ffi")
    ffi.cdef[[
        int isatty(int fd);
        int _isatty(int fd);
    ]]
    local is_tty
    if ffi.os == "Windows" then
        is_tty = ffi.C._isatty(0) ~= 0
    else
        is_tty = ffi.C.isatty(0) ~= 0
    end
    if is_tty then
        config.help = true
    end
end

-- Docopt Help banner
if config.help then
    print("spmmli-tool v" .. SPITOOL_VERSION .. " - Tool for cleaning luamml ghosts and validating LaTeX tagged PDFs")
    print([[
Usage:
  spmmli-tool [options] <file.pdf | file-luamml-mathml.html | file.tex>

Description:
  spmmli-tool prepares the MathML files generated by luamml for mathml-AF: it
  removes the invisible ghost characters (U+2063 with sptmp attributes)
  and writes the result to <name>-mathml.html.

  Depending on the input file:
  .pdf    validates the tag structure (rnv-wrapp) and the PDF/UA-2
          conformance (veraPDF), if the tools exist.
  .html   cleans an existing <name>-luamml-mathml.html.
  .tex    runs the TeX engine once in a temporary directory with AFghost=true to
          obtain the luamml HTML, then cleans it. The temporary PDF is
          discarded and not validated.

Options:
  -c, --check-only    Only validate; the input must be a .pdf file.
  -o, --overwrite     Force replacing existing output files (-mathml.html).
  -p, --path=<path>   Target directory where output files will be created
                      (created if it does not exist).
  -e, --engine=<cmd>  TeX engine used to compile (default: lualatex-dev if
                      available, otherwise lualatex).
  -s, --strict        Stop at the first failure (compilation or validation).
                      Without it, all files are processed; in both cases the
                      exit code is non-zero if anything failed.
  -h, --help          Show this usage summary and exit.
  -v, --version       Show spmmli-tool version number and exit.

Examples:
  $ spmmli-tool -c sample.pdf
  $ spmmli-tool -c -s sample
  $ spmmli-tool sample-luamml-mathml.html
  $ spmmli-tool -o -p build/ sample.tex

Issues and reports:
  Repository : https://github.com/pablgonz/spanish-mathml-intent
  Bug tracker: https://github.com/pablgonz/spanish-mathml-intent/issues
  Copyright(C) 2026 by Pablo González L <pablgonz<at>educarchile.cl>
]])
    os.exit(0)
end

-- Version banner
if config.version then
    print("spmmli-tool v" .. SPITOOL_VERSION)
    os.exit(0)
end

if config.engine and not config.engine:match("^[%w%._%-]+$") then
    io.stderr:write("Error: illegal characters in engine name: " .. config.engine .. "\n")
    os.exit(1)
end

if config.check_only and (config.path or config.overwrite) then
    io.stderr:write("[WARNING] -p/--path and -o/--overwrite have no effect with -c/--check-only.\n")
end

if #input_files == 0 then
    io.stderr:write("Error: no input file specified. Use --help for usage information.\n")
    os.exit(1)
end

-- Recursive directory removal helper
local function remove_dir_recursive(path)
    if not path or path == "" then return end
    local attr = lfs.attributes(path)
    if not attr or attr.mode ~= "directory" then return end

    for entry in lfs.dir(path) do
        if entry ~= "." and entry ~= ".." then
            local full_path = path .. "/" .. entry
            -- symlinkattributes: never follow links out of the temp dir
            local entry_attr = lfs.symlinkattributes(full_path)
            if entry_attr and entry_attr.mode == "directory" then
                remove_dir_recursive(full_path)
            else
                os.remove(full_path)
            end
        end
    end
    lfs.rmdir(path)
end

-- Secure temp directory helpers
local function system_temp_dir()
    if is_windows then
        return os.getenv("TEMP") or os.getenv("TMP") or "C:\\Windows\\Temp"
    else
        return os.getenv("TMPDIR") or "/tmp"
    end
end

local function secure_temp_subdir()
    -- POSIX: mktemp -d creates the directory atomically with mode 700
    if not is_windows and has_cmd("mktemp") then
        local template = system_temp_dir():gsub("/$", "") .. "/spmmli-tool-XXXXXX"
        local p = io.popen("mktemp -d " .. quote_arg(template) .. " 2>/dev/null")
        if p then
            local dir = p:read("*l")
            p:close()
            if dir and dir ~= "" and lfs.attributes(dir, "mode") == "directory" then
                return dir
            end
        end
    end

    -- Fallback (Windows, or no mktemp)
    local base = system_temp_dir()
    local sep = package.config:sub(1, 1)
    math.randomseed(os.time() + math.floor(os.clock() * 100000))
    local unique = string.format("%d_%d", os.time(), math.random(100000, 999999))
    local tmpdir = base .. sep .. "spmmli-tool-" .. unique

    local ok, err = lfs.mkdir(tmpdir)
    if not ok then
        io.stderr:write("Error: failed to create secure temp directory: " .. tostring(err) .. "\n")
        os.exit(1)
    end

    if not is_windows then
        os.execute("chmod 700 " .. quote_arg(tmpdir))
    end

    return tmpdir
end

-- Create the output directory (and parents) if needed
local function ensure_dir(path)
    if lfs.attributes(path, "mode") == "directory" then return true end
    local current = path:sub(1, 1) == "/" and "/" or ""
    for part in path:gmatch("[^/\\]+") do
        current = (current == "" or current:sub(-1) == "/") and (current .. part) or (current .. "/" .. part)
        if lfs.attributes(current, "mode") ~= "directory" then
            local ok, err = lfs.mkdir(current)
            if not ok then return nil, err end
        end
    end
    return true
end

-- Shared cleanup: Strips ghost tags/characters from dirty HTML
--
-- Each ghost is a <mi ... sptmp="outer|inner" ...>U+2063</mi> element. It is
-- removed with a pattern over the whole file content (not line by line), so
-- it works both for indented HTML (one element per line) and for flat HTML
-- (a whole formula on a single line). Elements with an unknown sptmp kind
-- are left untouched.
local function strip_ghosts(input_path, output_path)
    local html_file = io.open(input_path, "rb")
    if not html_file then
        return nil, "failed to open " .. input_path
    end
    local content = html_file:read("*a")
    html_file:close()

    local count_outer, count_inner = 0, 0
    -- U+2063 may appear as the literal character or as a character reference
    local ghost_forms = { "\u{2063}", "&#x2063;", "&#X2063;", "&#8291;" }
    local clean = content
    for _, ghost in ipairs(ghost_forms) do
        -- optional leading newline + indentation is removed together with the tag
        local pattern = "\n?[ \t]*<mi([^>]-)sptmp=\"(%a+)\"([^>]-)>"
            .. ghost:gsub("%p", "%%%0") .. "</mi>"
        clean = clean:gsub(pattern, function(_, kind)
            if kind == "outer" then
                count_outer = count_outer + 1
            elseif kind == "inner" then
                count_inner = count_inner + 1
            else
                return nil -- unknown kind: keep the match as is
            end
            return ""
        end)
    end

    local out = io.open(output_path, "wb")
    if not out then
        return nil, "failed to create " .. output_path
    end
    out:write(clean)
    out:close()

    local function count_lines(s)
        local _, n = s:gsub("\n", "\n")
        return n + ((s ~= "" and s:sub(-1) ~= "\n") and 1 or 0)
    end

    return {
        total_lines = count_lines(content),
        count_outer = count_outer,
        count_inner = count_inner,
        kept_lines  = count_lines(clean),
    }
end

local function report_stats(stats, output_name, empty_hint)
    print(string.format("  -> Lines read: %d", stats.total_lines))
    print(string.format("  -> 'outer' ghosts removed: %d", stats.count_outer))
    print(string.format("  -> 'inner' ghosts removed: %d", stats.count_inner))
    print(string.format("  -> Generated: %s (%d lines)", output_name, stats.kept_lines))
    if stats.count_outer == 0 and stats.count_inner == 0 then
        print("[WARNING] No ghost characters found.")
        print("  " .. empty_hint)
        return false
    end
    return true
end

-- Mode 1: PDF Validation Only (No TeX compilation)
local function run_pdf_mode(pdf_filename)
    local run_rnv = has_cmd("rnv-wrapp") and has_cmd("show-pdf-tags")
    local run_verapdf = has_cmd("verapdf")

    if not run_rnv and not run_verapdf then
        io.stderr:write("Error: no PDF validators (show-pdf-tags/rnv-wrapp or veraPDF) detected in PATH.\n")
        os.exit(1)
    end

    print("==================================================")
    print(" Testing PDF compliance: " .. pdf_filename)
    print("==================================================")

    if run_rnv and run_verapdf then
        print("Validators detected: show-pdf-tags, rnv-wrapp & veraPDF")
    elseif run_rnv then
        print("Validators detected: show-pdf-tags & rnv-wrapp")
    elseif run_verapdf then
        print("Validator detected: veraPDF")
    end

    local null_dev = is_windows and "NUL" or "/dev/null"
    local validation_failed = false

    -- 1. RNC tag structure check
    if run_rnv then
        local cmd_rnv = "show-pdf-tags --xml " .. quote_arg(pdf_filename) .. " | rnv-wrapp"
        local res_rnv = os.execute(cmd_rnv .. " > " .. null_dev .. " 2>&1")
        if res_rnv ~= true and res_rnv ~= 0 then
            print("\n[RNC Validation Error Output]:")
            os.execute(cmd_rnv)
            io.stderr:write("Error: tag structure validation (rnv-wrapp) failed for " .. pdf_filename .. "\n")
            validation_failed = true
        end
    end

    -- 2. PDF/UA-2 compliance check
    if run_verapdf then
        local cmd_vera = "verapdf --flavour ua2 --format text " .. quote_arg(pdf_filename)
        local res_vera = os.execute(cmd_vera .. " > " .. null_dev .. " 2>&1")
        if res_vera ~= true and res_vera ~= 0 then
            print("\n[PDF/UA-2 Validation Error Output]:")
            os.execute(cmd_vera)
            io.stderr:write("Error: veraPDF (PDF/UA-2) validation failed for " .. pdf_filename .. "\n")
            validation_failed = true
        end
    end

    if not validation_failed then
        print("PASS")
    else
        print("FAIL")
        had_failure = true
        if config.strict then
            os.exit(1)
        end
    end
end

-- Mode 2: Clean pre-existing *-luamml-mathml.html
local function run_clean_mode(html_filename)
    local basename = html_filename:match("^(.*)%-luamml%-mathml%.html$")
    if not basename then
        io.stderr:write("Error: expected filename ending in -luamml-mathml.html: " .. html_filename .. "\n")
        os.exit(1)
    end

    -- With -p/--path the output goes to that directory, named after the
    -- file only (the input directory is not repeated inside it)
    local output_name
    if config.path then
        output_name = config.path:gsub("[/\\]$", "") .. "/" .. file_name_only(basename) .. "-mathml.html"
    else
        output_name = basename .. "-mathml.html"
    end

    if not check_output_permission(output_name, config.overwrite) then
        os.exit(1)
    end
    if config.path then
        local ok, derr = ensure_dir(config.path)
        if not ok then
            io.stderr:write("Error: could not create directory '" .. config.path .. "': " .. tostring(derr) .. "\n")
            os.exit(1)
        end
    end

    print("==================================================")
    print(" Processing: " .. html_filename)
    print("==================================================")

    local stats, err = strip_ghosts(html_filename, output_name)
    if not stats then
        io.stderr:write("Error: " .. err .. "\n")
        os.exit(1)
    end

    local found_something = report_stats(stats, output_name,
        "If this .html was not generated via spmmli-tool from a .tex source, "
        .. "it might not contain ghosts -- try passing the .tex/.ltx file directly.")
    if not found_something then
        os.remove(output_name)
        return
    end
end

-- Mode 3: Compile .tex/.ltx and clean the generated HTML
local function run_generate_mode(tex_filename)
    local basename, ext = tex_filename:match("^(.*)%.(%a+)$")
    if not basename or (ext ~= "tex" and ext ~= "ltx") then
        io.stderr:write("Error: expected a .tex or .ltx file: " .. tostring(tex_filename) .. "\n")
        os.exit(1)
    end

    -- TeX names its output files after the jobname, which has no directory
    local jobname = file_name_only(basename)

    local output_name
    if config.path then
        output_name = config.path:gsub("[/\\]$", "") .. "/" .. jobname .. "-mathml.html"
    else
        output_name = basename .. "-mathml.html"
    end

    if not check_output_permission(output_name, config.overwrite) then
        os.exit(1)
    end

    if config.path then
        local ok, derr = ensure_dir(config.path)
        if not ok then
            io.stderr:write("Error: could not create directory '" .. config.path .. "': " .. tostring(derr) .. "\n")
            os.exit(1)
        end
    end

    local source = io.open(tex_filename, "r")
    if not source then
        io.stderr:write("Error: could not open " .. tex_filename .. "\n")
        os.exit(1)
    end
    source:close()

    local tmpdir = secure_temp_subdir()

    -- The temp directory is kept when compilation fails, so the .log
    -- mentioned in the error message is still there to be read
    local keep_tmp = false

    -- Guaranteed cleanup closure
    local function cleanup_and_exit(code)
        if not keep_tmp then
            remove_dir_recursive(tmpdir)
        end
        os.exit(code or 0)
    end

    local engine = config.engine
    if not engine then
        engine = has_cmd("lualatex-dev") and "lualatex-dev" or "lualatex"
    end
    if not has_cmd(engine) then
        io.stderr:write("Error: TeX engine '" .. engine .. "' not found in PATH.\n")
        cleanup_and_exit(1)
    end

    local latex_cmd = string.format(
        '%s --interaction=nonstopmode --output-directory=%s %s',
        engine,
        quote_arg(tmpdir),
        quote_arg("\\PassOptionsToPackage{AFghost=true}{spanish-mathml-intent}\\input{" .. tex_filename .. "}")
    )

    print("==================================================")
    print(" Compiling with AFghost=true for " .. tex_filename)
    print("==================================================")

    local null_dev = is_windows and "NUL" or "/dev/null"
    local res = os.execute(latex_cmd .. " > " .. null_dev .. " 2>&1")
    if res ~= true and res ~= 0 then
        keep_tmp = true
        had_failure = true
        io.stderr:write("Error: compilation failed. Log file available in: " .. tmpdir .. "\n")
        if config.strict then
            cleanup_and_exit(1)
        end
    end

    local generated_html = tmpdir .. "/" .. jobname .. "-luamml-mathml.html"
    local check = io.open(generated_html, "r")
    if not check then
        keep_tmp = true
        io.stderr:write("Error: " .. generated_html .. " was not generated. Verify spanish-mathml-intent package loading in " .. tex_filename .. "\n")
        had_failure = true
        cleanup_and_exit(1)
    end
    check:close()

    print("\n==================================================")
    print(" Cleaning ghost characters")
    print("==================================================")

    local stats, err = strip_ghosts(generated_html, output_name)
    if not stats then
        io.stderr:write("Error: " .. err .. "\n")
        cleanup_and_exit(1)
    end
    local found_something = report_stats(stats, output_name,
        "Forced AFghost=true during compilation -- if no ghosts were found, "
        .. "verify that spanish-mathml-intent is loaded in " .. tex_filename .. ".")
    if not found_something then
        os.remove(output_name)
    end

    -- Guaranteed cleanup of temporary directory upon completion
    -- (no os.exit here: other input files may still be pending)
    if not keep_tmp then
        remove_dir_recursive(tmpdir)
    end
end

-- Main Execution Loop
for _, arg_input in ipairs(input_files) do
    local resolved_file = resolve_input_file(arg_input)

    if not resolved_file then
        io.stderr:write("Error: could not find file '" .. arg_input .. "'\n")
        os.exit(1)
    end

    if not is_safe_filename(resolved_file) then
        io.stderr:write("Error: input filename contains unsafe or illegal characters: " .. resolved_file .. "\n")
        os.exit(1)
    end

    if config.check_only and not resolved_file:match("%.pdf$") then
        io.stderr:write("Error: -c/--check-only expects a .pdf file: " .. resolved_file .. "\n")
        os.exit(1)
    end

    if resolved_file:match("%.pdf$") then
        run_pdf_mode(resolved_file)
    elseif resolved_file:match("%.html$") then
        run_clean_mode(resolved_file)
    elseif resolved_file:match("%.tex$") or resolved_file:match("%.ltx$") then
        run_generate_mode(resolved_file)
    else
        io.stderr:write("Error: unrecognized file extension for '" .. resolved_file .. "'\n")
        os.exit(1)
    end
end

if had_failure then
    os.exit(1)
end
% \iffalse
%</spitool>
% \fi
