%%%% Copyright 2026 by Gerhard Schaden
%%%% Standalone reimplementation of the linguex user interface
%%%% (original linguex by Wolfgang Sternefeld; interlinear glossing
%%%%  interface modelled on cgloss4e by Hans-Peter Kolb & Craig Thiersch)
%%%% This program can be redistributed and/or modified under the terms
%%%% of the LaTeX Project Public License
\NeedsTeXFormat{LaTeX2e}[2020/10/01]% expl3 in kernel required
\ProvidesPackage{linguexx}[2026/09/09 Standalone linguistic examples, linguex-compatible interface v. 1.3]
%% v. 0.1: [legacy] option; empty \firstrefdash by default.
%% v. 0.2: tagging-safe under \DocumentMetadata (PDF tagging testphase):
%%         lists opened/closed through the environment interface; no TeX
%%         group around the example as a whole; explicit @endpe/text-unit
%%         cleanup after each example.  Labels set flush left in a
%%         \labelwidth box so the tagged block code does not re-box them
%%         flush right (which had shifted sub-labels right of the text
%%         margin).  Structure trees stay valid and geometry matches the
%%         untagged output, on all three engines, incl. footnote examples.
%%         Default \SubSubExlabelwidth reduced 1.9em->1.6em (sized for
%%         roman labels up to "vi."; equals \SubExlabelwidth), so the
%%         roman level no longer has a visibly larger label-to-text gap.
%% v. 0.3: PDF tagging objective 2 -- examples as proper list structure.
%%         Because examples are real \begin{list} environments, the tagged
%%         output already nests L > LI > Lbl > LBody with sub-levels as
%%         nested Ls; v0.3 additionally marks each example list ORDERED
%%         (/ListNumbering/Ordered) instead of the default label-less
%%         class.  Tag-guarded; no effect without active tagging.
%% v. 0.4: PDF tagging objective 3 -- spoken forms for judgment marks.
%%         Under active tagging each mark is wrapped in a Span carrying
%%         /Alt so a screen reader announces its meaning ("ungrammatical")
%%         rather than the glyph.  Defaults for * ? ?? ?* # % ; override
%%         via \DeclareJudgment[spoken=...] or \SetJudgmentSpoken.
%%         Tag-guarded; printed output and untagged runs are unchanged.
%% v. 0.5: fix an invalid PDF attribute value from v0.3.  Ordered example
%%         lists were routed through the block code's enumerate class,
%%         whose /ListNumbering value is /Ordered -- not one of the values
%%         the PDF spec allows, so validators reject it.  Each level now
%%         gets a valid class of its own: /Decimal for the number level,
%%         /LowerAlpha for letters, /LowerRoman for romans.  If the tagged
%%         list internal is unavailable the lists fall back to the default
%%         (valid) /None rather than the bad value.
%% v. 0.6: PDF tagging objective 4 -- interlinear glosses as structure.
%%         Each gloss column (an object word with its aligned glosses) is
%%         wrapped in a Span, so a screen reader reads and navigates the
%%         gloss word bundle by word bundle in object-then-gloss order
%%         instead of as loose text.  The paragraph mc is paused per
%%         column (\tag_mc_end_push:/..._begin_pop:) and each word gets
%%         its own mc under the column Span.  Tag-guarded; the printed
%%         grid and untagged output are unchanged.
%% v. 0.7: PDF tagging objective 5 -- language of a gloss tier.
%%         \GlossTierLang{tier}{code} records a language code for a tier;
%%         under tagging each word of that tier is wrapped in a Span with
%%         /Lang so a screen reader uses the right phonetics.  Tiers with
%%         no declared language are unchanged.  Tag-guarded.
%% v. 0.8: PDF tagging objective 6 -- Leipzig gloss abbreviations.
%%         \lpzg{sg} sets the abbreviation in small caps and, under
%%         tagging, wraps it in a Span carrying /E (expansion text) so a
%%         screen reader announces "singular" while print and copy keep
%%         SG.  Built-in standard Leipzig table, keyed by short form;
%%         \SetLeipzig{key}{expansion} extends/overrides; unknown keys
%%         print with no expansion.  Tag-guarded; self-contained.
%% v. 0.9: \lpzg accepts a whole compound label in one call (3sg.pst):
%%         split on periods, a leading person digit peeled off, each piece
%%         expanded and joined into one /E ("third person singular past").
%%         \GlossTierLang is now scoped: a document-wide default in the
%%         preamble, overridable per example by issuing it inside the
%%         example (local assignment, reverts afterwards).
%% v. 0.10: (reverted in 0.11) attempt to give \alt/\altg a spoken /Alt.
%% v. 0.11: revert the 0.10 \alt/\altg tagging.  Wrapping the alternatives
%%          formula in a Span carrying /Alt is invalid under PDF/UA-2 when
%%          the formula begins an example (a Span may not contain the
%%          Part/P that the math tagging then builds), and veraPDF rejects
%%          it.  \alt/\altg revert to the plain formula, which validates;
%%          giving them a spoken form needs the "positioning text" math
%%          interface and is deferred.
%% v. 0.12: remove \altg/\lxAltg (alternatives with translations).
%% v. 0.13: \alt rebuilt in text mode -- a tabular stack with a TikZ-drawn
%%          brace, no math and no amsmath, so the alternatives are ordinary
%%          tagged text.  Under tagging the stack is wrapped in a Span with a
%%          spoken /Alt ("A, B, or C", built with \text_purify:n).  Requires
%%          graphicx + tikz instead of amsmath.
%% v. 0.14: \altg/\lxAltg return, rebuilt in text mode.  Written twice in
%%          an interlinear gloss -- object words in the object line,
%%          glosses in the gloss line -- the two calls occupy the two
%%          tiers of one column and assemble a single paradigm: object
%%          stack, gloss stack to its right, braced on both sides and
%%          centred on the object/gloss midline.  Each call carries its
%%          own spoken /Alt under tagging; no math, so the PDF/UA-2
%%          failure that removed the old \altg does not recur.  Also:
%%          \alt now closes its stack with a right brace as well (the
%%          pre-0.13 look), and the TikZ brace direction is corrected --
%%          since 0.13 the brace was drawn mirrored.
%% v. 1.0: \alt/\lxAlt renamed to \altn/\lxAltn -- \alt collides with
%%          beamer, glossaries-extra, revtex/revsymb, tex4ht, and others;
%%          \altn is unclaimed.  \altg/\lxAltg unaffected (no collisions).
%% v. 1.1: \lpzglist -- the list of abbreviations the document actually
%%          uses, with full forms, sourced from every \lpzg/\lpzgadd call
%%          and customisable per list or document-wide; a real tagged
%%          list under tagging.  Phantom bracket alignment for interlinear
%%          glosses (opt-in via [phantomalign] or \GlossPhantomAlign):
%%          pads a gloss word by a \phantom the width of the object word's
%%          leading brackets/judgment marks, so real glyphs line up;
%%          \GlossPhantom{...} is the manual override.  Fix: the
%%          sub-example letters and the kernel accents \b, \c, \d now
%%          coexist everywhere, including inside one example: each
%%          letter dispatches on what follows, a period giving the
%%          sub-example command and anything else the accent, so
%%          "\b. \c Ca c'est chiant." works.  The hooks are \protected
%%          (hyperref \edef-expands titles); only \a is held globally,
%%          \b-\f just where a sub-level is reachable.  Previously all
%%          six were redefined document-wide under [lazy], the default,
%%          so \c{c} and "ç" errored and a hyperref title silently lost
%%          the accent.  Fix: \end{exe} closes a sub-level opened by an
%%          \a. inside the batch instead of leaking its \begingroup.
%%          \glt gains \GlossTransStyle (a declaration styling the free
%%          translation) and \GlossTransLang (its language, emitted as a
%%          Span with /Lang under tagging -- babel's \foreignlanguage
%%          reaches no structure element on TL2026).  Both opt-in; the
%%          default output and tag tree are unchanged.  Fix: \sublabel
%%          records the label of the LEVEL it is used at; it always
%%          recorded the letter counter, so a \refrange over roman
%%          sub-sub-examples closed with the enclosing letter
%%          ("(1b-i--b)") instead of the numeral.  Fix: an \altg in a gloss
%%          column with no partner is an error instead of silently
%%          taking the wrong shape and overlapping its neighbours.  \lpzgcheck{...}: an abbreviation used
%%          with no known expansion is reported at the end of any
%%          document (on by default), which a mistyped \lpzg key used
%%          to survive in silence unless a \lpzglist happened to catch
%%          it; unused=true also reports a \SetLeipzig never used.
%% v. 1.2: \altn's spoken /Alt expands a Leipzig abbreviation, as
%%          \altg's already did: \altn{a \lpzg{pl} of cats}{a dog} is
%%          announced as "a plural of cats or a dog" rather than "a pl
%%          of cats or a dog".  Only the spoken form changes -- the
%%          stack still prints the small-cap abbreviation, which still
%%          carries its own /E inside the stack (unlike in an \altg
%%          stack, which sets \lpzg plain).  Simple keys only, as in
%%          \altg: a compound or unknown key is spoken as printed.
%%          Fix: \z. is usable inside an exe batch.  Mixing the
%%          syntaxes is documented, so an \a. inside exe opens a
%%          sub-level -- but \z., the only thing that could close it,
%%          raised "\z. outside an example", being gated on a flag
%%          only the dot syntax sets.  It is now gated on the open
%%          sub-level itself; only the branch that ENDS the example
%%          stays dot-syntax-only, and a \z. at the main level of a
%%          batch is a package error naming \end{exe}.
%%          Fix: hyperref anchors for footnote sub-examples.
%%          \theHSubExNo/\theHSubSubExNo built their name from ExNo
%%          unconditionally, unlike the printed \theSubExNo, so a
%%          sub-example "a" in a footnote and one under main example 1
%%          both claimed "lxex.1.a"; hyperref keeps the first
%%          destination and drops the rest, so \ref to the footnote
%%          one linked to the main-text one.  Both now branch on
%%          \if@noftnote and anchor footnote sub-examples on the
%%          footnote series (lxfnex.<FnExNo>....).
%%          Fix: a stray \a. in prose, with no example of either kind
%%          open, is a package error naming itself instead of opening
%%          a list and a \begingroup that nothing closes -- which
%%          surfaced as "\begin{list} ended by \end{document}"
%%          arbitrarily far away.  \a. inside an exe batch reaches the
%%          same path legitimately and stays legal.
%%          Fix: a trailing or doubled period in a \lpzg label no
%%          longer records an EMPTY abbreviation.  \lpzg{sg.} splits
%%          into sg and an empty piece, and the empty piece was
%%          recorded like any other key, so \lpzgcheck reported "No
%%          expansion known for" nothing at all and the /E carried a
%%          trailing space.  Blank segments are skipped; the real
%%          pieces beside them are recorded exactly as before.
%%          Fix: a modifier INSIDE a \lpzg label adds to the small
%%          caps instead of replacing them.  Latin Modern has no bold
%%          small caps in any encoding, so NFSS kept the series and
%%          dropped the shape and \lpzg{\textbf{m}.pl} came out as a
%%          bold lowercase m.  The choice is now made per leaf, by
%%          asking the font that was selected (\lx@sc@real:); where
%%          the shape is real nothing changes, where it is missing the
%%          caps are made at \LpzgCapsScale of the size, with an
%%          /ActualText under tagging.  The label is also purified
%%          before it is parsed, so the markup no longer reaches the
%%          Leipzig table as a key.
%%          \lpzg may stand in a section title: it is declared to
%%          hyperref as its own argument, so the bookmark carries the
%%          label instead of the "Token not allowed in a PDF string"
%%          that an unknown command earns on every run.  \altn,
%%          \altg and the relative references are deliberately not
%%          declared -- neither has a one-line reading.
%%          Fix: \exg.[label] takes the custom label \ex.[label]
%%          takes.  \exg. expands to \ex. plus the gloss head, so the
%%          bracket was no longer the first thing \lx@exstart peeked
%%          at: it came out as the first word of the object line and
%%          the example took a number of its own.  Written against the
%%          command, deliberately: after \exg. a SPACED bracket is the
%%          object line's first word ("[" is one of the openers
%%          \GlossPhantomChars lists), which a space-skipping
%%          \@ifnextchar could not tell from a label.
%%          Fix: the brace's tip, where the two halves met in a pinch
%%          with a wedge hanging off it -- a cusp has two tangents and
%%          the outline was offset along one.  The point is the
%%          intersection of the outer edges, the notch that of the
%%          inner ones, clamped to one and two pen-halves so the tip
%%          is blunt like Computer Modern's rather than a needle.
%%          \AltBraceOuterSep 0.7em -> 0.35em and \AltJdgTuck
%%          0.45em -> 0.2em, both tuned to the hairline the brace
%%          used to be: the outer gap held the stack at arm's length
%%          from its words, and the tuck put a hanging judgment 0.24pt
%%          from the brace, which is to say on it.  0.2em also keeps
%%          the clearance steady across sizes, which 0.45em cannot now
%%          that the pen grows with the font and the amplitude does
%%          not.
%%          The brace of \altn and \altg is drawn as a filled
%%          OUTLINE with a typographic weight, instead of tikz's
%%          brace decoration stroked at a hairline: Computer Modern's
%%          own brace stem measures 1.20pt at 11pt, which is the
%%          0.11em default of the new \AltBracePen, and the outline
%%          tapers at the terminals and the tip as CM's does.  Same
%%          skeleton, cached by height; decorations.pathreplacing is
%%          no longer loaded.
%%          Fix: an abbreviation key that is not ASCII is typeset as
%%          the character it is.  A key is stored as a string, and
%%          under pdflatex a string is BYTES, so \lpzgadd{abß}
%%          printed the two UTF-8 bytes of ß in the T1 slots they
%%          happen to name.  The bytes are re-tokenised where the key
%%          is SET (and measured), nowhere else: identity, sorting and
%%          the .aux keep the string.  All spellings of a key are also
%%          normalised the same way now -- \SetLeipzig{f\'em} and
%%          \lpzg{fém} are one key, in either order.
%%          Fix: punctuation glued to the object call of an \altg is
%%          set after the paradigm's closing brace, and level with
%%          the middle of it, instead of inside the braces between
%%          the object and gloss columns.  The object call takes it
%%          along; the gloss call, which draws the closing brace,
%%          sets it down.
%%          \exannot[<spoken>]{<text>}: a structural label set in a
%%          COLUMN beside the examples rather than at the margin, where
%%          \exsource puts a source.  \ExAnnotColumn says where the
%%          column is, measured from the left edge of the text block, so
%%          it does not move with the nesting level; \ExAnnotSep is the
%%          least gap and is rigid, so an example that would come within
%%          it takes its annotation onto the next line rather than
%%          crowding it.  The annotation is carried in a box of fixed
%%          width ending at the right margin, which is what holds the
%%          column: \jambox ([langsci]) sets its box at its natural width
%%          behind glue that shrinks to nothing, so it holds a column only
%%          while no example can reach it (measured: four sub-examples of
%%          increasing length aligned to 0.01pt at a 3cm and a 7cm gutter,
%%          212.6/212.6/234.1/288.9pt at 11cm).  \jambox is unchanged.
%%          In a gloss the annotation goes at the end of the OBJECT line
%%          -- with a space in front of it or glued to the last word,
%%          either reads -- and is set level with it, at the same column;
%%          on any other tier, or in the middle of a line, it is an error.
%%          The glued spelling is what people write, and it used to stop
%%          the run with an error about where annotations go: without a
%%          space "libro\exannot{...}" is one word of the object tier and
%%          a rule reading only its head token never sees the annotation.
%%          Reported from a beamer deck.  Under tagging a spoken form --
%%          \SetAnnotSpoken{<text>}{<phrase>}, or the optional argument --
%%          reaches a Span as /Alt, so a reader hears "complementizer
%%          phrase" while the page still shows [CP] and copy-and-paste
%%          still yields it; with no spoken form there is no Span.
%%          \ExAnnotFit measures that column instead of taking it: each
%%          annotated example records where its text ended, through the
%%          .aux, and the column of ONE EXAMPLE and everything under it
%%          is put \ExAnnotSep past the longest of them.  The widest
%%          annotation of the block counts too, so a label too wide for
%%          the column moves the whole block's column rather than
%%          stepping left on its own -- which is what it did, and one
%%          label out of line is the failure \exannot exists to prevent.
%%          Off by default: nothing is recorded and no round trip
%%          happens unless it is asked for.  \pdfsavepos on pdftex and
%%          xetex, \savepos on luatex; no new package.  What crosses the
%%          .aux is the DIFFERENCE of two positions taken on one line, so
%%          it is a width and carries no page origin -- comparable across
%%          a page break, a twoside document and a two-column one.
%%          \GlossTransSide sets \glt's free translation in a column
%%          BESIDE the interlinear grid instead of under it, which is
%%          where the vertical space on a slide comes from;
%%          \GlossTransBelow restores the default.  A declaration and
%%          not something on \glt, because the decision has to be made
%%          while the grid is still being built: by the time \glt is
%%          reached the grid is a finished paragraph contributed to the
%%          enclosing list, and there is nothing left to set beside.
%%          \glt is unchanged and still takes no argument.
%%          \GlossTransRatio (.6) divides the measure and
%%          \GlossTransSep (2em) separates the columns -- both expex's
%%          own values, so that one example set with either package
%%          comes out in the same proportions.  And
%%          \GlossTransMinWidth (6em) is the width below which the side
%%          position is abandoned for the below one, with a warning.
%%          Top-level examples only, and no \exannot on the same gloss:
%%          both package errors.  Below the top level the measure is
%%          already reduced twice and a split taken from it puts every
%%          sibling's translation somewhere else; \exannot's column
%%          comes from \columnwidth, which says nothing once the grid
%%          is half of it.  doc/EXPEX-GAPS.md records both, and what
%%          would reopen the first.
%%          \SetAltSpoken{word} sets the connector a screen reader hears
%%          between stacked alternatives -- "aa or bb" was English in
%%          every document -- for \altn and \altg alike.  The optional
%%          argument sets the punctuation and the starred form keeps it
%%          before the connector, so the default is expressible as
%%          \SetAltSpoken*{or}[,]; both arguments are trimmed, so
%%          {ou} and { ou } are one setting.  LOCAL, unlike \SetLeipzig:
%%          a French example can be fenced in a group.
%%          A \label or \sublabel at the head of an example no longer
%%          stops the judgment scan: "\ex. \label{x}*?Foo" hung
%%          nothing and indented Foo by the width of the marks, so the
%%          examples that broke the alignment were exactly the
%%          cross-referenced ones.  The label is skipped past and
%%          replayed after the \item, where writing it in the body put
%%          it, so \ref, the anchor and \prefrange are unchanged.
%% Package options come in two independent groups.
%%
%% SYNTAX:
%%   [lazy]  (the default): the traditional linguex dot syntax --
%%           \ex., \a.-\f., \z., \exg. -- and nothing else.
%%   [gb4e]: only the gb4e environment syntax -- exe, xlist, \ex
%%           (without period, with optional bracketed judgment).  In
%%           this mode the letter commands \b., \c., \d. are never
%%           defined, so the kernel accent commands \b, \c, \d remain
%%           untouched and no hyperref workaround is needed.
%%   [langsci]: the gb4e syntax PLUS the \ea ... \z front-end of
%%           langsci-gb4e (Language Science Press's fork), where an
%%           example and its sub-levels are opened by \ea and closed by
%%           \z, with the depth read off the nesting rather than spelt
%%           out.  Implies [gb4e].  Its purpose is migration: combined
%%           with [lazy] a document moves from the dot syntax to \ea one
%%           example at a time, both syntaxes driving one engine and one
%%           counter.  Within a SINGLE example the two may not be mixed
%%           (see \iflx@inlangsci); between examples they may.
%%           Incompatible with [legacy], which is a package error rather
%%           than a silent choice of geometry.
%% Both may be requested together, [lazy,gb4e], for documents that
%% deliberately mix the syntaxes (this package's own manual does).
%%
%% DEFAULTS (lengths, dashes, sub-label delimiters):
%%   none (the default): this package's own defaults -- see
%%           \lx@defaults@lazy below.
%%   [legacy]: linguex's defaults, to the value -- see
%%           \lx@defaults@legacy below.  Nothing else changes: the
%%           engine, the error behaviour and the extensions are the
%%           same in both modes.  [legacy] is orthogonal to the syntax
%%           options, so [legacy,gb4e] is meaningful (gb4e syntax,
%%           linguex geometry), and [legacy] alone implies the dot
%%           syntax, exactly as [lazy] alone does.
%%
%% RELATIVE REFERENCES:
%%   [relreflinks] (the default): with hyperref loaded, \Next and \Last
%%           and their family are clickable, like the \ref they are an
%%           abbreviation of.  Without hyperref the option does nothing
%%           and costs nothing: no anchor is looked up and no line is
%%           written to the .aux.
%%   [norelreflinks]: print the number and never the link, as linguex
%%           did.  For a document that wants its own \hyperref wrapper
%%           around these, or none at all.
\newif\iflx@lazy
\newif\iflx@gbfour
\newif\iflx@langsci
% Set by \ExRaggedRight and read by \lx@ea@rag, both of which live in the
% \ea section under \iflx@langsci.  The \newif has to be OUT here, though.
% With the option off, TeX skips that section by scanning for the matching
% \fi, counting conditionals as it goes -- and it counts them by MEANING.
% A \newif inside the skipped text never runs, so \iflx@earagged is still
% undefined when the skipper meets it, is therefore not counted as a
% conditional, and the \fi that closes it closes the SECTION instead:
% everything below that line is then executed with the option off.  It
% cost a green langsci suite and a red everything-else to find, because
% the cases that exercise the section are the ones where it is not
% skipped.
\newif\iflx@earagged
% langsci-gb4e's own package options, and the switch its \singlegloss pair
% sets.  Declared HERE, with the rest of the flags, and not in the \ea
% section that reads them, for the reason given just above.
\newif\iflx@nojambox
\newif\iflx@manualexewidth
\newif\iflx@lowerpenalty
\newif\iflx@nocgloss
\newif\iflx@singlegloss \lx@singleglosstrue
\newif\iflx@legacy
\newif\iflx@phantomalign
\newif\iflx@relreflinks \lx@relreflinkstrue
\DeclareOption{lazy}{\lx@lazytrue}
\DeclareOption{gb4e}{\lx@gbfourtrue}
% [langsci] is a SUPERSET of the gb4e syntax -- \ea expands to what exe
% and xlist write -- so it turns [gb4e] on rather than competing with it.
% That also makes the "no syntax option means [lazy]" rule below do the
% right thing for [langsci] alone.
\DeclareOption{langsci}{\lx@langscitrue\lx@gbfourtrue}
% langsci-gb4e's four options, under their own names so that a preamble
% being ported does not have to be edited.  They are read only under
% [langsci]; given without it they are accepted and do nothing, which is
% what they would do in a document that has no \ea in it either.
\DeclareOption{nojambox}{\lx@nojamboxtrue}
\DeclareOption{manualexewidth}{\lx@manualexewidthtrue}
\DeclareOption{lowerpenalty}{\lx@lowerpenaltytrue}
\DeclareOption{nocgloss}{\lx@nocglosstrue}
\DeclareOption{legacy}{\lx@legacytrue}
\DeclareOption{phantomalign}{\lx@phantomaligntrue}
\DeclareOption{relreflinks}{\lx@relreflinkstrue}
\DeclareOption{norelreflinks}{\lx@relreflinksfalse}
\DeclareOption*{\PackageWarning{linguexx}{Unknown option
  '\CurrentOption' ignored}}
\ProcessOptions\relax
\iflx@gbfour\else\lx@lazytrue\fi
% [legacy,langsci] is refused rather than approximated.  [legacy] is
% linguex's geometry to the value, and langsci-gb4e is the house style of a
% different publisher with its own; a document asking for both has asked
% for two answers to every length in the package, and picking one silently
% is how a "compatibility" mode comes to be neither.  Nothing else in the
% option matrix is affected: [legacy,gb4e] stays meaningful.
\iflx@langsci\iflx@legacy
  \PackageError{linguexx}{[legacy] and [langsci] cannot be combined}%
    {[legacy] reproduces linguex's geometry; [langsci] follows
     langsci-gb4e's.\MessageBreak
     Choose one: drop [legacy] for the langsci front-end, or drop
     [langsci] and use [gb4e] for the environment syntax under linguex
     geometry.}%
\fi\fi

%% graphicx is NOT required here, and the line that required it is gone.
%% It was annotated "\scalebox etc." and the package calls \scalebox
%% nowhere -- nor \resizebox, \rotatebox, \reflectbox or
%% \includegraphics, in this version or in any committed one: the first
%% commit already carries the requirement and already has no caller.  (The
%% v0.13 note in the history above says "graphicx + tikz instead of
%% amsmath"; the tikz half is true and load-bearing, the graphicx half was
%% not true of anything in the repository.)  It survived because tikz
%% loads graphicx itself, so nothing a document could do would show the
%% difference -- the only way to see it was to look for the callers.
%% Should the TikZ brace ever be replaced, whatever replaces it declares
%% its own dependencies; that is the point of not declaring them here.
%%
%% TIKZ ITSELF STAYS, and this is the note saying not to try again.  It
%% was tried: tikz is loaded for one macro, the drawn brace, and on a
%% minimal document under pdflatex it was 0.13s of the 0.15s this package
%% cost and 11.5k of its 12.5k control sequences, which is a great deal to
%% pay for one macro.  The whole brace was reimplemented in pict2e, which
%% is a twentieth of the load, and it worked: the same curve to within
%% 0.12pt, the full suite green on all three engines, veraPDF green, and
%% even the latex+dvips route intact.
%%
%% It was still the wrong trade, for two reasons that only showed up
%% afterwards.  The first is what the brace should look like: a stroked
%% path has one width everywhere and a typographic brace does not, so the
%% brace is drawn as a filled outline now -- and pict2e cannot fill a
%% curved path at all (\polygon* takes straight edges), which leaves
%% flattening the outline into some 130 sampled points at 0.6s a brace.
%% pgf fills Beziers, so the same shape is fourteen segments and no
%% sampling.  The second is who pays: a linguist's document tends to load
%% pgf anyway, through beamer or forest or a tree package, and then the
%% saving is zero while the brace is worse.
%%
%% So the question is settled, not deferred.  Reopen it only if the brace
%% stops needing a filled curved path -- and then read
%% \__lxp_brace:nnn first, since that is the thing being traded away.
\RequirePackage{tikz}%              drawn brace for \lxAltn (no math mode)
\RequirePackage{xspace}%            space after \Last & co. (see below)
%% No decorations.pathreplacing: the brace was its "brace" decoration
%% until v1.2 and is now drawn as an outline of its own (see
%% \__lxp_brace:nnn), which needs nothing but a filled path.
% The only other dependency is the expl3 programming layer, part of the
% LaTeX kernel since 2020.  In particular, ulem is NOT loaded: if you
% want struck-through alternatives (\sout inside \altn), load ulem
% yourself, with whatever options you prefer.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Packages this one cannot share a document with                     %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% linguexx REPLACES these; it does not accompany them.  Each defines the
%% same user commands out of the same tradition -- \ex and the dot syntax
%% (linguex, expex), the exe/xlist environments (gb4e), \ea (langsci-gb4e)
%% -- and all of them, this one included, define \ex with \def.  So the
%% file read second wins, silently, and the document gets a working
%% mixture: the examples go on numbering and what breaks is a detail in
%% the middle of it.  A document being ported is exactly where this
%% happens, because the old \usepackage line is the last thing anyone
%% thinks to delete.
%%
%% Checked twice, because both orders occur and neither is more likely:
%% at load time for a package already read, and again at \begin{document}
%% for one read after this.
%%
%% cgloss4e is deliberately NOT in the list.  It defines \gll and \glt and
%% would collide the same way, but whether a document may keep a foreign
%% glossing package is the question doc/DEFERRED-DECISIONS.md leaves open
%% under [nocgloss] -- and an error here would answer it by accident,
%% which is what that file exists to prevent.
\def\lx@clash@one#1{%
  \@ifpackageloaded{#1}%
    {\PackageError{linguexx}{The package #1 is loaded as well as linguexx}%
      {linguexx reimplements #1's interface, so both define the same
       commands and\MessageBreak
       whichever is read second silently wins.\MessageBreak
       Load one of them, not both: linguexx's [gb4e] and [langsci] options
       provide\MessageBreak the other front-ends on this package's
       engine.}}%
    {}}
\def\lx@clashcheck{%
  \lx@clash@one{linguex}%
  \lx@clash@one{expex}%
  \lx@clash@one{gb4e}%
  \lx@clash@one{langsci-gb4e}}
\lx@clashcheck
\AtBeginDocument{\lx@clashcheck}
%% ... and one more for the package that is not one.  expex is plain TeX
%% and can be read with \input, which leaves nothing for
%% \@ifpackageloaded to find; what it does leave is a different \ex.  So
%% the definition this package installed is remembered at the end of this
%% file (\lx@ex@installed) and compared here.  A warning and not an error:
%% redefining \ex is a document's own business, and only the document
%% knows whether it meant to.
\AtBeginDocument{%
  \ifx\ex\lx@ex@installed\else
    \PackageWarning{linguexx}{%
      \string\ex\space is no longer linguexx's: something has redefined
      it since\MessageBreak the package was loaded (another example
      package, perhaps read\MessageBreak with \string\input).  The dot
      syntax will now do whatever that\MessageBreak definition does}%
  \fi}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Counters and number formatting (linguex-compatible names)          %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\newcounter{ExNo}
\newcounter{SubExNo}
\newcounter{SubSubExNo}
\newcounter{FnExNo}

% linguex resets ExNo at every \chapter in a class that has chapters.
% That is a numbering CONVENTION, not a bug, so [legacy] reproduces it;
% the default is continuous numbering through the document.
\iflx@legacy
  \@ifundefined{c@chapter}{}{\@addtoreset{ExNo}{chapter}}
\fi

% back-compat aliases (linguex spelt these with a prefix)
\let\Exarabic\arabic
\let\Exalph\alph
\let\Exroman\roman

% Reference dashes and sub-label delimiters.  Only the NAMES are reserved
% here; the values are set by the defaults block in the Layout section
% (\lx@defaults@lazy / \lx@defaults@legacy), so that both modes go through
% one place.  \firstrefdash separates number from letter in a reference,
% \secondrefdash letter from roman numeral: (12a-i) by default, (12-a-i)
% under [legacy].  The delimiters wrap the PRINTED sub-example labels:
% "a." and "i." by default, "a." and "(i)" under [legacy].
\newcommand\firstrefdash{}
\newcommand\secondrefdash{-}
\newcommand\SubExLBr{}
\newcommand\SubExRBr{.}
\newcommand\SubSubExLBr{}
\newcommand\SubSubExRBr{.}

% parenthesis suppression switch (v1 machinery, kept verbatim in spirit)
\newif\ifparens\parensfalse
% The delimiters around a top-level example number, in two layers.
%
% linguex has \ExLBr / \ExRBr (and the footnote pair) since its version
% 4.0 and documents them: a document that wants [1] rather than (1)
% writes \renewcommand{\ExLBr}{[}.  This package printed its parentheses
% from \theExLBr alone, so that line was accepted and did nothing -- the
% one kind of incompatibility a drop-in replacement cannot have, because
% the document still compiles and only the page is wrong.
%
% So the characters live in the linguex names, and the \the... layer --
% which is what \theExNo expands and what a \newlabel record carries --
% reads them through the suppression switch.  Both remain redefinable and
% mean different things: \ExLBr is the character, \theExLBr the
% character plus the switch.
\newcommand\ExLBr{(}
\newcommand\ExRBr{)}
\newcommand\FnExLBr{(}
\newcommand\FnExRBr{)}
\newcommand\theExLBr{\ifparens\else\ExLBr\fi}
\newcommand\theExRBr{\ifparens\else\ExRBr\fi}
\newcommand\theFnExLBr{\ifparens\else\FnExLBr\fi}
\newcommand\theFnExRBr{\ifparens\else\FnExRBr\fi}

% "am I in a footnote?" -- linguex's switch name kept for compatibility;
% TRUE means NOT in a footnote (sic, as in linguex)
\newif\if@noftnote\@noftnotetrue
%% A footnote is a NEW stream, and the example it interrupts is not open
%% inside it.  \@noftnotefalse says so for the numbering; the three
%% "an example is open" flags have to say so for the syntax, or a footnote
%% hung on an example cannot hold an example of its own written the other
%% way -- \ea in a footnote of an \ex. example would be read as \ea inside
%% that example and refused by the one-syntax-per-example rule.
%%
%% All three are local to the group the footnote text is typeset in, so
%% nothing leaks back to the interrupted example.  Every real opener
%% (\ex., \begin{exe}, \ea) resets \lx@subdepth for itself, so it is left
%% alone here: the only construct that would read it is a stray \a. in a
%% footnote, which is not an example in either syntax and is no better
%% defined for being given a depth of zero.
\AtBeginDocument{%
  \let\lx@orig@footnotetext\@footnotetext
  \long\def\@footnotetext#1{\lx@orig@footnotetext
    {\@noftnotefalse
     \lx@inexamplefalse \lx@inexefalse \lx@inlangscifalse
     #1}}}

%% What \ref prints, in two flavours, and where the parentheses live.
%%
%% linguex puts them in the counter format: \theExNo IS "(1)", so \ref
%% gives "(1)" and the label prints the same thing.  langsci-gb4e (and gb4e
%% under it) puts them in the LIST LABEL instead and leaves the counter
%% bare, so \ref there gives "1" and an author writes "(\ref{ex:x})" or
%% \xref -- which is why upstream's \xref is "(\ref{#1})" while this
%% package's, back when \ref always parenthesised, was a plain \ref.
%% Neither convention is more correct; but a document ported from one to
%% the other must not silently print "((1))".
%%
%% So the flavour follows the syntax the document is written in: [langsci]
%% selects the bare one, everything else the parenthesised one.  Each is
%% installed by one macro, and \ExParenRefs / \ExBareRefs let a document
%% ask for the other by name -- a [langsci] document whose prose already
%% says "as in \ref{ex:x}" wants \ExParenRefs and one line, not a pass over
%% its cross-references.
%%
%% What must NOT depend on the flavour is anything this package prints
%% itself.  The example's own number, \Next and its family, \refrange,
%% \Refrange, \xref, \xxref and \exp all spell the parentheses out --
%% through \theExLBr / \theExRBr, around the bare number \pref gives -- so
%% they print the same characters either way; only a plain \ref, and the
%% \cref that follows it, change.  \lx@labelExNo is the seam for the main
%% label: it is the printed number, parenthesised in both flavours, and it
%% is \theExNo in only one of them.
\def\lx@refs@paren{%
  \renewcommand{\theExNo}{\protect\theExLBr\arabic{ExNo}\protect\theExRBr}%
  \renewcommand{\theFnExNo}{\protect\theFnExLBr\roman{FnExNo}\protect\theFnExRBr}%
  \renewcommand{\theSubExNo}{%
    \hbox{\if@noftnote\protect\theExLBr\Exarabic{ExNo}\firstrefdash
        \Exalph{SubExNo}\protect\theExRBr
      \else
        \protect\theFnExLBr\Exroman{FnExNo}\firstrefdash%
        \Exalph{SubExNo}\protect\theFnExRBr
      \fi}}%
  \renewcommand{\theSubSubExNo}{%
    \hbox{\if@noftnote\protect\theExLBr%
            \Exarabic{ExNo}\firstrefdash\Exalph{SubExNo}\secondrefdash
               \Exroman{SubSubExNo}\protect\theExRBr%
      \else\protect\theFnExLBr\Exroman{FnExNo}\firstrefdash
                \Exalph{SubExNo}\secondrefdash\Exroman{SubSubExNo}\protect\theFnExRBr\fi}}%
  \def\lx@labelExNo{\theExNo}%
  \def\lx@labelFnExNo{\theFnExNo}}
\def\lx@refs@bare{%
  \renewcommand{\theExNo}{\arabic{ExNo}}%
  \renewcommand{\theFnExNo}{\roman{FnExNo}}%
  \renewcommand{\theSubExNo}{%
    \hbox{\if@noftnote\Exarabic{ExNo}\firstrefdash\Exalph{SubExNo}%
      \else\Exroman{FnExNo}\firstrefdash\Exalph{SubExNo}\fi}}%
  \renewcommand{\theSubSubExNo}{%
    \hbox{\if@noftnote
            \Exarabic{ExNo}\firstrefdash\Exalph{SubExNo}\secondrefdash
              \Exroman{SubSubExNo}%
      \else \Exroman{FnExNo}\firstrefdash\Exalph{SubExNo}\secondrefdash
              \Exroman{SubSubExNo}\fi}}%
  \def\lx@labelExNo{\protect\theExLBr\theExNo\protect\theExRBr}%
  \def\lx@labelFnExNo{\protect\theFnExLBr\theFnExNo\protect\theFnExRBr}}
\newcommand\ExParenRefs{\lx@refs@paren}
\newcommand\ExBareRefs{\lx@refs@bare}
\iflx@langsci \lx@refs@bare \else \lx@refs@paren \fi

% hyperref anchor names (avoid the \protect-laden \the... expansions).
%
% The sub-levels must branch on \if@noftnote exactly as the PRINTED
% labels (\theSubExNo, \theSubSubExNo) do.  Building them from ExNo
% unconditionally made a footnote sub-example and a main-text one collide
% whenever the footnote sat under the same ExNo: both claimed
% "lxex.<n>.a", hyperref kept the first destination and dropped the
% second, and a \ref to the footnote sub-example jumped to the main-text
% one.  The number printed was right either way, so only the anchor gave
% it away.  \if@noftnote is a plain \newif, hence fully expandable and
% safe in the \edef that hyperref runs over \theH... .
\def\theHExNo{lxex.\arabic{ExNo}}
\def\lx@Hexstem{\if@noftnote lxex.\arabic{ExNo}\else lxfnex.\arabic{FnExNo}\fi}
\def\theHSubExNo{\lx@Hexstem.\alph{SubExNo}}
\def\theHSubSubExNo{\lx@Hexstem.\alph{SubExNo}.\arabic{SubSubExNo}}
\def\theHFnExNo{lxfnex.\arabic{FnExNo}}

% The same anchors as \hyperlink TARGETS.  hyperref prefixes the name it
% builds from \theH<counter> with the counter's own name, so the
% destination of main example 5 is "ExNo.lxex.5" and that of footnote
% example 5 is "FnExNo.lxfnex.5".  These two spell that name for a
% NUMBER rather than for the counter's current value, which is what a
% relative reference needs and \theHExNo cannot give: \Next has to name
% an example the document has not reached.  They live here, beside the
% \theH... definitions they mirror, so that the two spellings of one
% anchor are edited together; nothing silently depends on their
% agreeing, though -- see \lx_relref_record: for why a disagreement
% costs a link and never a wrong jump.
\newcommand\lx@Hexname[1]{ExNo.lxex.#1}
\newcommand\lx@Hfnexname[1]{FnExNo.lxfnex.#1}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Layout parameters                                                  %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% The geometry is the same in both modes -- each level reserves a label
%% box, the text margin of a level is the label box plus a gap, measured
%% from the margin of the level above -- but the two modes PARAMETRIZE it
%% differently, because linguex did:
%%
%%   default: the sub-levels are given by their LABEL WIDTHS
%%            (\SubExlabelwidth, \SubSubExlabelwidth), each followed by
%%            the shared gap \Exlabelsep;
%%   legacy:  the sub-levels are given by their TEXT MARGINS
%%            (\SubExleftmargin, \SubSubExleftmargin), the label being set
%%            flush left inside that margin with no gap, and the main
%%            \Exlabelwidth is recomputed for every example from the width
%%            of the number itself.
%%
%% All the lengths of both sets exist in both modes; only the ones its own
%% mode reads have any effect.  The equivalence is
%%   \SubExleftmargin = \SubExlabelwidth + \Exlabelsep.

\newlength{\Extopsep}
\newlength{\Exredux}
\newlength{\Exindent}
\newlength{\Exlabelwidth}
\newlength{\Exlabelsep}
% The sub-example label boxes are wider than the letters strictly need.
% The surplus is the room a hanging judgment falls into: the clear space
% left of the text is (labelwidth - width of the printed letter) +
% \Exlabelsep.  The defaults are chosen so that TWO marks (e.g. "??",
% "?*") fit without touching the letter; for three or more as a routine
% matter, widen these.
%
% "Widen these" means the AUTHOR widens them.  Three marks (\a. ??\#)
% overrun the box and the marks cross the letter -- in both default sets
% and on all three engines -- and that is not to be fixed here: these
% numbers are not the package's to raise on behalf of a document it
% cannot see.  See doc/DEFERRED-DECISIONS.md, "Three judgment marks
% running into the sub-example letter", for the measurement and the
% ruling.
\newlength{\SubExlabelwidth}
\newlength{\SubSubExlabelwidth}
% linguex's names for the same two levels (see above)
\newlength{\SubExleftmargin}
\newlength{\SubSubExleftmargin}

% \Extopsep and \Exredux are the only lengths that depend on the font:
% they are set here (so that a \setlength in the PREAMBLE overrides them,
% which it cannot do in linguex) and re-derived \AtBeginDocument if -- and
% only if -- they still hold the value computed here, which catches the
% case of a font package that changes \baselineskip after we are loaded.
\newlength{\lx@auto@topsep}
\newlength{\lx@auto@redux}
\newcommand\lx@setskips{%
  \setlength{\Extopsep}{.66\baselineskip}%
  \setlength{\Exredux}{\lx@reduxfactor\baselineskip}%
  \setlength{\lx@auto@topsep}{\Extopsep}%
  \setlength{\lx@auto@redux}{\Exredux}}
\AtBeginDocument{%
  \ifdim\Extopsep=\lx@auto@topsep
    \ifdim\Exredux=\lx@auto@redux \lx@setskips \fi
  \fi}

%% ---- the two sets of defaults -------------------------------------------

\newcommand\lx@defaults@lazy{%
  \def\lx@reduxfactor{-.66}%
  \lx@setskips
  \setlength{\Exindent}{0pt}%
  \setlength{\Exlabelwidth}{2.6em}%
  \setlength{\Exlabelsep}{.6em}%
  \setlength{\SubExlabelwidth}{1.6em}%
  \setlength{\SubSubExlabelwidth}{1.6em}%
  \setlength{\SubExleftmargin}{\dimexpr\SubExlabelwidth+\Exlabelsep\relax}%
  \setlength{\SubSubExleftmargin}{\dimexpr\SubSubExlabelwidth+\Exlabelsep\relax}%
  \setlength{\JdgSep}{0.15em}%
  \lx@setannotcol \setlength{\ExAnnotSep}{1em}%
  \setlength{\GlossTransSep}{2em}%
  \setlength{\GlossTransMinWidth}{6em}%
  \setlength{\GlossTransRightSkip}{0pt plus 2em}%
  \def\firstrefdash{}%
  \def\secondrefdash{-}%
  \def\SubExLBr{}\def\SubExRBr{.}%
  \def\SubSubExLBr{}\def\SubSubExRBr{.}%
  \def\GlossSep{.5em plus .3em minus .1em}}

%% linguex's defaults, to the value: \Exlabelsep 1.3em, \Exindent 0pt,
%% \SubExleftmargin 2em, \SubSubExleftmargin 2.4em, \Extopsep
%% .66\baselineskip, \Exredux -\baselineskip, and a main label box whose
%% width is that of the current number, padded to the next digit (so "(1)"
%% sits in a two-digit box).  Judgments are flush against the text
%% (\JdgSep 0pt), sub-sub-examples print as "(i)", and references are
%% (12-a), (12-a-i).  \GlossSep approximates cgloss4e's word spacing (an
%% interword space plus its \glossglue), which is tighter than ours.
\newcommand\lx@defaults@legacy{%
  \def\lx@reduxfactor{-1}%
  \lx@setskips
  \setlength{\Exindent}{0pt}%
  \setlength{\Exlabelsep}{1.3em}%
  \setlength{\SubExleftmargin}{2em}%
  \setlength{\SubSubExleftmargin}{2.4em}%
  \setlength{\SubExlabelwidth}{\dimexpr\SubExleftmargin-\Exlabelsep\relax}%
  \setlength{\SubSubExlabelwidth}{\dimexpr\SubSubExleftmargin-\Exlabelsep\relax}%
  \setlength{\Exlabelwidth}{4em}% recomputed per example; see \lx@calc@Exlabelwidth
  \setlength{\JdgSep}{0pt}%
  \lx@setannotcol \setlength{\ExAnnotSep}{1em}%
  \setlength{\GlossTransSep}{2em}%
  \setlength{\GlossTransMinWidth}{6em}%
  \setlength{\GlossTransRightSkip}{0pt plus 2em}%
  \def\firstrefdash{-}%
  \def\secondrefdash{-}%
  \def\SubExLBr{}\def\SubExRBr{.}%
  \def\SubSubExLBr{(}\def\SubSubExRBr{)}%
  \def\GlossSep{.33em plus .4em minus .2em}}

% Which of the two is in force is decided by \lx_mode_select:n, at the end
% of the geometry section below -- the mode is a defaults table AND a set
% of geometry hooks, and it is chosen once, when both halves exist.
% \resetExdefaults is CALLED at the end of the package, once every length
% it touches has been declared; the user may call it again at any point to
% return to the defaults of the mode in force.

%% ---- the geometry hooks -------------------------------------------------
%% Each level's list declaration calls one of these; they are the ONLY
%% place where the two parameter sets differ.

% width of the narrowest digit, as linguex measured it (fonts in which the
% digits differ in width would otherwise pad inconsistently)
\newlength{\lx@digitwd}
\newlength{\lx@mindigitwd}
\newlength{\lx@currentlabel}
\newlength{\lx@padded}
\def\lx@minwidth#1{\settowidth{\lx@digitwd}{#1}%
  \ifdim\lx@digitwd<\lx@mindigitwd \lx@mindigitwd\lx@digitwd \fi}
% \Exlabelwidth := width of the smallest n-digit box (n = 2,3,4) that the
% current label fits INSIDE; if it fits none, its own width.  This is
% linguex's rule, and the reason a one-digit example number sits in a box
% wide enough for two: the numbering does not shift the text as it grows.
\def\lx@calc@Exlabelwidth{%
  \settowidth{\lx@mindigitwd}{0}%
  \lx@minwidth{1}\lx@minwidth{2}\lx@minwidth{3}\lx@minwidth{4}%
  \lx@minwidth{5}\lx@minwidth{6}\lx@minwidth{7}\lx@minwidth{8}%
  \lx@minwidth{9}%
  \settowidth{\lx@currentlabel}{\lx@itemlabel}%
  \Exlabelwidth\lx@currentlabel
  \settowidth{\lx@padded}{\theExLBr\hbox to 4\lx@mindigitwd{}\theExRBr}%
  \ifdim\lx@currentlabel<\lx@padded \Exlabelwidth\lx@padded \fi
  \settowidth{\lx@padded}{\theExLBr\hbox to 3\lx@mindigitwd{}\theExRBr}%
  \ifdim\lx@currentlabel<\lx@padded \Exlabelwidth\lx@padded \fi
  \settowidth{\lx@padded}{\theExLBr\hbox to 2\lx@mindigitwd{}\theExRBr}%
  \ifdim\lx@currentlabel<\lx@padded \Exlabelwidth\lx@padded \fi}

%% Both sets are defined unconditionally, under names of their own, and one
%% of the two is then selected.  Nothing is saved by defining only the set
%% in force, and having both present is what makes the mode a THING that
%% can be named, rather than a branch taken once at load time.

% legacy main level: label box recomputed from the number; text margin
% \Exindent + \Exlabelwidth + \Exlabelsep, as in the default mode
\def\lx@geom@main@legacy{\lx@calc@Exlabelwidth
  \labelwidth\Exlabelwidth \labelsep\Exlabelsep
  \leftmargin\dimexpr\Exindent+\Exlabelwidth+\Exlabelsep\relax
  \if@noftnote\else\addtolength{\topsep}{-.5\topsep}\fi}
% legacy sub-levels: the text margin IS the parameter, the label sits
% flush left inside it
\def\lx@geom@sub@legacy{\leftmargin\SubExleftmargin
  \labelwidth\SubExleftmargin \labelsep\z@ \topsep.3\Extopsep}
\def\lx@geom@subsub@legacy{\leftmargin\SubSubExleftmargin
  \labelwidth\SubSubExleftmargin \labelsep\z@ \topsep\z@}

% Only the default mode declares a /ListNumbering class per level; the
% legacy hooks above deliberately do not, so a legacy list keeps the block
% code's default (/None).  That is valid -- what is NOT valid is /Ordered,
% which is why neither mode routes through the enumerate class.
\def\lx@geom@main@default{%
  \lx@ol@set{lxOLdecimal}%
  \labelwidth\Exlabelwidth \labelsep\Exlabelsep
  \leftmargin\dimexpr\Exindent+\Exlabelwidth+\Exlabelsep\relax}
\def\lx@geom@sub@default{%
  \expandafter\lx@ol@set\expandafter{\lx@sub@olclass}%
  \labelwidth\SubExlabelwidth \labelsep\Exlabelsep
  \leftmargin\dimexpr\SubExlabelwidth+\Exlabelsep\relax \topsep\z@}
\def\lx@geom@subsub@default{%
  \expandafter\lx@ol@set\expandafter{\lx@subsub@olclass}%
  \labelwidth\SubSubExlabelwidth \labelsep\Exlabelsep
  \leftmargin\dimexpr\SubSubExlabelwidth+\Exlabelsep\relax \topsep\z@}

%%%% --- public API (Protocol B: geometry modes) ------------------------
%% A mode is a defaults table plus three geometry hooks -- and nothing
%% else: [legacy] differs from the default in exactly these four macros.
%%
%%   \lx_mode_new:nn {name}
%%     { defaults = \defaults , main = \main , sub = \sub , subsub = \subsub }
%%   \lx_mode_select:n {name}
%%
%% Keyval rather than an arity-encoded signature (\lx_mode_new:nNNNN) so
%% that a mode may grow a fifth thing to set without the function that
%% declares it having to change its name.
%%
%% Every value is a MACRO NAME, not a body, and this is load bearing: a
%% defaults table contains glue with significant spaces (".5em plus .3em
%% minus .1em"), and a body written as a braced argument under
%% \ExplSyntaxOn would have those spaces stripped.  Defining the macros
%% with plain \newcommand, in whatever catcode regime suits, and handing
%% over the names sidesteps the question entirely -- so a value that is
%% not a single control sequence is refused rather than stored.
%%
%% "defaults" is required: it is called by \resetExdefaults, and must set
%% every length of BOTH parameter sets, since the user may switch modes
%% afterwards.  The three hooks are optional and default to doing nothing;
%% they are called inside the list declaration of their level, and may set
%% \labelwidth, \labelsep, \leftmargin and \topsep; "main" runs AFTER
%% \lx@itemlabel is fixed, so it may size the label box from the label
%% (which is what legacy does).
%%
%% \lx_mode_select:n assigns locally, so a mode chosen inside a group is
%% undone at the end of it.
\ExplSyntaxOn
%% the mode in force, for a front-end that needs to know (read only)
\tl_new:N \l_lx_mode_tl
\msg_new:nnnn { linguexx } { unknown-mode }
  { Geometry~mode~'#1'~is~not~declared. }
  { Declare~it~with~\iow_char:N\\lx_mode_new:nn~before~selecting~it.~
    The~modes~this~package~defines~are~'default'~and~'legacy'. }
\msg_new:nnnn { linguexx } { mode-no-defaults }
  { Geometry~mode~'#1'~gives~no~'defaults'~macro. }
  { A~mode~must~say~what~\iow_char:N\\resetExdefaults~restores:~write~
    \iow_char:N\\lx_mode_new:nn{#1}{defaults=\iow_char:N\\myDefaults,~...}.~
    The~mode~was~not~declared. }
\msg_new:nnnn { linguexx } { mode-needs-cs }
  { Value~of~'#1'~in~a~geometry~mode~is~not~a~macro~name. }
  { Pass~the~NAME~of~a~macro~(#1=\iow_char:N\\myHook),~not~its~body:~a~
    body~written~here~would~lose~the~spaces~of~a~glue~specification.~
    '#1'~was~ignored. }
%% The scratch the keys fill, so that a mode is committed only once its
%% four macros are known and "defaults" among them.
\cs_new_eq:NN \l__lx_mode_defaults_cs \prg_do_nothing:
\cs_new_eq:NN \l__lx_mode_main_cs     \prg_do_nothing:
\cs_new_eq:NN \l__lx_mode_sub_cs      \prg_do_nothing:
\cs_new_eq:NN \l__lx_mode_subsub_cs   \prg_do_nothing:
\bool_new:N  \l__lx_mode_given_bool
\cs_new_protected:Npn \__lx_mode_key:Nnn #1#2#3
  {
    \tl_if_single_token:nTF {#3}
      {
        \token_if_cs:NTF #3
          { \cs_set_eq:NN #1 #3 }
          { \msg_error:nnn { linguexx } { mode-needs-cs } {#2} }
      }
      { \msg_error:nnn { linguexx } { mode-needs-cs } {#2} }
  }
\keys_define:nn { lx / mode }
  {
    defaults .code:n =
      {
        \__lx_mode_key:Nnn \l__lx_mode_defaults_cs { defaults } {#1}
        \bool_set_true:N \l__lx_mode_given_bool
      } ,
    main     .code:n = { \__lx_mode_key:Nnn \l__lx_mode_main_cs   { main }   {#1} } ,
    sub      .code:n = { \__lx_mode_key:Nnn \l__lx_mode_sub_cs    { sub }    {#1} } ,
    subsub   .code:n = { \__lx_mode_key:Nnn \l__lx_mode_subsub_cs { subsub } {#1} } ,
  }
\cs_new_protected:Npn \lx_mode_new:nn #1#2
  {
    \cs_set_eq:NN \l__lx_mode_defaults_cs \prg_do_nothing:
    \cs_set_eq:NN \l__lx_mode_main_cs     \prg_do_nothing:
    \cs_set_eq:NN \l__lx_mode_sub_cs      \prg_do_nothing:
    \cs_set_eq:NN \l__lx_mode_subsub_cs   \prg_do_nothing:
    \bool_set_false:N \l__lx_mode_given_bool
    \keys_set:nn { lx / mode } {#2}
    \bool_if:NTF \l__lx_mode_given_bool
      {
        \cs_set_eq:cN { lx@mode@defaults@#1 } \l__lx_mode_defaults_cs
        \cs_set_eq:cN { lx@mode@main@#1 }     \l__lx_mode_main_cs
        \cs_set_eq:cN { lx@mode@sub@#1 }      \l__lx_mode_sub_cs
        \cs_set_eq:cN { lx@mode@subsub@#1 }   \l__lx_mode_subsub_cs
      }
      { \msg_error:nnn { linguexx } { mode-no-defaults } {#1} }
  }
%% Tested on the defaults slot, the one a failed declaration never creates.
\cs_new_protected:Npn \lx_mode_select:n #1
  {
    \cs_if_exist:cTF { lx@mode@defaults@#1 }
      {
        \cs_set_eq:Nc \resetExdefaults { lx@mode@defaults@#1 }
        \cs_set_eq:Nc \lx@geom@main    { lx@mode@main@#1 }
        \cs_set_eq:Nc \lx@geom@sub     { lx@mode@sub@#1 }
        \cs_set_eq:Nc \lx@geom@subsub  { lx@mode@subsub@#1 }
        \tl_set:Nn \l_lx_mode_tl {#1}
      }
      { \msg_error:nnn { linguexx } { unknown-mode } {#1} }
  }

%% The two modes this package defines, and the choice between them.  Both
%% the declarations and the selection have to happen under \ExplSyntaxOn:
%% outside it "_" is catcode 8 and \lx_mode_new:nn is not a control
%% sequence at all.  Passing names rather than bodies is what makes that
%% harmless -- no key value here is anything but a control sequence, so
%% the ignored spaces of expl3 catcodes cost nothing.
\lx_mode_new:nn {default}
  {
    defaults = \lx@defaults@lazy , main = \lx@geom@main@default ,
    sub = \lx@geom@sub@default   , subsub = \lx@geom@subsub@default ,
  }
\lx_mode_new:nn {legacy}
  {
    defaults = \lx@defaults@legacy , main = \lx@geom@main@legacy ,
    sub = \lx@geom@sub@legacy      , subsub = \lx@geom@subsub@legacy ,
  }

\iflx@legacy
  \lx_mode_select:n {legacy}
\else
  \lx_mode_select:n {default}
\fi
\ExplSyntaxOff

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  The tagged-Span idiom, in one place                                %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% Everything this package adds to the structure tree is a Span: the
%% judgment mark with its spoken /Alt, the Leipzig abbreviation with its
%% /E, the gloss column, the object-language word with its /Lang, the free
%% translation with its own, and the two kinds of stacked alternatives.
%% All of them need the SAME four moves in the SAME order, and getting the
%% order wrong is exactly how v0.10 broke PDF/UA:
%%
%%   \tag_mc_end_push:      suspend whatever marked content is open --
%%                          typically the ambient paragraph's, since the
%%                          kernel may already have opened an MC for a P
%%                          that has not been shown yet.  Opening our BDC
%%                          inside that one without suspending it is
%%                          "nested marked content found" / "no mc to end",
%%                          and a veraPDF untagged-content failure once the
%%                          P's real MC never gets closed.
%%   \tag_struct_begin:n    the Span itself, carrying alt/E/lang.
%%   \tag_mc_begin:n        its own marked content, for LEAF content.
%%   ... content ...
%%   \tag_mc_end: \tag_struct_end: \tag_mc_begin_pop:n {}   -- unwound in
%%                          the mirror order, resuming the suspended MC.
%%
%% The split into an outer (open/close) and an inner (begin/end) pair is
%% not decoration: a Span whose content carries marked content of its OWN
%% -- the gloss column, whose words each open one -- must take the outer
%% pair only, or it would nest an MC inside its own MC.
%%
%% \lx@tag@if@active: is the one guard, and it is not only for Spans: every
%% tagging use site in the package needs the same two conditions (a kernel
%% that has the \tag_... commands at all, and tagging actually switched on).
%% They were hand-copied in five spellings, two of which tested
%% \cs_if_exist:N on \tag_if_active:T -- a conditional VARIANT rather than
%% the base name, which happens to work but tests the wrong thing.
\ExplSyntaxOn
\prg_new_conditional:Npnn \lx@tag@if@active: { TF , T , F }
  {
    \bool_lazy_all:nTF
      {
        { \cs_if_exist_p:N \tag_struct_begin:n }
        { \cs_if_exist_p:N \tag_if_active_p: }
        { \tag_if_active_p: }
      }
      { \prg_return_true: } { \prg_return_false: }
  }
%% outer half: suspend the ambient MC, open the Span
\cs_new_protected:Npn \lx@tag@span@open:n #1
  { \tag_mc_end_push: \tag_struct_begin:n {#1} }
\cs_generate_variant:Nn \lx@tag@span@open:n { e }
\cs_new_protected:Npn \lx@tag@span@close:
  { \tag_struct_end: \tag_mc_begin_pop:n {} }
%% ... plus the inner half, for a Span holding leaf content
\cs_new_protected:Npn \lx@tag@span@begin:n #1
  { \lx@tag@span@open:n {#1} \tag_mc_begin:n { tag = Span } }
\cs_generate_variant:Nn \lx@tag@span@begin:n { e }
\cs_new_protected:Npn \lx@tag@span@end:
  { \tag_mc_end: \lx@tag@span@close: }
%% the whole thing, guarded: #1 = keyvals, #2 = content.  Without active
%% tagging the content is typeset bare, so untagged output and engines
%% with no tagging support are byte-for-byte unaffected.
\cs_new_protected:Npn \lx@tag@span:nn #1#2
  {
    \lx@tag@if@active:TF
      { \lx@tag@span@begin:n {#1} #2 \lx@tag@span@end: }
      { #2 }
  }
%% ... and the same with the keyvals expanded first (a /E or /Alt built in
%% a token list).  Deliberately not \cs_generate_variant: the expansion has
%% to happen INSIDE the guard.  The values are author-supplied strings, and
%% expanding one on a run with no tagging to consume it would make an
%% untagged compile fail where the tagged one is what carries the risk.
\cs_new_protected:Npn \lx@tag@span@exp:nn #1#2
  {
    \lx@tag@if@active:TF
      { \lx@tag@span@begin:e {#1} #2 \lx@tag@span@end: }
      { #2 }
  }
%% The e-variants above are generated for OUR OWN commands, which always
%% exist.  Generating one for \tag_struct_begin:n (as v1.1 did) contradicts
%% its own use sites: they all guard for a kernel that does not have the
%% \tag_... commands, on which the variant generation would already have
%% failed at load time.

%% --- public API (Protocol A: tagging) --------------------------------
%% Aliases, not redefinitions: \cs_new_eq:NN copies the meaning and with
%% it the \protected status, so a public name is the private one in every
%% respect.  See the "For front-end authors" section of the manual.
\prg_new_eq_conditional:NNn \lx_tag_if_active: \lx@tag@if@active: { TF , T , F }
\cs_new_eq:NN \lx_tag_span_open:n   \lx@tag@span@open:n
\cs_new_eq:NN \lx_tag_span_close:   \lx@tag@span@close:
\cs_new_eq:NN \lx_tag_span_begin:n  \lx@tag@span@begin:n
\cs_new_eq:NN \lx_tag_span_end:     \lx@tag@span@end:
\cs_new_eq:NN \lx_tag_span:nn       \lx@tag@span:nn
\ExplSyntaxOff

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Judgment auto-detection (* ? \# \%) -- replaces linguex's          %%%%
%%%%  hardcoded catcode tokenizer                                        %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% A judgment typed at the very start of an example or sub-example
%% (\ex. *Sentence / \a. ??\%Sentence) is collected greedily and set
%% flush right in the LABEL area, so example texts align whether or not
%% they carry a judgment.  Recognized: the characters * and ? and the
%% control sequences \# and \%.  Anything else: use \jdg{...}, which
%% works anywhere and takes arbitrary marks.
%%
%% "The very start" is not the same thing as "the first token".  A label
%% is typed at the head of the example far more often than after its
%% first word --
%%
%%     \ex. \label{ex:2}*?La sola invasione romana della Tracia
%%
%% -- and \label makes no ink, so the star is still the first thing on
%% the line and the reader still expects it in the gutter.  Left alone
%% the scan stopped at \label, the marks stayed in the body, and the
%% example was indented by their width: the one thing this whole
%% mechanism exists to prevent, appearing only in the examples that
%% happen to be cross-referenced.
%%
%% Such a command is therefore SKIPPED and REPLAYED rather than stopped
%% at.  Skipped: it is taken out of the input so the loop can go on to
%% the marks behind it.  Replayed, and not executed here: \label belongs
%% after the \item, which is where writing it in the body has always put
%% it, and where \@currentlabel and the anchor for it are the example's.
%% \lx@emitjudge puts the collected commands back, in order, the moment
%% the mark is hung -- see there.
%%
%% The set is closed, and it is the two no-ink commands a linguexx
%% example is cross-referenced with: \label and \sublabel.  Recognition
%% is by meaning, so the hyperref, cleveref and babel redefinitions of
%% \label are all the same \label to it, and an optional argument
%% (cleveref's \label[eq]{...}) is carried across with the rest.

\ExplSyntaxOn
\tl_new:N \l__lx_judge_tl
\tl_new:N \l__lx_judge_cont_tl
\tl_new:N \l__lx_judge_pre_tl

% scan judgments, then execute #1 (the collected marks are in
% \lx@judgeprint; \lx@emitjudge hangs them via \jdg if nonempty, and
% replays whatever \l__lx_judge_pre_tl was skipped past)
\cs_new_protected:Npn \lx@scanjudgeto #1
  {
    \tl_clear:N \l__lx_judge_tl
    \tl_clear:N \l__lx_judge_pre_tl
    \tl_set:Nn \l__lx_judge_cont_tl {#1}
    \__lx_judge_loop:
  }
\cs_new_protected:Npn \lx@scanjudge
  { \lx@scanjudgeto { \lx@makeitem } }
\cs_new_protected:Npn \__lx_judge_loop:
  {
    \peek_remove_spaces:n
      {
        \peek_charcode_remove:NTF *
          { \tl_put_right:Nn \l__lx_judge_tl {*} \__lx_judge_loop: }
          {
        \peek_charcode_remove:NTF ?
          { \tl_put_right:Nn \l__lx_judge_tl {?} \__lx_judge_loop: }
          {
        \peek_meaning_remove:NTF \#
          { \tl_put_right:Nn \l__lx_judge_tl {\#} \__lx_judge_loop: }
          {
        \peek_meaning_remove:NTF \%
          { \tl_put_right:Nn \l__lx_judge_tl {\%} \__lx_judge_loop: }
          {
        \peek_meaning:NTF \label
          { \__lx_judge_skip:N }
          {
        \peek_meaning:NTF \sublabel
          { \__lx_judge_skip:N }
          { \tl_use:N \l__lx_judge_cont_tl }
          } } } } }
      }
  }
%% Take one skipped command out of the input and put it in
%% \l__lx_judge_pre_tl, argument and all, then go on scanning.  The
%% command token itself is still in the stream when we get here (the
%% peek above does not remove it), so #1 is it; what follows is any
%% number of bracketed optional arguments and then exactly one braced
%% one, which is the shape of both \label and \sublabel.
\cs_new_protected:Npn \__lx_judge_skip:N #1
  {
    \tl_put_right:Nn \l__lx_judge_pre_tl {#1}
    \__lx_judge_skip_args:
  }
%% The optional argument is looked for only BEFORE the mandatory one,
%% which is why \__lx_judge_skip_arg:n hands back to the judgment loop
%% and not to here: "\ex. \label{x}[DP der Hund] bellte." opens its
%% object line with a bracket, and a second look would eat the
%% constituent as an argument of the label it follows.
\cs_new_protected:Npn \__lx_judge_skip_args:
  {
    \peek_charcode:NTF [
      { \__lx_judge_skip_opt:w }
      { \__lx_judge_skip_arg:n }
  }
\cs_new_protected:Npn \__lx_judge_skip_opt:w [#1]
  {
    \tl_put_right:Nn \l__lx_judge_pre_tl { [#1] }
    \__lx_judge_skip_args:
  }
\cs_new_protected:Npn \__lx_judge_skip_arg:n #1
  {
    \tl_put_right:Nn \l__lx_judge_pre_tl { {#1} }
    \__lx_judge_loop:
  }
\cs_new:Npn \lx@judgeprint { \tl_use:N \l__lx_judge_tl }
\cs_new_protected:Npn \lx@setjudge #1
  { \tl_set:Nn \l__lx_judge_tl {#1} }

%% --- public API (Protocol A: judgments) -------------------------------
%% \lx_judgment_scan:    peek for judgment marks in the input, then emit
%%                       the item (i.e. \lx_judgment_scan:n{\lx_item_emit:}).
%%                       This is where the \lx_item_... commands end.
%% \lx_judgment_scan:n   peek for marks, then run the given code.
%% \lx_judgment_set:n    set the mark explicitly, for a front-end that
%%                       takes it as an argument rather than from the
%%                       input stream (the gb4e \ex[*]{...} case).  Any
%%                       mark is allowed here, not just the scanned set.
%% Either way the mark is hung by the next \lx_item_emit:.
\cs_new_eq:NN \lx_judgment_scan:  \lx@scanjudge
\cs_new_eq:NN \lx_judgment_scan:n \lx@scanjudgeto
\cs_new_eq:NN \lx_judgment_set:n  \lx@setjudge

%% Objective 3: spoken alternatives for judgment marks.  Each mark is a
%% symbol whose meaning a screen reader cannot infer ("asterisk"); under
%% PDF tagging we wrap it in a Span carrying /Alt so it is announced
%% ("ungrammatical").  \g_lx_judge_alt_prop maps a mark string to its
%% spoken form; defaults follow standard usage and can be overridden or
%% extended with \DeclareJudgment[spoken=...] or \SetJudgmentSpoken.
\prop_new:N \g_lx_judge_alt_prop
\prop_gput:Nnn \g_lx_judge_alt_prop {*}   {ungrammatical}
\prop_gput:Nnn \g_lx_judge_alt_prop {?}   {questionable}
\prop_gput:Nnn \g_lx_judge_alt_prop {??}  {highly~questionable}
\prop_gput:Nnn \g_lx_judge_alt_prop {?*}  {extremely~degraded}
\prop_gput:Nnn \g_lx_judge_alt_prop {*?}  {extremely~degraded}
\prop_gput:Nnn \g_lx_judge_alt_prop {\#}  {infelicitous}
\prop_gput:Nnn \g_lx_judge_alt_prop {\%}  {grammatical~for~some~speakers}
\tl_new:N \l__lx_judge_alt_tl

\cs_new_protected:Npn \lx@judge@setalt #1#2
  { \prop_gput:Nnn \g_lx_judge_alt_prop {#1} {#2} }

%% Hang a judgment mark (#2) to the left; when tagging is active and a
%% spoken form (#1) is non-blank, wrap the mark in a Span with /Alt so it
%% is read aloud as #1.  Otherwise typeset the mark exactly as before, so
%% untagged output and non-tagging engines are unaffected.
%% \lx@makeitem calls \lx@emitjudge (hence this) as the very FIRST thing
%% after \item, i.e. at the start of a fresh list-item paragraph, which is
%% precisely the moment the \lx@tag@span: helper exists for: the kernel's
%% own paragraph-tagging may already have an MC open for the not-yet-shown
%% P, and the helper suspends it.  See its comment.
\cs_new_protected:Npn \lx@hangjudge #1#2
  {
    \leavevmode
    \llap
      {
        \tl_if_blank:nTF {#1}
          { #2 }
          { \lx@tag@span:nn { tag = Span , alt = {#1} } {#2} }
        \hskip \JdgSep
      }
  }
%% Hang the mark, then put back whatever the scan skipped to reach it
%% (\label, \sublabel -- see \__lx_judge_skip:N).  This order is the
%% point: the mark hangs from the label edge with nothing between it and
%% the \item, and the replayed \label sits exactly where writing it in
%% the body used to put it -- after the item, in the same mode, with the
%% same \@currentlabel.  The replay does not depend on a mark having
%% been found -- a labelled example with no judgment goes through here
%% too -- and it clears as it goes, so no second \lx@emitjudge (the
%% \exg. path calls it after \lx@makeitem has) delivers the label twice.
\cs_new_protected:Npn \lx@emitjudge
  {
    \tl_if_empty:NF \l__lx_judge_tl
      {
        \tl_set:Ne \l__lx_judge_alt_tl
          { \prop_item:Ne \g_lx_judge_alt_prop { \tl_to_str:N \l__lx_judge_tl } }
        \exp_args:NV \lx@hangjudge \l__lx_judge_alt_tl { \lx@judgeprint }
      }
    \tl_if_empty:NF \l__lx_judge_pre_tl
      {
        \tl_use:N \l__lx_judge_pre_tl
        \tl_clear:N \l__lx_judge_pre_tl
      }
  }
\ExplSyntaxOff

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  The example machinery: \ex. \a. \b. ... , blank-line terminated    %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% Architecture: \ex. is a \par-delimited macro, exactly as in linguex --
%% that is what makes "blank line ends the example" work.  BUT unlike
%% linguex, the entire body is grabbed as one argument, so all list
%% opening and closing happens inside a single macro invocation:
%%   \ex.  ->  \begingroup + open the main list  [process body]
%%             [close open sublists] + close the main list + \endgroup
%% Sub-example depth is tracked in a count register that is only ever
%% advanced INSIDE the group opened for the new sublevel, so leaving the
%% group automatically pops the depth: the grouping structure itself is
%% the stack, and global drift (linguex's ExDepth disease) is
%% structurally impossible.
%%
%% Semantics kept from linguex: \a. always opens a NEW, deeper level
%% (first level: letters, second: roman); \b. \c. \d. \e. \f. are
%% interchangeable "next item at current level" commands (the printed
%% letter comes from the counter, not the command name).  Two sublevels
%% maximum.  \ex.[custom] sets a custom label without stepping the
%% counter.  Inside footnotes, examples number (i), (ii), ... on the
%% FnExNo counter automatically.
%%
%% NOT carried over from linguex (deliberately): embedded examples
%% (\ex. inside \a.) and the \exi./\ai. index variants.

\newcount\lx@subdepth

%% The example body is collected TOKEN BY TOKEN (expl3 peek_analysis),
%% not grabbed as a \par-delimited argument.  Collection stops, at brace
%% depth 0, at the first of:
%%   - a \par token (i.e. the blank line, as under linguex);
%%   - \z. (early termination; the rest of the stream continues as
%%     ordinary text, so no blank line is needed after it);
%%   - a structural boundary: an unmatched \end{...}, \endgroup, or
%%     group-end token.  The boundary token is put back, so
%%     "\ex. Text\end{frame}" simply works.
%% \begin{...}/\end{...} and \begingroup/\endgroup pairs INSIDE the body
%% are counted, so environments inside examples are unaffected.  Brace
%% groups are collected whole; a \par inside braces is therefore legal
%% (it was an error under the old grab).  \z. and the terminating \par
%% are only recognized at brace depth 0.
%% \ex dispatches on what follows: a period gives the classic dot
%% syntax; anything else is the gb4e item form.  Each branch is defined
%% by the mode in force, with an instructive error where a syntax is
%% not loaded.
\newif\iflx@inexe
%% TRUE while an example opened by \ea (the langsci front-end) is open --
%% i.e. "the example currently open is written in the langsci syntax".
%% It is what enforces ONE SYNTAX PER EXAMPLE: the dot commands refuse to
%% run while it is set, and \ea refuses to run while an example of the
%% other kind is open.  See the header of the \ea section for why the ban
%% exists and why it does not extend to [lazy,gb4e], whose within-example
%% mixing is a documented, tested promise.
%%
%% Cleared in \lx@example@cleanup (the example is over) and at the
%% footnote boundary beside \@noftnotefalse (a footnote inside an \ea
%% example holds examples of its OWN, which may be written either way --
%% without that reset the ban would fire on legitimate input).  It is
%% never set globally, and \ea opens no group of its own, so the reset in
%% the cleanup is not optional.
\newif\iflx@inlangsci
%% The dot-syntax half of the one-syntax-per-example rule: every command
%% that only means something in the dot syntax runs this first, naming
%% itself.  A no-op unless an \ea example is open, so it costs nothing in
%% a document that never loads [langsci].
\def\lx@nomix@dot#1{%
  \iflx@inlangsci
    \PackageError{linguexx}{#1 inside an \string\ea\space example}%
      {One example is written in ONE syntax throughout.  This one was
       opened with \string\ea, so its sub-levels are \string\ea\space and
       it ends at \string\z.\MessageBreak
       Convert the whole example, or leave this one as it stands and
       write the next in the other syntax -- between examples the two mix
       freely, which is what [lazy,langsci] is for.}%
  \fi}
%% \ex peeks for the dot of the dot syntax, and the peek has to tell it
%% from a period the writer meant as prose.  The continuation-style
%% example -- a mother item sets a context and each sub-item resumes the
%% sentence -- opens its items with a literal ellipsis, and langsci-gb4e,
%% whose \ex never peeked, always set that as text.
%%
%% The two are distinguishable, but not by \@ifnextchar, which SKIPS
%% SPACES.  TeX's own scanner has already eaten the space after the
%% control word, so what reaches the peek is:
%%
%%     \ex. body        ->  . <space> b o d y      (dot syntax)
%%     \ex ... text     ->  . . . <space> t e x t  (prose)
%%     \ex. ...body     ->  . <space> . . . b      (dot syntax, prose body)
%%
%% The period that opens the dot syntax is followed by the space that
%% separates it from the body; the first period of an ellipsis is followed
%% by the second, with nothing in between.  So the test is what comes
%% IMMEDIATELY after the first period -- \futurelet, which does not skip
%% spaces, where \@ifnextchar would skip that separating space and read
%% the third line above as prose, trading one misparse for another.
%%
%% \lx@ex@peek takes the first period as its delimiter and hands it back
%% on both branches: to \lx@ex@dot, which is delimited by it in turn, and
%% ahead of the two that remain on the text branch, so the item opens with
%% all three.
%%
%% \exg. and the \a.-\f. shorthands expand to \ex. plus a control
%% sequence, which is not a period and takes the dot branch as before.
%% The one form this cannot resolve is \ex....body -- dot syntax whose
%% body opens with an ellipsis and no space -- which is read as prose.
%% It is unwritable either way, and the spaced form above is not.
\let\lx@dotchar=.
\def\lx@ex@peek.{\futurelet\lx@peeked\lx@ex@peek@ii}
\def\lx@ex@peek@ii{%
  \ifx\lx@peeked\lx@dotchar
    \expandafter\lx@ex@peek@text
  \else
    \expandafter\lx@ex@peek@dot
  \fi}
\def\lx@ex@peek@text{\lx@ex@nodot.}
\def\lx@ex@peek@dot{\lx@ex@dot.}
\def\ex{\@ifnextchar.{\lx@ex@peek}{\lx@ex@nodot}}
\iflx@lazy
  \def\lx@ex@dot.{\lx@nomix@dot{\string\ex.}\lx@collectbody}
\else
  \def\lx@ex@dot.{%
    \PackageError{linguexx}{The dot syntax (\string\ex.) is not
      available under [gb4e]}{Use \string\ex\space inside
      \string\begin{exe} ... \string\end{exe}, or load linguexx
      without options (or with [lazy]) for the dot syntax.}}
\fi
\iflx@gbfour
  % \ex is the item command of BOTH environment-shaped front-ends: the
  % exe/xlist batch and an \ea example, which is a batch of one that
  % brought its own list.  Either flag makes it legal; neither, and it is
  % the error below.
  \def\lx@ex@nodot{%
    \iflx@inexe
      \let\lx@donext\lx@gbex
    \else\iflx@inlangsci
      \let\lx@donext\lx@gbex
    \else
      \let\lx@donext\lx@gbex@errthen
    \fi\fi
    \lx@donext}
  % error first, then carry on as if \ex had been legal, so that one
  % misplaced \ex does not cascade into a second error from its own body
  \def\lx@gbex@errthen{\lx@gbex@err\lx@gbex}
  \def\lx@gbex@err{%
    \PackageError{linguexx}{\string\ex\space outside exe/xlist}{Put
      \string\ex\space inside \string\begin{exe} ...
      \string\end{exe}\iflx@langsci, or after \string\ea\fi
      \iflx@lazy, or write \string\ex. (with the
      period) for the dot syntax\fi.}}
\else
  \def\lx@ex@nodot{%
    \PackageError{linguexx}{\string\ex\space must be followed by a
      period}{Write \string\ex. -- or load
      \string\usepackage[gb4e]{linguexx} for the environment
      syntax.}}
\fi

% gb4e item form: \ex[judgment]{text} or plain \ex text
\def\lx@gbex{\@ifnextchar[{\lx@gbex@opt}{\lx@gbex@plain}}
\def\lx@gbex@plain{%
  \ifcase\lx@subdepth
    \let\lx@donext\lx@mainitem
  \or \let\lx@donext\lx@subitem
  \or \let\lx@donext\lx@subsubitem
  \fi
  \lx@donext}
%% The level dispatch and the explicit-judgment item, factored out of
%% \lx@gbex@opt: they are what a front-end needs in order to give an item
%% a mark it did not read from the input, and there is no way to compose
%% that out of the scanning commands -- \lx@scanjudge clears the mark
%% before it peeks, so anything set beforehand is lost.
%% Neither opens a list: at depth 0 they assume the main list is already
%% open, which is the batch case (exe).  A front-end whose example brings
%% its own list calls \lx@mainlist first.
\def\lx@item@core{%
  \ifcase\lx@subdepth
    \lx@main@core
  \or \lx@subitem@core
  \or \lx@subsubitem@core
  \fi}
\def\lx@item@judged#1{\lx@item@core\lx@setjudge{#1}\lx@makeitem}
\long\def\lx@gbex@opt[#1]#2{\lx@item@judged{#1}#2}
%% --- public API (Protocol A: items at the current level) --------------
%% \lx_item_core:      step the counter of whatever level is open and set
%%                     its label.  Emits nothing, so a front-end may put
%%                     something between the label and the item.
%% \lx_item_judged:n   the same, plus the given mark, plus the item: an
%%                     item carrying a judgment that was NOT read from the
%%                     input.  Any mark is allowed, not just the scanned
%%                     set.  Both assume the list of the current level is
%%                     already open (see \lx_list_main_open:).
\ExplSyntaxOn
\cs_new_eq:NN \lx_item_core:    \lx@item@core
\cs_new_eq:NN \lx_item_judged:n \lx@item@judged
\ExplSyntaxOff

\ExplSyntaxOn
\tl_new:N \l__lx_body_tl
\tl_new:N \l__lx_body_tmp_tl
\seq_new:N \l__lx_body_stack_seq
\int_new:N \l__lx_body_env_int
\int_new:N \l__lx_body_grp_int
\int_new:N \l__lx_body_col_int

%% --- public API (Protocol A: body collection) -------------------------
%% \lx_body_collect:N \myfunction
%%   Collect the example body under the terminator rules described above
%%   and hand it to \myfunction as its single argument.  The function must
%%   be \long (bodies contain \par).  It is entered with the terminator
%%   already put back where the rules say it should be, so it may simply
%%   typeset: the dot syntax's own continuation, \lx@run@ex, is nothing
%%   but \lx_example_begin: + items + \lx_example_end:.
%%
%% The continuation is held in a macro rather than passed down the call
%% chain because the collection ends in \peek_analysis_map_break:n, which
%% discards the surrounding expansion context.  The assignment is GLOBAL:
%% the collector enters a TeX group for every brace group it collects, and
%% the unmatched-group-end branch below breaks out with an \egroup still
%% pending, so a local value could be restored away before it is used.
\cs_new_eq:NN \lx@body@cont \prg_do_nothing:
\cs_new_protected:Npn \lx_body_collect:N #1
  {
    \cs_gset_eq:NN \lx@body@cont #1
    \tl_clear:N \l__lx_body_tl
    \seq_clear:N \l__lx_body_stack_seq
    \int_zero:N \l__lx_body_env_int
    \int_zero:N \l__lx_body_grp_int
    \int_zero:N \l__lx_body_col_int
    \__lx_body_loop:
  }
%% \lx@run@ex is defined further down; naming it here is safe because the
%% body is not expanded until the first \ex. of the document.
\cs_new_protected:Npn \lx@collectbody { \lx_body_collect:N \lx@run@ex }
\cs_new_protected:Npn \__lx_body_loop:
  {
    \peek_analysis_map_inline:n
      {
        \int_case:nnF { "##3 }
          {
            { 1 } { \seq_push:NV \l__lx_body_stack_seq \l__lx_body_tl
                    \tl_clear:N \l__lx_body_tl }
            { 2 } { \seq_pop:NNTF \l__lx_body_stack_seq \l__lx_body_tmp_tl
                      {
                        \tl_put_right:Ne \l__lx_body_tmp_tl
                          { { \exp_not:V \l__lx_body_tl } }
                        \tl_set_eq:NN \l__lx_body_tl \l__lx_body_tmp_tl
                      }
                      { \peek_analysis_map_break:n
                          { \lx@body@info{group~end}\lx@runbody \egroup } }
                  }
          }
          {
            \bool_lazy_and:nnTF
              { \int_compare_p:nNn {##2} = { -1 } }
              { \seq_if_empty_p:N \l__lx_body_stack_seq }
              { \__lx_body_cs:n {##1} }
              { \tl_put_right:Ne \l__lx_body_tl {##1} }
          }
      }
  }
% control-sequence dispatch at brace depth 0.  Terminators: \par;
% a nested \ex./\exg. (embedded examples are unsupported, so this is a
% forgotten blank line: treat as boundary); unmatched \end, \endgroup,
% \color@endgroup (footnote machinery), each pair-counted against its
% opener inside the body.  \z is NOT handled here: it is an ordinary
% body macro, interpreted at typesetting time (see \lx@zpop below).
%
%% The nine names fall into four behaviours, and each is written once:
%%
%%   \par              -- the boundary proper, nothing to weigh up
%%   \begin & co.      -- an opener: count it and collect it
%%   \end & co.        -- a closer: uncount and collect it if its opener
%%                        was counted inside the body, otherwise it closes
%%                        something opened OUTSIDE and the example ends
%%   \ex, \exg         -- like a closer in its test, but with no counter of
%%                        its own to move: legitimate inside an environment
%%                        opened in the body, a forgotten blank line outside
%%
%% Anything else is body text and is collected.
%%
%% On \str_case:enF, which is load-bearing and not interchangeable with
%% its neighbours.  The "e" is the contract, not a flourish: #1 is not the
%% token itself.  \peek_analysis_map_inline:n hands over a wrapper that
%% yields the token when e-expanded, which is what the "ee" of the previous
%% \str_if_eq:eeTF chain was for and what the "e" here is for.  A variant
%% without it compares the wrapper and matches nothing.
%%
%% This is also the token loop of every example body, so the shape was
%% chosen with an eye on cost: the "e" variant expands #1 ONCE before the
%% case list is walked, where the chain re-expanded it for each of the nine
%% names tried.  Do not oversell that.  Measured under lualatex, 300
%% examples of 48 macros each: 2.598s before, 2.499s after, a repeatable
%% 4%.  On a document whose examples hold a handful of macros the
%% difference is 0.5%, which is noise.  Legibility is the reason to keep
%% this shape; the expansions are a bonus, and nobody should reach for a
%% harder-to-read variant chasing the rest of them.
%%
%% Three neighbouring spellings are wrong, and none of them announces itself:
%%
%%  - the names on the left are written BARE, not as \exp_not:N \par.  Only
%%    the test is expanded here; a case is compared under \exp_not:n, so an
%%    \exp_not:N written in one is compared as itself and the case can never
%%    match.  (The old chain used \str_if_eq:ee, which expanded BOTH sides,
%%    and there the \exp_not:N was required.  Carrying it over is the
%%    natural mistake, and it disables every case at once.)
%%  - \str_case_e:enF re-expands the test for every case in the list, so it
%%    reads the same and buys nothing.
%%  - stringifying into a str first and dispatching with \str_case:VnF is
%%    the version that looks fastest, and it silently matches NOTHING.
%%    \str_set:Ne stringifies with \tl_to_str:n, which terminates a control
%%    word with a space ("\par" becomes 5 characters); \str_case compares
%%    with \str_if_eq:nn, which is \pdfstrcmp under \exp_not:n and follows
%%    \string, with no such space (4 characters).  Every case misses, every
%%    token is collected as body text, and the example never ends -- which
%%    on the page is a paragraph that will not outdent and, in the tag
%%    tree, the rest of the document nested inside the last example.  It
%%    was caught by struct_label_depths, not by any rendering assertion.
\cs_new_protected:Npn \__lx_body_keep:n #1
  { \tl_put_right:Ne \l__lx_body_tl {#1} }
\cs_new_protected:Npn \__lx_body_open:Nn #1#2
  { \int_incr:N #1 \__lx_body_keep:n {#2} }
%% #3 is what the log calls the token, #4 the token itself, put back into
%% the stream ahead of whatever the boundary starts.  #4 is written out at
%% the call rather than recovered from #2: #2 is the wrapper above, and it
%% only survives being EXPANDED, whereas this one is re-emitted as code.
\cs_new_protected:Npn \__lx_body_close:Nnnn #1#2#3#4
  {
    \int_compare:nNnTF { #1 } > { 0 }
      { \int_decr:N #1 \__lx_body_keep:n {#2} }
      { \peek_analysis_map_break:n { \lx@body@info {#3} \lx@runbody #4 } }
  }
%% \ex / \exg met inside the body.  In a \begin{...}\end{...} pair opened
%% within the body -- an xlist, say -- it is a legitimate item command and
%% is collected; at environment depth 0 it can only be a forgotten blank
%% line, and starts a new example.  Either way there is no counter of its
%% own to move, which is what separates this from \__lx_body_close:Nnnn.
\cs_new_protected:Npn \__lx_body_nested:nnn #1#2#3
  {
    \int_compare:nNnTF { \l__lx_body_env_int } > { 0 }
      { \__lx_body_keep:n {#1} }
      { \peek_analysis_map_break:n { \lx@body@info {#2} \lx@runbody #3 } }
  }
\cs_new_protected:Npn \__lx_body_cs:n #1
  {
    \str_case:enF {#1}
      {
        { \par }
          { \peek_analysis_map_break:n { \lx@runbody } }
        { \ex }
          { \__lx_body_nested:nnn {#1} { \string\ex. } \ex }
        { \exg }
          { \__lx_body_nested:nnn {#1} { \string\exg. } \exg }
        { \begin }
          { \__lx_body_open:Nn \l__lx_body_env_int {#1} }
        { \end }
          { \__lx_body_close:Nnnn \l__lx_body_env_int {#1}
              { \string\end } \end }
        { \begingroup }
          { \__lx_body_open:Nn \l__lx_body_grp_int {#1} }
        { \endgroup }
          { \__lx_body_close:Nnnn \l__lx_body_grp_int {#1}
              { \string\endgroup } \endgroup }
        { \color@begingroup }
          { \__lx_body_open:Nn \l__lx_body_col_int {#1} }
        { \color@endgroup }
          { \__lx_body_close:Nnnn \l__lx_body_col_int {#1}
              { \string\color@endgroup } \color@endgroup }
      }
      { \__lx_body_keep:n {#1} }
  }
\cs_new_protected:Npn \lx@runbody
  { \exp_args:NV \lx@body@cont \l__lx_body_tl }
\ExplSyntaxOff

\def\lx@body@info#1{%
  \PackageInfo{linguexx}{Example terminated by structural boundary (#1)}}

%% \lx@mainlist is called AFTER \lx@itemlabel has been fixed (either from
%% the counter or from \ex.[custom]), because under [legacy] the width of
%% the label box is derived from the label itself.  \topsep is set before
%% \lx@geom@main, which may modify it (halved in footnotes, under legacy).
%%
%% Lists are opened and closed through the ENVIRONMENT interface
%% (\begin{list}...\end{list}) rather than the command pair
%% \list...\endlist.  The two are equivalent on a classic engine, but
%% under the tagged PDF code (\DocumentMetadata testphase "block") the
%% environment is the supported interface: part of its state restoration
%% is keyed to the environment hooks, which never fire in command-form
%% use, and the resulting imbalance corrupts the structure tree (visible
%% with an example inside a footnote).  Funnelling every open and close
%% through the two macros below keeps that decision in one place.
%% Example lists are semantically ordered.  PDF's /ListNumbering has a
%% fixed set of valid values (Decimal, LowerAlpha, LowerRoman, ...);
%% "Ordered" is NOT one of them, so routing through the block code's
%% enumerate class (whose value is /Ordered) is rejected by validators.
%% Instead we define one valid attribute class per level and hand it to
%% the list's structure element.  \lx@ol@class carries the class for the
%% list about to open; a guarded patch of the block list-begin applies it
%% and clears it.  If that internal ever disappears the lists simply keep
%% the block's default class (/None) --- still valid, never /Ordered.
\ExplSyntaxOn
\tl_new:N \lx@ol@class
\cs_new_protected:Npn \lx@ol@set #1 { \tl_set:Nn \lx@ol@class {#1} }
%% --- public API (Protocol A: list numbering) --------------------------
%% Set the /ListNumbering attribute class for the list about to open.
%% Valid values are the classes declared below (lxOLdecimal, lxOLalpha,
%% lxOLroman); a front-end that opens a list at a level the core does not
%% know about declares its own class with \tagpdfsetup{newattribute=...}.
\cs_new_eq:NN \lx_ol_class:n \lx@ol@set
\AddToHook{begindocument}
  {
    \cs_if_exist:NT \tagpdfsetup
      {
        \tagpdfsetup
          {
            newattribute = { lxOLdecimal } { /O /List /ListNumbering /Decimal } ,
            newattribute = { lxOLalpha   } { /O /List /ListNumbering /LowerAlpha } ,
            newattribute = { lxOLroman   } { /O /List /ListNumbering /LowerRoman } ,
            % for the sub-level numbering variants of [langsci] (\xlistA,
            % \xlistI, \xlistn): the printed label and /ListNumbering have
            % to agree, or a screen reader announces a numbering the page
            % does not show.
            newattribute = { lxOLupperalpha } { /O /List /ListNumbering /UpperAlpha } ,
            newattribute = { lxOLupperroman } { /O /List /ListNumbering /UpperRoman } ,
          }
      }
    \cs_if_exist:NT \__block_list_begin:
      {
        \cs_gset_eq:NN \lx@orig@list@begin: \__block_list_begin:
        \cs_gset:Npn \__block_list_begin:
          {
            \tl_if_empty:NF \lx@ol@class
              {
                \tl_set_eq:NN \l__tag_L_attr_class_tl \lx@ol@class
                \tl_clear:N \lx@ol@class
              }
            \lx@orig@list@begin:
          }
      }
  }
\ExplSyntaxOff
\def\lx@openlist#1{\begin{list}{}{#1}}
\def\lx@closelist{\end{list}}
%% --- public API (Protocol A: the list funnel) -------------------------
%% EVERY list a front-end opens must go through this pair.  Bypassing it
%% with \list/\begin{list} loses the /ListNumbering attribute patched in
%% above, and -- because the patch is keyed to the ENVIRONMENT hooks, which
%% command-form \list never fires -- corrupts the structure tree for an
%% example inside a footnote.  See the comment above \lx@ol@class.
\ExplSyntaxOn
\cs_new_eq:NN \lx_list_open:n \lx@openlist
\cs_new_eq:NN \lx_list_close: \lx@closelist
\ExplSyntaxOff
%% What every example list wants, at all three levels: no glue anywhere
%% the surrounding text could see, and the label set flush left in its own
%% box (\lx@flushlabel, not the list default, which right-aligns a label
%% narrower than \labelwidth).  Only \topsep and the level geometry differ
%% between the three, so only those are set beside this.
%% Two font hooks and a page-breaking one, all empty (or \relax) unless
%% [langsci] fills them in: they are what \exfont / \exnrfont and the
%% [lowerpenalty] option of langsci-gb4e have to reach, and there is no
%% other seam in the list that a declaration could be attached to.  Empty
%% is exactly what every document that never loads the option gets, and
%% \lx@bodyfont sits at the end of the parameter block so that a font it
%% selects is the one the item bodies are set in.
\let\lx@bodyfont\@empty
\let\lx@labelfont\@empty
\let\lx@itempenalty\relax
\def\lx@listdefaults{%
  \itemindent\z@ \listparindent\z@ \parsep\z@ \itemsep\z@
  \partopsep\z@ \parskip\z@
  \lx@itempenalty
  \let\makelabel\lx@flushlabel
  \lx@bodyfont}
\def\lx@mainlist{%
  \lx@openlist{\topsep\Extopsep
     \lx@geom@main
     \lx@listdefaults}}

%% In the exe environment the list opens before any item exists, so the
%% label the width is derived from is the one the NEXT \ex will print.
\def\lx@guesslabel{%
  \if@noftnote
    \def\lx@itemlabel{\theExLBr\the\numexpr\value{ExNo}+1\relax\theExRBr}%
  \else
    \def\lx@itemlabel{\theFnExLBr
      \romannumeral\numexpr\value{FnExNo}+1\relax\theFnExRBr}%
  \fi}
%% --- public API (Protocol A: opening the main list by hand) -----------
%% \lx_item_main: opens the list for you.  These two are for the BATCH
%% shape, where one list holds several examples: open it once with
%% \lx_label_guess: + \lx_list_main_open:, then give it items with
%% \lx_item_next:.  \lx_label_guess: is not optional in that shape --
%% under [legacy] the list geometry is read off a label that does not
%% exist yet, so it stands in the label the next item will print.
\ExplSyntaxOn
\cs_new_eq:NN \lx_list_main_open: \lx@mainlist
\cs_new_eq:NN \lx_label_guess:    \lx@guesslabel
\ExplSyntaxOff

%% No TeX group wraps the example as a whole: the list environment's own
%% group scopes everything set after the list opens, and wrapping the
%% environment in one extra group is exactly the pattern that trips the
%% text-unit accounting of the 2023 latex-lab "block" tagging code when
%% the example sits in a footnote.  What used to rely on that group is
%% now handled explicitly: \lx@inexample is reset in \lx@bodyend, and
%% \@currentlabel/\@currentHref (set locally by \refstepcounter BEFORE
%% the list opens, because [legacy] sizes the label box off the label)
%% are saved here and restored in \lx@bodyend, so a \label after the
%% example still refers to whatever preceded it.
%% The setup half is factored out under its own name because it, and not
%% \lx@run@ex, is what a front-end needs: \lx@run@ex additionally commits
%% to the dot syntax's \lx@exstart (which peeks for a "[" custom label)
%% and to the body arriving as one grabbed argument.  A front-end that
%% builds its items itself calls \lx@example@begin, then whichever item
%% commands it likes, then \lx@bodyend.
\def\lx@example@begin{%
  \ifdim\lastskip=\Extopsep\vspace{\Exredux}\fi
  \lx@subdepth\z@
  \setcounter{SubExNo}{0}\setcounter{SubSubExNo}{0}%
  \let\lx@saved@currentlabel\@currentlabel
  \ifdefined\@currentHref
    \let\lx@saved@currentHref\@currentHref
  \fi
  \lx@inexampletrue
  \lx@letters@on}
\long\def\lx@run@ex#1{\lx@example@begin\lx@exstart#1\lx@bodyend}

% closes the example: all open sublists, the main list, the group.
% Doubles as the sentinel marking the end of the grabbed body, which is
% what allows \z. to terminate the example early (see below).
\def\lx@bodyend{%
  \lx@glt@langend % while the \glt paragraph is still the current one
  \lx@closesubs
  \lx@closelist
  \lx@example@cleanup\ignorespacesafterend}
%% --- public API (Protocol A: the example lifecycle) -------------------
%% One example, start to finish.  \lx_example_end: closes every sublist
%% still open, so a front-end never has to track depth itself.
%%
%% \lx_example_end: is also the SENTINEL that marks the end of a grabbed
%% body -- \z. terminates an example early by scanning ahead to it (see
%% \lx@zexit).  A front-end may call it, but must not \let it to
%% something else, wrap it in a group, or place a copy of it anywhere but
%% at the very end of the example.
\ExplSyntaxOn
\cs_new_eq:NN \lx_example_begin: \lx@example@begin
\cs_new_eq:NN \lx_example_end:   \lx@bodyend
\ExplSyntaxOff
%% Under the tagged-PDF testphase code, the block layer wraps top-level
%% blocks in a "text-unit" structure element and defers its close to the
%% @endpe continuation paragraph.  Since the cleanup above cancels that
%% continuation, it must also perform the close the continuation's
%% para/end hook would have performed -- the same two operations, taken
%% from tagpdf's own end plug.  The internal names are deliberately
%% existence-guarded: this compensation is matched to the current
%% testphase internals and degrades to a no-op if they change.
\ExplSyntaxOn
\cs_new_protected:Npn \lx@tag@close@textunit
  {
    \lx@tag@if@active:T
      { \cs_if_exist:NT \__tag_gincr_para_main_end_int:
          { \__tag_gincr_para_main_end_int: \tag_struct_end: } }
  }
\ExplSyntaxOff
\def\lx@example@cleanup{%
  \lx@inexamplefalse
  \lx@inlangscifalse
  \lx@letters@off
  % The list's \end arms the "continue the paragraph" dance (@endpe +
  % an \everypar that clears it).  Our semantics is the opposite: a
  % blank line after an example starts a NEW, indented paragraph
  % (same-line continuation is handled explicitly by \z.).  Clear BOTH
  % halves of the dance, the way the kernel's own \everypar token
  % would: clearing only one of them is what desynchronises the tagged
  % text-unit bookkeeping.
  \if@endpe\everypar{}\@endpefalse\lx@tag@close@textunit\fi
  \let\@currentlabel\lx@saved@currentlabel
  \ifdefined\lx@saved@currentHref
    \let\@currentHref\lx@saved@currentHref
  \fi}

%% \lx@exstart@* : label, THEN list, THEN item (the order matters under
%% [legacy], where the list geometry is read off the label).
%% \lx@mainitem is the same thing minus the list, for an item inside an
%% exe environment, whose list is already open.
\def\lx@exstart{\@ifnextchar[{\lx@exstart@opt}{\lx@exstart@normal}}
%% The custom-label item, separated from the bracket syntax that reaches
%% it, so that a front-end can have custom labels without having to spell
%% them "[...]".  Note the order the label is set in: BEFORE \lx@mainlist,
%% because [legacy] sizes the label box from the label itself.
\def\lx@custom@item#1{\def\lx@itemlabel{#1}\lx@mainlist\lx@scanjudge}
%% The same, for a front-end whose list is ALREADY open -- an item inside a
%% batch, which is where [langsci]'s \exi, \exr, \exp and \sn live.  It
%% steps no counter and so records no anchor, exactly as \lx@custom@item
%% does not; that is the whole point of a custom label.
\def\lx@custom@item@here#1{\def\lx@itemlabel{#1}\lx@scanjudge}
\def\lx@exstart@opt[#1]{\lx@custom@item{#1}}
%% \lx@relref@record notes the hyperref anchor this example just got, so
%% that a later run's \Next can tell a number the document reaches from
%% one it does not.  It sits HERE, after the counter step, because that
%% is where hyperref has made the anchor and set \@currentHref; and it
%% sits here ONLY, so that the custom-label item above -- which steps no
%% counter and therefore has no anchor -- records nothing.  It expands to
%% nothing at all unless the links are switched on.
%% \lx@endprevpar closes the PREVIOUS item's paragraph before the counter
%% is stepped, and every counter-stepping core below starts with it.  It
%% looks redundant -- \item ends that paragraph anyway, a few tokens later
%% -- and is not: hyperref hangs the example's destination off
%% \refstepcounter, through \Hy@raisedlink, which in horizontal mode
%% contributes a zero-width \smash box to the line being built.  Stepping
%% the counter first therefore leaves the NEXT example's anchor at the end
%% of the PREVIOUS example's last line, where it does two kinds of damage:
%%   - the line breaker may put that box, and the \parfillskip after it, on
%%     a line of its own.  On a full line it does, and the example is
%%     followed by an empty line -- a hole between (b) and (c) with nothing
%%     in the source to explain it, which is how this was found;
%%   - across a page break the destination lands on the page the previous
%%     example ends on, so \ref jumps one page short.
%% In vertical mode \Hy@raisedlink adds the destination to the vertical
%% list and neither happens, which is why the outermost \ea -- reached in
%% vertical mode already -- never showed the bug.
%% \unskip twice, and the \if@inlabel guard, are \@item's own: \item is
%% what would otherwise be doing this, and a pending label (an \item whose
%% paragraph has not started) must be left for it to open.
\def\lx@endprevpar{\if@inlabel\else\ifhmode\unskip\unskip\par\fi\fi}
\def\lx@main@core{%
  \lx@endprevpar
  \if@noftnote
    \refstepcounter{ExNo}\def\lx@itemlabel{\lx@labelExNo}%
  \else
    \refstepcounter{FnExNo}\def\lx@itemlabel{\lx@labelFnExNo}%
  \fi
  \lx@relref@record}
\def\lx@exstart@normal{\lx@main@core\lx@mainlist\lx@scanjudge}
\def\lx@mainitem{\lx@main@core\lx@scanjudge}
%% --- public API (Protocol A: top-level items) -------------------------
%% \lx_item_main:    step the counter, OPEN the main list, then item.
%%                   For an example that brings its own list, i.e. the
%%                   \ex. case.
%% \lx_item_main:n   the same with the given label, and no counter step
%%                   (the \ex.[custom] case).
%% \lx_item_next:    step the counter and item, WITHOUT opening a list:
%%                   for a second example inside a batch whose list is
%%                   already open, i.e. the \ex inside exe case.
%% All three end by scanning for a judgment and emitting the item, so the
%% example text may follow immediately.
\ExplSyntaxOn
\cs_new_eq:NN \lx_item_main:  \lx@exstart@normal
\cs_new_eq:NN \lx_item_main:n \lx@custom@item
\cs_new_eq:NN \lx_item_next:  \lx@mainitem
\ExplSyntaxOff

% the label box: number flush left; an auto-detected judgment is hung
% into the margin at the text edge via \jdg's zero-width \llap, so
% example texts align whether or not they carry a judgment, at every
% nesting level and for marks of any width.  The kernel \item is used
% under a saved name, because the xlist environment binds \item to the
% sub-example machinery locally.
\let\lx@kernel@item\item
\AtBeginDocument{\let\lx@kernel@item\item}
%% The label is set flush left in a box of exactly \labelwidth.  Forcing
%% the width (rather than the older trick of a trailing \hfil in a
%% natural-width label) is what makes flush-left survive PDF tagging: the
%% latex-lab block code re-boxes any label narrower than \labelwidth to
%% \labelwidth using its own alignment, which defaults to flush RIGHT; a
%% label that already fills \labelwidth is left untouched.  Classic
%% (untagged) output is identical.
\def\lx@flushlabel#1{\makebox[\labelwidth][l]{\lx@labelfont#1}}
%% \lx@relref@anchor places the hyperref destination for this example when
%% linguexx is the one making them (see \lx_relref_record:).  It goes
%% after the judgment, so the mark still hangs from the label edge, and
%% before \ignorespaces, which must stay last.  It expands to nothing at
%% all in a document whose anchors hyperref is placing itself.
\def\lx@makeitem{\lx@kernel@item[\lx@itemlabel]\lx@emitjudge
  \lx@relref@anchor\ignorespaces}
%% --- public API (Protocol A: emitting the item) -----------------------
%% Emit the \item for the label currently in \lx@itemlabel and hang any
%% judgment collected since the last scan.  The \lx_item_... commands
%% above already end here; this is for a front-end that sets a label and
%% a judgment by itself.
\ExplSyntaxOn
\cs_new_eq:NN \lx_item_emit: \lx@makeitem
\ExplSyntaxOff

%% sub-examples ------------------------------------------------------------
%% \a. pushes a level; the depth advance happens INSIDE the new group.
%%
%% The letters \a-\f collide head-on with the kernel accent commands \b, \c
%% and \d, and BOTH meanings have to work, including in the same example:
%%
%%     \ex. \a. Fran\c cois est fatigue\'e.     % \c = cedilla
%%          \b. \Ca c'est chiant.               % \b = sub-example
%%
%% so they cannot simply be switched over wholesale.  Each letter therefore
%% DISPATCHES ON WHAT FOLLOWS IT: a period means the sub-example command,
%% anything else -- "{", a letter, a space -- hands over to the accent, i.e.
%% to whatever the letter meant before linguexx touched it.  "\c{c}" and an
%% inputenc-decomposed "ç" (which is literally "\c c") both take the accent
%% branch and work anywhere, inside an example as well as outside.
%%
%% Two things make this safe that an earlier attempt got wrong:
%%
%%  - The hooks are \protected.  hyperref rebuilds its bookmark strings by
%%    \edef-expanding the title; an unprotected \futurelet peek runs during
%%    that expansion and dies ("Use of \lx@dot@c doesn't match its
%%    definition"), or, in the very first version of this code, silently
%%    dropped the accent from heading and outline alike.  \protected leaves
%%    the token untouched in an \edef, so hyperref sees the accent command
%%    it expects.
%%  - Only \a is hooked GLOBALLY, for the whole document; \b-\f are hooked
%%    just where a sub-level is reachable (\lx@letters@local below).  \a has
%%    to be global because "\a." must be able to OPEN a level, before any
%%    sub-level exists -- and it is the one letter that is safe to hold
%%    globally, being no accent at all (in the kernel it is only meaningful
%%    inside tabbing) and not one of hyperref's PU bookmark accents.
%%    Holding \b/\c/\d globally is what clobbered them document-wide.
\def\lx@dot@a.{\lx@nomix@dot{\string\a.}\lx@subpush}
\def\lx@dot@b.{\lx@nomix@dot{\string\b.}\lx@subnext}
\def\lx@dot@c.{\lx@nomix@dot{\string\c.}\lx@subnext}
\def\lx@dot@d.{\lx@nomix@dot{\string\d.}\lx@subnext}
\def\lx@dot@e.{\lx@nomix@dot{\string\e.}\lx@subnext}
\def\lx@dot@f.{\lx@nomix@dot{\string\f.}\lx@subnext}
%% \@ifnextchar skips spaces before peeking, so "\c c" (the inputenc
%% expansion of "ç") and "\c{c}" both reach \lx@kernel@c with their argument
%% intact, while "\c." and even "\c ." reach \lx@dot@c.
\protected\def\lx@hook@a{\@ifnextchar.\lx@dot@a\lx@kernel@a}
\protected\def\lx@hook@b{\@ifnextchar.\lx@dot@b\lx@kernel@b}
\protected\def\lx@hook@c{\@ifnextchar.\lx@dot@c\lx@kernel@c}
\protected\def\lx@hook@d{\@ifnextchar.\lx@dot@d\lx@kernel@d}
\protected\def\lx@hook@e{\@ifnextchar.\lx@dot@e\lx@kernel@e}
\protected\def\lx@hook@f{\@ifnextchar.\lx@dot@f\lx@kernel@f}
%% Capture the accent meanings the hooks fall back to.  Called at load time
%% AND \AtBeginDocument, so a package loaded after us that rebinds one of
%% these letters is still picked up; idempotent, because re-capturing a hook
%% as its own fallback would loop forever.  \e and \f are undefined in the
%% kernel, so they fall back to a package error rather than to \relax, which
%% would silently swallow "\e something".
\def\lx@nokernel#1{%
  \PackageError{linguexx}{\string#1\space is not an accent command}%
    {\string#1\space only means something in this package as
     \string#1. (with a period), continuing a sub-example.}}
\def\lx@install@letters{%
  \ifx\a\lx@hook@a\else \let\lx@kernel@a\a \fi
  \ifx\b\lx@hook@b\else \let\lx@kernel@b\b \fi
  \ifx\c\lx@hook@c\else \let\lx@kernel@c\c \fi
  \ifx\d\lx@hook@d\else \let\lx@kernel@d\d \fi
  \ifx\e\lx@hook@e\else
    \ifdefined\e \let\lx@kernel@e\e \else \def\lx@kernel@e{\lx@nokernel\e}\fi
  \fi
  \ifx\f\lx@hook@f\else
    \ifdefined\f \let\lx@kernel@f\f \else \def\lx@kernel@f{\lx@nokernel\f}\fi
  \fi
  \let\a\lx@hook@a}
%% Two activation flavours, because the two syntaxes differ in grouping:
%%
%% \lx@letters@local -- plain \let, NO save.  For use where a TeX group
%%   already scopes the change and will undo it: the exe/xlist
%%   environments, and the \begingroup that \lx@subpush opens for a new
%%   sub-level.  Nests correctly by construction (each group unwinds to
%%   whatever the enclosing one had), which a single set of save slots
%%   could not do -- exe > xlist > ... does nest, even though examples
%%   themselves do not.
%% \lx@letters@on / @off -- explicit save and restore.  For the dot-syntax
%%   example path ONLY, which deliberately runs without a group of its own
%%   (see \lx@run@ex: an enclosing group breaks the tagged text-unit
%%   accounting for examples in footnotes).  Saves whatever the letters
%%   mean right before this example, so it is correct regardless of load
%%   order relative to hyperref and of anything that rebinds them between
%%   two examples.  Examples do not nest, so one slot per letter suffices.
%% Only [lazy] has the dot letters at all: under [gb4e] alone \a.-\f. are
%% documented not to exist, and the accents must stay untouched even inside
%% exe/xlist, so the activation has to be a no-op there -- the sub-level
%% openers below are shared by both syntaxes and would otherwise clobber
%% \b/\c/\d inside a gb4e batch.
\iflx@lazy
  \def\lx@letters@local{%
    \let\a\lx@hook@a \let\b\lx@hook@b \let\c\lx@hook@c
    \let\d\lx@hook@d \let\e\lx@hook@e \let\f\lx@hook@f}
\else
  \let\lx@letters@local\relax
\fi
\def\lx@letters@on{%
  \let\lx@saved@a\a \let\lx@saved@b\b \let\lx@saved@c\c
  \let\lx@saved@d\d \let\lx@saved@e\e \let\lx@saved@f\f
  \lx@letters@local}
\def\lx@letters@off{%
  \let\a\lx@saved@a \let\b\lx@saved@b \let\c\lx@saved@c
  \let\d\lx@saved@d \let\e\lx@saved@e \let\f\lx@saved@f}
%% The dot shorthands (\z. and the glossed \exg. \ag.-\fg.) are claimed
%% \AtBeginDocument, and only if the name is still free.  Both halves matter,
%% and each fixes a different failure:
%%
%%  - Claiming them AT LOAD TIME is what broke French.  babel-french defines
%%    \fg, the closing guillemet of \og...\fg; with linguexx loaded first,
%%    babel's own \newcommand\fg hit "Command \fg already defined" and the
%%    document did not build at all.  Deferring lets babel get there first.
%%  - Claiming them UNCONDITIONALLY is the other half.  With babel loaded
%%    first, the plain \def silently overwrote babel's \fg, and every
%%    \og...\fg in the document lost its closing guillemet -- no error, just
%%    a missing character.
%%
%% So a name someone else already owns is left alone, and the fact is put in
%% the log.  The cost is that \fg. is unavailable in a French document: write
%% \f. and then \gll instead.  This is not only about babel -- \eg is a very
%% common user-defined abbreviation for "e.g.", and the same guard hands it
%% back to whoever defined it.
\def\lx@claim@shorthand#1#2{%
  \@ifundefined{#1}%
    {\expandafter\def\csname #1\endcsname.{#2}}%
    {\PackageInfo{linguexx}{\@backslashchar#1\space is already defined;
       \@backslashchar#1.\space is therefore not available}}}
%% The same policy for a plain alias rather than a dot shorthand: \altn and
%% \altg are offered as short names for \lxAltn and \lxAltg, and the long
%% names always work.  Everything the block above says applies unchanged --
%% deferred so that a package loaded after us gets there first, and
%% conditional so that a name someone else owns is left alone rather than
%% silently overwritten.  The \global is what makes the \let outlast the
%% \AtBeginDocument hook's own grouping under some classes.
%%
%% Both are here, next to the shorthand claim, so that the two ways this
%% package takes a user-visible name sit under one rationale.  They were
%% written out separately at their own definitions, and the copies had
%% already drifted to a different message.
\def\lx@claim@alias#1#2{%
  \AtBeginDocument{%
    \@ifundefined{#1}%
      {\expandafter\global\expandafter\let\csname #1\endcsname#2}%
      {\PackageInfo{linguexx}{\@backslashchar#1\space is already defined;
         \MessageBreak only \string#2\space is available}}}}
%% Under [langsci] the name "z" is claimed by \lx@langsci@install instead,
%% which installs a \z that dispatches on the period: bare \z closes an \ea
%% level, \z. is the dot-syntax pop.  Claiming it twice would leave whichever
%% ran last, and the shorthand claim backs off silently when the name is
%% taken -- so [lazy,langsci] would have lost \z. depending on load order.
\def\lx@claim@shorthands{%
  \iflx@langsci\else\lx@claim@shorthand{z}{\lx@zpop}\fi
  \lx@claim@shorthand{exg}{\lx@exg@start}%
  \lx@claim@shorthand{ag}{\a.\lx@glosshead}%
  \lx@claim@shorthand{bg}{\b.\lx@glosshead}%
  \lx@claim@shorthand{cg}{\c.\lx@glosshead}%
  \lx@claim@shorthand{dg}{\d.\lx@glosshead}%
  \lx@claim@shorthand{eg}{\e.\lx@glosshead}%
  \lx@claim@shorthand{fg}{\f.\lx@glosshead}}
\iflx@lazy
  \lx@install@letters
  \AtBeginDocument{\lx@install@letters\lx@claim@shorthands}
\fi
% Under [gb4e] alone, \lx@install@letters is never called, so \a is not
% hooked either: \z., the glossed shorthands and \a.-\f. do not exist,
% and \a plus the kernel accents \b, \c, \d (and hyperref's use of them)
% stay untouched everywhere, exactly as before.  \lx@letters@local is
% still reached from exe/xlist under [gb4e], but only INSIDE those
% environments' own groups, where the dot letters are what the gb4e
% syntax documents as mixable.

%% \z. pops exactly ONE level, counting the surrounding prose as the
%% outermost level: from the roman level it returns to the letters (a
%% following \b. continues there); from the LETTER level, or in an
%% example without open sub-examples, it ENDS the example.  Consecutive
%% \z.'s therefore pop successively out of the example.  The rest of
%% the body after an example-ending \z. is reinjected after the close
%% (grabbed up to the \lx@bodyend sentinel): on the same line it is a
%% flush-left continuation, after a blank line an ordinary indented
%% paragraph.  \z. is interpreted at typesetting time and must stand at
%% brace depth 0 of the body; outside an example it is a package error.
%%
%% What the pop is gated on is \lx@subdepth, NOT \iflx@inexample: the
%% latter is set by the dot-syntax path alone, so gating the whole
%% command on it made \z. unusable inside an exe batch -- where "\a."
%% legitimately opens a sub-level through the global \a hook, and
%% nothing but \z. could close it again.  (Without the pop, the next
%% \ex in the batch was silently demoted to a sub-item, because
%% \lx@gbex@plain dispatches on \lx@subdepth.)  Only the branch that
%% ENDS the example stays gated on \iflx@inexample: an exe batch is
%% ended by \end{exe}, and \lx@zexit's \lx@bodyend sentinel does not
%% exist there at all.
\newif\iflx@inexample
\def\lx@zpop{%
  \ifnum\lx@subdepth>\@ne
    \let\lx@donext\lx@zpop@one   % roman level: back to the letters
  \else\iflx@inexample
    \let\lx@donext\lx@zexit      % dot syntax: the letter level ends it
  \else\ifnum\lx@subdepth>\z@
    \let\lx@donext\lx@zpop@one   % exe batch: letter level -> main level
  \else\iflx@inexe
    \let\lx@donext\lx@zpop@exeerr
  \else
    \let\lx@donext\lx@zpop@err
  \fi\fi\fi\fi
  \lx@donext}
%% \lx@glt@langend before the list closes, for the reason \end{xlist} does
%% it: a \glt language Span is opened from \everypar and has to be closed
%% while the translation paragraph is still the current one.  This pop is
%% the third exit an example has (\lx@bodyend and the environment ends are
%% the others) and it was the one that did not close it, so a \glt with a
%% \GlossTransLang inside a roman level, popped by \z. with the example
%% continuing after, left the Span open across the list close and failed
%% veraPDF on all three profiles.  Nothing showed on the page and no
%% structure assertion saw it; ua.tex now carries the shape.
\def\lx@zpop@one{\lx@glt@langend\lx@closelist\endgroup}
% Text on the same line as a main-level \z. is a CONTINUATION: it is set
% flush left under the example (no \parindent), separated from it by the
% example's normal bottom space, and closed with \par (the source's own
% terminating \par was consumed as the collection delimiter).  If the
% rest is blank -- i.e. \z. is followed by one or more empty lines --
% nothing is reinjected, and the next source paragraph is an ordinary,
% class-indented paragraph.
\ExplSyntaxOn
\cs_new_protected:Npn \lx@zexit #1 \lx@bodyend
  {
    \lx@bodyend
    \tl_if_blank:nF {#1} { \noindent \ignorespaces #1 \par }
  }
\ExplSyntaxOff
\def\lx@zpop@err{%
  \PackageError{linguexx}{\string\z. outside an example}%
    {\string\z. closes one sub-example level, or ends the example when
     at its main level.}}
\def\lx@zpop@exeerr{%
  \PackageError{linguexx}{\string\z. at the main level of an exe batch}%
    {Inside \string\begin{exe} ... \string\end{exe}, \string\z. closes an
     open sub-example level (one opened by \string\a.). At the main level
     of the batch there is nothing to close: end the batch with
     \string\end{exe}.}}

%% \exg. / \ag. ... \fg. : glossed shorthands, expanding to
%% \ex. \gll resp. \a. \gll etc., with judgment detection re-run in
%% front of the gloss so that "\exg. *Das ist ..." hangs the star left
%% of the first column:
\def\lx@glosshead{\lx@scanjudgeto{\lx@emitjudge\gll}}
%% \exg.[label] takes the custom label \ex.[label] takes -- and it has to
%% be spelt out, because the plain expansion cannot deliver it: \exg. puts
%% \lx@glosshead in front of the body, so the "[" that \lx@exstart peeks
%% for is no longer the first thing it sees, and the label came out as the
%% first word of the object line while the example took a number of its
%% own.  Here the bracket is read BEFORE \ex. is, and handed on in the one
%% position \lx@exstart looks at.
%%
%% GLUED, and deliberately not \@ifnextchar: after \exg. a bracket that a
%% space separates is the first word of the object line, which is a thing
%% this package documents and supports -- "[" is one of the openers
%% \GlossPhantomChars lists, and phantom alignment exists to hang it in
%% the gutter.  \@ifnextchar skips spaces, so it cannot tell
%% "\exg.[(7')] Der Hund ..." from "\exg. [DP der Hund] bellte.", and
%% would take the constituent bracket of every such gloss for a label,
%% leaving the example unnumbered and its tiers a word out of step.  So
%% the two are told apart the one way they differ: an optional argument is
%% written against the command it belongs to, as in \ex.[(7')] and in the
%% manual's every example of one.  \ex. itself keeps \@ifnextchar (it has
%% no object line for a bracket to open, and its spaced form is old
%% documented behaviour); this is the difference between them, and it is
%% in the manual.
\let\lx@bracketchar=[
\def\lx@exg@start{\futurelet\lx@peeked\lx@exg@peek}
\def\lx@exg@peek{%
  \ifx\lx@peeked\lx@bracketchar
    \expandafter\lx@exg@opt
  \else
    \expandafter\lx@exg@plain
  \fi}
\def\lx@exg@opt[#1]{\ex.[#1]\lx@glosshead}
\def\lx@exg@plain{\ex.\lx@glosshead}

\def\lx@subpush{%
  \ifcase\lx@subdepth
    \let\lx@donext\lx@subpush@i
  \or
    \let\lx@donext\lx@subpush@ii
  \else
    \let\lx@donext\lx@subpush@err
  \fi
  \lx@donext}

% the two sub-level list declarations, shared by the dot-command path
% (\a.) and the environment path (xlist)
\def\lx@sublist@i{%
  \setcounter{SubExNo}{0}%
  \lx@openlist{\lx@geom@sub \lx@listdefaults}}
\def\lx@sublist@ii{%
  \setcounter{SubSubExNo}{0}%
  \lx@openlist{\lx@geom@subsub \lx@listdefaults}}

% depth 0 -> open letter level.  \lx@letters@local goes right after the
% \begingroup, so \b.-\f. are live for the rest of this level (and are
% undone by the matching \endgroup in \lx@closesubs / \z.).  It matters
% here and not only in \lx@run@ex: "\a." inside an exe batch reaches this
% path through the global \a hook, with no \lx@run@ex anywhere.
%
% Which is also why the guard accepts \iflx@inexe as well as
% \iflx@inexample.  What it rejects is a stray "\a." in ordinary prose,
% with no example of either kind open: that used to open a list and a
% \begingroup that nothing would ever close, and the failure surfaced as
% "\begin{list} ended by \end{document}" arbitrarily far away, naming
% neither \a. nor the line it stood on.  Erring instead of pushing (the
% same shape as \lx@subpush@err one level down) keeps the document in a
% state the error message describes.
\def\lx@subpush@i{%
  \iflx@inexample
    \let\lx@donext\lx@subpush@i@go
  \else\iflx@inexe
    \let\lx@donext\lx@subpush@i@go
  \else
    \let\lx@donext\lx@subpush@outside@err
  \fi\fi
  \lx@donext}
%% The push is the open plus the item.  They are separated because two
%% front-ends need the open alone: the xlist environment (whose items come
%% from \ex) and \eal (which opens the letter level under a head example
%% that has no text of its own).  \lx@subopen@* must be called only where
%% the depth dispatch has already happened -- \lx@subpush is that dispatch.
\def\lx@subopen@i{%
  \begingroup\advance\lx@subdepth\@ne
  \lx@letters@local
  \lx@sublist@i}
\def\lx@subopen@ii{%
  \begingroup\advance\lx@subdepth\@ne
  \lx@letters@local
  \lx@sublist@ii}
\def\lx@subpush@i@go{\lx@subopen@i\lx@subitem}
\def\lx@subpush@outside@err{%
  \PackageError{linguexx}{\string\a. with no example to attach it to}%
    {A sub-example has to sit inside an example: open one with
     \string\ex.\space (or \string\begin{exe}) first.\MessageBreak
     Left to stand, \string\a.\space here would open a list that nothing
     closes, and you would be told about it as "\string\begin{list} ended
     by \string\end{document}" instead.}}

% depth 1 -> open roman level
\def\lx@subpush@ii{\lx@subopen@ii\lx@subsubitem}

%% Reachable from both syntaxes since [langsci], so it has to name the
%% command the writer actually typed: an \ea document may contain no \a.
%% at all, and under [langsci] alone the dot letters do not exist.
\def\lx@subpush@err{%
  \PackageError{linguexx}{Only two sub-example levels are supported}%
    {Remove the third level: \iflx@inlangsci the innermost
     \string\ea\space and its \string\z.\else the third \string\a. and
     its \string\z.\fi}}

\def\lx@subnext{%
  \ifcase\lx@subdepth
    \let\lx@donext\lx@subnext@err
  \or
    \let\lx@donext\lx@subitem
  \or
    \let\lx@donext\lx@subsubitem
  \fi
  \lx@donext}

\def\lx@subnext@err{%
  \PackageError{linguexx}{\string\b. (or \string\c. etc.) without a
    preceding \string\a.}{Start the sublevel with \string\a. first.}}

%% --- public API (Protocol A: sub-levels) ------------------------------
%% \lx_sub_push:  open a NEW, deeper sub-level (letters, then romans) and
%%                give it its first item.  Two levels maximum; a third is
%%                a package error.
%% \lx_sub_next:  the next item at the level currently open.
%% The depth is not a parameter and cannot be set: \lx_sub_push: advances
%% it inside the \begingroup it opens for the new level, so the grouping
%% structure IS the stack and \lx_example_end: pops whatever is left.  A
%% front-end must never advance or reset \lx@subdepth itself -- that is
%% the global-drift bug (linguex's "ExDepth disease") this design exists
%% to make impossible.
\ExplSyntaxOn
\cs_new_eq:NN \lx_sub_push: \lx@subpush
\cs_new_eq:NN \lx_sub_next: \lx@subnext
\ExplSyntaxOff

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Environment interface: exe and xlist (gb4e-compatible)             %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% begin code for the sub-level environment: the environment's own group
% plays the role of the \begingroup in \lx@subpush, so the depth
% advance is undone at the matching \end
\def\lx@subenv@begin{%
   \advance\lx@subdepth\@ne
   \lx@letters@local
   \ifcase\lx@subdepth
     \let\lx@donext\relax % unreachable
   \or
     \let\lx@donext\lx@sublist@i
   \or
     \let\lx@donext\lx@sublist@ii
   \else
     \let\lx@donext\lx@subpush@err
   \fi
   \lx@donext
   \def\item{\lx@subnext}}
%% --- public API (Protocol A: sub-levels, environment form) ------------
%% The \lx_sub_push: of an ENVIRONMENT-shaped front-end: opens the deeper
%% level without giving it an item, and lets the environment's own group
%% play the role of the \begingroup that \lx_sub_push: opens, so the depth
%% advance is undone by the matching \end.  Use this one if and only if
%% the level is bounded by an environment; \lx_sub_next: gives the items.
\ExplSyntaxOn
\cs_new_eq:NN \lx_subenv_begin: \lx@subenv@begin
\ExplSyntaxOff

%% exe is a BATCH: each \ex inside it is a new top-level example;
%% xlist embeds a sub-level (letters, then romans), with \ex as the
%% item command (plain \item works too).  Judgments are given
%% gb4e-style as \ex[*]{text} -- the optional argument may be ANY mark,
%% not only the auto-detected set -- or typed directly after a plain
%% \ex, as everywhere else in this package.  Both environments drive
%% the same engine as \ex./\a., so the two syntaxes share one counter
%% and may be mixed freely, even within one example (an xlist inside a
%% dot-command \a., or \a. inside exe).
\iflx@gbfour
\NewDocumentEnvironment{exe}{}
  {\lx@nomix@dot{\string\begin{exe}}%
   \ifdim\lastskip=\Extopsep\vspace{\Exredux}\fi
   \lx@subdepth\z@
   \setcounter{SubExNo}{0}\setcounter{SubSubExNo}{0}%
   \lx@inexetrue
   % No letter activation here: "\a. inside exe" (documented as mixable)
   % reaches the dot syntax through the global \a hook, and \lx@subpush
   % then activates \b.-\f. inside the \begingroup it opens for the level.
   \lx@guesslabel
   \lx@mainlist
   % consecutive examples in a batch get the same vertical rhythm as
   % consecutive \ex. examples (net \Extopsep between them)
   \itemsep\Extopsep}
  % A "\a." inside the batch opened a sub-level through \lx@subpush, i.e.
  % a \begingroup that only \lx@closesubs closes; \end{exe} used to close
  % the main list alone and leak it, which left \lx@subdepth and the dot
  % letters stuck at the sub-level for the rest of the document.  An xlist
  % inside the batch needs nothing here: its own environment group has
  % already undone its depth advance by the time we get here.
  {\lx@glt@langend \lx@closesubs \lx@closelist}

\NewDocumentEnvironment{xlist}{}
  {\lx@nomix@dot{\string\begin{xlist}}%
   \lx@subenv@begin
   \lx@inexetrue}
  {\lx@glt@langend \lx@closelist}
\fi % iflx@gbfour

%% The printed sub-level labels, and the /ListNumbering class that has to
%% agree with them, behind one name each.  They are macros rather than
%% literals so that a front-end can open a level with a numbering of its
%% own -- [langsci]'s \xlista, \xlisti, \xlistn, \xlistA, \xlistI.  A
%% variant sets them in the environment's own group, so the choice cannot
%% outlive the list it was made for.
%%
%% Both halves must move together.  The label is what the page shows and
%% the class is what a screen reader announces; changing one and not the
%% other is a defect no rendering shows and veraPDF does not reject,
%% because /ListNumbering /LowerRoman on a list printing "A." is
%% well-formed PDF and simply false.
\def\lx@sublabel{\SubExLBr\Exalph{SubExNo}\SubExRBr}
\def\lx@subsublabel{\SubSubExLBr\Exroman{SubSubExNo}\SubSubExRBr}
\def\lx@sub@olclass{lxOLalpha}
\def\lx@subsub@olclass{lxOLroman}
\def\lx@subitem@core{%
  \lx@endprevpar
  \refstepcounter{SubExNo}%
  \def\lx@itemlabel{\lx@sublabel}}
\def\lx@subitem{\lx@subitem@core\lx@scanjudge}
\def\lx@subsubitem@core{%
  \lx@endprevpar
  \refstepcounter{SubSubExNo}%
  \def\lx@itemlabel{\lx@subsublabel}}
\def\lx@subsubitem{\lx@subsubitem@core\lx@scanjudge}

% close all sublists still open at the end of the example body;
% every \endgroup restores \lx@subdepth to its value one level up,
% so this terminates by construction
\def\lx@closesubs{%
  \ifnum\lx@subdepth>\z@
    \lx@closelist\endgroup
    \expandafter\lx@closesubs
  \fi}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Front-end: \ea ... \z (langsci-gb4e)                               %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% langsci-gb4e -- Language Science Press's fork of gb4e -- writes what exe
%% and xlist write, without the environments.  \ea opens an example, or,
%% inside one, the next level down; \z closes whatever the matching \ea
%% opened.  The depth is never spelt out, it is read off the nesting:
%%
%%   \ea               (1)
%%     \ea             (1)  a.
%%     \ex                  b.
%%     \z
%%   \z
%%
%% Everything here is a front-end over Protocol A -- \lx@example@begin,
%% \lx@guesslabel, \lx@mainlist, \lx@gbex, \lx@subopen@i/ii, \lx@bodyend --
%% and it obeys that protocol's two standing rules.  It never touches
%% \lx@subdepth (the grouping structure is the stack; every level's depth
%% advance sits inside the \begingroup that \lx@subopen@* opens, so leaving
%% pops it).  And it opens NO group around an example as a whole: see the
%% note at \lx@example@begin -- an extra group there is what trips the
%% text-unit accounting of the block tagging code for an example inside a
%% footnote.  That is also why \iflx@inlangsci has to be cleared by hand in
%% \lx@example@cleanup: there is no group to unwind it.
%%
%% Why it exists: MIGRATION.  A document in the dot syntax moves to \ea one
%% example at a time under [lazy,langsci], because both syntaxes drive this
%% package's one engine -- so they share the counter, the labels, the
%% .aux anchors and the geometry, and converting an example renumbers
%% nothing and moves no cross-reference.
%%
%% ONE SYNTAX PER EXAMPLE.  \a. inside an \ea example, and \ea inside an
%% \ex. example or an exe batch, are package errors (\lx@nomix@dot and
%% \lx@ea@mixerr).  This does NOT touch the within-example mixing that
%% [lazy,gb4e] documents and tests -- \a. inside exe, an xlist inside \a.
%% -- which stays exactly as it was: it is a shipped promise (the manual's
%% "Mixing the two syntaxes"), and it works because \a. and \ex agree about
%% what \z. means, namely "close one level".
%%
%% \ea does not agree, and cannot be made to.  It opens the level, its list
%% AND its first item in one command, so a \z. inside an \ea example would
%% close one of the three and leave the example half-open, while a \z after
%% an \a. would close the letter level and leave the letters live for the
%% rest of the document.  Both render without complaint -- the first drops
%% the following text into the wrong list, the second silently demotes the
%% next example to a sub-item -- which is the failure this package already
%% carries a scar from (see the note above \lx@zpop).  There is no right
%% answer to pick between the two readings, so neither is picked: the ban
%% turns the choice into an error message at the line that asks for it.

\iflx@langsci

%% Ragged right.  langsci-gb4e sets every \ea example \raggedright; this
%% front-end does not, and the reason is migration rather than taste.  With
%% it on, each converted example reflows against the ones not yet
%% converted, and a page diff can no longer tell a conversion MISTAKE from
%% a conversion.  \ExRaggedRight asks for langsci's own behaviour;
%% langsci's \eanoraggedright and \ealnoraggedright are provided for source
%% compatibility and set justified whatever the switch says.
%%
%% The choice is threaded through the openers as a TOKEN rather than read
%% off the flag where it is used: it has to be applied inside the list's
%% group (so that it ends with the example), while the entry point that
%% knows which variant was called runs outside it, and an assignment made
%% there would leak into every following example.
%% (\newif\iflx@earagged is declared with the option flags at the top of
%% the file, and has to be: see the note there.)
\newcommand\ExRaggedRight{\lx@earaggedtrue}
\def\lx@ea@rag{\iflx@earagged\raggedright\fi}

%% \ea: open an example, or the next level of the one already open.
%% #1 is the justification token, applied inside the list.
\def\lx@ea{\lx@ea@do\lx@ea@rag}
\def\lx@eanorag{\lx@ea@do\relax}
\def\lx@ea@do#1{%
  \iflx@inlangsci
    \def\lx@donext{\lx@ea@deeper{#1}}%
  \else\iflx@inexample
    \let\lx@donext\lx@ea@mixerr
  \else\iflx@inexe
    \let\lx@donext\lx@ea@mixerr
  \else
    \def\lx@donext{\lx@ea@top{#1}}%
  \fi\fi\fi
  \lx@donext}

%% Top level.  The shape is the exe environment's, minus the batch: guess
%% the label the item is about to print (under [legacy] the list geometry
%% is read off it -- not that [legacy] can be combined with [langsci], but
%% the funnel is shared and is used the one documented way), open the main
%% list, then let \lx@gbex give the item.  Going through \lx@gbex and not
%% \lx@mainitem is what makes \ea[*]{...} work: in langsci \ea expands to
%% "\begin{exe}\ex", so the bracket judgment belongs to the \ex it ends in.
\def\lx@ea@top#1{%
  \lx@example@begin
  \lx@inlangscitrue
  \edef\lx@ea@openline{\the\inputlineno}%
  \lx@guesslabel
  \lx@mainlist
  #1%
  \lx@gbex}

%% Inside an example: one level down, item and all.  The dispatch is
%% \lx@subpush's, but the item comes from \lx@gbex rather than from
%% \lx@subitem, again so that \ea[?]{...} is read at every level.
\def\lx@ea@deeper#1{%
  \ifcase\lx@subdepth
    \def\lx@donext{\lx@subopen@i #1\lx@gbex}%
  \or
    \def\lx@donext{\lx@subopen@ii #1\lx@gbex}%
  \else
    \let\lx@donext\lx@subpush@err
  \fi
  \lx@donext}

%% \z: close one level, or end the example when there is none open.  At
%% depth 0 that is the whole example, so it goes through \lx@bodyend, the
%% one exit every front-end uses -- which closes the translation's language
%% span, any sublist still open, the main list, and restores what
%% \lx@example@begin saved.
\def\lx@z{%
  \iflx@inlangsci
    \ifnum\lx@subdepth>\z@
      \let\lx@donext\lx@zpop@one
    \else
      \let\lx@donext\lx@bodyend
    \fi
  \else
    \let\lx@donext\lx@z@err
  \fi
  \lx@donext}

%% \eal ... \zl: an example whose body is a sub-list, i.e. a head number
%% with no text of its own and the letters underneath.  langsci defines it
%% as "\begin{exe}\ex\begin{xlist}", unconditionally top-level, and it is
%% kept that way: the reading in which a nested \eal means "item here, then
%% open the level below" is a plausible generalisation, and inventing it
%% here would be new syntax that langsci-gb4e does not have.  Nested, it
%% says so and stops.
\def\lx@eal{\lx@eal@do\lx@ea@rag}
\def\lx@ealnorag{\lx@eal@do\relax}
\def\lx@eal@do#1{%
  \iflx@inlangsci
    \let\lx@donext\lx@eal@nesterr
  \else\iflx@inexample
    \let\lx@donext\lx@ea@mixerr
  \else\iflx@inexe
    \let\lx@donext\lx@ea@mixerr
  \else
    \def\lx@donext{\lx@eal@top{#1}}%
  \fi\fi\fi
  \lx@donext}
%% \lx@item@judged{} and not \lx@mainitem: the head item takes no text, so
%% there is nothing to scan for a judgment, and \lx@scanjudge would peek at
%% \lx@subopen@i instead of at the input.  The empty argument sets an empty
%% mark, which \lx@emitjudge skips.
\def\lx@eal@top#1{%
  \lx@example@begin
  \lx@inlangscitrue
  \edef\lx@ea@openline{\the\inputlineno}%
  \lx@guesslabel
  \lx@mainlist
  #1%
  \lx@item@judged{}%
  \lx@subopen@i}
\def\lx@zl{\lx@z\lx@z}

%% Errors.  Each names the syntax the example is actually in, because that
%% is the thing the writer has to know and the one thing the line they are
%% looking at does not say.
\def\lx@ea@mixerr{%
  \PackageError{linguexx}{\string\ea\space inside an example written in
    the other syntax}%
   {One example is written in ONE syntax throughout, and this one was
    opened with \string\ex.\space or \string\begin{exe}.\MessageBreak
    Convert the whole example to \string\ea\space ... \string\z, or write
    this level the way the example started: \string\a.\space for the dot
    syntax, \string\begin{xlist} for the environments.\MessageBreak
    Between examples the two mix freely -- that is what [lazy,langsci]
    is for.}}
\def\lx@z@err{%
  \PackageError{linguexx}{\string\z\space with no \string\ea\space to
    close}%
   {\string\z\space closes a level opened by \string\ea.\MessageBreak
    An exe batch ends at \string\end{exe}\iflx@lazy, and an example opened
    with \string\ex.\space ends at a blank line or early at \string\z.
    (with the period)\fi.}}
\def\lx@eal@nesterr{%
  \PackageError{linguexx}{\string\eal\space inside an example}%
   {\string\eal\space opens a top-level example whose body is a
    sub-list; it has no nested meaning, in this package or in
    langsci-gb4e.\MessageBreak
    Write \string\ea\space here, and open the level below it with a
    second \string\ea.}}

%% Installing the names.  This claim is louder than \lx@claim@shorthand,
%% which backs off silently when a name is taken: \z. and \eg are
%% conveniences, and somebody else's \eg is more likely to be theirs than
%% ours.  \ea and \z are not conveniences -- they ARE the syntax the option
%% was loaded for, and a document that lost them would lose every example
%% it wrote, silently.  So the collision is a warning and the name is
%% taken.  It earns its keep: \ea is a very common private abbreviation for
%% \expandafter, and this is where its owner finds out.
%%
%% Run at load time AND \AtBeginDocument, like \lx@install@letters and for
%% the same reason: a package loaded after this one may rebind them.
%% Comparing against our own meaning first keeps the second run quiet.
\def\lx@langsci@claim#1#2{%
  \@ifundefined{#1}{}%
    {\expandafter\ifx\csname #1\endcsname#2\else
       \expandafter\ifx\csname #1\endcsname\relax\else
         \PackageWarning{linguexx}{\@backslashchar#1\space was already
           defined; [langsci] has taken the name over}%
       \fi
     \fi}%
  \expandafter\let\csname #1\endcsname#2}
%% Under [lazy,langsci] the name "z" carries both syntaxes' pops and
%% dispatches on the period, which is the whole of what the two spellings
%% have to tell apart: bare \z closes an \ea level, \z. is the dot-syntax
%% pop.  \@ifnextchar skips spaces before peeking, so "\z ." reaches the
%% dot form too -- the same reading \lx@hook@c gives "\c .".
\iflx@lazy
  \def\lx@z@dispatch{\@ifnextchar.{\lx@z@dot}{\lx@z}}
  \def\lx@z@dot.{\lx@nomix@dot{\string\z.}\lx@zpop}
\else
  \let\lx@z@dispatch\lx@z
\fi
%% An \ea that is never closed.  This is the one mistake the syntax makes
%% easy and the other two cannot make at all: an \ex. example ends at a
%% blank line and an exe batch at its \end, but an \ea example ends when
%% someone writes \z and at nothing else.  Left to the kernel it surfaces
%% as "\begin{list} on input line N ended by \end{document}", which names
%% the list rather than the command and arrives at the end of the document
%% rather than where the closer is missing -- the very failure
%% \lx@subpush@outside@err exists to keep a stray \a. from producing.  It
%% gets the same treatment: an error naming \ea and the line it stood on.
%%
%% The line recorded is the TOP-level opener's, because that is the example
%% still open; a \z missing from an inner level leaves the outer one open
%% too (the outer \z pops the inner level instead), so the message says
%% what it knows and no more.  \AtEndDocument runs before \@checkend, so
%% this is what the writer sees; the example is then closed the way \z
%% would have closed it, and the document finishes with this error alone
%% instead of this one plus the kernel's.
\def\lx@ea@openline{0}
\AtEndDocument{%
  \iflx@inlangsci
    \PackageError{linguexx}{the \string\ea\space on line
      \lx@ea@openline\space was never closed}%
     {That example is still open at the end of the document.  Every
      \string\ea\space is closed by a \string\z, including a nested one:
      if an inner \string\z\space is the one missing, the outer
      \string\z\space closes the inner level and leaves this
      example open.\MessageBreak
      It has been closed here so that the rest of the document could be
      typeset, so everything after that line has been set inside it.}%
    \lx@bodyend
  \fi}

%%%% ---- the rest of langsci-gb4e -----------------------------------------
%% Everything the package offers beyond \ea ... \z: its list and item
%% variants, \jambox, the box and reference helpers, and the width,
%% separation and font knobs.  All of it under [langsci] and nowhere else.
%%
%% The criterion is WHAT WORKS UPSTREAM, not what upstream's two documents
%% describe.  Those documents -- the LangSci author guidelines and the
%% langsci-gb4e manual -- cover a fraction of the package: \eal ... \zl,
%% the sub-list numbering variants, \attop, \xbox, the four package options
%% and a dozen more names appear in neither, and are used in real LangSci
%% sources all the same.  A document being ported does not care which half
%% of the package its author reached for, so a name that has a working
%% behaviour in a released langsci-gb4e has one here.  (v1.2 briefly took
%% the other line and provided the documented surface alone; a paper whose
%% examples are written with \eal ... \zl is what settled it.)
%%
%% The limit of that rule is a name with NO working behaviour to copy.
%% xlistabr labels every item "(xnumii." upstream and qlist raises "No
%% counter 'xnum' defined": there is nothing there to be compatible with,
%% and providing them would mean inventing a meaning for a name whose
%% precedent is a bug.  Those two are refused BY NAME, through
%% \lx@ls@retire below, so a document using one stops at the line that has
%% to change rather than at "Undefined control sequence".
%%
%% Names are TAKEN, with a warning, exactly as \ea and \z are: the option
%% was asked for this syntax, and a document that silently lost \jambox
%% would lose it everywhere at once.  \@ifundefined makes an undefined
%% name \relax while testing it, so everything installed this way is
%% defined with \def rather than \newcommand.
\def\lx@ls@take#1{%
  \@ifundefined{#1}{}%
    {\PackageWarning{linguexx}{\@backslashchar#1\space was already
       defined; [langsci] has taken the name over}}}

%% A name langsci-gb4e has and has never made work.  Defined, and defined
%% to complain: an undefined name says nothing about where it came from.
\def\lx@ls@retire#1#2{%
  \expandafter\def\csname #1\endcsname{%
    \PackageError{linguexx}{\@backslashchar#1\space is not provided}%
     {#2}}}
%% The same for an environment.  \begin has already opened the group by the
%% time the error fires, so the \end half has to exist and do nothing.
\def\lx@ls@retire@env#1#2{%
  \lx@ls@retire{#1}{#2}%
  \expandafter\let\csname end#1\endcsname\relax}

%% ---- widths -------------------------------------------------------------
%% \exewidth takes a SAMPLE, as upstream does: the label box is made as
%% wide as the text given, so \exewidth{(23)} reserves room for a two-digit
%% number.  It sets this package's own \Exlabelwidth, so it and
%% \setlength{\Exlabelwidth}{...} are two spellings of one thing.
\lx@ls@take{exewidth}\def\exewidth#1{\settowidth{\Exlabelwidth}{#1}}
\lx@ls@take{twodigitexamples}\def\twodigitexamples{\exewidth{(23)}}
\lx@ls@take{threedigitexamples}\def\threedigitexamples{\exewidth{(234)}}
\lx@ls@take{fourdigitexamples}\def\fourdigitexamples{\exewidth{(2345)}}
%% autoexewidth, on unless [manualexewidth] was given: widen the box once
%% the numbers reach three and four digits, on upstream's thresholds.  It
%% earns its keep even though this package sizes labels better than gb4e
%% did, because the DEFAULT mode uses a fixed \Exlabelwidth -- a document
%% with more than 98 examples would otherwise have its own text pushed
%% right as the number outgrew the box.  It only ever widens, and only at a
%% main-level list, which is where a number is printed.
%% WIDENS ONLY, which \exewidth on its own does not: the default
%% \Exlabelwidth is already wider than "(235)", so setting the sample
%% outright made a three-digit document's text jump LEFT of a two-digit
%% one's -- the opposite of the point, and measured rather than reasoned
%% about (WBIG landed at 102.08 against WSMALL's 105.58).  Upstream cannot
%% hit this because its own default box is narrower.
\iflx@manualexewidth\else
  \newlength\lx@autowd
  \def\lx@autowiden#1{%
    \settowidth{\lx@autowd}{#1}%
    \ifdim\lx@autowd>\Exlabelwidth \setlength{\Exlabelwidth}{\lx@autowd}\fi}
  \def\lx@autoexewidth{%
    \ifnum\value{ExNo}>998\relax
      \lx@autowiden{(1235)}%
    \else
      \ifnum\value{ExNo}>98\relax \lx@autowiden{(235)}\fi
    \fi}
  \let\lx@orig@mainlist\lx@mainlist
  \def\lx@mainlist{\lx@autoexewidth\lx@orig@mainlist}
\fi

%% ---- separations --------------------------------------------------------
\lx@ls@take{gblabelsep}\def\gblabelsep#1{\setlength{\Exlabelsep}{#1}}
%% \subexsep and \judgewidth have no counterpart here, and say so rather
%% than approximate.  \subexsep sets a label separation for the sub-levels
%% alone; this package has ONE \Exlabelsep for all three levels (see the
%% layout section), and quietly aliasing the two would leave \gblabelsep
%% and \subexsep fighting over one length.  \judgewidth sets the width of a
%% reserved judgment column; here a judgment HANGS into the label gutter
%% and reserves nothing, which is precisely why marked and unmarked
%% examples align -- there is no width to set.
\lx@ls@take{subexsep}\def\subexsep#1{%
  \PackageWarning{linguexx}{\string\subexsep\space has no effect: this
    package uses one \string\Exlabelsep\space for every level.\MessageBreak
    Set that instead}}
\lx@ls@take{judgewidth}\def\judgewidth#1{%
  \PackageWarning{linguexx}{\string\judgewidth\space has no effect: a
    judgment here hangs into the label gutter rather than occupying a
    column of its own.\MessageBreak
    Widen \string\SubExlabelwidth\space for more room, or see
    [phantomalign]}}

%% ---- fonts --------------------------------------------------------------
%% Upstream's names -- the eight of the manual's section 11 -- wired to the
%% hooks the list and the gloss engine expose.  The \fn... variants apply
%% inside a footnote, where upstream switches them by hand and \if@noftnote
%% already knows.
%%
%% The manual writes them with an argument -- "\exfont{\itshape}" -- and
%% upstream's are declarations taking none, so that spelling silently does
%% nothing there.  These are declarations too, matching the behaviour a
%% ported document actually gets; \renewcommand is the way to set them.
\lx@ls@take{exfont}\def\exfont{\normalsize\upshape}
\lx@ls@take{glossfont}\def\glossfont{\normalsize\upshape}
\lx@ls@take{transfont}\def\transfont{\normalsize\upshape}
\lx@ls@take{exnrfont}\def\exnrfont{\exfont}
\lx@ls@take{fnexfont}\def\fnexfont{\footnotesize\upshape}
\lx@ls@take{fnglossfont}\def\fnglossfont{\footnotesize\upshape}
\lx@ls@take{fntransfont}\def\fntransfont{\footnotesize\upshape}
\lx@ls@take{fnexnrfont}\def\fnexnrfont{\fnexfont}
\def\lx@bodyfont{\if@noftnote\exfont\else\fnexfont\fi}
\def\lx@labelfont{\if@noftnote\exnrfont\else\fnexnrfont\fi}
%% \singlegloss (upstream's default) sets the gloss single-spaced whatever
%% \baselinestretch the document runs at.  Where the document never changes
%% \baselinestretch the two settings are indistinguishable, which is what
%% makes following upstream's default safe here.
\lx@ls@take{singlegloss}\def\singlegloss{\lx@singleglosstrue}
\lx@ls@take{nosinglegloss}\def\nosinglegloss{\lx@singleglossfalse}
%% Upstream's \examplesroman / \examplesitalics also try to set \exfont,
%% and cannot: \exfont takes no argument, so "\exfont{\itshape}" there
%% calls it and then typesets a group that does nothing.  What they do do is
%% set the OBJECT tier, and that is what these do -- through
%% \GlossTierFont, which is this package's name for it.
\lx@ls@take{examplesroman}\def\examplesroman{\GlossTierFont{1}{\textup}}
\lx@ls@take{examplesitalics}\def\examplesitalics{\GlossTierFont{1}{\textit}}

%% ---- item variants -------------------------------------------------------
%% Manual section 7.  \exi{label} gives an item the label it is handed, stepping no counter --
%% and so recording no anchor, which is what \lx@custom@item@here is for.
%% Like \ex it takes either a bracket judgment with a braced body, or plain
%% text after it.  \exr and \exp label an item with another example's
%% number, \sn with nothing at all.
\def\lx@exi#1{%
  \iflx@inexe
    \def\lx@donext{\lx@exi@go{#1}}%
  \else\iflx@inlangsci
    \def\lx@donext{\lx@exi@go{#1}}%
  \else
    \let\lx@donext\lx@exi@err
  \fi\fi
  \lx@donext}
%% The plain form goes through \lx@custom@item@here, which is the seam this
%% needs and the reason that seam exists -- an earlier version set the label
%% and scanned by hand, which worked and left \lx@custom@item@here unused,
%% so a mutation that made it step a counter changed nothing and the suite
%% stayed green over dead code.
\def\lx@exi@go#1{\@ifnextchar[{\lx@exi@judged{#1}}{\lx@custom@item@here{#1}}}
\def\lx@exi@judged#1[#2]#3{%
  \def\lx@itemlabel{#1}\lx@setjudge{#2}\lx@makeitem#3}
\def\lx@exi@err{%
  \PackageError{linguexx}{\string\exi\space outside an example}%
   {\string\exi\space labels an item of a list that is already open: put it
    inside \string\begin{exe} ... \string\end{exe}, or after
    \string\ea.\MessageBreak
    For a custom label on an example of its own, write \string\ex.[label]
    instead.}}
%% Both label an item with a PARENTHESISED number, upstream's "(\ref{#1})"
%% and "(\ref{#1}$'$)", so both spell the delimiters out around the bare
%% number \pref gives -- and print the same label under either reference
%% flavour (see \lx@refs@bare).  \exp puts the prime INSIDE the
%% parentheses, which is the whole reason \pref exists.
\lx@ls@take{exr}\def\exr#1{\lx@exi{\theExLBr\pref{#1}\theExRBr}}
\lx@ls@take{sn}\def\sn{\lx@exi{}}
%% \exp is the one name that cannot simply be taken: it is the LaTeX
%% kernel's math operator, so a paper writing $\exp(x)$ and one writing
%% \exp{ex:5} both have a claim on it.  Upstream takes it and the math
%% operator is silently gone.  Here both meanings are kept and the mode
%% decides, which is unambiguous in each direction: an item command means
%% nothing inside math, and the operator means nothing in a list of
%% examples.  No warning, because nothing was lost.
%%
%% Installed at \AtBeginDocument as well as at load, and the operator
%% re-captured each time.  Under \DocumentMetadata the tagged-math code
%% RE-DECLARES \exp after this package is read -- it becomes
%% "\qopname\relax o{exp}" -- so a definition made only at load time is
%% silently replaced, and \exp{ex:5} then reaches the operator and halts
%% the compile inside the kernel's math-grabbing code.  Untagged builds
%% never see it, which is what made it worth a comment: the case that
%% catches it is the tagged one.
%%
%% The guard is what keeps the second pass from capturing OUR meaning as
%% the fallback, which would leave \exp calling itself for ever.
\def\lx@exp@dispatch{%
  \ifmmode\expandafter\lx@kernel@exp\else\expandafter\lx@ls@exp\fi}
\def\lx@exp@install{%
  \ifx\exp\lx@exp@dispatch\else \let\lx@kernel@exp\exp \fi
  \let\exp\lx@exp@dispatch}
\lx@exp@install
\def\lx@ls@exp#1{\lx@exi{\theExLBr\pref{#1}\ExPrimeMark\theExRBr}}
%% Upstream writes the prime as $'$.  Math mode is exactly what this package
%% does not enter -- it would put a Formula element in the tree, the PDF/UA-2
%% failure the package exists to avoid -- so the mark is a raised text quote,
%% and a command, so that a document wanting the real U+2032 on an engine
%% whose font has it can say so.
%%
%% \raisebox and NOT \textsuperscript, which looks like text and is not:
%% \textsuperscript is built on \ensuremath, so it enters math mode for the
%% very mark that was rewritten to stay out of it.
%%
%% Be clear about what that costs today, because it is less than it looks
%% and the next reader will otherwise "simplify" this back.  On TL2026
%% \textsuperscript here compiles cleanly on all three engines and puts NO
%% Formula element in the tree, so nothing in the test suite can tell the
%% two spellings apart: langsci-ua's no-Formula assertion passes either way.
%% The choice follows the package's rule -- stay out of math -- rather than
%% a measurement, because "the tagger does not currently emit a Formula for
%% \ensuremath" is a property of this TeX Live and not a promise.
%% \ExPrimeRaise is separate so the height can be tuned without repeating
%% the box.
\newcommand\ExPrimeRaise{0.4ex}
\lx@ls@take{ExPrimeMark}%
\def\ExPrimeMark{\raisebox{\ExPrimeRaise}{\textquotesingle}}

%% ---- sub-level numbering variants ---------------------------------------
%% One environment per numbering, as upstream has them.  Each sets the
%% printed label AND the /ListNumbering class -- see \lx@sublabel for why
%% those two must never be set apart -- and sets them inside the
%% environment's own group, so a variant list cannot change the numbering of
%% the next one.
%%
%% \lx@xlist@style takes the numbering command, the attribute class and the
%% two delimiters, and sets both levels: which of the two is used depends on
%% the depth the environment opens at, and only the one used has any effect.
\def\lx@xlist@style#1#2#3#4{%
  \def\lx@sublabel{#3#1{SubExNo}#4}%
  \def\lx@subsublabel{#3#1{SubSubExNo}#4}%
  \def\lx@sub@olclass{#2}\def\lx@subsub@olclass{#2}}
\def\lx@xlist@env#1#2#3#4{%
  \lx@nomix@dot{a numbering variant of \string\begin{xlist}}%
  \lx@xlist@style{#1}{#2}{#3}{#4}%
  \lx@subenv@begin \lx@inexetrue}
\NewDocumentEnvironment{xlista}{}
  {\lx@xlist@env{\Exalph}{lxOLalpha}{}{.}}{\lx@glt@langend \lx@closelist}
\NewDocumentEnvironment{xlisti}{}
  {\lx@xlist@env{\Exroman}{lxOLroman}{}{.}}{\lx@glt@langend \lx@closelist}
\NewDocumentEnvironment{xlistn}{}
  {\lx@xlist@env{\Exarabic}{lxOLdecimal}{}{.}}{\lx@glt@langend \lx@closelist}
\NewDocumentEnvironment{xlistA}{}
  {\lx@xlist@env{\Alph}{lxOLupperalpha}{}{.}}{\lx@glt@langend \lx@closelist}
\NewDocumentEnvironment{xlistI}{}
  {\lx@xlist@env{\Roman}{lxOLupperroman}{}{.}}{\lx@glt@langend \lx@closelist}
%% The two that are refused instead, for the reason given at the head of
%% this section: neither has ever produced what its name says, so there is
%% no upstream behaviour to be compatible with.  Reproducing the defect
%% would help nobody, and picking a meaning for the name would be new
%% syntax wearing an old one's clothes -- which is the thing this package
%% does not do by default.  If a document turns up that wants either, the
%% shape both would take is in the history of this file (v1.2, before this
%% section was rewritten): xlistabr as \lx@xlist@env{\Exalph}{lxOLalpha}
%% with parenthesis delimiters, qlist as a free-standing lettered list with
%% symmetric margins built through \lx@openlist.
%% The message says what to write instead and stops there.  Diagnosing
%% another package to its users is not this one's place, and a reader who
%% has hit the line already has the only question that matters: what do I
%% put here?  The reasoning behind the two names is a maintenance matter
%% and stays in the repository's own notes.
\lx@ls@retire@env{xlistabr}{Use the xlist environment instead.}
\lx@ls@retire@env{qlist}{Use an ordinary LaTeX list instead.}

%% ---- \jambox ------------------------------------------------------------
%% Alexis Dimitriadis's jambox, as langsci-gb4e carries it and as the author
%% guidelines use it (their section 12.3): line material up
%% a fixed distance from the right margin, overflowing left or wrapping to
%% the next line when it will not fit.  \exsource is this package's own
%% version of the same idea and aligns flush right.
%%
%% \exannot is the third, and it is not a spelling of this one: \jambox
%% holds a column only while the gutter is wide enough that no example can
%% reach it, because its box is set at its natural width behind glue that
%% shrinks to nothing.  Widen the gutter to bring the column near the
%% examples -- the case \exannot exists for -- and the column stops holding,
%% silently.  Measured, four sub-examples of increasing length at one
%% gutter: aligned to 0.01pt at 3cm and at 7cm, 212.6 / 212.6 / 234.1 /
%% 288.9pt at 11cm.  See the \exannot section for what it does instead.
%%
%% Nothing here is changed on the strength of that.  Under [langsci] this
%% is what langsci-gb4e has, and a document converting to linguexx one
%% example at a time is checked by diffing its output against its own
%% previous run: a \jambox that placed its box differently would show up in
%% exactly the shape the migration promise is about.
%%
%% Upstream's note that it does not work in ragged-right mode holds here
%% too, and matters more, because \ExRaggedRight can turn ragged setting on:
%% the algorithm finishes the line with \hfill, which has nothing to push
%% against once \rightskip is already infinitely stretchable.
\iflx@nojambox\else
  \@ifundefined{jamwidth}{\newdimen\jamwidth}{}\jamwidth=2in
  \newsavebox\lx@jambox
  \lx@ls@take{jambox}%
  \def\jambox{\@ifnextchar[{\lx@jambox@opt}%
     {\@ifnextchar*{\lx@jambox@set}{\lx@jambox@opt[\the\jamwidth]}}}
  \def\lx@jambox@set*#1{\sbox\lx@jambox{#1}\jamwidth=\wd\lx@jambox
    \lx@jambox@opt[\the\jamwidth]{\usebox\lx@jambox}}
  \def\lx@jambox@opt[#1]#2{{\sbox\lx@jambox{#2}%
    \ifdim\wd\lx@jambox<#1\relax
      \@tempdima=#1\relax \advance\@tempdima by-\wd\lx@jambox
      \unskip\nobreak\hfill\penalty250
      \hskip 1.2em minus 1.2em
      \hbox{}\nobreak\hfill\usebox\lx@jambox\nobreak
      \hskip\@tempdima minus \@tempdima\hbox{}%
    \else
      \hfill\penalty50\hbox{}\nobreak\hfill\usebox\lx@jambox
    \fi
    \parfillskip=0pt \finalhyphendemerits=0 \par}}
\fi

%% ---- boxes and references ------------------------------------------------
\lx@ls@take{attop}\def\attop#1{\leavevmode\vtop{\strut\vskip-\baselineskip\vbox{#1}}}
%% \atcenter is upstream's $\vcenter{...}$ written in text mode: a box of
%% height h and depth d is raised by (d-h)/2 plus the height the centre is
%% meant to sit at, so that its middle lands there rather than its baseline.
%% Math mode is what this package does not enter -- a Formula element here
%% would cost the document its PDF/UA-2 conformance -- and that is the one
%% thing this cannot copy exactly.
%%
%% \vcenter centres on the MATH AXIS, and the axis is only readable as
%% \fontdimen22\textfont2, which outside math mode is \nullfont: asking for
%% it there raises "Font \nullfont has only 7 fontdimen parameters" and
%% yields zero, which is a visibly wrong box and was this macro's first
%% version.  \ExAtCenterAxis stands in for it, at the usual text-mode
%% approximation of half the x-height.  Against a real \vcenter at 10pt,
%% with identical content, the box sits 0.38pt lower -- measured, not
%% estimated.  It is a command so that a document which
%% cares can set the axis it wants.
\newcommand\ExAtCenterAxis{.5ex}
\newsavebox\lx@atcbox
\lx@ls@take{atcenter}%
\def\atcenter#1{%
  \leavevmode
  \sbox\lx@atcbox{\vbox{#1}}%
  \raisebox{\dimexpr .5\dp\lx@atcbox - .5\ht\lx@atcbox
            + \ExAtCenterAxis \relax}{\usebox\lx@atcbox}}
\lx@ls@take{xbox}\def\xbox#1#2{\noindent\parbox[t]{#1}{#2}\noindent}
\lx@ls@take{nobreakbox}\def\nobreakbox#1{\xbox{\linewidth}{#1}}
%% Manual section 7.4.  \xref is upstream's "(\ref{#1})", and is written
%% that way here -- \pref inside the delimiters -- so that it prints "(1)"
%% under either reference flavour, including the bare one [langsci] selects
%% (see \lx@refs@bare).  \xxref puts ONE pair around the range --
%% upstream's "(2--3)", not the "(2)--(3)" that this package's own
%% \Refrange gives.  The dash is \rangedash, so a document that has set that
%% gets it here too.
\lx@ls@take{xref}\def\xref#1{\theExLBr\pref{#1}\theExRBr}
\lx@ls@take{xxref}\def\xxref#1#2{\theExLBr\pref{#1}\rangedash\pref{#2}\theExRBr}

%% ---- the rest of the \ea family -----------------------------------------
%% \eas ... \zs is upstream's "example in a box": one that will not break
%% across a page.  Upstream boxes it in a tabular, purely as a layout device;
%% that would wrap running prose in a Table element, which is wrong for a
%% screen reader and wrong for PDF/UA, so this one uses a minipage -- the
%% same box, tagged as what it is.  A footnote inside one reaches the
%% minipage-footnote numbering recorded in doc/DEFERRED-DECISIONS.md, and
%% that is the one thing to know about it.
\def\lx@eas{\lx@eas@do\lx@ea@rag}
\def\lx@eas@do#1{%
  \iflx@inlangsci
    \let\lx@donext\lx@eas@nesterr
  \else\iflx@inexample
    \let\lx@donext\lx@ea@mixerr
  \else\iflx@inexe
    \let\lx@donext\lx@ea@mixerr
  \else
    \def\lx@donext{\lx@eas@top{#1}}%
  \fi\fi\fi
  \lx@donext}
\def\lx@eas@top#1{%
  \lx@example@begin
  \lx@inlangscitrue
  \edef\lx@ea@openline{\the\inputlineno}%
  \lx@guesslabel
  \lx@mainlist
  #1%
  \lx@item@judged{}%
  \minipage[t]{\linewidth}}
\def\lx@zs{\endminipage\lx@z}
\def\lx@eas@nesterr{%
  \PackageError{linguexx}{\string\eas\space inside an example}%
   {\string\eas\space opens a top-level example boxed so that it cannot
    break across a page; it has no nested meaning, in this package or in
    langsci-gb4e.\MessageBreak
    Write \string\ea\space here.}}
%% \eafirst, \zlast and \zllast suppress the vertical space an example would
%% otherwise put above or below itself -- for one that opens or closes a
%% float, a frame or a table cell.  Upstream spells the amount as a multiple
%% of \baselineskip, which is its own geometry; here it is this package's
%% \Extopsep, so that they suppress what this package actually put there.
%% \zllast is upstream's \removelastskip, which needs no translation.
\def\lx@eafirst{\vspace{-\Extopsep}\lx@ea}
\def\lx@zlast{\lx@z\vspace{-\Extopsep}}
\def\lx@zllast{\lx@zl\removelastskip}

%% ---- what has to wait for the glossing section --------------------------
%% \glt, \GlossTransStyle and \lx@glossfont are defined further down this
%% file than the \ea front-end sits, so these four run at the end of the
%% package instead of here.  Written in place they would be overwritten by
%% the definitions they are meant to wrap -- which is how \lx@glossfont was
%% found: it was silently \let to \@empty again two thousand lines later.
\AtEndOfPackage{%
  \@ifundefined{gltoffset}{\newlength{\gltoffset}}{}%
  \lx@ls@take{nogltOffset}\def\nogltOffset{\setlength{\gltoffset}{0pt}}%
  \lx@ls@take{resetgltOffset}\def\resetgltOffset{%
     \setlength{\gltoffset}{.17\baselineskip}}%
  \resetgltOffset
  \let\lx@orig@glt\glt
  \def\glt{\par\nobreak\vskip\gltoffset\nobreak\lx@orig@glt}%
  \lx@ls@take{trans}\let\trans\glt
  \def\GlossTransStyle{\if@noftnote\transfont\else\fntransfont\fi}%
  \def\lx@glossfont{%
    \iflx@singlegloss \def\baselinestretch{1}\selectfont \fi
    \if@noftnote\glossfont\else\fnglossfont\fi}}

%% ---- [nocgloss] ----------------------------------------------------------
%% Accepted and reported, not obeyed.  Upstream it stops the gloss macros
%% being defined so that another package can own \gll; here the glossing is
%% not a bundled file but the thing \exg., \altg, \GlossTierLang, \lpzg and
%% the whole tagged word-bundle structure are built on, and switching it off
%% would leave every one of those without an answer.  What each should then
%% do has more than one defensible answer and no document yet asking the
%% question -- see doc/DEFERRED-DECISIONS.md -- so nothing is decided by
%% default here.
\iflx@nocgloss
  \PackageWarning{linguexx}{[nocgloss] is accepted and has no effect: this
    package's glossing is not separable from \string\exg.,
    \string\altg\space and the tagged gloss structure.\MessageBreak
    If you need another package to own \string\gll, say so -- see
    doc/DEFERRED-DECISIONS.md}
\fi

%% ---- page breaking -------------------------------------------------------
%% [lowerpenalty] lets examples break across a page, which is what a book
%% with long examples wants and what LangSci sets.
\iflx@lowerpenalty
  \def\lx@itempenalty{\@itempenalty-1000\relax}
\fi

\def\lx@langsci@install{%
  \lx@langsci@claim{ea}\lx@ea
  \lx@langsci@claim{z}\lx@z@dispatch
  \lx@langsci@claim{eal}\lx@eal
  \lx@langsci@claim{zl}\lx@zl
  \lx@langsci@claim{eanoraggedright}\lx@eanorag
  \lx@langsci@claim{ealnoraggedright}\lx@ealnorag
  \lx@langsci@claim{eas}\lx@eas
  \lx@langsci@claim{zs}\lx@zs
  \lx@langsci@claim{eafirst}\lx@eafirst
  \lx@langsci@claim{zlast}\lx@zlast
  \lx@langsci@claim{zllast}\lx@zllast
  \lx@langsci@claim{exi}\lx@exi
  \lx@exp@install}
\lx@langsci@install
\AtBeginDocument{\lx@langsci@install}

\fi % iflx@langsci

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Relative references: \Next \Last \NNext \LLast \TextNext           %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% Every relative reference takes an optional sub-example part, exactly as
% linguex's did: \Last[a] points at letter a of the example it resolves to.
% The part is joined with \firstrefdash -- the same hook \theSubExNo uses
% for a \ref to a \sublabel -- so a relative reference and a labelled one
% spell the same example the same way in both modes: (1a) by default,
% (1-a) under [legacy].  Without the argument nothing is inserted and the
% output is what it always was.
%
% \lx@relsub is what carries the part into the formatters, which is why
% every one of these commands sets it INSIDE the group it already opened
% around its body: a part given to one reference must not survive into the
% next.  The p-twins forward their own optional argument rather than set
% \lx@relsub themselves, so there is one place where the part is built.
%
% Every one of them ends in \xspace, as linguex's \printExNo did, and for
% the same reason: these are control WORDS, so TeX's tokenizer has already
% eaten the space in "\Last shows that" before any macro can see it, and
% the reference would set solid against the next word -- "(1)shows".  The
% \xspace has to sit OUTSIDE the group, after the closing brace: inside it
% the next token is the group's own }, which \xspace recognises as \egroup
% and treats as a reason NOT to add a space.  That is also why the p-twins
% carry one of their own -- the \xspace of the \Last they forward to fires
% against their closing brace and does nothing.  \pref and \ref need none:
% they end in an argument, so the source space after } survives.
\newcommand\lx@relsub{}
\newcommand\lx@setrelsub[1]{\IfValueT{#1}{\def\lx@relsub{\firstrefdash#1}}}

%%%% Clickable relative references ---------------------------------------
%%
%% \ref{ex:1} has been a link ever since hyperref existed and \Last has
%% not, although the two spell the same reference; the reader cannot see
%% from the page which of them will move.  The targets were there all
%% along: hyperref puts a destination at every \refstepcounter, so every
%% example already has one whether or not it carries a \label, and what
%% was missing was only the link.
%%
%% The catch -- and the reason this is machinery rather than a \hyperlink
%% inside \lx@fmtEx -- is that a relative reference names a number by
%% ARITHMETIC, and the arithmetic can name a number the document never
%% reaches: \Next in the last section, \LLast before example 2.  A link
%% to a destination that does not exist is not an error.  The backend
%% quietly substitutes a whole-page one, so the click lands somewhere
%% plausible and wrong, and the three engines do not even agree that this
%% is worth a word (pdftex warns, luatex warns in other words, xdvipdfmx
%% says nothing).  So the existence of the target is checked here, once,
%% for all three: with no target the number is printed exactly as it was
%% before and the reference is reported at the end of the run.  A
%% relative reference is never a link to nowhere.
%%
%% Knowing whether a target exists takes the .aux, hence a run behind.
%% Every example records the anchor hyperref gave it -- \@currentHref,
%% the anchor hyperref ACTUALLY made, rather than one built here to the
%% same recipe.  That is deliberate: a reference builds the name it wants
%% with \lx@Hexname, so if the two recipes ever disagree the lookup
%% fails and the reference degrades to an unlinked number, which is the
%% old behaviour plus a warning.  Building both ends the same way would
%% turn the same disagreement into a link that goes somewhere wrong.
%%
%% If the two runs disagree about the SET of anchors, the answer is a
%% rerun rather than a report: a stale .aux makes both halves of the
%% report untrustworthy, the missing-target list included.  That is the
%% bargain \lpzglist strikes, and \tableofcontents before it.
%%
%% The optional sub-example part is printed but not aimed at: \Last[b]
%% links to the example, not to its letter b.  Letter anchors do exist
%% (\theHSubExNo), but their letter comes from \alph while the printed
%% one comes from \Exalph, a documented hook -- so in a document that
%% renumbers its sub-examples every part-link would dangle, and the part
%% is hand-typed text that need not name a real letter in the first
%% place.  Landing two lines high is the better failure.
\ExplSyntaxOn
\bool_new:N \g__lx_relref_link_bool  % hyperref present and [norelreflinks]
                                     % not given
\bool_new:N \g__lx_relref_own_bool   % ... and the anchors are ours to place
\tl_new:N   \g__lx_relref_pending_tl % an anchor waiting for its \item
\prop_new:N \g__lx_relref_seen_prop  % anchors already placed in this frame
\int_new:N  \g__lx_relref_frame_int  % which frame that was
\int_gset:Nn \g__lx_relref_frame_int { -1 }
\prop_new:N \g__lx_relref_dest_prop  % anchors the .aux knows: the lookup
\seq_new:N  \g__lx_relref_aux_seq    % the same, in order: the rerun test
\seq_new:N  \g__lx_relref_new_seq    % anchors recorded on THIS run
\prop_new:N \g__lx_relref_ambig_prop % anchors TWO examples claimed
\seq_new:N  \g__lx_relref_bad_seq    % references that found no target
\seq_new:N  \g__lx_relref_ambig_seq  % references whose anchor is shared
\str_new:N  \l__lx_relref_dest_str

%% Written to and read back from the .aux.  A destination name is a
%% string on both sides -- it reaches this from the .aux as letters and
%% from \@currentHref as whatever hyperref built it out of, and a prop
%% would take two spellings of "ExNo.lxex.3" for two different keys.
%%
%% A name recorded TWICE is recorded as ambiguous instead.  Two examples
%% claim one anchor whenever ExNo is reset mid-document -- by
%% \setcounter, or by [legacy]'s per-chapter reset in a class that has
%% chapters -- because \theHExNo is built from the counter alone.
%% hyperref keeps the first destination of a name and drops the rest, so
%% the number 1 of chapter 3 is a link to the number 1 of chapter 1.
%% That is a defect in the anchors themselves and it predates the links:
%% a \label in the second chapter has always led to the first, and
%% mending it means changing what \theHExNo records, which is not this
%% mechanism's to decide.  What IS this mechanism's business is not
%% shipping a wrong jump of its own, so an ambiguous name is treated
%% exactly like a missing one -- the number is printed, no link is made,
%% and the reference is reported.
\cs_new_protected:Npn \lx@relref@dest #1
  {
    \str_set:Nn \l__lx_relref_dest_str {#1}
    \prop_if_in:NVTF \g__lx_relref_dest_prop \l__lx_relref_dest_str
      { \prop_gput:NVn \g__lx_relref_ambig_prop \l__lx_relref_dest_str { } }
      { \prop_gput:NVn \g__lx_relref_dest_prop \l__lx_relref_dest_str { } }
    \seq_gput_right:NV \g__lx_relref_aux_seq \l__lx_relref_dest_str
  }
%% One example, one anchor -- and, where the anchors are ours, one
%% \hypertarget to go with it.
%%
%% Two sources for the name, because there are two ways an example comes
%% to have a destination.  Ordinarily hyperref makes one at every
%% \refstepcounter and \@currentHref names it, and taking the name from
%% there rather than rebuilding it is what keeps a divergence of recipes
%% costing a link instead of a wrong jump.  With hyperref's implicit
%% anchors switched off there is no such name, and linguexx makes the
%% destination itself; then the name has to be built here, by the same
%% \lx@Hexname a reference will use.
\cs_new:Npn \lx_relref_name:
  {
    \legacy_if:nTF { @noftnote }
      { \lx@Hexname { \int_use:N \c@ExNo } }
      { \lx@Hfnexname { \int_use:N \c@FnExNo } }
  }
\cs_new_protected:Npn \lx_relref_record:
  {
    \bool_if:NT \g__lx_relref_link_bool
      {
        \bool_if:NTF \g__lx_relref_own_bool
          { \str_set:Ne \l__lx_relref_dest_str { \lx_relref_name: } }
          { \str_set:Ne \l__lx_relref_dest_str { \@currentHref } }
        \lx_relref_if_unseen_here:T
          {
            \bool_if:NT \g__lx_relref_own_bool
              { \tl_gset_eq:NN \g__lx_relref_pending_tl
                  \l__lx_relref_dest_str }
            \seq_gput_right:NV \g__lx_relref_new_seq \l__lx_relref_dest_str
            \legacy_if:nT { @filesw }
              {
                \iow_now:Ne \@auxout
                  { \token_to_str:N \lx@relref@dest
                      { \l__lx_relref_dest_str } }
              }
          }
      }
  }
%% The anchor waits for the \item and is placed inside it, where there is
%% horizontal mode to place it in.  \lx@main@core runs before the list is
%% even open, which is the right moment to KNOW the anchor and the wrong
%% one to emit it.
%%
%% \Hy@raisedlink is hyperref's own wrapper for an anchor that must not
%% disturb the line it sits on -- the same one hyperref uses for the
%% anchors it places at \refstepcounter, which is exactly the thing being
%% stood in for here.
\cs_new_protected:Npn \lx_relref_anchor:
  {
    \tl_if_empty:NF \g__lx_relref_pending_tl
      {
        \exp_args:NV \lx_relref_anchor_place:n \g__lx_relref_pending_tl
        \tl_gclear:N \g__lx_relref_pending_tl
      }
  }
\cs_new_protected:Npn \lx_relref_anchor_place:n #1
  { \Hy@raisedlink { \hyper@anchorstart {#1} \hyper@anchorend } }
\cs_new_eq:NN \lx@relref@anchor \lx_relref_anchor:
%% Has this anchor already been placed WHERE WE ARE?
%%
%% In beamer a frame is set once per overlay slide, with the example
%% counters restored each time, so an example on a two-slide frame comes
%% past twice carrying the same number.  Left alone that is a second
%% destination of the same name -- which hyperref drops, warning once per
%% repeat -- and a second .aux record, which the guard against SHARED
%% anchors would then read as two examples claiming one name and refuse,
%% on that ground, to link the very examples in question.  Both are wrong
%% for one reason: it is one example, seen twice.
%%
%% The name cannot tell them apart, because a genuinely reset counter
%% produces the same repetition and DOES deserve the shared-anchor
%% treatment.  What tells them apart is WHERE the repeat happens.  A
%% frame's passes repeat within one frame; a reset counter reuses names
%% across the document.  So the question asked here is not "is this the
%% first pass" but "have I already placed this name in THIS frame", and
%% \c@framenumber -- constant across a frame's slides, one up per frame --
%% says which frame that is.
%%
%% Asking about the pass instead was the first attempt and it was wrong,
%% for a case that is not exotic at all: an example inside \only<2->{...}
%% is not typeset on the first pass, so its FIRST appearance is on a later
%% one, and a rule that skipped every later pass gave it no anchor at all.
%% A reference to it then found no target and was reported as dangling --
%% correctly, by a mechanism that had thrown the target away itself.
%% Asking about the frame gets that right: on the pass where the example
%% finally appears, its name is new to the frame, so it is placed.
%%
%% And the pass it is placed on has to be one where the example can be
%% SEEN, which is not the pass where it is first RUN.  \pause, \uncover
%% and \onslide execute their material on every slide of the frame --
%% the counter steps, and an anchor placed there is placed on that slide
%% -- and drop its ink on the slides where it is covered.  So an example
%% after a \pause runs on slide 1 and appears on slide 3, and anchoring
%% it where it ran sends the reader to a slide with nothing on it to see.
%% Nothing on the page shows the mistake, because on that page there is
%% nothing there.  Reported from the same deck as the \only case, and the
%% commoner of the two: \pause is everywhere in lecture slides.
%% \beamer@coveringdepth is 0 exactly when the material is visible on the
%% slide being set, and counts up through nested covering, so it answers
%% for \pause, \uncover and \onslide alike; \only needs no answer, since
%% it does not run at all on the slides it excludes.
%%
%% An example covered on EVERY slide of its frame is therefore never
%% anchored, and a reference to it is reported as pointing at an example
%% that does not exist.  That is the right answer: it does not.
%%
%% What this does not catch is a counter reset INSIDE one frame, where the
%% second example is skipped rather than reported.  A reset between frames
%% is caught, which is the form a reset takes in practice (a new section,
%% a new set of numbers); a reset inside a single slide is not a thing
%% anyone does.  Outside beamer there is no \c@framenumber, the question
%% is never asked, and every example records exactly as it did before.
\prg_new_protected_conditional:Npnn \lx_relref_if_unseen_here: { T }
  {
    \cs_if_exist:cTF { c@framenumber }
      {
        \bool_lazy_and:nnTF
          { \cs_if_exist_p:N \beamer@coveringdepth }
          { \int_compare_p:nNn { \beamer@coveringdepth } > { 0 } }
          { \prg_return_false: }
          {
        \int_compare:nNnF { \c@framenumber } = { \g__lx_relref_frame_int }
          {
            \int_gset:Nn \g__lx_relref_frame_int { \c@framenumber }
            \prop_gclear:N \g__lx_relref_seen_prop
          }
        \prop_if_in:NVTF \g__lx_relref_seen_prop \l__lx_relref_dest_str
          { \prg_return_false: }
          {
            \prop_gput:NVn \g__lx_relref_seen_prop
              \l__lx_relref_dest_str { }
            \prg_return_true:
          }
          }
      }
      { \prg_return_true: }
  }
\cs_new_eq:NN \lx@relref@record \lx_relref_record:

%% #1 = the anchor wanted, #2 = the number as printed (for the report),
%% #3 = the whole reference, parentheses and sub-part included.  #3 is
%% what reaches the page in every branch: the link is added to the
%% reference, it never replaces or reformats it.
\cs_new_protected:Npn \lx_relref_emit:nnn #1#2#3
  {
    \bool_if:NTF \g__lx_relref_link_bool
      {
        \str_set:Ne \l__lx_relref_dest_str {#1}
        \prop_if_in:NVTF \g__lx_relref_ambig_prop \l__lx_relref_dest_str
          {
            \seq_gput_right:Ne \g__lx_relref_ambig_seq
              { #2 ~ (line~ \int_use:N \inputlineno ) }
            #3
          }
          {
            \prop_if_in:NVTF \g__lx_relref_dest_prop \l__lx_relref_dest_str
              { \exp_args:NV \hyperlink \l__lx_relref_dest_str {#3} }
              {
                \seq_gput_right:Ne \g__lx_relref_bad_seq
                  { #2 ~ (line~ \int_use:N \inputlineno ) }
                #3
              }
          }
      }
      {#3}
  }
%% Both series are formatted through one function, given the number as an
%% expression rather than as text: the anchor and the printed number have
%% to come from the same arithmetic, and evaluating it twice at each call
%% site is how they would drift.  The main series prints its number in
%% arabic and the footnote series in roman, which is the only difference
%% between the two.
\cs_new_protected:Npn \lx@fmtEx #1
  {
    \lx_relref_emit:nnn
      { \lx@Hexname { \int_eval:n {#1} } }
      { \int_eval:n {#1} }
      { \theExLBr \int_eval:n {#1} \lx@relsub \theExRBr }
  }
\cs_new_protected:Npn \lx@fmtFnEx #1
  {
    \lx_relref_emit:nnn
      { \lx@Hfnexname { \int_eval:n {#1} } }
      { \int_to_roman:n {#1} }
      { \theFnExLBr \int_to_roman:n {#1} \lx@relsub \theFnExRBr }
  }

\msg_new:nnn { linguexx } { relref-rerun }
  {
    Example~anchors~out~of~date.~
    Rerun~LaTeX~to~get~the~links~of~\iow_char:N \\Next~and~
    \iow_char:N \\Last~right.
  }
\msg_new:nnn { linguexx } { relref-dangling }
  {
    No~example~carries~the~number~a~relative~reference~asks~for:~#1.\\
    The~number~is~printed~as~before,~but~it~is~not~a~link.
  }
\msg_new:nnn { linguexx } { relref-ambiguous }
  {
    More~than~one~example~carries~the~number~a~relative~reference~
    asks~for:~#1.\\
    The~example~counter~was~reset,~so~two~examples~share~one~hyperref~
    anchor~and~a~link~would~lead~to~the~first~of~them.\\
    The~number~is~printed~as~before,~but~it~is~not~a~link.
  }
\cs_new_protected:Npn \lx_relref_check:
  {
    \bool_if:NT \g__lx_relref_link_bool
      {
        \str_if_eq:eeTF
          { \seq_use:Nn \g__lx_relref_aux_seq { , } }
          { \seq_use:Nn \g__lx_relref_new_seq { , } }
          {
            \seq_if_empty:NF \g__lx_relref_bad_seq
              { \msg_warning:nne { linguexx } { relref-dangling }
                  { \seq_use:Nn \g__lx_relref_bad_seq { ,~ } } }
            \seq_if_empty:NF \g__lx_relref_ambig_seq
              { \msg_warning:nne { linguexx } { relref-ambiguous }
                  { \seq_use:Nn \g__lx_relref_ambig_seq { ,~ } } }
          }
          { \msg_warning:nn { linguexx } { relref-rerun } }
      }
  }
\AtEndDocument { \lx_relref_check: }

%% Switched on once, if hyperref is there at all.
%%
%% hyperref's implicit=false suppresses the anchor at \refstepcounter, so
%% under it there is no destination to aim at -- and this used to stand
%% down on that ground.  It was the wrong conclusion from a true premise.
%% beamer sets implicit=false (it anchors its own \labels and manages its
%% own navigation), which is to say that in the class a great many
%% linguistics slides are written in, \ref moved and \Last did not: the
%% inconsistency this whole mechanism exists to remove, left in place by
%% the guard meant to protect it.  Nothing about implicit=false makes a
%% destination impossible; it only means hyperref will not make one
%% unasked.  So linguexx makes it, and the mechanism runs as before on
%% names it placed rather than names it read.
%%
%% Switching them on also puts a \providecommand of \lx@relref@dest into
%% the .aux, as hyperref does for its own label lines: a document that
%% drops linguexx still has last run's .aux, and reading it must not be
%% an undefined control sequence.  It goes in HERE, at \begin{document},
%% and not at the first example: \@auxout is the MAIN .aux at this point,
%% so the line precedes every record in every file -- including the ones
%% an \include writes into an .aux of its own, and including an .aux that
%% \includeonly has left over from a previous run.
\cs_new_protected:Npn \lx_relref_enable:
  {
    \bool_gset_true:N \g__lx_relref_link_bool
    \legacy_if:nT { @filesw }
      { \iow_now:Ne \@auxout
          { \token_to_str:N \providecommand
            \token_to_str:N \lx@relref@dest [1]{} } }
  }
\legacy_if:nT { lx@relreflinks }
  {
    \AtBeginDocument
      {
        \cs_if_exist:NT \hyperlink
          {
            \lx_relref_enable:
            \cs_if_exist:cT { ifHy@implicit }
              { \legacy_if:nF { Hy@implicit }
                  { \bool_gset_true:N \g__lx_relref_own_bool } }
          }
      }
  }
\ExplSyntaxOff

\NewDocumentCommand\Last{o}{{\lx@setrelsub{#1}%
  \if@noftnote
    \lx@fmtEx{\value{ExNo}}%
  \else\ifnum\value{FnExNo}>\z@
    \lx@fmtFnEx{\value{FnExNo}}%
  \else
    \lx@fmtEx{\value{ExNo}}%
  \fi\fi}\xspace}

\NewDocumentCommand\Next{o}{{\lx@setrelsub{#1}%
  \if@noftnote
    \lx@fmtEx{\value{ExNo}+1}%
  \else
    \lx@fmtFnEx{\value{FnExNo}+1}%
  \fi}\xspace}

\NewDocumentCommand\NNext{o}{{\lx@setrelsub{#1}%
  \if@noftnote
    \lx@fmtEx{\value{ExNo}+2}%
  \else
    \lx@fmtFnEx{\value{FnExNo}+2}%
  \fi}\xspace}

\NewDocumentCommand\LLast{o}{{\lx@setrelsub{#1}%
  \if@noftnote
    \lx@fmtEx{\value{ExNo}-1}%
  \else\ifnum\value{FnExNo}>\@ne
    \lx@fmtFnEx{\value{FnExNo}-1}%
  \else
    \lx@fmtEx{\value{ExNo}}%
  \fi\fi}\xspace}

\NewDocumentCommand\TextNext{o}{{\lx@setrelsub{#1}%
  \lx@fmtEx{\value{ExNo}+1}}\xspace}

% parenthesis-free twins
\newcommand\pref[1]{{\parenstrue\ref{#1}}}
\NewDocumentCommand\pLast{o}{{\parenstrue\IfValueTF{#1}{\Last[#1]}{\Last}}\xspace}
\NewDocumentCommand\pNext{o}{{\parenstrue\IfValueTF{#1}{\Next[#1]}{\Next}}\xspace}
\NewDocumentCommand\pNNext{o}%
  {{\parenstrue\IfValueTF{#1}{\NNext[#1]}{\NNext}}\xspace}
\NewDocumentCommand\pLLast{o}%
  {{\parenstrue\IfValueTF{#1}{\LLast[#1]}{\LLast}}\xspace}
\NewDocumentCommand\pTextNext{o}%
  {{\parenstrue\IfValueTF{#1}{\TextNext[#1]}{\TextNext}}\xspace}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Interlinear glossing: \gll \glll \glt (cgloss4e replacement)       %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% \gll  object line \\ gloss line \\
%% \glll object line \\ gloss line \\ second gloss line \\
%% \glt  free translation (typeset on a new line)
%%
%% Words are separated by spaces; {braced material} counts as one word
%% (so {ein Beispiel} shares a single gloss).  If lines have unequal
%% length, missing cells are empty.  Column pairs are set as \vtop boxes
%% flowing in a ragged-right paragraph, so long examples wrap between
%% columns.  Fonts per line: \eachwordone/\eachwordtwo/\eachwordthree
%% (default \textnormal, so beamer's sans-serif is respected without
%% any patching).  Inter-column glue: \GlossSep (a macro holding a glue
%% specification).

\let\eachwordone\textnormal
\let\eachwordtwo\textnormal
\let\eachwordthree\textnormal
% \GlossSep (the glue between gloss columns) is a macro, not a length, and
% its value is set in the defaults block of the Layout section above.
%
% What it costs to break the grid there.  The glue between two columns used
% to be a free breakpoint, and a gloss is set \raggedright, so every way of
% breaking it has badness 0 and the penalty is the ONLY thing telling one
% from another.  That is fine while nothing competes with it and wrong as
% soon as something does: \exannot offers its own breakpoint at \penalty50
% -- "put the annotation on the next line rather than crowd it" -- and
% against a free one TeX took the free one, splitting
%
%     il  mio  libro        (italien ; cf. a. fr. le mien livre)
%
% between "mio" and "libro" to make room.  Reported from a class deck; it
% needs a gloss whose annotation is too wide for the room left and not so
% wide that the grid break stops helping, which is why it took a while to
% see and why brk2 in the tests pins both sides of that window.
%
% 200 rather than 50-and-a-bit: the two numbers are compared as squares
% (40000 against 2500), so the margin wants to be plain rather than exact.
% It does not stop a long gloss wrapping -- with the penalty in front of
% the glue the penalty IS the breakpoint, and every alternative way of
% breaking the same gloss pays it the same number of times, so which one
% TeX picks is unchanged.  It cannot cause an overfull line either: under
% \raggedright every break is badness 0, so a feasible one always exists.
\def\lx@gl@colpenalty{200}

%% ---- the free translation beside the grid, not under it ------------------
%% \GlossTransSide asks for the translation to be set in a column beside the
%% interlinear grid; \GlossTransBelow (the default) puts it under, as it has
%% always been.  Group-scoped declarations, like \ExAnnotFit,
%% \ExRaggedRight and \GlossPhantomAlign, and deliberately NOT self-resetting
%% at the end of an example: three neighbours behave that way, TeX grouping
%% is the documented idiom, and a declaration cancelled by the machinery
%% would be the surprising one.  The risk that buys -- a flag left set
%% changing later glosses -- is the \altg toggle's shape, so it is worth
%% knowing about; see doc/EXPEX-GAPS.md, which records the argument on both
%% sides.
%%
%% The decision has to be made BEFORE the grid is set, which is why it is a
%% declaration and not something on \glt: by the time \glt is reached the
%% grid is already a finished paragraph contributed to the enclosing list,
%% and there is nothing left to set beside.  See the same file.
\newif\iflx@glside
\newcommand\GlossTransSide{\lx@glsidetrue}
\newcommand\GlossTransBelow{\lx@glsidefalse}
%% How the measure is divided.  \GlossTransRatio is a factor, not a length,
%% because it multiplies one: the grid gets that fraction of what is left
%% after \GlossTransSep, and the translation the rest.
%%
%% .6 and 2em are expex's own values (its ssratio and sssep), so that the
%% same example set with either package comes out in the same proportions --
%% the gloss is the thing being read closely and wants the room.  Taken from
%% expex.tex's own \lingset line and not from its manual, whose parameter
%% table (§12.2, printed p. 52) says sssep is 3em; the source is what runs.
%% The arithmetic is expex's too: the ratio applies to what is left AFTER
%% the separator, not to the whole measure (compare \ep@setssdims there).
\newcommand\GlossTransRatio{.6}
\newlength{\GlossTransSep}
%% Below this, the side position stops being one -- two columns too narrow
%% to read are worse than the arrangement they replaced -- and the gloss
%% falls back to putting the translation underneath, saying so in the log.
\newlength{\GlossTransMinWidth}
%% A little stretch at the right of the translation's column, which is
%% expex's ssrightskip and is there for the same reason: a narrow measure
%% gives TeX lines it cannot break any shorter, and a justified column then
%% overflows rather than ending short.  Seen at 1.11pt in this package's own
%% manual before this was added -- small, silent, and on every side gloss
%% narrow enough to produce it.
\newlength{\GlossTransRightSkip}

%% Per-tier fonts: tier n is set with the one-argument command declared
%% by \GlossTierFont{n}{cmd}.  Tiers 1--3 are pre-declared to dispatch
%% through \eachwordone/\eachwordtwo/\eachwordthree, so redefining those
%% still works (and beamer's sans-serif is respected as before);
%% undeclared tiers fall back to \textnormal.
\newcommand\GlossTierFont[2]{%
  \expandafter\def\csname lx@glfont@#1\endcsname{#2}}
\expandafter\def\csname lx@glfont@1\endcsname{\eachwordone}
\expandafter\def\csname lx@glfont@2\endcsname{\eachwordtwo}
\expandafter\def\csname lx@glfont@3\endcsname{\eachwordthree}

\ExplSyntaxOn
\tl_new:N  \l__lx_gl_tmp_tl
\seq_new:N \l__lx_gl_lines_seq
\int_new:N \l__lx_gl_ntiers_int
\int_new:N \l__lx_gl_maxwords_int
\quark_new:N \q__lx_sep
\cs_generate_variant:Nn \seq_set_split:Nnn { NnV }

% split #2 on space tokens into seq #1, dropping empty items
\cs_new_protected:Npn \__lx_split:Nn #1#2
  {
    \tl_set:Nn \l__lx_gl_tmp_tl {#2}
    \tl_replace_all:Nnn \l__lx_gl_tmp_tl { ~ } { \q__lx_sep }
    \seq_set_split:NnV #1 { \q__lx_sep } \l__lx_gl_tmp_tl
    \seq_remove_all:Nn #1 { }
  }
\cs_generate_variant:Nn \__lx_split:Nn { cn , cV }

% tier font application: \lx@glfontuse{tier}{word}
\cs_new_protected:Npn \lx@glfontuse #1#2
  {
    \cs_if_exist_use:cF { lx@glfont@ #1 }
      { \textnormal }
    {#2}
  }

%% Objective 4: interlinear glosses as structure.  Visually a gloss is a
%% grid; for a screen reader what matters is that the object word and its
%% gloss(es) are read together and are navigable as a unit.  The content
%% is emitted column by column (word 1 of every tier, then word 2, ...),
%% so the reading order is already word-by-word; here we additionally
%% wrap each column in a Span so the aligned bundle is one structure
%% element rather than loose text in a paragraph.  Tag-guarded: no effect
%% without active tagging, and the printed grid is unchanged.
\ExplSyntaxOn
%% Objective 5: language of a gloss tier.  \GlossTierLang{tier}{lang}
%% records a (BCP-47) language code for a tier; under tagging each word
%% of that tier is wrapped in a Span carrying /Lang, so a screen reader
%% pronounces the object language with its own phonetics instead of the
%% document's.  Tiers with no declared language behave exactly as before.
%% Scoping: \GlossTierLang uses a LOCAL assignment on a local property.
%% Issued in the preamble or document body it sets the document-wide
%% default (it persists to the end of its enclosing group); issued inside
%% an example -- whose body is typeset within the example's list group --
%% it overrides only that example and reverts afterwards.
\prop_new:N \l_lx_gl_lang_prop
\tl_new:N  \l__lx_gl_lang_tl
\NewDocumentCommand \GlossTierLang { m m }
  { \prop_put:Nnn \l_lx_gl_lang_prop {#1} {#2} }

%% Objective 6: Leipzig glossing abbreviations with spoken expansions.
%% \lpzg{sg} typesets the abbreviation in small caps and, under tagging,
%% wraps it in a Span carrying /E (the PDF "expansion text" of an
%% abbreviation), so a screen reader announces "singular" while the page
%% still shows SG and copy-and-paste still yields SG.  The table below is
%% the standard Leipzig Glossing Rules list, keyed by the printed short
%% form (lower case); extend or override an entry with
%% \SetLeipzig{key}{expansion}.  An unknown key is printed in small caps
%% with no expansion (no warning).  Nothing happens without active
%% tagging; the printed output is unaffected either way.
\prop_new:N \g_lx_lpzg_prop
\tl_new:N  \l__lx_lpzg_tl
\str_new:N \l__lx_lpzg_key_str   % scratch: one key in its normal form
\prop_gset_from_keyval:Nn \g_lx_lpzg_prop
  {
    1 = first~person ,
    2 = second~person ,
    3 = third~person ,
    a = agent ,
    abl = ablative ,
    abs = absolutive ,
    acc = accusative ,
    adj = adjective ,
    adv = adverbial ,
    agr = agreement ,
    all = allative ,
    antip = antipassive ,
    appl = applicative ,
    art = article ,
    aux = auxiliary ,
    ben = benefactive ,
    caus = causative ,
    clf = classifier ,
    com = comitative ,
    comp = complementizer ,
    compl = completive ,
    cond = conditional ,
    cop = copula ,
    cvb = converb ,
    dat = dative ,
    decl = declarative ,
    def = definite ,
    dem = demonstrative ,
    det = determiner ,
    dist = distal ,
    distr = distributive ,
    du = dual ,
    dur = durative ,
    erg = ergative ,
    excl = exclusive ,
    f = feminine ,
    foc = focus ,
    fut = future ,
    gen = genitive ,
    imp = imperative ,
    incl = inclusive ,
    ind = indicative ,
    indf = indefinite ,
    inf = infinitive ,
    ins = instrumental ,
    intr = intransitive ,
    ipfv = imperfective ,
    irr = irrealis ,
    loc = locative ,
    m = masculine ,
    n = neuter ,
    neg = negative ,
    nmlz = nominalizer ,
    nom = nominative ,
    obj = object ,
    obl = oblique ,
    p = patient ,
    pass = passive ,
    pfv = perfective ,
    pl = plural ,
    poss = possessive ,
    pred = predicative ,
    prf = perfect ,
    prog = progressive ,
    proh = prohibitive ,
    prox = proximal ,
    prs = present ,
    pst = past ,
    ptcp = participle ,
    purp = purposive ,
    q = question~particle ,
    quot = quotative ,
    recp = reciprocal ,
    refl = reflexive ,
    rel = relative ,
    res = resultative ,
    s = argument~of~intransitive~verb ,
    sbj = subject ,
    sbjv = subjunctive ,
    sg = singular ,
    top = topic ,
    tr = transitive ,
    voc = vocative ,
  }
%% The normal form of a key: purified, then a string.  Everything that
%% stores or compares one goes through this -- the table, the recorder,
%% the .aux, the lists, the ignore lists -- so that all of it agrees.
%%
%% A string alone is not enough, and a label reaches the package in more
%% spellings than it looks.  Its own letters are catcode 11, a piece
%% peeled off a compound one comes out of a str variable at catcode 12,
%% and the keys read back from the .aux are letters again: three
%% spellings of "sg" that a seq takes for three different entries.  Above
%% ASCII there is a fourth, which is what \text_purify:n is here for: an
%% accented letter can be written as a character or as an accent command,
%% and under pdflatex only purifying maps both to the same bytes.
%% \SetLeipzig{f\'em} and \lpzg{fém} are one key, in either order, on
%% every engine -- and \lpzg{\textbf{m}} is the key m, which is what the
%% purification was first needed for.
%% The \tl_to_str:n is not decoration: \text_purify:n hands back LETTERS
%% where the recorder, the .aux and the seqs all hold catcode-12
%% characters, and a seq compares token lists catcode and all.  Without
%% it, every site that goes through a str variable keeps working and the
%% two that compare seqs quietly stop matching -- \lpzgcheck{ignore={...}}
%% reports the very key it was told to exempt, and the "no list accounts
%% for this key" check asks for a rerun that changes nothing.
\cs_new:Npn \lx@lpzg@key:n #1
  { \exp_args:Ne \tl_to_str:n { \text_purify:n {#1} } }
%% Declarations are recorded as well as stored, so that \lpzgcheck can
%% report one that is never used.  Only what the DOCUMENT declares is
%% recorded: the built-in table below has ~100 entries, and reporting the
%% ninety-odd a paper does not happen to need would be pure noise.
\seq_new:N \g__lx_lpzg_declared_seq
\NewDocumentCommand \SetLeipzig { m m }
  {
    \str_set:Ne \l__lx_lpzg_key_str { \lx@lpzg@key:n {#1} }
    \prop_gput:NVn \g_lx_lpzg_prop \l__lx_lpzg_key_str {#2}
    \seq_if_in:NVF \g__lx_lpzg_declared_seq \l__lx_lpzg_key_str
      { \seq_gput_right:NV \g__lx_lpzg_declared_seq \l__lx_lpzg_key_str }
  }
%% \lpzg accepts a compound gloss label as a single argument, following the
%% Leipzig convention: a leading person digit is written flush against the
%% number (3sg), and further categories are separated by a period
%% (3sg.nom).  The whole label is typeset once in small caps; for the
%% spoken expansion (/E) it is parsed into pieces -- each period-separated
%% segment, with any leading 1/2/3 peeled off as a person -- and each
%% piece is expanded from the table.  Pieces not in the table pass through
%% verbatim; if nothing at all expands, no /E is emitted (the small caps
%% stand alone, exactly as for a truly unknown label).
\tl_new:N   \l__lx_lpzg_exp_tl
\str_new:N  \l__lx_lpzg_rest_str
\bool_new:N \l__lx_lpzg_any_bool
\seq_new:N  \l__lx_lpzg_seg_seq
\cs_new_protected:Npn \lx@lpzg@append #1
  {
    \tl_if_empty:NTF \l__lx_lpzg_exp_tl
      { \tl_set:Nn  \l__lx_lpzg_exp_tl {#1} }
      { \tl_put_right:Nn \l__lx_lpzg_exp_tl { ~ #1 } }
  }
\cs_new_protected:Npn \lx@lpzg@lookup #1
  {
    \lx@lpzg@record {#1}
    \prop_get:NnNTF \g_lx_lpzg_prop {#1} \l__lx_lpzg_tl
      { \bool_set_true:N \l__lx_lpzg_any_bool
        \exp_args:NV \lx@lpzg@append \l__lx_lpzg_tl }
      { \lx@lpzg@append {#1} }
  }
\cs_new_protected:Npn \lx@lpzg@seg #1
  {
    \str_set:Ne \l__lx_lpzg_rest_str { \str_head:n {#1} }
    \bool_lazy_any:nTF
      {
        { \str_if_eq_p:Vn \l__lx_lpzg_rest_str { 1 } }
        { \str_if_eq_p:Vn \l__lx_lpzg_rest_str { 2 } }
        { \str_if_eq_p:Vn \l__lx_lpzg_rest_str { 3 } }
      }
      {
        \exp_args:Ne \lx@lpzg@lookup { \str_head:n {#1} }
        \str_set:Ne \l__lx_lpzg_rest_str { \str_tail:n {#1} }
        \str_if_empty:NF \l__lx_lpzg_rest_str
          { \exp_args:NV \lx@lpzg@lookup \l__lx_lpzg_rest_str }
      }
      { \lx@lpzg@lookup {#1} }
  }
\cs_new_protected:Npn \lx@lpzg@build #1
  {
    \tl_clear:N \l__lx_lpzg_exp_tl
    \bool_set_false:N \l__lx_lpzg_any_bool
    \seq_set_split:Nnn \l__lx_lpzg_seg_seq { . } {#1}
    %% Blank segments are dropped rather than looked up.  A trailing or
    %% doubled period ("\lpzg{sg.}") splits into {sg} and an EMPTY piece,
    %% and the empty piece was recorded as a used key like any other, so
    %% \lpzgcheck later reported "No expansion known for" nothing at all
    %% -- and the /E carried a trailing space.  Harmless, but the warning
    %% named a key the author could not find in the source.
    \seq_map_inline:Nn \l__lx_lpzg_seg_seq
      { \tl_if_blank:nF {##1} { \lx@lpzg@seg {##1} } }
  }
%% A modifier written INSIDE the label -- \lpzg{\textbf{m}.pl}, to pick one
%% cell of a paradigm out of the line around it -- must ADD its feature to
%% the small caps, not replace them.  \textsc{\textbf{m}.pl} says exactly
%% that, and NFSS obliges wherever the family HAS the shape: pdflatex's
%% default T1/cmr does, and so does any OpenType face carrying smcp in its
%% bold weight (the TeX Gyre families do).  Latin Modern does not --
%% lmromancaps exists in regular and oblique only, in every encoding --
%% and NFSS's fallback there keeps the series and drops the SHAPE, so the
%% label came out as a bold lowercase "m" with the small caps gone.  That
%% is the one feature a gloss label cannot lose: the small caps are what
%% marks it as a category rather than a word.  It is silent, too: the log
%% says "Font shape `TU/lmr/bx/sc' undefined", which reads like a remark
%% about a font, and the page still looks deliberate.
%%
%% So the choice is made per LEAF, and made by asking the font that was
%% actually selected rather than by keeping a list of families:
%% \lx@sc@real: compares the font \scshape lands on with the one in force
%% without it.  A substitution is invisible in \f@shape (which stays "sc"
%% either way) and plain in \fontname.  Shape real: \textsc, exactly as
%% before, byte for byte -- which is why nothing about the pdflatex output
%% of any existing document moves.  Shape missing: the caps are MADE --
%% the letters uppercased and set at \LpzgCapsScale of the current size in
%% whatever font the modifier chose, so the boldface survives and the
%% height still matches the real small caps beside it (Latin Modern's sit
%% at 0.7526 of its cap height; the default is that figure, rounded).
%%
%% The modifiers reach the leaves because \lpzg redefines them, locally
%% and for the length of its own argument only, to re-enter \lx@sc:n
%% inside their own font change.  That is what makes the decision per leaf
%% instead of per label: in \lpzg{\textbf{m}.pl} the ".pl" is real medium
%% small caps and only the "m" is made.  \l__lx_sc_made_bool stops a
%% nested modifier from making caps twice -- uppercasing is idempotent,
%% scaling by 0.75 twice is not.
\newcommand \LpzgCapsScale {0.75}
\tl_new:N   \g__lx_sc_up_tl
\tl_new:N   \g__lx_sc_sc_tl
\tl_new:N   \l__lx_sc_caps_tl
\tl_new:N   \l__lx_sc_keys_tl
\bool_new:N \l__lx_sc_made_bool
%% Upright against small caps, at whatever series and family are in force
%% -- NOT the current font against the small-caps one.  The two differ
%% exactly where this matters: inside a \textsc the current font ALREADY
%% is the small-caps one, so comparing against it reports the shape
%% present in the one place it can be missing (\textsc{\bfseries ...}: the
%% shape is "sc", the font is the plain bold one).  \f@shape is no use
%% either, for the same reason -- it says "sc" whether or not a font
%% answered to it.  \selectfont in both probes, because LaTeX defers font
%% selection: \font on its own may still be whatever ran last.
\prg_new_protected_conditional:Npnn \lx@sc@real: { TF }
  {
    \group_begin:
      \upshape \selectfont \tl_gset:Ne \g__lx_sc_up_tl { \fontname \font }
    \group_end:
    \group_begin:
      \scshape \selectfont \tl_gset:Ne \g__lx_sc_sc_tl { \fontname \font }
    \group_end:
    \tl_if_eq:NNTF \g__lx_sc_up_tl \g__lx_sc_sc_tl
      { \prg_return_false: } { \prg_return_true: }
  }
%% Made caps are uppercase letters, so the text layer would hand out "M"
%% where the source says "m".  Under active tagging they therefore carry
%% marked content of their own with an /ActualText of the letters as
%% written, which is what copy-and-paste and a screen reader get; the
%% end_push/begin_pop idiom is the usual one, because this MC opens inside
%% the abbreviation's own Span and must not nest in its MC.
\cs_new_protected:Npn \lx@sc@made:n #1
  {
    \group_begin:
      \bool_set_true:N \l__lx_sc_made_bool
      \fontsize { \fp_eval:n { \LpzgCapsScale * \f@size } } { \f@baselineskip }
      \selectfont
      \tl_set:Ne \l__lx_sc_caps_tl { \text_uppercase:n {#1} }
      \lx@tag@if@active:TF
        {
          %% The keyvals are expanded HERE, into a token list, and the list
          %% is handed over whole.  \tag_mc_begin:n does not expand what it
          %% is given far enough for a \text_purify:n written into the
          %% value to run, and the /ActualText then reads out the expl3
          %% internals of the call instead of the letters -- which
          %% copy-and-paste and a screen reader would both hand on.
          %% Inside the guard, though, exactly like the /E of the Span
          %% around it: an untagged run must not expand it at all.
          \tl_set:Ne \l__lx_sc_keys_tl
            { tag = Span , actualtext = { \text_purify:n {#1} } }
          \tag_mc_end_push:
          \exp_args:NV \tag_mc_begin:n \l__lx_sc_keys_tl
          \l__lx_sc_caps_tl
          \tag_mc_end: \tag_mc_begin_pop:n {}
        }
        { \l__lx_sc_caps_tl }
    \group_end:
  }
\cs_new_protected:Npn \lx@sc:n #1
  {
    \bool_if:NTF \l__lx_sc_made_bool
      { #1 }
      { \lx@sc@real:TF { \textsc {#1} } { \lx@sc@made:n {#1} } }
  }
%% The redefinitions.  Local to the label, and deliberately not a patch of
%% \textbf & co.: outside \lpzg they are the kernel's, unchanged.  A
%% command NOT in this list (\textcolor, say) keeps its own meaning and
%% its argument stays inside whatever small caps are in force around it,
%% which is what \textsc did for everything before.
%%
%% One redefinition: command #1 becomes "switch #2, then \lx@sc:n".  The
%% shape is written once here rather than once per command, so that a
%% modifier added below cannot get a different one by a slip of copying.
%% The list itself stays one command per line, and not a mapped clist,
%% so that \textbf is still greppable to the place that redefines it.
\cs_new_protected:Npn \lx@sc@modifier:NN #1#2
  { \cs_set_protected:Npn #1 ##1 { \group_begin: #2 \lx@sc:n {##1} \group_end: } }
\cs_new_protected:Npn \lx@sc@modifiers:
  {
    \lx@sc@modifier:NN \textbf     \bfseries
    \lx@sc@modifier:NN \textmd     \mdseries
    \lx@sc@modifier:NN \textit     \itshape
    \lx@sc@modifier:NN \textsl     \slshape
    \lx@sc@modifier:NN \textup     \upshape
    \lx@sc@modifier:NN \textrm     \rmfamily
    \lx@sc@modifier:NN \textsf     \sffamily
    \lx@sc@modifier:NN \texttt     \ttfamily
    \lx@sc@modifier:NN \textnormal \normalfont
    \lx@sc@modifier:NN \emph       \em
  }
%% Printing one label: the modifiers, then the label itself.  The group is
%% the reason \lpzg can redefine anything at all.
\cs_new_protected:Npn \lx@lpzg@print:n #1
  { \group_begin: \lx@sc@modifiers: \lx@sc:n {#1} \group_end: }
%% The label is PURIFIED before it is parsed.  \text_purify:n takes the
%% markup off and leaves the letters, so \lpzg{\textbf{m}.pl} looks up m
%% and pl exactly as \lpzg{m.pl} does.  Without it the first segment
%% reached the table as "\textbf {m}", which expands to nothing: no /E for
%% that piece, a \lpzglist entry named after a control sequence, and a
%% \lpzgcheck warning about a key the author cannot find in the source.
\cs_new_protected:Npn \lx@lpzg@parse:n #1
  { \exp_args:Ne \lx@lpzg@build { \lx@lpzg@key:n {#1} } }
\NewDocumentCommand \lpzg { m }
  {
    \lx@lpzg@parse:n {#1}
    \bool_if:NTF \l__lx_lpzg_any_bool
      { \lx@tag@span@exp:nn { tag = Span , E = { \l__lx_lpzg_exp_tl } }
          { \lx@lpzg@print:n {#1} } }
      { \lx@lpzg@print:n {#1} }
  }

%% An abbreviation may stand in a section title, and hyperref has to be
%% told what it means there.  A title is \edef-ed into a PDF string for the
%% bookmark, where a command that is not on hyperref's list produces
%%
%%   Token not allowed in a PDF string (Unicode): removing `\lpzg'
%%
%% on every run.  The bookmark came out right in spite of it -- dropping
%% the command leaves the label behind as text -- but only by accident,
%% and the warning is real noise in a document that glosses in its
%% headings.  A PDF string has no small caps and no structure, so the
%% right value is the label as written, which is \@firstofone: \lpzg{3sg}
%% bookmarks as "3sg", the same letters an /ActualText hands out.
%%
%% \lpzg ONLY, deliberately.  \altn and \altg print a stack of
%% alternatives, and \Next and its family print a number the surrounding
%% list machinery works out -- neither has a sensible one-line reading,
%% and neither belongs in a heading in the first place.  They keep
%% hyperref's own treatment (dropped, with the warning that says so),
%% which is the honest outcome for a command that cannot be spelt in a
%% string.
%%
%% Deferred to \AtBeginDocument because hyperref may be loaded after this
%% package, and guarded because it may not be loaded at all.
%% \pdfstringdefDisableCommands appends to a hook rather than setting it,
%% so a document that has its own entries keeps them.
\AtBeginDocument
  {
    \cs_if_exist:NT \pdfstringdefDisableCommands
      { \pdfstringdefDisableCommands { \cs_set_eq:NN \lpzg \use:n } }
  }

%% \lpzglist: the list of abbreviations the document actually uses.
%%
%% Every piece \lpzg resolves is recorded -- the ATOMIC pieces, so
%% \lpzg{3sg.pst} contributes 3, sg and pst separately, which is what a
%% list of abbreviations wants.  Each key is recorded once and written to
%% the .aux, so \lpzglist may stand anywhere, in particular in the front
%% matter, before the uses it reports on: it prints the keys of the
%% PREVIOUS run plus everything used so far in this one (so a list at the
%% end of the document is already complete on the first run).  A key that
%% the .aux did not know about, or that no list accounts for, means some
%% list came out short: \AtEndDocument asks for a rerun, as for a table of
%% contents.
%%
%% Keys with no known expansion (project labels not in the Leipzig table
%% and not declared with \SetLeipzig) are omitted and reported once; ask
%% for unexplained=keep to list them with an empty explanation instead.
%%
%% Every key is stored and compared in the normal form of
%% \lx@lpzg@key:n, which is where the reason for it is written out.
\seq_new:N  \g__lx_lpzg_used_seq   % keys seen on THIS run, in order of use
\prop_new:N \g__lx_lpzg_used_prop  % the same, as a set: the duplicate guard
\seq_new:N  \g__lx_lpzg_aux_seq    % keys read back from the .aux (last run)
\bool_new:N \g__lx_lpzg_doc_bool   % true once the .aux is open for writing
\bool_new:N \g__lx_lpzg_rerun_bool % a key the .aux did not know about
\AtBeginDocument { \bool_gset_true:N \g__lx_lpzg_doc_bool }
%% written to and read from the .aux
\cs_new_protected:Npn \lx@lpzg@used #1
  { \seq_gput_right:Ne \g__lx_lpzg_aux_seq { \lx@lpzg@key:n {#1} } }
\bool_new:N \l__lx_lpzg_norecord_bool % true while \lpzglist typesets itself
\cs_new_protected:Npn \lx@lpzg@record #1
  {
    \bool_if:NF \l__lx_lpzg_norecord_bool
      { \lx@lpzg@record@ {#1} }
  }
\cs_new_protected:Npn \lx@lpzg@record@ #1
  {
    \str_set:Ne \l__lx_lpzg_key_str { \lx@lpzg@key:n {#1} }
    \prop_if_in:NVF \g__lx_lpzg_used_prop \l__lx_lpzg_key_str
      {
        \prop_gput:NVn \g__lx_lpzg_used_prop \l__lx_lpzg_key_str { }
        \seq_gput_right:NV \g__lx_lpzg_used_seq \l__lx_lpzg_key_str
        %% Recorded in the preamble, a key is there before any list is
        %% typeset, so it needs neither the .aux nor a rerun.  Recorded in
        %% the body and unknown to the .aux, it means the .aux is one run
        %% behind and a list placed before this point is short of an entry.
        \bool_if:NT \g__lx_lpzg_doc_bool
          {
            \seq_if_in:NVF \g__lx_lpzg_aux_seq \l__lx_lpzg_key_str
              { \bool_gset_true:N \g__lx_lpzg_rerun_bool }
            \legacy_if:nT { @filesw }
              { \iow_now:Ne \@auxout
                  { \token_to_str:N \lx@lpzg@used { \l__lx_lpzg_key_str } } }
          }
      }
  }
%% \lpzgadd{key,key,...} records abbreviations used outside \lpzg (in a
%% figure, in running text) so that they appear in the list all the same.
\NewDocumentCommand \lpzgadd { m }
  { \clist_map_inline:nn {#1} { \lx@lpzg@record@ {##1} } }

\seq_new:N   \l__lx_lpzglist_seq        % the entries to print
\seq_new:N   \l__lx_lpzglist_tmp_seq
\seq_new:N   \l__lx_lpzglist_unknown_seq
\seq_new:N   \g__lx_lpzglist_printed_seq
\bool_new:N  \g__lx_lpzglist_used_bool
\bool_new:N  \l__lx_lpzglist_rerun_bool
\bool_new:N  \l__lx_lpzglist_first_bool
\box_new:N   \l__lx_lpzglist_box
\dim_new:N   \l__lx_lpzglist_wd_dim
\msg_new:nnn { linguexx } { lpzglist-rerun }
  {
    Abbreviation~list~out~of~date.~
    Rerun~LaTeX~to~get~\iow_char:N \\lpzglist~right.
  }
\msg_new:nnn { linguexx } { lpzglist-unknown }
  {
    No~expansion~known~for~#1~--~omitted~from~
    \iow_char:N \\lpzglist.\\
    Declare~it~with~\iow_char:N \\SetLeipzig,~
    or~pass~unexplained=keep~to~list~it~unexplained.
  }

%% Presentation.  Everything below is either a key of \lpzglist (set for
%% one list) or of \lpzglistsetup (set for all of them), or one of the two
%% user commands \lpzglistentry / \lpzglisttitle, which may be redefined
%% wholesale with \renewcommand.
\str_new:N   \l__lx_lpzglist_style_str
\str_new:N   \l__lx_lpzglist_incl_str
\tl_new:N    \l__lx_lpzglist_title_tl
\tl_new:N    \l__lx_lpzglist_titlestyle_tl
\tl_new:N    \l__lx_lpzglist_sep_tl
\clist_new:N \l__lx_lpzglist_ignore_clist
\clist_new:N \l__lx_lpzglist_add_clist
\bool_new:N  \l__lx_lpzglist_sort_bool
\bool_new:N  \l__lx_lpzglist_keepun_bool
\skip_new:N  \l__lx_lpzglist_itemsep_skip
\keys_define:nn { lx / lpzglist }
  {
    style        .choices:nn =
      { list , inline }
      { \str_set:NV \l__lx_lpzglist_style_str \l_keys_choice_tl } ,
    include      .choices:nn =
      { used , all }
      { \str_set:NV \l__lx_lpzglist_incl_str \l_keys_choice_tl } ,
    sort         .bool_set:N  = \l__lx_lpzglist_sort_bool ,
    sort         .default:n   = { true } ,
    unexplained  .choices:nn  =
      { omit , keep }
      {
        \bool_set:Nn \l__lx_lpzglist_keepun_bool
          { \str_if_eq_p:Vn \l_keys_choice_tl { keep } }
      } ,
    unexplained  .default:n   = { keep } ,
    title        .tl_set:N    = \l__lx_lpzglist_title_tl ,
    titlestyle   .tl_set:N    = \l__lx_lpzglist_titlestyle_tl ,
    sep          .tl_set:N    = \l__lx_lpzglist_sep_tl ,
    itemsep      .skip_set:N  = \l__lx_lpzglist_itemsep_skip ,
    ignore       .clist_set:N = \l__lx_lpzglist_ignore_clist ,
    add          .clist_set:N = \l__lx_lpzglist_add_clist ,
    format       .code:n      =
      { \cs_set_protected:Npn \lpzglistentry ##1##2 {#1} } ,
  }
\keys_set:nn { lx / lpzglist }
  {
    style = list , include = used , sort = true , unexplained = omit ,
    title = { } , titlestyle = \lpzglisttitle , sep = { ;~ } ,
    itemsep = 0pt ,
  }
\NewDocumentCommand \lpzglistsetup { m }
  { \keys_set:nn { lx / lpzglist } {#1} }
%% One entry: #1 the key, #2 its expansion (empty if none is known).
\NewDocumentCommand \lpzglistentry { m m }
  {
    \str_if_eq:VnTF \l__lx_lpzglist_style_str { inline }
      { \lpzg {#1} ~ #2 }
      { \item [ \lpzg {#1} ] #2 }
  }
\NewDocumentCommand \lpzglisttitle { m }
  {
    \cs_if_exist:NTF \section
      { \section * {#1} }
      { \par \noindent \textbf {#1} \par \nobreak \smallskip }
  }

%% Collect the keys to print, in printing order.  Keys arrive as strings
%% from the recorder, but as letters from the built-in table and from the
%% add= and ignore= lists, so every one of them is normalised here too.
\cs_generate_variant:Nn \seq_remove_all:Nn { NV }
\cs_new_protected:Npn \lx@lpzglist@put #1
  {
    \str_set:Ne \l__lx_lpzg_key_str { \lx@lpzg@key:n {#1} }
    \seq_if_in:NVF \l__lx_lpzglist_seq \l__lx_lpzg_key_str
      { \seq_put_right:NV \l__lx_lpzglist_seq \l__lx_lpzg_key_str }
  }
\cs_new_protected:Npn \lx@lpzglist@collect
  {
    \seq_clear:N \l__lx_lpzglist_seq
    \seq_clear:N \l__lx_lpzglist_unknown_seq
    \str_if_eq:VnTF \l__lx_lpzglist_incl_str { all }
      { \prop_map_inline:Nn \g_lx_lpzg_prop { \lx@lpzglist@put {##1} } }
      {
        %% the .aux (a full run) first, this run's new keys after it, so
        %% that sort=false yields the order of first use
        \seq_map_inline:Nn \g__lx_lpzg_aux_seq  { \lx@lpzglist@put {##1} }
        \seq_map_inline:Nn \g__lx_lpzg_used_seq { \lx@lpzglist@put {##1} }
      }
    \clist_map_inline:Nn \l__lx_lpzglist_add_clist { \lx@lpzglist@put {##1} }
    \clist_map_inline:Nn \l__lx_lpzglist_ignore_clist
      {
        \str_set:Ne \l__lx_lpzg_key_str { \lx@lpzg@key:n {##1} }
        \seq_remove_all:NV \l__lx_lpzglist_seq \l__lx_lpzg_key_str
      }
    \seq_set_eq:NN \l__lx_lpzglist_tmp_seq \l__lx_lpzglist_seq
    \seq_clear:N \l__lx_lpzglist_seq
    \seq_map_inline:Nn \l__lx_lpzglist_tmp_seq
      {
        \prop_if_in:NnTF \g_lx_lpzg_prop {##1}
          { \seq_put_right:Nn \l__lx_lpzglist_seq {##1} }
          {
            \seq_put_right:Nn \l__lx_lpzglist_unknown_seq {##1}
            \bool_if:NT \l__lx_lpzglist_keepun_bool
              { \seq_put_right:Nn \l__lx_lpzglist_seq {##1} }
          }
      }
    \bool_if:NT \l__lx_lpzglist_sort_bool
      {
        \seq_sort:Nn \l__lx_lpzglist_seq
          {
            \str_compare:nNnTF {##1} > {##2}
              { \sort_return_swapped: } { \sort_return_same: }
          }
      }
  }
%% A key is stored as a string, and a string is BYTES.  On the unicode
%% engines that is the same thing as the character; under pdflatex it is
%% not, and a key with anything outside ASCII in it was typeset one byte
%% at a time: \lpzgadd{abß} printed "abÃ§", because the two bytes of ß
%% went to the T1 slots of Ã and §.  Only the printing was ever wrong --
%% the .aux round trip, the /E expansion text (tagpdf reads the bytes as
%% UTF-8, which is what they are) and the warnings in the log all come out
%% right, which is why this survived so long: everything ABOUT the key was
%% correct except the key on the page.
%%
%% So the bytes are turned back into characters where they are set, and
%% nowhere else -- identity, comparison, sorting and the .aux keep the
%% string they have always had.  Re-tokenising is what does it: under
%% pdflatex the bytes above 127 are active (that is inputenc's utf8
%% machinery) and reassemble themselves into one character; under xelatex
%% and lualatex the character is already one token and rescanning gives it
%% back unchanged.  Everything else is forced to catcode 12 first: a key
%% is text, not markup, and a stray % or \ in one is a printing problem,
%% not a licence to run code the author did not write.  Spaces stay
%% spaces, since this is called from expl3 code, where they would be
%% dropped.
\tl_new:N \l__lx_lpzglist_show_tl
\cs_new_protected:Npn \lx@lpzglist@textcat:
  {
    \char_set_catcode_other:N \\  \char_set_catcode_other:N \{
    \char_set_catcode_other:N \}  \char_set_catcode_other:N \$
    \char_set_catcode_other:N \&  \char_set_catcode_other:N \#
    \char_set_catcode_other:N \^  \char_set_catcode_other:N \_
    \char_set_catcode_other:N \~  \char_set_catcode_other:N \%
    \char_set_catcode_space:n { 32 }
  }
\cs_new_protected:Npn \lx@lpzglist@show:n #1
  {
    \tl_set_rescan:Nnn \l__lx_lpzglist_show_tl
      { \lx@lpzglist@textcat: } {#1}
  }
%% One entry, expansion looked up (empty for an unknown key kept on
%% request).  The expansion is looked up under the STRING key and the
%% entry is handed the readable one, so a redefined \lpzglistentry (or a
%% format= of one list) receives text it can set, uppercase or measure.
\cs_new_protected:Npn \lx@lpzglist@item #1
  {
    \lx@lpzglist@show:n {#1}
    \prop_get:NnNTF \g_lx_lpzg_prop {#1} \l__lx_lpzg_tl
      { \exp_args:NVV \lpzglistentry \l__lx_lpzglist_show_tl \l__lx_lpzg_tl }
      { \exp_args:NV \lpzglistentry \l__lx_lpzglist_show_tl { } }
  }
%% The label column is as wide as the widest abbreviation.  Measured with
%% \textsc, which is what \lpzg prints, and NOT with \lpzg itself: \lpzg
%% opens structure elements, and a box that is measured and thrown away
%% would leave them behind in the tree.  Measured from the readable form
%% because that is what gets set: measuring the byte spelling and printing
%% the character would reserve a column wider than the labels in it, by
%% one glyph per byte above 127.
\cs_new_protected:Npn \lx@lpzglist@widest
  {
    \dim_zero:N \l__lx_lpzglist_wd_dim
    \seq_map_inline:Nn \l__lx_lpzglist_seq
      {
        \lx@lpzglist@show:n {##1}
        \hbox_set:Nn \l__lx_lpzglist_box
          { \textsc { \l__lx_lpzglist_show_tl } }
        \dim_set:Nn \l__lx_lpzglist_wd_dim
          { \dim_max:nn \l__lx_lpzglist_wd_dim { \box_wd:N \l__lx_lpzglist_box } }
      }
  }
\cs_new_protected:Npn \lx@lpzglist@dolist
  {
    \lx@lpzglist@widest
    %% No \lx@ol@set here: the abbreviation list is NOT an ordered list,
    %% and it keeps the block code's own class for a labelled list.  A
    %% class of its own carrying /ListNumbering /None looks right and is
    %% not: PDF/UA-2 8.2.5.25 forbids /None precisely when the items have
    %% Lbl elements, which these do (veraPDF rejects the file).  What the
    %% block code puts there instead is /Unordered, which is what a
    %% labelled list is; poppler warns about the value, veraPDF accepts it.
    \begin { list } { }
      {
        \dim_set:Nn \labelwidth { \l__lx_lpzglist_wd_dim }
        \dim_set:Nn \leftmargin { \labelwidth + \labelsep }
        \dim_zero:N \itemindent
        \dim_zero:N \listparindent
        \dim_zero:N \rightmargin
        \skip_set_eq:NN \itemsep \l__lx_lpzglist_itemsep_skip
        \skip_zero:N \parsep
        \skip_zero:N \partopsep
        %% \lx@flushlabel, not \hfil: under active tagging the block code
        %% re-boxes a label flush RIGHT unless it already fills its own
        %% \labelwidth box (the same reason the example labels use it).
        \cs_set_eq:NN \makelabel \lx@flushlabel
      }
      \seq_map_inline:Nn \l__lx_lpzglist_seq { \lx@lpzglist@item {##1} }
    \end { list }
  }
%% The \unskip guards the separator against an entry whose expansion is
%% empty (an unexplained key kept on request), which would otherwise leave
%% the space of the entry format sitting in front of the semicolon.
\cs_new_protected:Npn \lx@lpzglist@doinline
  {
    \bool_set_true:N \l__lx_lpzglist_first_bool
    \seq_map_inline:Nn \l__lx_lpzglist_seq
      {
        \bool_if:NTF \l__lx_lpzglist_first_bool
          { \bool_set_false:N \l__lx_lpzglist_first_bool }
          { \unskip \l__lx_lpzglist_sep_tl }
        \lx@lpzglist@item {##1}
      }
    \unskip
  }
\cs_new_protected:Npn \lx@lpzglist@render
  {
    \tl_if_blank:VF \l__lx_lpzglist_title_tl
      { \l__lx_lpzglist_titlestyle_tl { \l__lx_lpzglist_title_tl } }
    \str_if_eq:VnTF \l__lx_lpzglist_style_str { inline }
      { \lx@lpzglist@doinline }
      { \lx@lpzglist@dolist }
  }
\NewDocumentCommand \lpzglist { O{} }
  {
    \group_begin:
      %% the list's own \lpzg calls must not enlarge the set they report on
      \bool_set_true:N \l__lx_lpzg_norecord_bool
      \keys_set:nn { lx / lpzglist } {#1}
      \lx@lpzglist@collect
      \bool_gset_true:N \g__lx_lpzglist_used_bool
      %% what this list accounts for: everything it prints, everything it
      %% was told to leave out, and the keys it could not explain (those
      %% are reported here and now, not as a rerun request)
      \seq_map_inline:Nn \l__lx_lpzglist_seq
        { \seq_gput_right:Nn \g__lx_lpzglist_printed_seq {##1} }
      \seq_map_inline:Nn \l__lx_lpzglist_unknown_seq
        { \seq_gput_right:Nn \g__lx_lpzglist_printed_seq {##1} }
      \clist_map_inline:Nn \l__lx_lpzglist_ignore_clist
        { \seq_gput_right:Ne \g__lx_lpzglist_printed_seq
            { \lx@lpzg@key:n {##1} } }
      \seq_if_empty:NF \l__lx_lpzglist_seq { \lx@lpzglist@render }
      \bool_lazy_and:nnT
        { ! \l__lx_lpzglist_keepun_bool }
        { ! \seq_if_empty_p:N \l__lx_lpzglist_unknown_seq }
        {
          \msg_warning:nnx { linguexx } { lpzglist-unknown }
            { \seq_use:Nnnn \l__lx_lpzglist_unknown_seq { ~and~ } { ,~ } { ,~and~ } }
          %% so \lpzgcheck does not report the same keys a second time
          \seq_map_inline:Nn \l__lx_lpzglist_unknown_seq
            { \seq_gput_right:Nn \g__lx_lpzg_warned_seq {##1} }
        }
    \group_end:
  }
%% Anything used but not accounted for by a list means the .aux was one
%% run behind: ask for a rerun, as \tableofcontents does.
\cs_new_protected:Npn \lx@lpzglist@checkrerun
  {
    \bool_if:NT \g__lx_lpzglist_used_bool
      {
        \bool_set_eq:NN \l__lx_lpzglist_rerun_bool \g__lx_lpzg_rerun_bool
        \seq_map_inline:Nn \g__lx_lpzg_used_seq
          {
            \seq_if_in:NnF \g__lx_lpzglist_printed_seq {##1}
              { \bool_set_true:N \l__lx_lpzglist_rerun_bool }
          }
        \bool_if:NT \l__lx_lpzglist_rerun_bool
          { \msg_warning:nn { linguexx } { lpzglist-rerun } }
      }
  }

%%%% \lpzgcheck: consistency of the abbreviations themselves --------------
%%
%% Until now the only report on abbreviations was a side effect of building
%% a list: \lpzglist warns about the keys it cannot explain.  A document
%% that never calls \lpzglist got nothing at all, so a mistyped
%% \lpzg{pres} for \lpzg{prs} printed PRES in small caps and passed in
%% silence.  These checks run at the end of every document instead, whether
%% or not a list was asked for.
%%
%%   unknown  (default TRUE)  a key used but with no known expansion.
%%            Almost always a typo or a forgotten \SetLeipzig.
%%   unused   (default FALSE) a key declared with \SetLeipzig and never
%%            used.  Off by default because a standing set of declarations
%%            in a shared preamble, only partly used in any one paper, is a
%%            perfectly reasonable way to work and would warn on every run.
%%   ignore   keys to exempt from `unknown': abbreviations deliberately
%%            left unexplained.
\bool_new:N  \g__lx_lpzgcheck_unknown_bool
\bool_new:N  \g__lx_lpzgcheck_unused_bool
\clist_new:N \g__lx_lpzgcheck_ignore_clist
\bool_gset_true:N \g__lx_lpzgcheck_unknown_bool
\seq_new:N \g__lx_lpzg_warned_seq  % unknowns a \lpzglist already reported
\keys_define:nn { lx / lpzgcheck }
  {
    unknown .bool_gset:N  = \g__lx_lpzgcheck_unknown_bool ,
    unknown .default:n    = { true } ,
    unused  .bool_gset:N  = \g__lx_lpzgcheck_unused_bool ,
    unused  .default:n    = { true } ,
    ignore  .clist_gset:N = \g__lx_lpzgcheck_ignore_clist ,
  }
\NewDocumentCommand \lpzgcheck { m }
  { \keys_set:nn { lx / lpzgcheck } {#1} }
\msg_new:nnn { linguexx } { lpzg-unknown }
  {
    No~expansion~known~for~#1.\\
    Check~the~spelling,~declare~it~with~\iow_char:N \\SetLeipzig,~or~
    exempt~it~with~\iow_char:N \\lpzgcheck{ignore={...}}.
  }
\msg_new:nnn { linguexx } { lpzg-unused }
  {
    Declared~with~\iow_char:N \\SetLeipzig~but~never~used:~#1.
  }
\seq_new:N \l__lx_lpzgcheck_tmp_seq
\seq_new:N \l__lx_lpzgcheck_ign_seq
\cs_new_protected:Npn \lx@lpzgcheck@run
  {
    %% used but unexplained.  Keys a \lpzglist already reported are skipped:
    %% the two checks are independent, but saying it twice is not a service.
    \bool_if:NT \g__lx_lpzgcheck_unknown_bool
      {
        %% The recorded keys are in the normal form of \lx@lpzg@key:n;
        %% the ignore list arrives as the author typed it, and comparing
        %% the two spellings would never match -- the same normalisation
        %% \lpzglist's own ignore= needs.
        \seq_clear:N \l__lx_lpzgcheck_ign_seq
        \clist_map_inline:Nn \g__lx_lpzgcheck_ignore_clist
          { \seq_put_right:Ne \l__lx_lpzgcheck_ign_seq
              { \lx@lpzg@key:n {##1} } }
        \seq_clear:N \l__lx_lpzgcheck_tmp_seq
        \seq_map_inline:Nn \g__lx_lpzg_used_seq
          {
            \prop_if_in:NnF \g_lx_lpzg_prop {##1}
              {
                \seq_if_in:NnF \l__lx_lpzgcheck_ign_seq {##1}
                  { \seq_if_in:NnF \g__lx_lpzg_warned_seq {##1}
                      { \seq_put_right:Nn \l__lx_lpzgcheck_tmp_seq {##1} } }
              }
          }
        \seq_if_empty:NF \l__lx_lpzgcheck_tmp_seq
          {
            \msg_warning:nne { linguexx } { lpzg-unknown }
              { \seq_use:Nnnn \l__lx_lpzgcheck_tmp_seq
                  { ~and~ } { ,~ } { ,~and~ } }
          }
      }
    %% declared and never used
    \bool_if:NT \g__lx_lpzgcheck_unused_bool
      {
        \seq_clear:N \l__lx_lpzgcheck_tmp_seq
        \seq_map_inline:Nn \g__lx_lpzg_declared_seq
          {
            \seq_if_in:NnF \g__lx_lpzg_used_seq {##1}
              { \seq_put_right:Nn \l__lx_lpzgcheck_tmp_seq {##1} }
          }
        \seq_if_empty:NF \l__lx_lpzgcheck_tmp_seq
          {
            \msg_warning:nne { linguexx } { lpzg-unused }
              { \seq_use:Nnnn \l__lx_lpzgcheck_tmp_seq
                  { ~and~ } { ,~ } { ,~and~ } }
          }
      }
  }
\AtEndDocument { \lx@lpzglist@checkrerun \lx@lpzgcheck@run }

%% The column Span takes the OUTER half of the idiom only: its content is
%% the tier words, and each of those opens marked content of its own.
\cs_new_protected:Npn \lx@gl@colbegin
  { \lx@tag@if@active:T { \lx@tag@span@open:n { tag = Span } } }
\cs_new_protected:Npn \lx@gl@colend
  { \lx@tag@if@active:T { \lx@tag@span@close: } }
%% #1 = tier number.  If the tier has a declared language, wrap the word
%% in its own Span with /Lang (nested in the column Span); otherwise emit
%% plain marked content as before.  Neither half of the helper fits: the
%% struct is conditional but the marked content is not, and there is no
%% ambient MC to suspend -- the column Span already suspended it.
\cs_new_protected:Npn \lx@gl@wordbegin #1
  { \lx@tag@if@active:T
      {
        \prop_get:NnNTF \l_lx_gl_lang_prop {#1} \l__lx_gl_lang_tl
          { \exp_args:Ne \tag_struct_begin:n
              { tag = Span , lang = \l__lx_gl_lang_tl } }
          { }
        \tag_mc_begin:n { tag = Span }
      } }
\cs_new_protected:Npn \lx@gl@wordend #1
  { \lx@tag@if@active:T
      {
        \tag_mc_end:
        \prop_if_in:NnT \l_lx_gl_lang_prop {#1} { \tag_struct_end: }
      } }
\ExplSyntaxOff
\ExplSyntaxOn
%% Phantom bracket alignment (opt-in, off by default).  When a word in the
%% object line opens with a run of delimiter/judgment characters -- e.g.
%% "[ein" in "ich bin [ein Idiot]" -- the gloss word below it normally
%% left-aligns with the "[", not with "ein".  With alignment on, the gloss
%% word (and any further tiers) is preceded by a \phantom of that leading
%% run, SET IN THE OBJECT-TIER FONT, so its first real glyph sits under the
%% first real glyph of the object word regardless of the gloss-tier size
%% (e.g. \footnotesize glosses still line up).  The phantom ships no ink and
%% no marked content, so tagging is unaffected.  Enable with the package
%% option [phantomalign] or \GlossPhantomAlign; \GlossPhantomAlignOff turns
%% it back off.  \GlossPhantomChars sets the leading marks that count
%% (default: the judgment marks * ? \# \% and the openers ( [ < ), and the
%% same set is what an \altn stack peels into its hanging gutter (see
%% \__lxp_alt_setstack:), and an \altg one -- but there only on the call
%% that sets the object tier, and on a solo stack: a judgment is a claim
%% about the object language, not about its translation, so a mark in a
%% gloss alternative is left where it was typed.
\bool_new:N \l__lx_gl_phantom_bool
\iflx@phantomalign \bool_set_true:N \l__lx_gl_phantom_bool \fi
%% The set is a seq of marks in their STRING form, one entry per mark, and
%% an entry is either a single character ("*") or a control sequence
%% ("\#").  It used to be a string of characters, which could not hold the
%% two marks it most needed to.  A bare # is a macro parameter character
%% and a bare % opens a comment, so neither can be typed in an example at
%% all -- "\ex. #Ceci" is "You can't use macro parameter character # in
%% horizontal mode", from TeX and not from here -- and \# and \% are the
%% only spellings a document can contain.  A character set cannot hold
%% those: stringified they are two characters each, and matching them
%% one character at a time would match a leading "\" and pad every word
%% that begins with any control sequence.  So the whole mark, not its
%% first character, is what an entry holds and what the peel compares.
%%
%% That also brought this into line with the judgment scanner, which has
%% always recognised \# and \% by meaning (see \__lx_judge_loop:).  The
%% two disagreed for as long as the set was a string: "\ex. \#Ceci" hung
%% its mark in the margin while "\altn{un nez}{\#le nez}" did not peel the
%% same mark into the gutter, and the # and % in the documented default
%% set were unreachable.
\seq_new:N \l__lx_gl_phantomchars_seq
\tl_new:N  \l__lx_gl_prefix_tl
\tl_new:N  \l__lx_gl_rest_tl
\tl_new:N  \l__lx_gl_obj_tl
\str_new:N \l__lx_gl_head_str
\quark_new:N \q__lx_gl_stop
\cs_new_protected:Npn \GlossPhantomAlign
  { \bool_set_true:N \l__lx_gl_phantom_bool }
\cs_new_protected:Npn \GlossPhantomAlignOff
  { \bool_set_false:N \l__lx_gl_phantom_bool }
%% One token of #1 is one mark, which is what makes a mixed list work
%% without a separator: \tl_map_inline: hands over a character and a
%% control sequence alike, one at a time, so \GlossPhantomChars{*?\dag}
%% declares three marks and needs no commas.  Stored stringified, so the
%% comparison in the peel is by name and not by catcode -- a "*" in a
%% package that made it active still matches.
\cs_new_protected:Npn \GlossPhantomChars #1
  {
    \seq_clear:N \l__lx_gl_phantomchars_seq
    \tl_map_inline:nn {#1}
      { \seq_put_right:Ne \l__lx_gl_phantomchars_seq { \tl_to_str:n {##1} } }
  }
\GlossPhantomChars { *?([< \# \% }
% Manual per-word override: \GlossPhantom{stuff} typesets an invisible box
% the width of #1 SET IN THE OBJECT-TIER FONT.  Placed at the front of a
% gloss word it aligns that word past #1 regardless of the automatic
% detection or the gloss-tier size -- for material the auto-scanner cannot
% see (a macro-wrapped bracket, a whole prefix word) or a bespoke target.
\cs_new_protected:Npn \GlossPhantom #1
  { \phantom { \lx@glfontuse {1} {#1} } }
%% Peel the leading run of marks off #1: #2 collects them, #3 keeps the
%% remainder.  Both consumers come through here -- the gloss aligner, which
%% wants the marks to size a \phantom and throws the rest away, and an
%% \altn or \altg stack, which wants both halves as the two cells of a row.
%% One rule, one copy, for the same reason \__lxp_alt_build:NNnnN is one
%% function: the two were written out separately and had already drifted,
%% the gloss one peeling characters off a stringified word and the stack
%% one taking tokens, so they disagreed about \# (neither took it) and
%% about "{[}ein" (only the stack peeled it).
%%
%% Tokens, not characters.  The head is taken with an UNDELIMITED argument,
%% so nothing is expanded and markup inside the alternative (\sout,
%% \textbf, \lpzg) is untouched, and a control sequence arrives whole
%% rather than as a backslash followed by its name.  It is compared
%% stringified against the set, where a character and a control sequence
%% cannot collide: one stringifies to a single character, the other always
%% to a backslash and a name.
%%
%% A leading BRACE GROUP is not a mark and stops the peel, even when the
%% group holds one.  An undelimited argument strips the braces, so without
%% this test "{[}ein" would peel as "[" + "ein" -- and someone who braced
%% the bracket did so to keep it out of exactly this kind of scanning.
%% (That is what the gloss half used to do, by accident of stringifying;
%% the stack half used to peel it.  This makes the accident the rule.)
\cs_new_protected:Npn \__lx_gl_peel:nNN #1#2#3
  {
    \tl_clear:N #2
    \tl_set:Nn #3 {#1}
    \__lx_gl_peel_loop:NN #2#3
  }
\cs_new_protected:Npn \__lx_gl_peel_loop:NN #1#2
  {
    \tl_if_empty:NF #2
      { \exp_args:NV \__lx_gl_peel_head:nNN #2 #1#2 }
  }
\cs_new_protected:Npn \__lx_gl_peel_head:nNN #1#2#3
  {
    \tl_if_head_is_group:nF {#1}
      { \__lx_gl_peel_test:w #1 \q__lx_gl_stop #2#3 }
  }
\cs_new_protected:Npn \__lx_gl_peel_test:w #1#2 \q__lx_gl_stop #3#4
  {
    \str_set:Ne \l__lx_gl_head_str { \tl_to_str:n {#1} }
    \seq_if_in:NVT \l__lx_gl_phantomchars_seq \l__lx_gl_head_str
      {
        \tl_put_right:Nn #3 {#1}
        \tl_set:Nn #4 {#2}
        \__lx_gl_peel_loop:NN #3#4
      }
  }
%% The gloss aligner's entry point: it needs the marks only, to size the
%% \phantom that pads the tiers below.
\cs_new_protected:Npn \__lx_gl_leadprefix:N #1
  { \exp_args:NV \__lx_gl_peel:nNN #1 \l__lx_gl_prefix_tl \l__lx_gl_rest_tl }
%% Word #3 of seq #2 into #1, WITHOUT expanding the word.
%%
%% The obvious retrieval, \exp_args:NNf with \seq_item:Nn, does not stop
%% where it looks as though it stops.  \seq_item:Nn is expandable, so
%% f-expansion evaluates it and then carries on into the item it produced
%% and expands ITS leading token too.  For a robust mark that is one step
%% too many: \dag expands to \protect\dag~, and the peel is then offered
%% \protect and finds no mark.  The word is typeset from the seq
%% separately, so the page stayed right and only the alignment was lost.
%%
%% This was invisible for as long as \GlossPhantomChars held characters
%% only -- a character cannot be expanded -- and it survived the first
%% half of admitting control sequences too, because \# and \% are not
%% robust in LaTeX2e and so pass through f-expansion unharmed.  It shows
%% up the moment a document declares a mark of its own that is.
%%
%% \seq_map_indexed_inline: hands the item over untouched, which is what
%% the comment at the call site asks for and what f-expansion only
%% approximated.
\cs_new_protected:Npn \__lx_gl_word_get:NNn #1#2#3
  {
    \tl_clear:N #1
    \seq_map_indexed_inline:Nn #2
      { \int_compare:nNnT {##1} = {#3} { \tl_set:Nn #1 {##2} } }
  }
\cs_generate_variant:Nn \__lx_gl_word_get:NNn { Ncn }
%% An \exannot on the OBJECT line of a gloss.  It is lifted off tier 1
%% before the columns are built and set after them, on the object tier's
%% own baseline, at \ExAnnotColumn -- so a glossed example carries the same
%% column as an unglossed one, and the label sits level with the language
%% it labels rather than under the free translation.
%%
%% Lifted rather than typeset in place: as a word of tier 1 it would be a
%% column of the grid, sharing \GlossSep with the object words and pushing
%% the gloss beneath it, which is the one thing an annotation must not do.
%% And it cannot be RUN where it stands -- \exannot closes its paragraph,
%% and the gloss's paragraph is the grid.
\tl_new:N \l__lx_gl_annot_tl
%% Lifted off the RAW LINE, before anything splits it into words.
%%
%% The annotation is not a word of the language being glossed and its
%% argument is prose: "(cf. \str{le mien livre})" has three spaces in it.
%% \gll splits the object line ON SPACES, so an annotation left in place
%% until then does not survive as one thing -- it is torn into "libro\exannot{(cf.",
%% "\str{le", "mien", "livre})", each of which becomes a COLUMN of the grid,
%% with gloss cells appearing under the pieces.  Reported from a class deck
%% as "a line break after mio", because that is what it looks like: the
%% extra columns no longer fit and the grid wraps.  Nothing errors, and the
%% pieces read as if the gloss simply had more words in it.
%%
%% Lifting first also settles what an annotation in the middle of a line
%% means, by making the question go away: everything from \exannot to the
%% end of the line IS the annotation call, which is right when it comes
%% last and is the only reading available when the argument may contain
%% anything at all.  There is no longer a mid-line case to diagnose, and
%% the error below is about the TIER, which is a question the line-level
%% split can still answer.
%%
%% \q_nil is a spacer and its whole job is to stop TeX helping.  A DELIMITED
%% argument that consists of exactly one brace group has that group's braces
%% removed, so "libro\exannot{(italien)}" handed #2 the tokens "(italien)"
%% and not "{(italien)}"; the annotation was then rebuilt as
%% "\exannot (italien)", whose mandatory argument is the single token "(",
%% and the page came out with "(" in the column and "italien)" flush right.
%% Compiled clean, of course.  With \q_nil after it the argument is not a
%% lone group any more and the braces stay; it is taken off again below.
\tl_new:N \l__lx_gl_line_tl
\tl_new:N \l__lx_gl_annot_head_tl
\cs_new_protected:Npn \__lx_gl_annot_split:w #1 \exannot #2 \q_stop
  {
    \tl_set:Nn \l__lx_gl_annot_head_tl {#1}
    \tl_set:Nn \l__lx_gl_annot_tl { \exannot #2 }
    \tl_remove_once:Nn \l__lx_gl_annot_tl { \q_nil }
  }
\cs_new_protected:Npn \__lx_gl_annot_lift:N #1
  {
    \tl_if_in:NnT #1 { \exannot }
      {
        %% A gloss is a translation, so a label on a gloss tier has nothing
        %% to align with.  Lifted onto the object line even so: the run is
        %% stopping on an error either way, and an error is not a reason to
        %% shred the author's annotation into gloss columns as well.
        \int_compare:nNnF { \l__lx_gl_ntiers_int } = { 1 }
          { \msg_error:nn { linguexx } { annot-in-gloss } }
        \exp_after:wN \__lx_gl_annot_split:w #1 \q_nil \q_stop
        \tl_set_eq:NN #1 \l__lx_gl_annot_head_tl
      }
  }
%% \hfill, not \hfil: the gloss paragraph is \raggedright, so \rightskip is
%% already 0pt plus 1fil and an \hfil would merely share the slack with it,
%% leaving the annotation halfway.  A fill outranks a fil and takes all of
%% it.  \vtop puts the annotation on the FIRST tier's baseline, the way
%% every other column's first row sits.
%% The lifted \exannot is not executed as written -- \exannot ends its
%% paragraph, and the paragraph here is the grid.  Inside this group it
%% means the in-gloss placement instead, so the author's optional <spoken>
%% argument is still read by the command that owns it rather than picked
%% apart here.  \parfillskip is set OUTSIDE the group, or the group would
%% restore it before \lx@gloss@multi reaches its \par.
\cs_new_protected:Npn \__lx_gl_annot_emit:
  {
    \group_begin:
      \cs_set_eq:NN \exannot \lx@annot@glossuse
      \l__lx_gl_annot_tl
    \group_end:
    \parfillskip \c_zero_dim
  }
%% ---- the grid and the translation as two columns -------------------------
%% Under \GlossTransSide the grid is set into a box of its own instead of
%% straight into the list, \glt opens a second box beside it, and both are
%% set down together at the example's exit.  Three things this arrangement
%% has to get right, all of them found by probing rather than by reasoning
%% (doc/EXPEX-GAPS.md records the numbers):
%%
%%   * The placement must SUSPEND the ambient marked content.  Both boxes
%%     carry marked content of their own, and the paragraph that puts them
%%     down has already opened some; dropping them inside it is "nested
%%     marked content", which every veraPDF profile still calls compliant.
%%     Measured: 17 records logged while parsing, against 0 for an ordinary
%%     gloss, with all three verdicts passing in both cases.  Only reading
%%     veraPDF's log as well as its verdict tells them apart, which is what
%%     the suite's `ua` case does.
%%   * Reading order follows PLACEMENT, not typesetting: the BDC/EMC
%%     operators travel inside the box and reach the page's content stream
%%     when the box is shipped.  The two are put down grid-then-translation,
%%     which is the reading order wanted; swapping the placement for a
%%     typographic reason would silently reverse it and leave the page
%%     identical.
%%   * The \glt language Span has to be closed INSIDE the translation box,
%%     before the \egroup.  That is free here, because the flush lives in
%%     \lx@glt@langend itself, which closes the Span first -- and it is the
%%     one of the three that announces itself if got wrong (a hard TeX
%%     error, not a silent tagging defect).
\box_new:N \l__lx_gl_gridbox
\box_new:N \l__lx_gl_transbox
\dim_new:N \l__lx_gl_gridwd
\dim_new:N \l__lx_gl_transwd
\bool_new:N \g__lx_gl_sidepending_bool
\bool_new:N \g__lx_gl_transbox_bool

\msg_new:nnnn { linguexx } { side-sub }
  { \iow_char:N \\GlossTransSide~is~for~top-level~examples~only. }
  {
    A~side~translation~is~refused~below~the~top~level.~The~measure~there~
    is~already~reduced~twice,~so~both~columns~come~out~narrow~and~the~
    grid~starts~wrapping;~and~a~split~taken~from~the~indented~measure~
    puts~every~sibling's~translation~at~a~different~place.~Put~the~
    declaration~round~a~top-level~example,~or~use~\iow_char:N
    \\GlossTransBelow~here.
  }
\msg_new:nnnn { linguexx } { side-annot }
  { \iow_char:N \\exannot~cannot~go~on~a~gloss~with~a~side~translation. }
  {
    \iow_char:N \\exannot's~column~is~measured~from~\iow_char:N
    \\columnwidth,~which~says~nothing~once~the~grid~has~been~narrowed~to~
    half~of~it.~Drop~one~or~the~other.
  }
\msg_new:nnn { linguexx } { side-narrow }
  {
    No~room~for~a~side~translation~here~(#1pt~left~for~it,~\iow_char:N
    \\GlossTransMinWidth~is~#2pt);~setting~it~underneath~instead.
  }

%% Decide, and divide the measure.  Returns with \l__lx_gl_side_bool saying
%% whether it is really happening -- the declaration asks, this answers.
%% expl3 has \vbox_set:Nw / \vbox_set_end: but no \vtop counterpart, and a
%% \vbox is the wrong box here: its reference point is its LAST baseline, so
%% two of them beside each other line up on their bottom lines and the grid
%% and the translation would drift apart by however many lines they differ.
%% A \vtop's reference point is its FIRST baseline, which is the alignment
%% wanted.  Same idiom as \vbox_set:Nw, with \tex_vtop:D in place of
%% \tex_vbox:D.
\cs_new_protected:Npn \lx@gl@vtop_set:Nw #1
  {
    \tex_setbox:D #1 \tex_vtop:D
      \c_group_begin_token
        \color_group_begin:
        %% No \parskip reset here, and that is a deliberate absence.  A
        %% paragraph beginning in vertical mode has \parskip glue put before
        %% it, and inside a fresh \vtop that glue would be the box's first
        %% item -- making the box's HEIGHT zero, so that everything real
        %% hangs below the baseline and the gloss sits a line under its own
        %% example number.  Which would matter if \parskip could be nonzero
        %% here, and it cannot: a gloss is always inside an example list,
        %% LaTeX's \list sets \parskip from \parsep, and the example lists
        %% set \parsep to zero.  Measured, with a document-level
        %% \parskip=2\baselineskip: 0.0pt inside the box.  A reset was
        %% written here first and no mutation could kill it.
        %% tests/glt-side.tex keeps that document (PSKOBJ) as a guard on the
        %% list's \parsep rather than on this box.
        \parindent \c_zero_dim
  }
\cs_new_protected:Npn \lx@gl@vtop_set_end:
  {
        \par
      \color_group_end:
    \c_group_end_token
  }

\bool_new:N \l__lx_gl_side_bool
\cs_new_protected:Npn \lx@gl@side@decide:
  {
    \bool_set_false:N \l__lx_gl_side_bool
    \legacy_if:nT { lx@glside }
      {
        \int_compare:nNnTF { \lx@subdepth } > { 0 }
          { \msg_error:nn { linguexx } { side-sub } }
          {
            \tl_if_empty:NF \l__lx_gl_annot_tl
              { \msg_error:nn { linguexx } { side-annot } }
            %% In two steps, and through a register.  \GlossTransRatio is a
            %% factor and TeX wants a <unit of measure> after one: a real
            %% dimen is that, a \dimexpr is not reliably, and writing it in
            %% one line earns "Illegal unit of measure".
            \dim_set:Nn \l__lx_gl_transwd
              { \linewidth - \GlossTransSep }
            \dim_set:Nn \l__lx_gl_gridwd
              { \GlossTransRatio \l__lx_gl_transwd }
            \dim_sub:Nn \l__lx_gl_transwd { \l__lx_gl_gridwd }
            \dim_compare:nNnTF { \l__lx_gl_transwd } < { \GlossTransMinWidth }
              {
                \msg_warning:nnee { linguexx } { side-narrow }
                  { \dim_to_decimal_in_unit:nn { \l__lx_gl_transwd } { 1pt } }
                  { \dim_to_decimal_in_unit:nn { \GlossTransMinWidth } { 1pt } }
              }
              { \bool_set_true:N \l__lx_gl_side_bool }
          }
      }
  }
\cs_gset_protected:Npn \lx@gloss@multi #1
  {
    \seq_set_split:Nnn \l__lx_gl_lines_seq { \\ } {#1}
    \seq_remove_all:Nn \l__lx_gl_lines_seq { }
    \int_zero:N \l__lx_gl_ntiers_int
    \int_zero:N \l__lx_gl_maxwords_int
    \tl_clear:N \l__lx_gl_annot_tl
    \seq_map_inline:Nn \l__lx_gl_lines_seq
      {
        \int_incr:N \l__lx_gl_ntiers_int
        \seq_clear_new:c
          { l__lx_gl_tier_ \int_use:N \l__lx_gl_ntiers_int _seq }
        \tl_set:Nn \l__lx_gl_line_tl {##1}
        \__lx_gl_annot_lift:N \l__lx_gl_line_tl
        \__lx_split:cV
          { l__lx_gl_tier_ \int_use:N \l__lx_gl_ntiers_int _seq }
          \l__lx_gl_line_tl
        \int_set:Nn \l__lx_gl_maxwords_int
          { \int_max:nn { \l__lx_gl_maxwords_int }
              { \seq_count:c
                  { l__lx_gl_tier_ \int_use:N \l__lx_gl_ntiers_int _seq } } }
      }
    \lx@gl@side@decide:
    \bool_if:NT \l__lx_gl_side_bool
      { \lx@gl@vtop_set:Nw \l__lx_gl_gridbox }
    \group_begin:
    \bool_if:NT \l__lx_gl_side_bool
      { \hsize \l__lx_gl_gridwd \linewidth \l__lx_gl_gridwd }
    %% \leavevmode unconditionally, and it is not decoration.  The grid is a
    %% PARAGRAPH of column boxes; opening the side box leaves us in internal
    %% vertical mode, where the first column is contributed as a vertical
    %% item of its own and only the \hskip after it starts a paragraph.  The
    %% grid then comes out with its first column alone on one line and the
    %% rest on the next, at any width, which reads as a wrap and is not one:
    %% it happened with a 60pt gloss in a 222pt column, and a wider column
    %% hid it rather than fixing it.
    \leavevmode
    \raggedright
    \lx@glossfont
    \bool_set_true:N \l_lx_gl_inside_bool
    \int_gset:Nn \g__lxp_altg_role_int { 0 }
    \int_step_inline:nn { \l__lx_gl_maxwords_int }
      { % column ##1
        \lx@gl@colbegin
        \bool_if:NTF \l__lx_gl_phantom_bool
          {
            % Retrieve the object word WITHOUT expanding it: the auto-scan
            % must see the leading literal marks, not expand a macro
            % (e.g. \textbf{[}ein) and reach a "[" that the printed word only
            % shows through a control word.  Full expansion here would make
            % the scan fire on macro-wrapped brackets and double up with a
            % manual \GlossPhantom.  \__lx_gl_word_get:Ncn expands nothing
            % at all -- see there for why f-expansion was not enough.
            \__lx_gl_word_get:Ncn \l__lx_gl_obj_tl
              { l__lx_gl_tier_1_seq } {##1}
            \__lx_gl_leadprefix:N \l__lx_gl_obj_tl
          }
          { \tl_clear:N \l__lx_gl_prefix_tl }
        \vtop
          {
            \int_step_inline:nn { \l__lx_gl_ntiers_int }
              { % tier ####1
                \hbox:n
                  { \strut
                    \int_compare:nNnT {####1} > { 1 }
                      { \tl_if_empty:NF \l__lx_gl_prefix_tl
                          { \phantom
                              { \lx@glfontuse {1} { \l__lx_gl_prefix_tl } } } }
                    \lx@gl@wordbegin {####1}
                    \lx@glfontuse {####1}
                      { \seq_item:cn { l__lx_gl_tier_ ####1 _seq } {##1} }
                    \lx@gl@wordend {####1} }
              }
          }%
        \lx@gl@colend
        %% An \altg paradigm is TWO calls in ONE column: the object tier
        %% announces it (role 0 -> 1) and the gloss tier completes it
        %% (1 -> 0).  The protocol rides on that toggle alone, so a column
        %% that announces without completing does not merely lose its own
        %% brace -- it leaves the toggle set, and every later \altg in the
        %% gloss takes the opposite role: object stacks get the gloss shape
        %% (raised half a baseline, indented) and land on top of their
        %% neighbours.  Same for a third tier carrying an \altg, which the
        %% toggle reads as a fresh object call.  Both used to typeset
        %% happily as overlapping text; catch them here, where the column
        %% that broke it is still known, and reset so the rest of the gloss
        %% is unaffected.
        \int_compare:nNnT { \g__lxp_altg_role_int } = { 1 }
          {
            \int_gset:Nn \g__lxp_altg_role_int { 0 }
            %% and whatever punctuation that object call took along comes
            %% back here rather than vanishing with the paradigm that was
            %% never completed: the run is stopping on an error either
            %% way, and an error is not a reason to eat the author's text.
            \g__lxp_altg_pending_tl \tl_gclear:N \g__lxp_altg_pending_tl
            \PackageError { linguexx }
              { \string\altg\space in~gloss~column~##1~has~no~partner }
              { An~\string\altg\space in~a~gloss~must~be~written~TWICE~in~
                the~SAME~column:~once~among~the~object~words,~once~among~
                their~glosses.~A~column~with~only~one,~or~a~third~tier~
                carrying~one~as~well,~cannot~be~paired~up. }
          }
        \penalty \lx@gl@colpenalty \hskip \GlossSep \relax
      }
    %% \unskip takes off the last column's \GlossSep and leaves the
    %% \lx@gl@colpenalty in front of it standing.  Deliberately: an
    %% \unpenalty here removes a breakpoint that costs 40000 demerits and
    %% sits immediately before \exannot's own, which costs 2500, so it is
    %% never the one taken -- and a line that cannot be shown to do
    %% anything reads as though it did.
    \unskip
    \tl_if_empty:NF \l__lx_gl_annot_tl { \__lx_gl_annot_emit: }
    \par
    \group_end:
    %% The box is closed here and PLACED at the example's exit, because the
    %% translation that goes beside it has not been read yet.
    \bool_if:NT \l__lx_gl_side_bool
      {
        \lx@gl@vtop_set_end:
        \bool_gset_true:N \g__lx_gl_sidepending_bool
      }
  }
\ExplSyntaxOff

%% User commands.  \gll and \glll keep their exact historical syntax and
%% are wrappers over the same engine; \gl ... \endgl takes any number of
%% lines, each terminated by \\ (a \\ before \endgl is optional).
%% Empty unless [langsci] fills it in (\glossfont, \singlegloss).  It is
%% applied inside the gloss's own group, so whatever it selects covers
%% every tier and nothing outside.
\let\lx@glossfont\@empty
\long\def\gll#1\\#2\\{\lx@gloss@multi{#1\\#2}}
\long\def\glll#1\\#2\\#3\\{\lx@gloss@multi{#1\\#2\\#3}}
%% langsci-gb4e carries \gllll ... \gllllllll, four to eight tiers.  The
%% engine already takes any number (\gl ... \endgl is exactly that), so
%% these are wrappers and nothing else -- and they are wrapped in the
%% option rather than defined always, because a name this package does not
%% otherwise promise is not something to start providing by default.
\iflx@langsci
  \long\def\gllll#1\\#2\\#3\\#4\\{\lx@gloss@multi{#1\\#2\\#3\\#4}}
  \long\def\glllll#1\\#2\\#3\\#4\\#5\\{\lx@gloss@multi{#1\\#2\\#3\\#4\\#5}}
  \long\def\gllllll#1\\#2\\#3\\#4\\#5\\#6\\%
    {\lx@gloss@multi{#1\\#2\\#3\\#4\\#5\\#6}}
  \long\def\glllllll#1\\#2\\#3\\#4\\#5\\#6\\#7\\%
    {\lx@gloss@multi{#1\\#2\\#3\\#4\\#5\\#6\\#7}}
  \long\def\gllllllll#1\\#2\\#3\\#4\\#5\\#6\\#7\\#8\\%
    {\lx@gloss@multi{#1\\#2\\#3\\#4\\#5\\#6\\#7\\#8}}
\fi
\long\def\gl#1\endgl{\lx@gloss@multi{#1}}
\def\endgl{\PackageError{linguexx}{\string\endgl without \string\gl}{}}
%% \glt starts the free translation on a new line, kept on the same page as
%% the gloss it belongs to (\nobreak).  Structurally it needs nothing extra:
%% being its own paragraph it already becomes its own P in the tag tree,
%% with the translation as its text, which is what a screen reader needs to
%% read it after the gloss.
%%
%% \GlossTransStyle is applied as a DECLARATION rather than as a
%% one-argument command like \GlossTierFont: the translation is delimited by
%% the end of the paragraph, not by braces, so there is no argument to wrap
%% -- and a declaration scopes itself to the rest of the example (the list
%% environment's group) without \glt having to know where the translation
%% ends.  Empty by default, so the output is unchanged unless it is set.
\newcommand\GlossTransStyle{}
%%
%% \GlossTransLang{code} marks the LANGUAGE of the free translation, the way
%% \GlossTierLang does for a gloss tier: under tagging the translation is
%% wrapped in a Span carrying /Lang, so a screen reader pronounces it with
%% the right phonetics.  This is not redundant with babel: on TL2026,
%% \foreignlanguage inside the translation leaves no /Lang in the structure
%% tree at all (only the document-level one from \DocumentMetadata), so
%% without this there is no way to mark a translation whose language differs
%% from the document's -- the normal case in a paper written in one language
%% and glossing into another.  Opt-in and empty by default: no Span, and the
%% tag tree is byte-for-byte what it was.
%%
%% Where the Span ENDS is the whole difficulty: the translation runs to the
%% end of its paragraph, so \glt cannot wrap an argument.  It is opened
%% lazily -- the first paragraph content triggers it via \everypar, by which
%% time the paragraph's own P and MC are open, so the Span nests inside them
%% instead of straddling them (opening it directly at \glt, while still in
%% vertical mode, is the "nested marked content"/"no mc to end" failure that
%% \lx@hangjudge documents) -- and closed by \lx@glt@langend, which every
%% example exit runs while the translation paragraph is still open.
\ExplSyntaxOn
\tl_new:N   \l_lx_gl_translang_tl
\bool_new:N \g__lx_gl_transopen_bool
\NewDocumentCommand \GlossTransLang { m }
  { \tl_set:Nn \l_lx_gl_translang_tl {#1} }
\cs_new_protected:Npn \lx@glt@langbegin
  {
    \tl_if_empty:NF \l_lx_gl_translang_tl
      {
        \lx@tag@if@active:T
          {
            \lx@tag@span@begin:e
              { tag = Span , lang = \l_lx_gl_translang_tl }
            \bool_gset_true:N \g__lx_gl_transopen_bool
          }
      }
  }
%% Closed from every example exit (\lx@bodyend and the exe/xlist ends),
%% before the list closes -- i.e. while the translation paragraph is still
%% the current one.  The flag is global because the open and the close
%% necessarily sit in different groups; it is what makes the close a no-op
%% for an example with no \glt, with no \GlossTransLang, or without tagging.
\cs_new_protected:Npn \lx@glt@langend
  {
    \bool_if:NT \g__lx_gl_transopen_bool
      {
        \lx@tag@span@end:
        \bool_gset_false:N \g__lx_gl_transopen_bool
      }
    %% ... and, in the same breath, put down a side gloss that is waiting.
    %% Here rather than anywhere else because this is the one macro every
    %% example exit already runs -- all nine of them -- and because the Span
    %% above has to be closed while the translation paragraph, and so the
    %% box holding it, is still open.  Doing it in this order is not a
    %% convention: the other order is a hard TeX error.
    \lx@gl@side@flush:
  }

%% Opened by \glt when a side grid is waiting for it.  A box, so that the
%% translation's own width is the column's and not the item's; \glt's
%% \everypar hook then fires inside it exactly as it does outside.
\cs_new_protected:Npn \lx@gl@side@transopen
  {
    \bool_if:NT \g__lx_gl_sidepending_bool
      {
        \lx@gl@vtop_set:Nw \l__lx_gl_transbox
        \hsize \l__lx_gl_transwd \linewidth \l__lx_gl_transwd
        \skip_set_eq:NN \tex_rightskip:D \GlossTransRightSkip
        \parindent \c_zero_dim
        \bool_gset_true:N \g__lx_gl_transbox_bool
      }
  }

%% The flush.  Idempotent and a no-op for every example that has no side
%% gloss, which is what lets it sit in a macro nine exits already call.
%% \tag_mc_end_push: / \tag_mc_begin_pop:n suspend the marked content of
%% the paragraph doing the placing: both boxes carry their own, and nesting
%% them inside it is what every veraPDF profile calls compliant and the log
%% does not (17 records against 0, measured).
\cs_new_protected:Npn \lx@gl@side@flush:
  {
    \bool_if:NT \g__lx_gl_sidepending_bool
      {
        \bool_if:NT \g__lx_gl_transbox_bool
          {
            \lx@gl@vtop_set_end:
            \bool_gset_false:N \g__lx_gl_transbox_bool
          }
        \bool_gset_false:N \g__lx_gl_sidepending_bool
        \noindent
        \lx@tag@if@active:T { \tag_mc_end_push: }
        \box_use_drop:N \l__lx_gl_gridbox
        \skip_horizontal:N \GlossTransSep
        \box_use_drop:N \l__lx_gl_transbox
        \lx@tag@if@active:T { \tag_mc_begin_pop:n { } }
        \par
      }
  }
\ExplSyntaxOff
%% The \everypar hook fires once, for the translation's first paragraph, and
%% clears itself: \glt is followed by ordinary text, not by a construct that
%% starts further paragraphs of its own.
\newcommand\glt{%
  %% AFTER the \par\nobreak, not before it.  \nobreak is a penalty, and a
  %% \vtop whose first item is not a box has height zero -- so opening the
  %% translation box first put a penalty at the top of it, gave it no
  %% height, and hung the whole translation half a line below the grid it
  %% is supposed to start level with.  Same shape as the \parskip glue that
  %% \lx@gl@vtop_set:Nw zeroes, and the second time this bug appeared.
  \par\nobreak
  \lx@gl@side@transopen
  \GlossTransStyle
  \everypar{\everypar{}\lx@glt@langbegin}%
  \ignorespaces}
\let\gln\glt % cgloss4e-compat alias

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Stacked alternatives: what \altn and \altg share                   %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% \altn (a braced stack in running text) and \altg (a braced stack that
%% occupies one column of an interlinear gloss, written once per tier) are
%% different constructs with the same four internals: collect the brace
%% groups, build the spoken /Alt from them, set them as a tabular, draw a
%% TikZ brace to the result's extents.  Those four live here, once; what
%% remains in each section is only what genuinely differs -- how the stack
%% is anchored vertically, and \altg's two-call cross-tier protocol.
\ExplSyntaxOn
%% Collection.  #1 = the seq to fill, #2 = what to run when the groups run
%% out.  Both commands take one leading mandatory argument and then any
%% number of further brace groups, so the loop peeks rather than counts;
%% a space ends collection (in a gloss it also splits columns, hence the
%% documented "no spaces between the brace groups, break lines with %").
\cs_new_protected:Npn \__lxp_grab:NN #1#2
  { \peek_catcode:NTF \c_group_begin_token
      { \__lxp_grab_arg:NNn #1#2 } {#2} }
\cs_new_protected:Npn \__lxp_grab_arg:NNn #1#2#3
  {
    \seq_put_right:Nn #1 {#3}
    \__lxp_grab:NN #1#2
  }
%% Spoken /Alt: "A", "A or B", "A, B, or C".  #1 = the seq, #2 = the tl to
%% receive it.
%%
%% The three joining strings are NOT hardcoded.  \SetAltSpoken{<word>}
%% replaces the connector, so a French document says "A ou B" and a German
%% one "A oder B"; the optional argument replaces the punctuation between
%% the earlier items, and the starred form puts that punctuation before the
%% connector as well, which is what English does and most other languages
%% do not.  The package's own default is exactly \SetAltSpoken*{or}[,] --
%% a hook that cannot express the default it replaces is a hook that hides
%% one of the two behaviours from the author.  \text_purify:n strips formatting (\sout, \textbf, ...) for
%% speech, and \lpzg is reduced to its Leipzig expansion, so
%% \altn{\lpzg{sg}}{\lpzg{pl}} speaks "singular or plural" rather than
%% "SG or PL".  Only SIMPLE keys are expanded here: a compound
%% ("3sg.pst") or unknown key speaks as printed.
%%
%% The pieces are collected in a GLOBAL scratch and copied out at the end.
%% The \lpzg redefinition has to be scoped by a group, the result has to
%% survive it, and the destination is a local (l_-named) token list: the
%% previous code wrote to it with \tl_gput_right: from inside the group,
%% which straddled the scope and broke the expl3 naming contract at once.
\tl_new:N \g__lxp_alt_build_tl
\int_new:N \l__lxp_alt_n_int
%% The joining strings, and the one command that sets them.  Spacing is the
%% package's business rather than the author's: \SetAltSpoken{~ou~} and
%% \SetAltSpoken{ou} must not be two different settings, so the word is
%% stored bare and the spaces are put around it here.
%%
%% The three are LOCAL, like \GlossTransRatio and the xlist variants and
%% unlike \SetLeipzig -- setting one is choosing a behaviour for a stretch
%% of document, not extending a table, so it has to be possible to fence a
%% French example inside an English document and have the next example go
%% back.  A preamble setting is made at top level and so applies throughout
%% either way, which is why global would have looked correct for as long as
%% nobody scoped it.
\tl_new:N   \l__lxp_alt_word_tl
\tl_new:N   \l__lxp_alt_punct_tl
\bool_new:N \l__lxp_alt_serial_bool
\tl_set:Nn  \l__lxp_alt_word_tl  { or }
\tl_set:Nn  \l__lxp_alt_punct_tl { , }
\bool_set_true:N \l__lxp_alt_serial_bool
\NewDocumentCommand \SetAltSpoken { s m O{,} }
  {
    \tl_set:Nn \l__lxp_alt_word_tl  {#2}
    \tl_set:Nn \l__lxp_alt_punct_tl {#3}
    %% Braces keep the spaces inside them, so \SetAltSpoken{ ou } would
    %% otherwise store " ou " and speak "aa  ou  bb" -- two spaces at each
    %% side of the word, invisible on the page and invisible in the log.
    %% Spacing is this command's business rather than the author's, which
    %% only holds if the two spellings are one setting.
    \tl_trim_spaces:N \l__lxp_alt_word_tl
    \tl_trim_spaces:N \l__lxp_alt_punct_tl
    \IfBooleanTF {#1}
      { \bool_set_true:N  \l__lxp_alt_serial_bool }
      { \bool_set_false:N \l__lxp_alt_serial_bool }
  }
%% The two readers below take the values with :NV rather than :Nn.  No test
%% can currently tell them apart -- both were tried as mutations and both
%% survive -- because the build list is emitted through \lx@tag@span@begin:e,
%% whose e-expansion turns a stored variable token into its value anyway.
%% Keep :NV regardless: the build list is GLOBAL and these three are LOCAL,
%% so storing the token rather than the value would make a global list hold
%% a reference that is only correct while the local is still in scope. That
%% is the same straddle the comment on \g__lxp_alt_build_tl above records
%% having already cost this code once, and it would come back the moment
%% the expansion point moved rather than at the commit that caused it.
%%
%% Between two earlier alternatives: the punctuation, then a space.
\cs_new_protected:Npn \__lxp_alt_separate:
  {
    \tl_gput_right:NV \g__lxp_alt_build_tl \l__lxp_alt_punct_tl
    \tl_gput_right:Nn \g__lxp_alt_build_tl { ~ }
  }
%% Before the last one: the connector, preceded by the punctuation only
%% when this is a list of three or more AND the serial comma is wanted.  An
%% empty connector collapses to a single space rather than two, so
%% \SetAltSpoken{} is a way to join with punctuation alone.
\cs_new_protected:Npn \__lxp_alt_connect:
  {
    \bool_lazy_and:nnT
      { \l__lxp_alt_serial_bool }
      { \int_compare_p:nNn { \l__lxp_alt_n_int } > { 2 } }
      { \tl_gput_right:NV \g__lxp_alt_build_tl \l__lxp_alt_punct_tl }
    \tl_gput_right:Nn \g__lxp_alt_build_tl { ~ }
    \tl_if_empty:NF \l__lxp_alt_word_tl
      {
        \tl_gput_right:NV \g__lxp_alt_build_tl \l__lxp_alt_word_tl
        \tl_gput_right:Nn \g__lxp_alt_build_tl { ~ }
      }
  }
\cs_new_protected:Npn \__lxp_buildalt:NN #1#2
  {
    \tl_gclear:N \g__lxp_alt_build_tl
    \int_set:Nn \l__lxp_alt_n_int { \seq_count:N #1 }
    \group_begin:
      \cs_set:Npn \lpzg ##1
        { \prop_if_in:NnTF \g_lx_lpzg_prop {##1}
            { \prop_item:Nn \g_lx_lpzg_prop {##1} } {##1} }
      \int_step_inline:nn { \l__lxp_alt_n_int }
        {
          \int_compare:nNnT { ##1 } > { 1 }
            {
              \int_compare:nNnTF { ##1 } = { \l__lxp_alt_n_int }
                { \__lxp_alt_connect: }
                { \__lxp_alt_separate: }
            }
          \tl_gput_right:Ne \g__lxp_alt_build_tl
            { \text_purify:n { \seq_item:Nn #1 {##1} } }
        }
    \group_end:
    \tl_set_eq:NN #2 \g__lxp_alt_build_tl
  }
%% The stack, as a tabular.  #1 = box to set, #2 = seq, #3 = column spec,
%% #4 = a hook run inside the box before the tabular (a font, a local
%% redefinition).
%%
%% Under active tagging, plain LaTeX auto-tags every tabular as a real
%% /Table (with /TR, /TD ...); this stack is presentational, not data, and
%% the auto Table would be built (wrongly) as a child of whatever structure
%% element is open at THIS point -- typically the ambient paragraph, since
%% the surrounding Span is not opened until the emit, around \box_use:N.
%% That produced both an invalid Table-under-P/LI nesting and, whenever
%% more than one stack occurred in a paragraph, corrupted paragraph-tagging
%% bookkeeping (veraPDF/tagpdf: "Parent-Child ... Relation is not
%% allowed").  Building the box with table auto-tagging suspended avoids
%% this: the box then carries no struct/MC of its own, so its content is
%% correctly attributed to the Span when \box_use:N later places it inside
%% one.  \hbox_gset:Nn (not \hbox_set:Nn) is required because the group
%% that scopes the \tagpdfsetup change would otherwise also discard the box.
%%
%% The row separator is \\[0pt] and not \\ , which is not cosmetic.  In a
%% tabular, \\ is \@arraycr, and \@arraycr begins with \@ifstar: an
%% alternative that opens with a judgment mark therefore hands the row
%% separator the starred form \\* ("no page break here"), the star is
%% eaten as syntax, and the mark vanishes from the page.  \altn{est}{*sont}
%% printed "sont" -- silently, with a clean compile, and only ever in the
%% second and later alternatives, since nothing precedes the first.  The
%% explicit [0pt] settles the scan before it reaches the mark: \@arraycr
%% finds no star, then finds the bracket group it is allowed to find.  It
%% also disarms the same trap one step further on, where an alternative
%% opening with "[" -- \altn{[+wh]}{[-wh]} -- would otherwise be read as
%% \\'s optional extra-space argument and raise "Illegal unit of measure".
%% 0pt is a true no-op here: \@xargarraycr struts the row to
%% \dp\@arstrutbox + 0pt, which is the depth \@arstrut already gives it.
\cs_new_protected:Npn \__lxp_setstack:NNnn #1#2#3#4
  {
    \group_begin:
      \cs_if_exist:NT \tagpdfsetup { \tagpdfsetup { table/tagging = false } }
      \hbox_gset:Nn #1
        {
          #4
          \use:e
            {
              \exp_not:N \begin { tabular } [c] { @{} #3 @{} }
              \seq_use:Nn #2 { \\ [ 0pt ] }
            }
          \end { tabular }
        }
    \group_end:
  }
%% One brace, side #1 (L or R), drawn between the ordinates #2 (bottom)
%% and #3 (top).  The decoration bulges to the side determined by the path
%% direction: an upward path gives an opening brace, a downward path a
%% closing one (the v0.13 downward path drew the left brace mirrored), so
%% the two sides are not the same path with a different anchor.
%%
%% baseline=0pt is what lets the same drawer serve both callers.  \altn
%% hands it 0pt..height and raises the whole picture itself, so its
%% baseline sits at the bounding box's bottom edge -- which is where a
%% tikzpicture puts it by default anyway, hence no change there.  \altg
%% hands it -depth..height and needs the baseline on y=0, in the middle.
%% The brace is FILLED, not stroked, and that is the whole point of it.
%% A stroke has one width everywhere; a real brace has not.  Measured off
%% Computer Modern's \left\{ at 11pt: the stem is a constant 1.20pt (which
%% is 0.11em, hence \AltBracePen), it tapers to 0.42pt at the two
%% terminals, and the middle narrows to 0.72pt at the cusp.  Stroked at
%% one width the shape is recognisably a brace and unmistakably not a
%% typographic one -- which is what tikz's brace decoration drew here
%% until v1.2, at 0.4pt, a third of the weight of the type it stands next
%% to.
%%
%% So the outline is drawn instead: the same skeleton as before (pgf's
%% brace decoration, four cubics and two lines, transcribed), offset to
%% either side by half the pen width, with the width tapering at the four
%% places CM tapers.  \AltBraceAmplitude and \AltBraceWidth keep their
%% meanings, and \AltBracePen is the new one.
%%
%% Offsetting is done at the SEGMENT ENDS, and the control points are
%% carried along the endpoint normals.  Exact Bezier offsets are not
%% Beziers at all, but these curves are shallow -- an amplitude of a few
%% points over a corner of a few points -- and the approximation is
%% invisible: measured against the true offset it stays inside a tenth of
%% a point.  Two normals are not axis-aligned and have to be computed (the
%% terminals and the tip); everywhere else the skeleton is tangent to the
%% straight run, so the normal is horizontal and the offset is a signed
%% amplitude.
%%
%% The geometry is CACHED, keyed by everything it depends on.  Building it
%% is some thirty floating-point evaluations -- 15ms a brace, five times
%% what the old decoration cost -- and a document draws the same few
%% heights over and over: two rows, three rows, four.  With the cache a
%% run pays for each distinct height once and the drawing itself costs
%% what the decoration did.  The path is built in the brace's own frame,
%% with its foot at the origin, so the same entry serves every brace of
%% that height wherever it stands; the caller's #2 becomes a shift.
\newcommand \AltBracePen {0.11em}
\prop_new:N \g__lxp_brace_cache_prop
\tl_new:N  \l__lxp_brace_key_tl
\tl_new:N  \l__lxp_brace_path_tl
\fp_new:N  \l__lxp_brace_amp_fp   % amplitude: how far the tip protrudes
\fp_new:N  \l__lxp_brace_len_fp   % the span the brace is drawn along
\fp_new:N  \l__lxp_brace_mid_fp   % half of it: where the tip sits
\fp_new:N  \l__lxp_brace_c_fp     % corner length (pgf's, clamped)
\fp_new:N  \l__lxp_brace_h_fp     % half the pen width
\fp_new:N  \l__lxp_brace_ht_fp    % ... at the terminals, tapered
\fp_new:N  \l__lxp_brace_hp_fp    % ... at the tip
\fp_new:N  \l__lxp_brace_x_fp     % the shaft's own abscissa
\fp_new:N  \l__lxp_brace_s_fp     % which way the tip points: -1 or +1
\fp_new:N  \l__lxp_brace_nt_fp    % terminal normal, along the brace
\fp_new:N  \l__lxp_brace_na_fp    % terminal normal, across it
\fp_new:N  \l__lxp_brace_qt_fp    % tip normal, along
\fp_new:N  \l__lxp_brace_qa_fp    % tip normal, across
\fp_new:N  \l__lxp_brace_tip_fp   % how far the point reaches past the cusp
\fp_new:N  \l__lxp_brace_notch_fp % ... and how deep the notch cuts inside it
%% one point of the outline: #1 along the brace from its foot, #2 across
%% it, away from the stack.  Each argument is used ONCE -- they arrive as
%% expressions, and a second mention would evaluate the expression twice.
\cs_new_protected:Npn \__lxp_brace_pt:nn #1#2
  {
    \tl_put_right:Nx \l__lxp_brace_path_tl
      {
        ( \fp_to_decimal:n { \l__lxp_brace_x_fp + \l__lxp_brace_s_fp * (#2) } ,
          \fp_to_decimal:n { #1 } )
      }
  }
\cs_new_protected:Npn \__lxp_brace_line:nn #1#2
  {
    \tl_put_right:Nn \l__lxp_brace_path_tl { ~ -- ~ }
    \__lxp_brace_pt:nn {#1} {#2}
  }
\cs_new_protected:Npn \__lxp_brace_curve:nnnnnn #1#2#3#4#5#6
  {
    \tl_put_right:Nn \l__lxp_brace_path_tl { ~ .. ~ controls ~ }
    \__lxp_brace_pt:nn {#1} {#2}
    \tl_put_right:Nn \l__lxp_brace_path_tl { ~ and ~ }
    \__lxp_brace_pt:nn {#3} {#4}
    \tl_put_right:Nn \l__lxp_brace_path_tl { ~ .. ~ }
    \__lxp_brace_pt:nn {#5} {#6}
  }
\cs_new_protected:Npn \__lxp_brace_build:n #1
  {
    \fp_set:Nn \l__lxp_brace_amp_fp
      { \dim_to_decimal:n { \AltBraceAmplitude } }
    \fp_set:Nn \l__lxp_brace_c_fp
      { min ( \l__lxp_brace_amp_fp , 0.25 * \l__lxp_brace_len_fp ) }
    \fp_set:Nn \l__lxp_brace_mid_fp { 0.5 * \l__lxp_brace_len_fp }
    \fp_set:Nn \l__lxp_brace_h_fp
      { 0.5 * \dim_to_decimal:n { \AltBracePen } }
    \fp_set:Nn \l__lxp_brace_ht_fp { 0.35 * \l__lxp_brace_h_fp }
    \fp_set:Nn \l__lxp_brace_hp_fp { 0.62 * \l__lxp_brace_h_fp }
    %% the two normals that are not horizontal.  The terminal leaves along
    %% (0.45c, 0.9A) and the tip arrives along (0.15c, 0.3A) -- the
    %% skeleton's own tangents, normalised and turned a quarter turn.
    \fp_set:Nn \l_tmpa_fp
      {
        sqrt ( ( 0.45 * \l__lxp_brace_c_fp ) ^ 2
               + ( 0.9 * \l__lxp_brace_amp_fp ) ^ 2 )
      }
    \fp_set:Nn \l__lxp_brace_nt_fp
      { -0.9 * \l__lxp_brace_amp_fp / \l_tmpa_fp }
    \fp_set:Nn \l__lxp_brace_na_fp
      { 0.45 * \l__lxp_brace_c_fp / \l_tmpa_fp }
    \fp_set:Nn \l_tmpa_fp
      {
        sqrt ( ( 0.15 * \l__lxp_brace_c_fp ) ^ 2
               + ( 0.3 * \l__lxp_brace_amp_fp ) ^ 2 )
      }
    \fp_set:Nn \l__lxp_brace_qt_fp
      { -0.3 * \l__lxp_brace_amp_fp / \l_tmpa_fp }
    \fp_set:Nn \l__lxp_brace_qa_fp
      { 0.15 * \l__lxp_brace_c_fp / \l_tmpa_fp }
    %% How far the point of the tip reaches past the skeleton's own cusp.
    %% A cusp has two tangents, not one, so the outline cannot simply be
    %% offset there: the two half-braces' outer edges are parallel to
    %% their own branches, and where they MEET is the point.  On the
    %% symmetry axis that intersection sits hp / qa beyond the cusp (qa is
    %% the tangent's component along the brace, so a sharper cusp reaches
    %% further), and the inner edges meet the same distance inside it.
    %%
    %% Both are clamped, and by different amounts, because a full miter is
    %% not what a brace has.  Computer Modern's tip is BLUNT -- the point
    %% is cut off with a small flat -- while its notch is a clean deep V,
    %% so the outer clamp is one pen-half and the inner two; taken to the
    %% full intersection the tip comes out as a needle that no typeface
    %% draws.  The clamp earns its keep on short braces as well, and this
    %% is the reason PostScript clamps a miter too: the corner length is a
    %% quarter of the span, so on a two-line stack the cusp closes up,
    %% hp/qa runs away, and the point would reach 3pt past the brace.
    \fp_set:Nn \l__lxp_brace_tip_fp
      {
        min ( \l__lxp_brace_hp_fp / \l__lxp_brace_qa_fp ,
              1.0 * \l__lxp_brace_hp_fp )
      }
    \fp_set:Nn \l__lxp_brace_notch_fp
      {
        min ( \l__lxp_brace_hp_fp / \l__lxp_brace_qa_fp ,
              2.0 * \l__lxp_brace_hp_fp )
      }
    \str_if_eq:nnTF {#1} { L }
      {
        \fp_set:Nn \l__lxp_brace_x_fp
          { \dim_to_decimal:n { \AltBraceWidth - 1pt } }
        \fp_set:Nn \l__lxp_brace_s_fp { -1 }
      }
      {
        \fp_set:Nn \l__lxp_brace_x_fp { \dim_to_decimal:n { 1pt } }
        \fp_set:Nn \l__lxp_brace_s_fp { 1 }
      }
    \tl_clear:N \l__lxp_brace_path_tl
    %% up the far side: foot, corner, tip, corner, head
    \__lxp_brace_pt:nn
      { \l__lxp_brace_ht_fp * \l__lxp_brace_nt_fp }
      { \l__lxp_brace_ht_fp * \l__lxp_brace_na_fp }
    \__lxp_brace_curve:nnnnnn
      { 0.15 * \l__lxp_brace_c_fp }
      { 0.3 * \l__lxp_brace_amp_fp + \l__lxp_brace_h_fp }
      { 0.5 * \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp + \l__lxp_brace_h_fp }
      { \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp + \l__lxp_brace_h_fp }
    \__lxp_brace_line:nn
      { \l__lxp_brace_mid_fp - \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp + \l__lxp_brace_h_fp }
    \__lxp_brace_curve:nnnnnn
      { \l__lxp_brace_mid_fp - 0.5 * \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp + \l__lxp_brace_h_fp }
      { \l__lxp_brace_mid_fp - 0.15 * \l__lxp_brace_c_fp
        + \l__lxp_brace_hp_fp * \l__lxp_brace_qt_fp }
      { 0.7 * \l__lxp_brace_amp_fp
        + \l__lxp_brace_hp_fp * \l__lxp_brace_qa_fp }
      { \l__lxp_brace_mid_fp + \l__lxp_brace_hp_fp * \l__lxp_brace_qt_fp }
      { \l__lxp_brace_amp_fp + \l__lxp_brace_hp_fp * \l__lxp_brace_qa_fp }
    %% the point itself, and the same offset taken on the OTHER branch:
    %% one point served both until v1.2, and the two halves then met in a
    %% pinch with a flat wedge hanging off it -- invisible at reading size
    %% and unmistakable at thirty times it.
    \__lxp_brace_line:nn
      { \l__lxp_brace_mid_fp }
      { \l__lxp_brace_amp_fp + \l__lxp_brace_tip_fp }
    \__lxp_brace_line:nn
      { \l__lxp_brace_mid_fp - \l__lxp_brace_hp_fp * \l__lxp_brace_qt_fp }
      { \l__lxp_brace_amp_fp + \l__lxp_brace_hp_fp * \l__lxp_brace_qa_fp }
    \__lxp_brace_curve:nnnnnn
      { \l__lxp_brace_mid_fp + 0.15 * \l__lxp_brace_c_fp
        - \l__lxp_brace_hp_fp * \l__lxp_brace_qt_fp }
      { 0.7 * \l__lxp_brace_amp_fp
        + \l__lxp_brace_hp_fp * \l__lxp_brace_qa_fp }
      { \l__lxp_brace_mid_fp + 0.5 * \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp + \l__lxp_brace_h_fp }
      { \l__lxp_brace_mid_fp + \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp + \l__lxp_brace_h_fp }
    \__lxp_brace_line:nn
      { \l__lxp_brace_len_fp - \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp + \l__lxp_brace_h_fp }
    \__lxp_brace_curve:nnnnnn
      { \l__lxp_brace_len_fp - 0.5 * \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp + \l__lxp_brace_h_fp }
      { \l__lxp_brace_len_fp - 0.15 * \l__lxp_brace_c_fp }
      { 0.3 * \l__lxp_brace_amp_fp + \l__lxp_brace_h_fp }
      { \l__lxp_brace_len_fp - \l__lxp_brace_ht_fp * \l__lxp_brace_nt_fp }
      { \l__lxp_brace_ht_fp * \l__lxp_brace_na_fp }
    %% across the head and back down the near side
    \__lxp_brace_line:nn
      { \l__lxp_brace_len_fp + \l__lxp_brace_ht_fp * \l__lxp_brace_nt_fp }
      { - \l__lxp_brace_ht_fp * \l__lxp_brace_na_fp }
    \__lxp_brace_curve:nnnnnn
      { \l__lxp_brace_len_fp - 0.15 * \l__lxp_brace_c_fp }
      { 0.3 * \l__lxp_brace_amp_fp - \l__lxp_brace_h_fp }
      { \l__lxp_brace_len_fp - 0.5 * \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp - \l__lxp_brace_h_fp }
      { \l__lxp_brace_len_fp - \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp - \l__lxp_brace_h_fp }
    \__lxp_brace_line:nn
      { \l__lxp_brace_mid_fp + \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp - \l__lxp_brace_h_fp }
    \__lxp_brace_curve:nnnnnn
      { \l__lxp_brace_mid_fp + 0.5 * \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp - \l__lxp_brace_h_fp }
      { \l__lxp_brace_mid_fp + 0.15 * \l__lxp_brace_c_fp
        + \l__lxp_brace_hp_fp * \l__lxp_brace_qt_fp }
      { 0.7 * \l__lxp_brace_amp_fp
        - \l__lxp_brace_hp_fp * \l__lxp_brace_qa_fp }
      { \l__lxp_brace_mid_fp }
      { \l__lxp_brace_amp_fp - \l__lxp_brace_notch_fp }
    %% ... and the notch on the inside is that same intersection taken the
    %% other way, but it REPLACES the two offset ends rather than sitting
    %% between them.  Inside a cusp the two edges cross over each other, so
    %% a path through both ends and the crossing doubles back: the fill
    %% rule counts the little triangle twice with opposite signs and leaves
    %% a white star sitting in the point.  The curves are ended at the
    %% crossing instead, which is where the ink really stops.
    \__lxp_brace_curve:nnnnnn
      { \l__lxp_brace_mid_fp - 0.15 * \l__lxp_brace_c_fp
        - \l__lxp_brace_hp_fp * \l__lxp_brace_qt_fp }
      { 0.7 * \l__lxp_brace_amp_fp
        - \l__lxp_brace_hp_fp * \l__lxp_brace_qa_fp }
      { \l__lxp_brace_mid_fp - 0.5 * \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp - \l__lxp_brace_h_fp }
      { \l__lxp_brace_mid_fp - \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp - \l__lxp_brace_h_fp }
    \__lxp_brace_line:nn
      { \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp - \l__lxp_brace_h_fp }
    %% ... and the last corner closes the path back onto the foot
    \tl_put_right:Nn \l__lxp_brace_path_tl { ~ .. ~ controls ~ }
    \__lxp_brace_pt:nn
      { 0.5 * \l__lxp_brace_c_fp }
      { 0.5 * \l__lxp_brace_amp_fp - \l__lxp_brace_h_fp }
    \tl_put_right:Nn \l__lxp_brace_path_tl { ~ and ~ }
    \__lxp_brace_pt:nn
      { 0.15 * \l__lxp_brace_c_fp }
      { 0.3 * \l__lxp_brace_amp_fp - \l__lxp_brace_h_fp }
    \tl_put_right:Nn \l__lxp_brace_path_tl { ~ .. ~ cycle }
  }
%% One brace, side #1 (L or R), between the ordinates #2 (bottom) and #3
%% (top).  The box contract is what it always was, and both callers depend
%% on it: width \AltBraceWidth, spanning #2 to #3, baseline on y = 0.
%% \altn hands 0pt..height and raises the picture itself; \altg hands
%% -depth..height and needs the baseline in the middle.
\cs_new_protected:Npn \__lxp_brace:nnn #1#2#3
  {
    \fp_set:Nn \l__lxp_brace_len_fp { \dim_to_decimal:n { #3 - #2 } }
    \tl_set:Nx \l__lxp_brace_key_tl
      {
        #1 / \fp_to_decimal:n { \l__lxp_brace_len_fp }
           / \dim_to_decimal:n { \AltBraceAmplitude }
           / \dim_to_decimal:n { \AltBraceWidth }
           / \dim_to_decimal:n { \AltBracePen }
      }
    \prop_get:NVNF \g__lxp_brace_cache_prop \l__lxp_brace_key_tl
        \l__lxp_brace_path_tl
      {
        \__lxp_brace_build:n {#1}
        \prop_gput:NVV \g__lxp_brace_cache_prop \l__lxp_brace_key_tl
          \l__lxp_brace_path_tl
      }
    \begin { tikzpicture } [ baseline = 0pt , x = 1pt , y = 1pt ]
      \useasboundingbox ( 0pt , #2 ) rectangle ( \AltBraceWidth , #3 ) ;
      \begin { scope } [ shift = { ( 0pt , #2 ) } ]
        \exp_args:NV \fill \l__lxp_brace_path_tl ;
      \end { scope }
    \end { tikzpicture }
  }
\ExplSyntaxOff

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Stacked alternatives: \altn / \lxAltn                              %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\ExplSyntaxOn
\seq_new:N  \l__lxp_alt_seq
\tl_new:N   \l__lxp_alt_align_tl
\tl_new:N   \l__lxp_alt_alt_tl
\box_new:N  \l__lxp_alt_box
\dim_new:N  \l__lxp_alt_ht_dim
\dim_new:N  \l__lxp_alt_dp_dim

%% \lxAltn collects its brace-delimited alternatives (one leading mandatory
%% argument, then any number of further brace groups) and sets them as a
%% braced vertical stack.  This is deliberately NOT math: the stack is a
%% tabular and the brace is drawn with TikZ, so the alternatives are ordinary
%% tagged text.  Under active tagging the whole thing is wrapped in a Span
%% carrying a spoken /Alt ("A, B, or C"), built with \text_purify:n so that
%% formatting in an alternative (\sout, \textbf, ...) is stripped for speech.
\NewDocumentCommand \lxAltn { O{c} m }
  {
    \tl_set:Nn   \l__lxp_alt_align_tl {#1}
    \seq_clear:N \l__lxp_alt_seq
    \seq_put_right:Nn \l__lxp_alt_seq {#2}
    \__lxp_grab:NN \l__lxp_alt_seq \__lxp_alt_print:
  }
%% Hanging judgments inside the stack (opt-in, with [phantomalign]).
%%
%% \altn{est}{*sont} left-aligned puts "est" under the star and "sont" one
%% star-width to its right, so the two words the reader is asked to compare
%% are the one pair of things NOT aligned.  This is the same defect
%% [phantomalign] already fixes between gloss tiers -- align on the first
%% real glyph, not on the leading mark -- and it is fixed here the same
%% way and under the same switch, closing the gap that section's comment
%% used to record ("\altg alternative columns are not covered"; \altg's
%% still are not, see below).
%%
%% The mechanism is a second tabular column rather than an \llap: the mark
%% has to stay INSIDE the braces (it judges its own alternative, not the
%% stack), and a right-aligned gutter column sizes itself to the widest
%% mark and sets every mark flush against the words.
%%
%% The two columns are butted together with @{} and NOT separated by
%% \JdgSep, which is what the first version of this did.  The distance
%% between a mark and the word it marks is already in the font -- it is
%% whatever "*sont" gives when typed -- and alignment has no business
%% changing it: the only thing that may move is the OTHER rows, which come
%% right to meet this one.  With \JdgSep in between, the aligned stack was
%% \JdgSep wider than the same stack unaligned and the mark visibly
%% detached from its word, which reads as a defect rather than as
%% alignment.  \lx@hangjudge does need \JdgSep, but its situation is not
%% this one: there the mark is \llap-ed into the label area, a box away
%% from the text, with no sidebearing to inherit.  A happy side effect is
%% that stacks are now identical under [lazy] and [legacy], which set
%% \JdgSep differently.
%%
%% The split layout is used only when some alternative actually carries a
%% mark.  Without that test a stack of plain alternatives would still gain
%% the \JdgSep gutter gap under [phantomalign] and shift right by it, which
%% would make an opt-in about alignment change the look of stacks that have
%% nothing to align.
%%
%% \altg shares this, through \__lxp_altg_setstack:nN -- but only for the
%% call that sets the OBJECT tier, and for a solo stack, which is an object
%% stack too.  A judgment is a claim about the object language; the gloss
%% is a translation of it and is not itself grammatical or not, so a mark
%% in a gloss alternative stays where it was typed rather than earning a
%% gutter.  That is a linguistic decision, not a typographic one, which is
%% why the caller passes it in instead of this code inferring it.
%%
%% (An earlier version of this comment claimed \altg could not have it at
%% all, because "the tiers must keep the same width".  They need not: the
%% two calls sit side by side in one gloss column, and the gloss cell's
%% indent is \g__lxp_altg_indent_dim, which \__lxp_altg_shape_obj: derives
%% from the object emit's OWN width -- so widening the object stack moves
%% the gloss stack by exactly as much, with nothing to keep in step by
%% hand.  The claim was wrong; it is recorded here because it is the kind
%% of invariant that looks true from the tier-per-call structure alone.)
\seq_new:N  \l__lxp_alt_split_seq
\tl_new:N   \l__lxp_alt_pre_tl
\tl_new:N   \l__lxp_alt_rest_tl
\bool_new:N \l__lxp_alt_anyjudge_bool

%% Build the two-cell rows, and record whether any mark was found at all.
%% The peel itself is \__lx_gl_peel:nNN, shared with the gloss aligner --
%% see the comment there for why there is one copy of it and not two.  A
%% stack wants BOTH halves: the marks go in the gutter cell and the rest
%% in the alternative cell.
\cs_new_protected:Npn \__lxp_alt_split:N #1
  {
    \seq_clear:N \l__lxp_alt_split_seq
    \bool_set_false:N \l__lxp_alt_anyjudge_bool
    \seq_map_inline:Nn #1
      {
        \__lx_gl_peel:nNN {##1} \l__lxp_alt_pre_tl \l__lxp_alt_rest_tl
        \tl_if_empty:NF \l__lxp_alt_pre_tl
          { \bool_set_true:N \l__lxp_alt_anyjudge_bool }
        \seq_put_right:Ne \l__lxp_alt_split_seq
          { \exp_not:V \l__lxp_alt_pre_tl & \exp_not:V \l__lxp_alt_rest_tl }
      }
  }
%% Build a braced stack, with or without the hanging gutter.  #1 = box to
%% set, #2 = seq of alternatives, #3 = the column spec the caller wants for
%% the alternatives themselves, #4 = the hook \__lxp_setstack:NNnn runs
%% inside the box, #5 = a bool saying whether THIS call may hang a mark.
%%
%% Both \altn and \altg go through here, and that is the point rather than
%% tidiness.  The decision has four parts that have to agree -- may this
%% call hang, is the option on, which seq is set, and which spec -- and
%% when they were written out twice they twice came close to drifting: the
%% gutter's spec had to be edited in two places when the \JdgSep between
%% its columns was dropped, and \altg's \lpzg hook was written out once per
%% branch, so a change to one branch's font handling would silently not
%% apply to the other.  One copy cannot drift from itself.
\cs_new_protected:Npn \__lxp_alt_build:NNnnN #1#2#3#4#5
  {
    \bool_lazy_and:nnTF
      { \bool_if_p:N #5 } { \bool_if_p:N \l__lx_gl_phantom_bool }
      { \__lxp_alt_split:N #2 }
      { \bool_set_false:N \l__lxp_alt_anyjudge_bool }
    \bool_if:NTF \l__lxp_alt_anyjudge_bool
      { \__lxp_setstack:NNnn #1 \l__lxp_alt_split_seq { r @{} #3 } {#4} }
      { \__lxp_setstack:NNnn #1 #2 {#3} {#4} }
  }
%% the column spec the optional argument asks for
\cs_new:Npn \__lxp_alt_colspec:
  { \str_case:VnF \l__lxp_alt_align_tl { {l}{l} {r}{r} } {c} }
%% the stacked alternatives, as a tabular; also records total height.  The
%% optional argument picks the column spec; nothing else is needed inside
%% the box, so the hook is empty.
\cs_new_protected:Npn \__lxp_alt_setstack:
  {
    \__lxp_alt_build:NNnnN \l__lxp_alt_box \l__lxp_alt_seq
      { \__lxp_alt_colspec: } { } \c_true_bool
    \dim_set:Nn \l__lxp_alt_dp_dim { \box_dp:N \l__lxp_alt_box }
    \dim_set:Nn \l__lxp_alt_ht_dim
      { \box_ht:N \l__lxp_alt_box + \l__lxp_alt_dp_dim }
  }
%% one brace, side L or R, sized to the stack's total height and raised as
%% a whole.  The raise is pinned to the stack's own depth, not half the
%% total height: a stack is seldom split evenly around its baseline (an
%% all-caps or struck-through row above pulls height, ordinary rows below
%% add less depth), so using the true box_dp keeps the brace's centre level
%% with the stack's actual midpoint instead of drifting toward whichever
%% side is taller.
\cs_new_protected:Npn \__lxp_alt_brace:n #1
  {
    \raisebox { \dim_eval:n { \AltBraceRaise - \l__lxp_alt_dp_dim } }
      { \__lxp_brace:nnn {#1} { 0pt } { \l__lxp_alt_ht_dim } }
  }
%% The opening inner gap.  Normally \AltBraceSep, as the closing one is --
%% but a stack with a hanging judgment tucks \AltJdgTuck closer to the
%% opening brace.
%%
%% The room for that is real and would otherwise go to waste.  A brace's
%% arm curls AWAY from the content between its tips, so at the height of
%% any given row the ink sits well left of the box's right edge, and the
%% gutter's mark can move into that hollow without ever touching it.  What
%% the tuck buys is the thing a reserved gutter costs: the marked row sits
%% close to the brace and the unmarked rows are no longer pushed a full
%% mark-width in.  Measured at 10pt on a one-star stack, brace to mark goes
%% from 4.26pt untucked to 2.28pt, and brace to the unmarked row from
%% 9.66pt to 7.68pt.
%%
%% The default was 0.45em while the brace was a hairline, and it does not
%% survive a brace with weight: at 0.45em the mark clears the new outline
%% by 0.24pt, which is to say it touches it.  Two things moved at once --
%% the ink got thicker, and the pen is an em while \AltBraceAmplitude is an
%% absolute 4pt, so the hollow no longer keeps its proportions as the font
%% grows.  0.2em is where the clearance comes back to what the hairline
%% gave (2.28pt at 10pt) AND stops depending on the size: 2.28 / 2.28 /
%% 2.34 / 2.28 / 2.22pt at 10/12/14/17/20pt, where 0.3em gives 1.26pt at
%% 10pt and 0.30pt at 20pt.  The old comment claimed the clearance widened
%% with the font; that was true of a hairline whose ink did not grow, and
%% is not true now.
%%
%% What it costs is the other half of the bargain: at 0.45em a judged stack
%% put its words within half a point of where an unjudged one puts them,
%% and at 0.2em they sit about 3pt further in.  That is the trade a brace
%% of this weight forces -- the hollow is smaller than it was -- and the
%% mark not touching the brace is worth more than the words not moving.
%%
%% Only the OPENING gap changes.  The closing brace rides along with the
%% box, so the stack gets narrower rather than sliding sideways, and the
%% distance from the preceding word to the brace is untouched.  And only
%% when a mark is actually there: an unmarked stack is set exactly as
%% before, which is what makes this safe to have on by default.
%%
%% \AltJdgTuck is a length in em, and at its default the clearance holds
%% across sizes rather than drifting -- see the measurements above.  It is
%% a balance of two things that scale differently: the mark and the pen
%% grow with the font, \AltBraceAmplitude does not.
%% Nothing here is clamped, on purpose: \AltJdgTuck is a tunable like
%% \AltBraceRaise or \AltBraceWidth, and a knob that silently ignores what
%% it is set to is worse than one that does as it is told.  But a tuck too
%% deep is a SILENT fault -- TeX has no opinion about overlapping ink, so
%% the mark simply prints on top of the brace, with no error and no
%% warning, and only looking at the page finds it.  So the value is
%% checked and reported instead of corrected.
%%
%% The threshold is the brace's own box: past \AltBraceWidth + \AltBraceSep
%% the stack has been pulled beyond the far edge of the box the brace is
%% drawn in, which is where the ink stops being reliably clear of it.  It
%% is deliberately a little early -- at 10pt the mark still has about
%% 1.5pt of air there, and does not actually touch until roughly 6.8pt of
%% tuck -- because the true limit depends on the mark, the font size and
%% \AltBraceAmplitude together, and a warning that fires slightly early is
%% worth more than a threshold that pretends to a precision it has not
%% got.  Reported once per document: a stack is an inline construction and
%% a long paper could otherwise carry hundreds of copies of it.
\bool_new:N \g__lxp_alt_tuckwarned_bool
%% The guidance is in the MESSAGE, not in the more-text: l3msg shows the
%% fourth argument of \msg_new:nnnn only for errors, where the user can ask
%% for it, so anything put there for a warning is written and never read.
\msg_new:nnnn { linguexx } { tuck-too-deep }
  {
    \iow_char:N \\AltJdgTuck~(#1)~is~deeper~than~the~brace~it~tucks~
    into~(\iow_char:N \\AltBraceWidth~+~\iow_char:N \\AltBraceSep~=~#2),~
    so~a~judgment~mark~may~print~on~top~of~the~opening~brace.~Nothing~has~
    been~clamped~--~the~value~set~has~been~used.~Look~at~the~page,~and~
    reduce~it~if~the~two~overlap;~
    \iow_char:N \\renewcommand\iow_char:N \\AltJdgTuck{0pt}~restores~the~
    untucked~layout.~Reported~once~per~document.
  }
  { This~is~a~layout~warning~only;~nothing~is~wrong~with~the~document. }
\cs_new_protected:Npn \__lxp_alt_tuckcheck:
  {
    \dim_compare:nNnT { \AltJdgTuck } > { \AltBraceWidth + \AltBraceSep }
      {
        \bool_if:NF \g__lxp_alt_tuckwarned_bool
          {
            \bool_gset_true:N \g__lxp_alt_tuckwarned_bool
            \msg_warning:nnee { linguexx } { tuck-too-deep }
              { \dim_eval:n { \AltJdgTuck } }
              { \dim_eval:n { \AltBraceWidth + \AltBraceSep } }
          }
      }
  }
\cs_new_protected:Npn \__lxp_alt_opensep:
  {
    \bool_if:NTF \l__lxp_alt_anyjudge_bool
      { \__lxp_alt_tuckcheck:
        \hspace { \dim_eval:n { \AltBraceSep - \AltJdgTuck } } }
      { \hspace { \AltBraceSep } }
  }
%% the printed object (brace + stack + brace), no tagging.  The outer
%% gap (\AltBraceOuterSep) clears the brace's outward bulge from
%% whatever precedes/follows; the inner gap just separates the brace from
%% the stack it braces.
\cs_new_protected:Npn \__lxp_alt_emit:
  { \leavevmode \hspace{\AltBraceOuterSep} \__lxp_alt_brace:n { L } \__lxp_alt_opensep:
    \box_use:N \l__lxp_alt_box
    \hspace{\AltBraceSep} \__lxp_alt_brace:n { R } \hspace{\AltBraceOuterSep} }
%% the printed object wrapped in a Span carrying the spoken /Alt
\cs_new_protected:Npn \__lxp_alt_emit_tagged:
  {
    \__lxp_buildalt:NN \l__lxp_alt_seq \l__lxp_alt_alt_tl
    \leavevmode
    \hspace{\AltBraceOuterSep}
    \lx@tag@span@begin:e { tag = Span , alt = { \l__lxp_alt_alt_tl } }
    \__lxp_alt_brace:n { L } \__lxp_alt_opensep:
    \box_use:N \l__lxp_alt_box
    \hspace{\AltBraceSep} \__lxp_alt_brace:n { R }
    \lx@tag@span@end:
    \hspace{\AltBraceOuterSep}
  }
\cs_new_protected:Npn \__lxp_alt_print:
  {
    \__lxp_alt_setstack:
    \lx@tag@if@active:TF { \__lxp_alt_emit_tagged: } { \__lxp_alt_emit: }
  }
\ExplSyntaxOff

% Tunable dimensions of the drawn brace.  AltBraceSep is the INNER gap,
% between the brace and the stack it braces; AltBraceOuterSep is the
% OUTER gap, between the brace and whatever precedes/follows it in the
% surrounding text.  They differ because the brace bulges outward past
% its own bounding box (the tip reaches further than the box declared for
% it, by about 1.1pt at 11pt) only on the outer side -- the inner side has
% no such overflow to clear, so it can sit much closer.
%
% The outer gap was 0.7em while the brace was a hairline, and 0.7em is
% most of a word space on top of the one the source already has: with a
% brace of the type's own weight the stack read as a thing held at arm's
% length from the words it belongs to.  Halved.  What has to be cleared is
% the tip's overflow, not the box, so 0.35em still leaves about 2.8pt of
% air past the bulge at 11pt.
\newcommand\AltBraceWidth{0.9ex}
\newcommand\AltBraceAmplitude{4pt}
\newcommand\AltBraceSep{0.15em}
\newcommand\AltBraceOuterSep{0.35em}
\newcommand\AltBraceRaise{0pt}
% How far a stack carrying a hanging judgment tucks toward its opening
% brace, into the hollow the brace's arm leaves.  0pt reverts to the
% untucked layout; see \__lxp_alt_opensep: for what the default buys.
\newcommand\AltJdgTuck{0.2em}

% Alias \altn -> \lxAltn unless somebody already owns \altn.
\lx@claim@alias{altn}\lxAltn


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Glossed alternatives: \altg / \lxAltg                              %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\ExplSyntaxOn
\seq_new:N \l__lxp_altg_seq
\box_new:N \l__lxp_altg_stack_box
\box_new:N \l__lxp_altg_out_box
\box_new:N \l__lxp_altg_punct_box
\dim_new:N \l__lxp_altg_punct_dim
\dim_new:N \l__lxp_altg_ht_dim
\dim_new:N \l__lxp_altg_bxht_dim
\dim_new:N \l__lxp_altg_bxdp_dim
\tl_new:N  \l__lxp_altg_alt_tl
%% cross-cell protocol state (the gloss grid typesets column by column,
%% object cell first, so the two calls of one paradigm are adjacent)
\int_new:N \g__lxp_altg_role_int   % 0 = the next in-gloss \altg is the object call
\int_new:N \g__lxp_altg_count_int  % alternatives announced by the object call
\dim_new:N \g__lxp_altg_indent_dim % width of the object emit, for the gloss call
\tl_new:N  \g__lxp_altg_pending_tl % punctuation the object call took along
\bool_new:N \l_lx_gl_inside_bool   % true while \gll/\gl cells are typeset

%% \lxAltg{alt}{alt}... : one stacked column of alternatives.  Inside an
%% interlinear gloss it is written TWICE -- once in the object line with
%% the object words, once in the gloss line with their glosses:
%%
%%   \exg. Die \altg{Frau}{Socke}{Maus}{Tonne} ist da.\\
%%         The.\lpzg{sg} \altg{woman.\lpzg{sg}}{sock.\lpzg{sg}}%
%%           {mouse.\lpzg{sg}}{ton.\lpzg{sg}} is.\lpzg{prs} there.\\
%%
%% The two calls occupy the two tiers of one gloss column and assemble a
%% single paradigm: object column on the left, gloss column to its right
%% (offset by \AltgColSep), braced on BOTH sides, and centred on the
%% midline between the object tier and the gloss tier -- with four
%% alternatives, rows 2 and 3 ride the object and gloss lines and rows 1
%% and 4 protrude symmetrically, with surrounding lines kept clear.  The
%% example number, which \exg. places on the object baseline, stays put.
%% Both calls must have the same number of alternatives (package error
%% otherwise), and no spaces may separate the brace groups (a space ends
%% collection AND splits gloss columns; break lines with %).
%%
%% Outside a gloss, a single \lxAltg sets one both-braced stack on the
%% current baseline, like \lxAltn with a closing brace added.
%%
%% Like \lxAltn this is deliberately NOT math.  Inside the stacks \lpzg is
%% reduced to plain small caps (each stack forms a single marked-content
%% span under tagging, so nested abbreviation Spans are suppressed);
%% expansions reappear in the spoken form.  Under active tagging each
%% call is wrapped in a Span carrying /Alt speaking its own list ("Frau,
%% Socke, Maus, or Tonne" / "woman.singular, ..."), with simple \lpzg
%% keys expanded from the Leipzig table and compound or unknown keys
%% passed through verbatim.
\NewDocumentCommand \lxAltg { m }
  {
    \seq_clear:N \l__lxp_altg_seq
    \seq_put_right:Nn \l__lxp_altg_seq {#1}
    \__lxp_grab:NN \l__lxp_altg_seq \__lxp_altg_print:
  }
%% the stack: one [c]-centred tabular; per-side extents recorded.  The hook
%% carries the tier font (the gloss call sets \AltgTransFont) and the local
%% \lpzg: \lx@lpzg@build parses the label without printing anything, so the
%% abbreviation is set plain inside the stack -- each stack is a single
%% marked-content span, with no room for a nested abbreviation Span -- but
%% still counts as used and reaches \lpzglist like any other.
%% #2 says whether this call may hang a judgment: true for the object tier
%% and for a solo stack, false for the gloss tier.  See the note on
%% \__lxp_alt_setstack: for why the caller decides and this does not.
\cs_new_protected:Npn \__lxp_altg_setstack:nN #1#2
  {
    \__lxp_alt_build:NNnnN \l__lxp_altg_stack_box \l__lxp_altg_seq { l }
      { #1 \renewcommand \lpzg [1]
          { \lx@lpzg@parse:n {##1} \lx@lpzg@print:n {##1} } }
      #2
    \dim_set:Nn \l__lxp_altg_bxht_dim { \box_ht:N \l__lxp_altg_stack_box }
    \dim_set:Nn \l__lxp_altg_bxdp_dim { \box_dp:N \l__lxp_altg_stack_box }
    \dim_set:Nn \l__lxp_altg_ht_dim
      { \l__lxp_altg_bxht_dim + \l__lxp_altg_bxdp_dim }
  }
%% one brace, side L or R, on the baseline: unlike \altn's, it is not
%% raised as a whole, so it is drawn from the stack's depth to its height
%% and the picture's own baseline (y=0) does the anchoring.
\cs_new_protected:Npn \__lxp_altg_brace:n #1
  {
    \__lxp_brace:nnn {#1}
      { \dim_eval:n { - \l__lxp_altg_bxdp_dim } } { \l__lxp_altg_bxht_dim }
  }
%% Punctuation glued to the object call travels to the end of the
%% paradigm.  A paradigm is ONE block spanning both tiers, and its closing
%% brace is drawn by the gloss call, half a line lower and a whole gloss
%% column further right; but a sentence-final period is written where a
%% period is written, right after the object \altg that ends the object
%% line:
%%
%%     \exg. On-i su se \altg{posvadjal-i}{*posvadjal-e}.\\
%%           they  are refl \altg{argued.m.pl}{argued.f.pl}\\
%%
%% Typeset where it stands, that period lands immediately after the object
%% STACK -- inside the braces, in the gutter between the object column and
%% the gloss column, reading as though it belonged to the first
%% alternative.  Nothing on the object line can be placed after the
%% closing brace, either, because the width of the paradigm is not known
%% until the gloss call has run.  So the object call takes the punctuation
%% with it and the gloss call sets it down, past the brace and level with
%% the middle of it -- the one height that belongs to both tiers, since
%% the sentence the period ends is not the first alternative's.
%%
%% Only punctuation is taken, and only what is glued to the call with no
%% space: a space ends the gloss column anyway, so anything after one is a
%% column of its own and already lands past the paradigm.  \altn needs
%% none of this -- it draws both of its braces in the one call.
%% One peek, then a test against the candidates -- not a peek per
%% candidate inside a map.  A peek reads what comes next in the INPUT, and
%% inside a mapping what comes next is the mapping's own continuation, so
%% a loop of \peek_charcode_remove: over the candidates never sees the
%% document at all: it reports no punctuation on a line ending in a period.
\tl_const:Ne \c__lxp_altg_punct_tl { \tl_to_str:n { .,;:!?)] } }
\cs_new_protected:Npn \__lxp_altg_takepunct:
  { \peek_after:Nw \__lxp_altg_punct_if: }
%% The candidates are compared to the peeked token by CHARACTER CODE, and
%% the loop over them runs after the peek rather than around it.
%% \token_to_str:N is no use here: \l_peek_token is a control sequence LET
%% to the token ahead, so \string gives back its own name, not the
%% character it stands for, and every comparison against the set succeeds
%% or fails on the wrong string.
\cs_new_protected:Npn \__lxp_altg_punct_if:
  {
    \token_if_cs:NF \l_peek_token
      {
        \tl_map_inline:Nn \c__lxp_altg_punct_tl
          {
            \token_if_eq_charcode:NNT \l_peek_token ##1
              { \tl_map_break:n { \__lxp_altg_punct_take:N } }
          }
      }
  }
\cs_new_protected:Npn \__lxp_altg_punct_take:N #1
  {
    \tl_gput_right:Nn \g__lxp_altg_pending_tl {#1}
    \__lxp_altg_takepunct:
  }
%% Level with the MIDDLE of the closing brace, not with either line.  The
%% period ends the sentence the whole paradigm stands in, so it belongs to
%% both tiers; set on the object line it reads as the first alternative's,
%% which is the very thing that was wrong with leaving it where it stood.
%%
%% \AltBraceSep in front of it, the same clearance the brace keeps from
%% the stack on its other side.  At this height it is not optional: the
%% brace's TIP is what points at the punctuation, it reaches the right
%% edge of the brace's own box, and a period set flush against it comes
%% out looking like a blob on the end of the brace rather than a period.
%%
%% The raise is three terms, all measured rather than guessed:
%%
%%   0.5\baselineskip   the gloss baseline up to the paradigm's own
%%                      baseline -- the y=0 the two braces are drawn
%%                      about, half a line above the tier this box sits on
%%   (ht-dp)/2          that baseline up to the middle of the brace, which
%%                      spans exactly [-dp,+ht] of the gloss stack: the
%%                      stack is a [c] tabular, so its box is centred on
%%                      the math axis and not on its own middle, and the
%%                      two differ by the axis height
%%   -(ht-dp)/2 of the  the punctuation's own baseline, so that its INK is
%%   punctuation        centred on that height and not hung from it.  A
%%                      period would otherwise sit half a dot's height
%%                      low, and a comma would hang lower still
%%
%% Set down flat afterwards -- no height, no depth -- so the gloss line
%% keeps the baseline grid it was clamped onto, and outside the /Alt Span,
%% because the period is not one of the alternatives it speaks.
\cs_new_protected:Npn \__lxp_altg_putpunct:
  {
    \tl_if_empty:NF \g__lxp_altg_pending_tl
      {
        \hbox_set:Nn \l__lxp_altg_punct_box
          { \hspace { \AltBraceSep } \g__lxp_altg_pending_tl }
        \dim_set:Nn \l__lxp_altg_punct_dim
          {
            0.5 \baselineskip
            + ( \l__lxp_altg_bxht_dim - \l__lxp_altg_bxdp_dim ) / 2
            - ( \box_ht:N \l__lxp_altg_punct_box
                - \box_dp:N \l__lxp_altg_punct_box ) / 2
          }
        \hbox_set:Nn \l__lxp_altg_punct_box
          { \raisebox { \l__lxp_altg_punct_dim }
              { \box_use:N \l__lxp_altg_punct_box } }
        \box_set_ht:Nn \l__lxp_altg_punct_box { 0pt }
        \box_set_dp:Nn \l__lxp_altg_punct_box { 0pt }
        \box_use:N \l__lxp_altg_punct_box
        \tl_gclear:N \g__lxp_altg_pending_tl
      }
  }
%% the three printed shapes.  Object cell: left brace + stack, dropped
%% half a baseline; natural height kept (reserves the protrusion above),
%% depth clamped to zero so the gloss tier lands on the grid.  Gloss
%% cell: indent by the object emit's width + \AltgColSep, then stack +
%% right brace, raised half a baseline; height clamped (grid), natural
%% depth kept (reserves the protrusion below).  Solo: both braces on the
%% current baseline.
\cs_new_protected:Npn \__lxp_altg_shape_obj:
  {
    \hbox_set:Nn \l__lxp_altg_out_box
      { \hspace { \AltBraceOuterSep }
        \raisebox { \dim_eval:n { -0.5 \baselineskip } }
          { \__lxp_altg_brace:n { L } \__lxp_alt_opensep:
            \box_use:N \l__lxp_altg_stack_box } }
    \dim_gset:Nn \g__lxp_altg_indent_dim
      { \box_wd:N \l__lxp_altg_out_box + \AltgColSep }
    \box_set_dp:Nn \l__lxp_altg_out_box { 0pt }
    \box_use:N \l__lxp_altg_out_box
  }
\cs_new_protected:Npn \__lxp_altg_shape_gloss:
  {
    \hbox_set:Nn \l__lxp_altg_out_box
      { \skip_horizontal:n { \g__lxp_altg_indent_dim }
        \raisebox { \dim_eval:n { 0.5 \baselineskip } }
          { \box_use:N \l__lxp_altg_stack_box \hspace { \AltBraceSep }
            \__lxp_altg_brace:n { R } }
        %% against the brace, INSIDE \AltBraceOuterSep: that separator
        %% holds the paradigm off the words around it, and a period is not
        %% one of them -- it belongs to the brace it follows.
        \__lxp_altg_putpunct:
        \hspace { \AltBraceOuterSep } }
    \box_set_ht:Nn \l__lxp_altg_out_box { 0pt }
    \box_use:N \l__lxp_altg_out_box
  }
\cs_new_protected:Npn \__lxp_altg_shape_solo:
  {
    \hspace { \AltBraceOuterSep }
    \__lxp_altg_brace:n { L } \__lxp_alt_opensep:
    \box_use:N \l__lxp_altg_stack_box
    \hspace { \AltBraceSep } \__lxp_altg_brace:n { R }
    \hspace { \AltBraceOuterSep }
  }
%% emit one shape, tag-guarded: under active tagging the shape is wrapped
%% in a Span carrying the spoken /Alt of this call's list.
\cs_new_protected:Npn \__lxp_altg_emit:n #1
  {
    \leavevmode
    \lx@tag@if@active:TF
      {
        \__lxp_buildalt:NN \l__lxp_altg_seq \l__lxp_altg_alt_tl
        \lx@tag@span@begin:e { tag = Span , alt = { \l__lxp_altg_alt_tl } }
        #1
        \lx@tag@span@end:
      }
      { #1 }
  }
\cs_new_protected:Npn \__lxp_altg_print:
  {
    \bool_if:NTF \l_lx_gl_inside_bool
      {
        \int_compare:nNnTF { \g__lxp_altg_role_int } = { 0 }
          { % object call
            \int_gset:Nn \g__lxp_altg_role_int { 1 }
            \int_gset:Nn \g__lxp_altg_count_int
              { \seq_count:N \l__lxp_altg_seq }
            \__lxp_altg_setstack:nN { } \c_true_bool
            \__lxp_altg_emit:n { \__lxp_altg_shape_obj: }
            \__lxp_altg_takepunct:
          }
          { % gloss call
            \int_gset:Nn \g__lxp_altg_role_int { 0 }
            \int_compare:nNnF { \seq_count:N \l__lxp_altg_seq }
                            = { \g__lxp_altg_count_int }
              { \PackageError { linguexx }
                  { \string\altg : \int_use:N \g__lxp_altg_count_int\space
                    object~alternatives~but~
                    \int_eval:n { \seq_count:N \l__lxp_altg_seq } ~glosses }
                  { The~two~\string\altg\space calls~of~one~paradigm~must~
                    list~the~same~number~of~alternatives. } }
            \__lxp_altg_setstack:nN { \AltgTransFont } \c_false_bool
            \__lxp_altg_emit:n { \__lxp_altg_shape_gloss: }
          }
      }
      { % solo, outside a gloss
        \__lxp_altg_setstack:nN { } \c_true_bool
        \__lxp_altg_emit:n { \__lxp_altg_shape_solo: }
      }
  }
\ExplSyntaxOff

% Tunables.  The braces share \AltBraceWidth/\AltBraceAmplitude/\AltBraceSep
% with \lxAltn (declared above).
\newcommand\AltgColSep{1.2em}%   glue between object and gloss columns
\newcommand\AltgTransFont{\normalfont}% font of the gloss stack

% Alias \altg -> \lxAltg unless somebody already owns \altg.
\lx@claim@alias{altg}\lxAltg

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Explicit judgment markers: \jdg                                    %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\newlength{\JdgSep}% value set in the defaults block (Layout section)
%% \jdg[<spoken>]{<mark>} hangs <mark> in the margin.  With an explicit
%% <spoken> (or a known default for <mark>), the mark is announced by a
%% screen reader as that phrase; otherwise it is typeset as before.
\ExplSyntaxOn
\NewDocumentCommand \jdg { O{} m }
  {
    \tl_if_blank:nTF {#1}
      { \exp_args:Ne \lx@hangjudge { \prop_item:Ne \g_lx_judge_alt_prop { \tl_to_str:n {#2} } } {#2} }
      { \lx@hangjudge {#1} {#2} }
  }
%% \DeclareJudgment[spoken=<phrase>]{\cmd}{<mark>} names a mark.  The
%% spoken phrase, if given, is both attached to \cmd and registered for
%% <mark> so a leading scanned <mark> is announced the same way.
\keys_define:nn { lx / judge }
  { spoken .tl_set:N = \l__lx_judge_decl_tl }
%% The first mandatory argument is the command being DEFINED, the second is
%% what it prints -- an order easy to read the wrong way round.  Getting it
%% wrong used to HANG the run rather than complain: \DeclareRobustCommand
%% takes the first token of whatever it is handed, so
%% \DeclareJudgment{\%\%}{\%\%} quietly redefined \%, which the judgment
%% scanner peeks for, and the compile then spun forever with nothing in the
%% log to say why.  A hang is the worst failure to debug, so check that the
%% argument is a single control sequence and, if it is not, say what the two
%% arguments actually mean.
\msg_new:nnnn { linguexx } { judgment-not-a-command }
  { \iow_char:N \\DeclareJudgment~needs~one~command~here,~not~'#1'. }
  {
    Write~\iow_char:N \\DeclareJudgment[spoken=...]{\iow_char:N \\mymark}
    {<mark>}:~the~first~mandatory~argument~is~the~command~being~defined,~
    the~second~is~the~mark~it~prints.
  }
\cs_new_protected:Npn \lx@judge@declare #1#2#3
  {
    \tl_clear:N \l__lx_judge_decl_tl
    \keys_set:nn { lx / judge } {#1}
    \tl_if_empty:NF \l__lx_judge_decl_tl
      { \exp_args:Nne \lx@judge@setalt {#3} { \l__lx_judge_decl_tl } }
    \exp_args:Nne \DeclareRobustCommand #2
      { \exp_not:N \jdg [ \l__lx_judge_decl_tl ] { \exp_not:n {#3} } }
  }
\cs_new_protected:Npn \lx@judge@declare@cs #1#2#3
  {
    \token_if_cs:NTF #2
      { \lx@judge@declare {#1} {#2} {#3} }
      { \msg_error:nnn { linguexx } { judgment-not-a-command } {#2} }
  }
\NewDocumentCommand \DeclareJudgment { O{} m m }
  {
    \tl_if_single_token:nTF {#2}
      { \lx@judge@declare@cs {#1} {#2} {#3} }
      { \msg_error:nnn { linguexx } { judgment-not-a-command } {#2} }
  }
\cs_set_eq:NN \DeclareJudgement \DeclareJudgment
%% \SetJudgmentSpoken{<mark>}{<phrase>} overrides or adds a spoken form.
\NewDocumentCommand \SetJudgmentSpoken { m m }
  { \lx@judge@setalt {#1} {#2} }
\ExplSyntaxOff

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Cross-reference ranges                                             %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\newcommand\rangedash{--}
\newcommand\lx@setsub[2]{\expandafter\gdef\csname lx@sub@#1\endcsname{#2}}
%% \sublabel records in the .aux the BARE label of the sub-example it marks
%% -- just the letter or numeral, no \SubExLBr/\SubExRBr delimiters -- which
%% is what \prefrange needs for the closing half of a compact range
%% ("(1a--c)": the range ends in a letter, not in a second full reference).
%%
%% It has to record the label of the level it is USED at.  Recording
%% \Exalph{SubExNo} unconditionally, as this did, is right only at the letter
%% level: at the roman level SubExNo names the ENCLOSING letter, so every
%% roman sub-sub-example under one letter recorded the same value and
%% \refrange over them printed "(1b-i--b)" instead of "(1b-i--iii)" -- a
%% silently wrong reference, since nothing about it is an error.
%%
%% At the main level there is no sub-label to record (and \Exalph{SubExNo}
%% would be \alph{0}, i.e. "Counter too large"), so nothing is written;
%% \lx@subletter then falls back to a full \pref, which is the sensible
%% reading of a range whose end is a whole example.
\newcommand\lx@writesub[2]{%
  \immediate\write\@auxout{\string\lx@setsub{#1}{#2}}}
\newcommand\sublabel[1]{\label{#1}%
  \if@filesw
    \ifcase\lx@subdepth
      % main level: no sub-label, \lx@subletter falls back to \pref
    \or \lx@writesub{#1}{\Exalph{SubExNo}}%
    \or \lx@writesub{#1}{\Exroman{SubSubExNo}}%
    \fi
  \fi}
\newcommand\lx@subletter[1]{%
  \ifcsname lx@sub@#1\endcsname
    \csname lx@sub@#1\endcsname
  \else
    \pref{#1}%
  \fi}
\newcommand\prefrange[2]{\pref{#1}\rangedash\lx@subletter{#2}}
\newcommand\refrange[2]{(\prefrange{#1}{#2})}
%% Spelt with \pref and the delimiters rather than with \ref, so that the
%% two full references it prints are parenthesised under \ExBareRefs too:
%% what \Refrange promises is "(2)--(3)", not "whatever \ref gives, twice".
\newcommand\Refrange[2]{%
  \theExLBr\pref{#1}\theExRBr\rangedash\theExLBr\pref{#2}\theExRBr}

%% cleveref knows nothing about the example counters, so \cref{ex:one} comes
%% out as "?? (1)" -- its marker for a type it has no name for.  The names
%% are declared EMPTY rather than "example": linguistics prose refers to an
%% example by its number alone ("as in (1a)"), and \theExNo already supplies
%% the parentheses.  \cref then prints exactly what \ref does, and what it
%% adds here is its list and range handling -- \cref{a,b} giving "(1) and
%% (2)" -- rather than a word in front.
%%
%% Guarded twice over.  Only if cleveref is actually loaded, in either order,
%% which is why this waits for \AtBeginDocument; and only for a counter the
%% document has not named itself, because \AtBeginDocument runs after the
%% preamble and would otherwise silently overwrite a \crefname the author
%% wrote deliberately.  \Crefname sets cref@<counter>@name as well as the
%% capitalised one, so testing the lower-case name catches either.
\def\lx@crefname#1{%
  \@ifundefined{cref@#1@name}{\crefname{#1}{}{}\Crefname{#1}{}{}}{}}
\AtBeginDocument{%
  \@ifpackageloaded{cleveref}{%
    \lx@crefname{ExNo}\lx@crefname{SubExNo}%
    \lx@crefname{SubSubExNo}\lx@crefname{FnExNo}%
  }{}}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Right-aligned in-line source: \exsource                            %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

\newcommand{\ExSourceFont}{\normalfont\footnotesize}
\DeclareRobustCommand{\exsource}[1]{%
  \unskip\nobreak\hskip0pt plus 1fill\relax
  \penalty50
  \hskip1em plus 1fill\relax
  \hbox{}\nobreak\hskip0pt plus 1fill\relax
  \mbox{\ExSourceFont#1}}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  The annotation column: \exannot                                    %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% \exannot[<spoken>]{<text>} sets <text> in a COLUMN: the same horizontal
%% position for every annotated example on the page, whatever its nesting
%% depth and whatever its own length.
%%
%%   (1) a. que Pierre est fatigue      [CP]
%%       b. Pierre est fatigue          [TP]
%%
%% \exsource is the flush-right member of the family and is for a source
%% attribution, which belongs at the margin; this one is for a structural
%% label, which belongs BESIDE the example it labels.  \ExAnnotColumn says
%% where "beside" is, measured from the left edge of the text block.
%%
%% Two things make the column a column, and both are places where this
%% differs from \jambox (which [langsci] carries, and which this does not
%% replace -- see the note there):
%%
%%   * The column is measured from \columnwidth, NOT \linewidth.  Inside a
%%     list \linewidth has already lost the indentation, so a \linewidth-
%%     based column moves one indent step per nesting level: measured
%%     247 / 271 / 295pt for \ex. / \a. / a sub-sub item on one page.
%%     \columnwidth is the same for all three, and is also the right
%%     measure in twocolumn (where \textwidth is both columns) and in a
%%     minipage (which sets it along with \linewidth).
%%
%%   * The annotation is carried in a box of FIXED WIDTH, ending at the
%%     right margin, so its left edge is at \ExAnnotColumn no matter what
%%     else is on the line.  \jambox sets its box at its natural width and
%%     follows it with a skip that shrinks to nothing (\hskip 1.2em minus
%%     1.2em, and a second one of the size of the gutter), so where that
%%     box lands is whatever the line's glue arithmetic makes it: as soon
%%     as an example is long enough to reach the column, the shrink takes
%%     up the difference and the box slides right.  Silently -- no overfull
%%     warning, no error.  Measured, four sub-examples of increasing length
%%     at one \jambox gutter: aligned to 0.01pt at 3cm and at 7cm, and
%%     212.6 / 212.6 / 234.1 / 288.9pt at 11cm.  So \jambox holds a column
%%     only while the column is far enough right that no example can reach
%%     it, which is exactly what a column set NEAR the examples is not.
%%
%% The annotation ends the paragraph, as \jambox does, because
%% \parfillskip has to be zero for the last line to hold the column.  So it
%% comes last in the example PART it annotates -- which is not the same as
%% last in the example: a dot-syntax example is ended by a blank line, not
%% by a \par, so \a. and its siblings may follow an annotated head, and
%% tests/exannot.tex pins that (ANNHEAD) rather than leaving it to be
%% rediscovered.

\newlength{\ExAnnotColumn}% where the column is, from the left text edge
\newlength{\ExAnnotSep}   % least gap between the example and the column;
                          % set it rigid -- see \lx@annot@fill:n below
\newcommand{\ExAnnotFont}{\normalfont}

%% \ExAnnotColumn is derived from \columnwidth, which a class or a geometry
%% package loaded AFTER this one still changes.  Re-derived at
%% \begin{document} if -- and only if -- it still holds the value computed
%% at load time, which is exactly the guard \Extopsep and \Exredux use for
%% their own late-changing basis (\baselineskip); a \setlength in the
%% preamble therefore wins, as it does for those two.
%%
%% .75 rather than something tighter: a default that collides is worse than
%% one that is merely far, because a collision costs a line.  The command
%% exists to be given the value the document wants.
\newlength{\lx@auto@annotcol}
\newcommand\lx@setannotcol{%
  \setlength{\ExAnnotColumn}{.75\columnwidth}%
  \setlength{\lx@auto@annotcol}{\ExAnnotColumn}}
\AtBeginDocument{%
  \ifdim\ExAnnotColumn=\lx@auto@annotcol \lx@setannotcol \fi}

\ExplSyntaxOn
\box_new:N \l__lx_annot_box
\dim_new:N \l__lx_annot_dim

%% The room the column leaves for the annotation.  Clamped at zero: a
%% \ExAnnotColumn past the right edge would otherwise make a box of
%% negative width, which draws the annotation back over the example
%% instead of complaining.
\cs_new:Npn \lx@annot@width:
  { \dim_max:nn { \c_zero_dim } { \columnwidth - \ExAnnotColumn } }

%% Spoken forms, the same arrangement as the judgment marks': a table from
%% the printed text to what a screen reader should say, consulted when
%% \exannot is given no explicit <spoken>.  "[CP]" is an abbreviation a
%% reader cannot expand and a synthesiser will not even spell out; /Alt on
%% a Span replaces it for speech while the page still shows [CP] and
%% copy-and-paste still yields [CP].
\prop_new:N \g_lx_annot_alt_prop
\cs_new_protected:Npn \lx@annot@setalt #1#2
  { \prop_gput:Nnn \g_lx_annot_alt_prop {#1} {#2} }
\NewDocumentCommand \SetAnnotSpoken { m m }
  { \lx@annot@setalt {#1} {#2} }

\msg_new:nnn { linguexx } { annot-too-wide }
  {
    \iow_char:N \\exannot~is~#1pt~wide,~and~\iow_char:N \\ExAnnotColumn~
    leaves~only~#2pt;~this~one~starts~left~of~the~column.
  }
\msg_new:nnnn { linguexx } { annot-in-gloss }
  {
    \iow_char:N \\exannot~in~a~gloss~belongs~at~the~end~of~the~OBJECT~
    line.
  }
  {
    Put~it~after~the~last~object-language~word~--~with~a~space~or~without~
    one,~either~reads~--~and~before~the~\iow_char:N \\\iow_char:N \\ ~that~
    ends~that~line.~A~gloss~is~a~translation:~an~annotation~on~a~gloss~
    tier~would~have~nothing~to~align~with,~and~one~in~the~middle~of~a~
    line~has~no~room~to~go.
  }

%% The annotation itself, with no glue and no paragraph.
\cs_new_protected:Npn \lx@annot@content:nn #1#2
  {
    \tl_if_blank:nTF {#1}
      { { \ExAnnotFont #2 } }
      { \lx@tag@span:nn { tag = Span , alt = {#1} } { { \ExAnnotFont #2 } } }
  }

%% Set the annotation and work out how wide the box that carries it to the
%% column has to be.  An annotation too wide for the room the column leaves
%% is set at its natural width instead -- it then starts LEFT of the column
%% and the alignment is gone, which is a thing to be told about rather than
%% to discover on the page, hence the warning.
\cs_new_protected:Npn \lx@annot@setbox:nn #1#2
  {
    \hbox_set:Nn \l__lx_annot_box { \lx@annot@content:nn {#1} {#2} }
    \legacy_if:nTF { lx@annotfit }
      {
        %% Under \ExAnnotFit the group's own measurements decide, and they
        %% already account for the widest annotation in it -- so there is
        %% nothing here to be too wide FOR, and no warning to give.
        \lx@annot@setfitdim
        \dim_set:Nn \l__lx_annot_dim { \lx@annot@fitdim }
        \lx@annot@checkused { \l__lx_annot_dim }
        \exp_args:Ne \lx@annot@recordused { \dim_use:N \l__lx_annot_dim }
      }
      {
        \dim_set:Nn \l__lx_annot_dim { \lx@annot@width: }
        \dim_compare:nNnT { \l__lx_annot_dim } < { \box_wd:N \l__lx_annot_box }
          {
            \msg_warning:nnee { linguexx } { annot-too-wide }
              { \dim_to_decimal_in_unit:nn { \box_wd:N \l__lx_annot_box } { 1pt } }
              { \dim_to_decimal_in_unit:nn { \l__lx_annot_dim } { 1pt } }
            \dim_set:Nn \l__lx_annot_dim { \box_wd:N \l__lx_annot_box }
          }
      }
  }

%% The glue that carries #1 to the column, and the ONE place it is written.
%% Every token has a job:
%%
%%   \unskip          drop the space that ended the example text;
%%   \hfill \penalty  the breakpoint.  \hfill is a fill, so it outranks the
%%                    fil of \rightskip under \raggedright (\ExRaggedRight,
%%                    and every gloss) -- an \hfil would merely share the
%%                    slack with it and leave the annotation halfway;
%%   \hskip \ExAnnotSep   the least gap, and it has to be RIGID to be a
%%                    LEAST gap: the box below already holds the column on
%%                    its own, so shrink here would not move the annotation
%%                    -- it would let an example come closer to it than
%%                    \ExAnnotSep, up to touching, rather than take the
%%                    break above.  Keep \ExAnnotSep rubber-free for the
%%                    same reason;
%%   \hbox:n{} \hfill the post-break half.  Without an empty box to make
%%                    the second fill non-discardable, a broken line starts
%%                    at the LEFT margin and the annotation lands nowhere
%%                    near the column -- which is the whole point of
%%                    breaking rather than shoving.
%% The two \ExAnnotFit readings hang here, and nowhere else: L before the
%% first \unskip, so it is the end of the example's own material and not of
%% the glue this adds; R after the box, whose right edge IS the margin.
%% Both are no-ops without the option.
\cs_new_protected:Npn \lx@annot@fill:n #1
  {
    \lx@annot@measure:n { L }
    \unskip \nobreak \hfill \penalty 50
    \hskip \ExAnnotSep
    \hbox:n { } \nobreak \hfill
    #1
    \lx@annot@measure:n { R }
  }
\cs_new_protected:Npn \lx@annot@measure:n #1
  {
    \legacy_if:nT { lx@annotfit }
      {
        \str_if_eq:nnTF {#1} { L }
          { \exp_args:Ne \lx@annot@recordL
              { \int_eval:n { \box_wd:N \l__lx_annot_box } } }
          { \lx@annot@recordR }
      }
  }
\cs_new_protected:Npn \lx@annot@usebox:
  { \hbox_to_wd:nn { \l__lx_annot_dim } { \box_use:N \l__lx_annot_box \hfil } }

%% Placed in an example: fill to the column, then close the paragraph --
%% \parfillskip has to be zero or its fil would fight the fills above.
\cs_new_protected:Npn \lx@annot@block:nn #1#2
  {
    \leavevmode
    \lx@annot@setbox:nn {#1} {#2}
    \lx@annot@fill:n { \lx@annot@usebox: }
    \parfillskip \c_zero_dim
    \par
  }

%% Placed on the object line of a gloss: the same glue, and an ordinary box
%% -- nothing here has to reach for the object tier, because the gloss's
%% columns are \vtop s, so the baseline of the paragraph the grid is set in
%% IS the object tier's baseline and an inline box on it is level with the
%% object words by construction.  (A \vtop of its own was tried and is
%% indistinguishable: with a single row its reference point is the same
%% baseline.  It is not here, because a box that does nothing reads as if
%% it did.)  The \strut keeps the line's height where the object tier's
%% own struts put it when the annotation is the tallest thing on it.
%%
%% No \par: the paragraph being closed is the gloss's own grid, and
%% \lx@gloss@multi closes it.
\cs_new_protected:Npn \lx@annot@gloss:nn #1#2
  {
    \lx@annot@setbox:nn {#1} {#2}
    \lx@annot@fill:n { \hbox:n { \strut \lx@annot@usebox: } }
  }

%% Resolve <spoken>: an explicit one wins, otherwise the table is asked for
%% one registered against the printed text.  #1 is the emitter to call.
\cs_new_protected:Npn \lx@annot@resolve:Nnn #1#2#3
  {
    \tl_if_blank:nTF {#2}
      {
        \exp_args:Ne #1
          { \prop_item:Ne \g_lx_annot_alt_prop { \tl_to_str:n {#3} } } {#3}
      }
      { #1 {#2} {#3} }
  }

\NewDocumentCommand \exannot { O{} m }
  {
    %% Inside a gloss the command is never RUN: \lx@gloss@multi lifts it off
    %% the object line and emits its content itself (see there), whether it
    %% stands as a word of its own or is glued to the last object word.
    %% Reaching this branch means it was written somewhere a gloss cannot
    %% place it -- in the middle of a line, where there is nowhere for it to
    %% go -- and running it would end the gloss's paragraph in the middle of
    %% the grid.  Say so instead.
    \bool_if:NTF \l_lx_gl_inside_bool
      { \msg_error:nn { linguexx } { annot-in-gloss } }
      { \lx@annot@resolve:Nnn \lx@annot@block:nn {#1} {#2} }
  }

%% What \exannot means while \lx@gloss@multi emits the annotation it lifted
%% off the object line (see there).
\NewDocumentCommand \lx@annot@glossuse { O{} m }
  { \lx@annot@resolve:Nnn \lx@annot@gloss:nn {#1} {#2} }
\ExplSyntaxOff

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  \ExAnnotFit: the column measured from the examples themselves      %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% \ExAnnotColumn is a number the author has to find, and the number that
%% looks right depends on the examples underneath it.  \ExAnnotFit finds it
%% instead: each annotated example records where its text ended, and the
%% column of a GROUP -- one top-level example and everything under it -- is
%% put \ExAnnotSep past the longest of them.  Nothing else changes; the
%% column is still one column, still held by a fixed-width box.
%%
%% This costs a .aux round trip, so the first run of a fresh document sets
%% the fallback column (\ExAnnotColumn) and the second one is right.  A
%% third is not needed and the package says so: see the convergence note
%% below.
%%
%% Two measurements per annotation, not one, and neither is a length this
%% package could compute for itself:
%%
%%   L  where the example's text ENDED, taken at the \exannot itself, after
%%      line breaking -- the thing no \settowidth can reach, because the
%%      material has already been contributed to a paragraph by then.  (The
%%      alternative, setting the body a second time into a scratch box, is
%%      not open to this package at all: under tagging the second pass would
%%      emit a second set of structure elements.)
%%   R  the right margin, taken just past the annotation's own box, whose
%%      right edge sits there by construction.
%%
%% It is R-L that is recorded and minimised, never L on its own, and that is
%% what makes the arrangement safe across a page break, a twoside document
%% and a two-column one: both readings come from the SAME line, so their
%% difference is a width and carries no page origin with it.
%%
%% Checked on a twoside document (inner 20mm, outer 45mm) with one group
%% deliberately split over an even and an odd page: the two annotations came
%% out at x 363.59 and 292.72, which are the same 176.6pt from the right
%% margin of their own page.  By hand, not in tests/: pinning it needs a
%% three-page document built on \vspace* to place the break, and that is a
%% brittle thing to assert on.  What the suite does pin is the differencing
%% itself -- take R alone and tests/exannot-fit.tex fails at once.
%%
%% R-L is also what makes it converge, which L alone would not.  When an
%% annotation wraps, L is still the end of the example's text and R is still
%% the right margin -- of the next line, but the same margin -- so a
%% measurement does not depend on whether the previous run's column happened
%% to leave room for it.  The one case that does feed back is an example
%% whose TEXT is long enough to break, since the annotation's box is part of
%% what the paragraph breaker is fitting; the convergence check below is for
%% that case, and it is why there is one rather than a promise.
%%
%% The widest ANNOTATION in the group is recorded alongside, and the column
%% is never narrower than that.  Without it, an annotation too wide for the
%% column is set at its natural width and starts left of the column on its
%% own -- one label out of line, which is the failure this whole section
%% exists to prevent.  Here the group's column moves left far enough for the
%% widest of them and every annotation stays in line.

\newif\iflx@annotfit
\newcommand\ExAnnotFit{\lx@annotfittrue}
\newcommand\ExAnnotNoFit{\lx@annotfitfalse}

%% The three names the .aux will carry are \providecommand'd into it first,
%% as the relative-reference anchors are and as hyperref does for its own
%% label lines: a document that drops linguexx, or turns \ExAnnotFit off,
%% still has last run's .aux, and reading it must not be an undefined
%% control sequence.
%%
%% UNCONDITIONALLY, at \begin{document}, whether or not the fitting is ever
%% asked for.  Three lines in an .aux is not a cost worth reasoning about,
%% and every attempt to write them only when they are needed is wrong:
%%
%%   * hung on \iflx@annotfit at \begin{document}, it misses \ExAnnotFit
%%     issued in the BODY -- which is a real spelling, since the switch
%%     respects grouping and turning the fitting on for one block is the
%%     natural way to use it.  The compile succeeds and leaves an .aux full
%%     of records that nothing declares: a file that breaks the NEXT run.
%%     examples/ua-demo.tex uses that spelling and did exactly this.
%%   * written instead by \ExAnnotFit itself, it cannot be got ahead of the
%%     records under \include.  \include{chapa} writes \@input{chapa.aux}
%%     into the main .aux when it STARTS, so anything a later \ExAnnotFit
%%     puts in the main .aux is read after chapa's records; and putting it
%%     in chapa's own .aux fixes that file while leaving the ordering to
%%     luck in the next one.  Measured on a two-file document: the
%%     declaration landed at main.aux line 3 and \@input{chapa.aux} at
%%     line 2.
%%
%% Here there is no ordering question left.  The main .aux is the first file
%% read and these are its first lines, so they precede every record in every
%% file, including an .aux an \includeonly has left over from a previous run.
\if@filesw
  \AtBeginDocument{%
    \immediate\write\@mainaux{\string\providecommand\string\lxannotL[3]{}}%
    \immediate\write\@mainaux{\string\providecommand\string\lxannotR[2]{}}%
    \immediate\write\@mainaux{\string\providecommand\string\lxannotUsed[3]{}}}%
\fi

%% pdftex and xetex spell it \pdfsavepos, luatex \savepos.  Nothing else is
%% needed -- no zref, no pdftexcmds: the position is written to the .aux by
%% an ordinary \write whatsit, which is executed at shipout, when the value
%% the preceding \pdfsavepos asked for is the one \pdflastxpos holds.  That
%% pairing is the whole protocol, and it is why each measurement carries its
%% own \write instead of the two sharing one.
\ExplSyntaxOn
\cs_if_exist:NTF \savepos
  {
    \cs_new_eq:NN \lx@annot@savepos \savepos
    \cs_new_eq:NN \lx@annot@lastxpos \lastxpos
  }
  {
    \cs_new_eq:NN \lx@annot@savepos \pdfsavepos
    \cs_new_eq:NN \lx@annot@lastxpos \pdflastxpos
  }

%% The group.  One top-level example and everything under it, keyed by the
%% counter rather than by \theExNo: \theExNo carries \theExLBr and
%% \theExRBr, which an \edef writing the .aux expands to "(1)" and a
%% \csname reading it back does not expand at all, so the two spellings of
%% one key never met.  Footnote examples are their own series and get their
%% own keys, or a footnote's group would be merged with the main-text
%% example it hangs off.
%% \if@noftnote is TRUE outside a footnote -- it reads "no footnote here",
%% not "in a footnote".  Both branches were once the other way round, and
%% the symptom was not an error: every group keyed itself "f0" and the
%% whole document shared one column, which looks like a deliberate design
%% rather than like a bug.
\cs_new:Npn \lx@annot@group
  { \if@noftnote m \int_use:N \c@ExNo \else f \int_use:N \c@FnExNo \fi }
\ExplSyntaxOff

%% ---- what the .aux carries ----------------------------------------------
%% Written with @ and letters only: the .aux is read back with @ a letter
%% but _ still a subscript, so an expl3 name could not be used here.
%%
%%   \lxannotL{group}{x}{annotation width}   at the end of the example text
%%   \lxannotR{group}{x}                     at the right margin
%%   \lxannotUsed{group}{width}              what that run actually set
%%
%% Accumulated into \lxannot@min@<group> (the smallest R-L: the room the
%% LONGEST example in the group leaves) and \lxannot@wd@<group> (the widest
%% annotation).  Both in sp, as integers, because that is what the engine
%% reports and comparing integers needs no dimen registers.
\newcount\lx@annot@lx
\newcount\lx@annot@wd
\def\lxannotL#1#2#3{\global\lx@annot@lx=#2\relax \global\lx@annot@wd=#3\relax
  \@ifundefined{lxannot@wd@#1}%
    {\expandafter\xdef\csname lxannot@wd@#1\endcsname{\the\lx@annot@wd}}%
    {\ifnum\lx@annot@wd>\csname lxannot@wd@#1\endcsname\relax
       \expandafter\xdef\csname lxannot@wd@#1\endcsname{\the\lx@annot@wd}\fi}}
\def\lxannotR#1#2{%
  \@tempcnta=#2\relax \advance\@tempcnta by-\lx@annot@lx
  \@ifundefined{lxannot@min@#1}%
    {\expandafter\xdef\csname lxannot@min@#1\endcsname{\the\@tempcnta}}%
    {\ifnum\@tempcnta<\csname lxannot@min@#1\endcsname\relax
       \expandafter\xdef\csname lxannot@min@#1\endcsname{\the\@tempcnta}\fi}}
%% #3 is F when that run had measurements to go on and B when it fell back
%% to \ExAnnotColumn, and the difference is the whole of the convergence
%% check below.
\def\lxannotUsed#1#2#3{%
  \expandafter\xdef\csname lxannot@used@#1\endcsname{#2}%
  \expandafter\xdef\csname lxannot@mode@#1\endcsname{#3}}

%% ---- the fitted width ---------------------------------------------------
%% max(room the longest example leaves - \ExAnnotSep, widest annotation),
%% and never wider than the text block.  With no record for the group --
%% the first run, a group whose examples have not shipped yet -- the
%% fallback is \ExAnnotColumn, so an unconverged document is merely at the
%% author's column rather than at some arbitrary width.
%% A procedure, not an expandable width: it ASSIGNS, and an assignment
%% cannot happen inside the \dimexpr that consuming it as a width would
%% put it in ("You can't use \dimexpr in horizontal mode", from the
%% \csname lookup rather than from anything about horizontal mode).  The
%% answer is in \lx@annot@fitdim.
\newdimen\lx@annot@fitdim
\def\lx@annot@setfitdim{%
  \global\lx@annot@fellbackfalse
  \@ifundefined{lxannot@min@\lx@annot@group}%
    {\global\lx@annot@fellbacktrue
     \lx@annot@fitdim=\dimexpr\columnwidth-\ExAnnotColumn\relax}%
    {\lx@annot@fitdim=\dimexpr
       \csname lxannot@min@\lx@annot@group\endcsname sp-\ExAnnotSep\relax
     \@tempdima=\csname lxannot@wd@\lx@annot@group\endcsname sp\relax
     \ifdim\lx@annot@fitdim<\@tempdima \lx@annot@fitdim=\@tempdima \fi
     \ifdim\lx@annot@fitdim>\columnwidth \lx@annot@fitdim=\columnwidth \fi}}

%% ---- convergence --------------------------------------------------------
%% Two states deserve the warning and they are not the same state:
%%
%%   * this run had no measurements for a group and used \ExAnnotColumn.
%%     The column is not where it will end up; say so.  This is the whole of
%%     a fresh document's first run.
%%   * this run's width differs from what the LAST run used, and the last
%%     run was itself fitted.  Something is still moving -- the one way that
%%     can happen is an example whose own text breaks, where the annotation
%%     box is part of what the paragraph breaker is fitting.
%%
%% What deserves NO warning, and got one in the first version, is the
%% ordinary second run: it differs from the first precisely because the
%% first fell back, and its own page is already right.  Warning there tells
%% every two-run build to run a third time for nothing, which is how a
%% rerun warning stops being read.
\newif\iflx@annot@unsettled
\newif\iflx@annot@fellback
\def\lx@annot@checkused#1{%
  \iflx@annot@fellback
    \global\lx@annot@unsettledtrue
  \else
    \@ifundefined{lxannot@used@\lx@annot@group}{}%
      {\if F\csname lxannot@mode@\lx@annot@group\endcsname
         \ifdim#1=\csname lxannot@used@\lx@annot@group\endcsname\relax\else
           \global\lx@annot@unsettledtrue\fi
       \fi}%
  \fi}
\AtEndDocument{%
  \iflx@annot@unsettled
    \PackageWarning{linguexx}{Annotation columns are not settled.
      Rerun to get \string\ExAnnotFit\space right}%
  \fi}

%% ---- recording ----------------------------------------------------------
%% The group key is frozen by the \edef, because the \write is executed at
%% SHIPOUT, by which time \lx@annot@group names whatever example is current
%% there.  The position is the opposite case and must not be frozen -- it is
%% not known until shipout either -- hence \noexpand\the.  Getting those two
%% the same way round is the whole of the protocol.
\def\lx@annot@recordL#1{%
  \lx@annot@savepos
  \edef\lx@annot@tmp{\write\@auxout{\string\lxannotL{\lx@annot@group}%
      {\noexpand\the\noexpand\lx@annot@lastxpos}{#1}}}\lx@annot@tmp}
\def\lx@annot@recordR{%
  \lx@annot@savepos
  \edef\lx@annot@tmp{\write\@auxout{\string\lxannotR{\lx@annot@group}%
      {\noexpand\the\noexpand\lx@annot@lastxpos}}}\lx@annot@tmp}
%% No savepos for this one: what the run USED is known here and now, and it
%% is the only one of the three that is.
\def\lx@annot@recordused#1{%
  \edef\lx@annot@tmp{\write\@auxout{\string\lxannotUsed{\lx@annot@group}%
      {#1}{\iflx@annot@fellback B\else F\fi}}}\lx@annot@tmp}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%  Install the defaults of the mode in force                          %%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Done here, at the end, so that every length and macro it touches exists.
%% Because this runs at LOAD time (and not \AtBeginDocument, as in
%% linguex), a \setlength in the preamble overrides it, as one would
%% expect.

\resetExdefaults

%% The \ex this package ends up with, for the \begin{document} check far
%% above: taken here, after every branch that defines one ([gb4e] gives
%% \ex to the environment front-end), so that what is remembered is what
%% a document actually gets.  Under an option that defines no \ex at all
%% both sides of the \ifx are undefined, which compares equal and says
%% nothing, correctly.
\let\lx@ex@installed\ex

%%%% EOF
