%%%% 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/07/31 Standalone linguistic examples, linguex-compatible interface v. 1.2] %% 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.....). %% 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. %% 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. %% 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. \newif\iflx@lazy \newif\iflx@gbfour \newif\iflx@legacy \newif\iflx@phantomalign \DeclareOption{lazy}{\lx@lazytrue} \DeclareOption{gb4e}{\lx@gbfourtrue} \DeclareOption{legacy}{\lx@legacytrue} \DeclareOption{phantomalign}{\lx@phantomaligntrue} \DeclareOption*{\PackageWarning{linguexx}{Unknown option '\CurrentOption' ignored}} \ProcessOptions\relax \iflx@gbfour\else\lx@lazytrue\fi \RequirePackage{graphicx}% \scalebox etc. \RequirePackage{tikz}% drawn brace for \lxAltn (no math mode) \usetikzlibrary{decorations.pathreplacing} % 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. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% 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 \newcommand\theExLBr{\ifparens\else(\fi} \newcommand\theExRBr{\ifparens\else)\fi} \newcommand\theFnExLBr{\ifparens\else(\fi} \newcommand\theFnExRBr{\ifparens\else)\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 \AtBeginDocument{% \let\lx@orig@footnotetext\@footnotetext \long\def\@footnotetext#1{\lx@orig@footnotetext{\@noftnotefalse#1}}} \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}} % 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..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}} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% 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. \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}% \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}% \def\firstrefdash{-}% \def\secondrefdash{-}% \def\SubExLBr{}\def\SubExRBr{.}% \def\SubSubExLBr{(}\def\SubSubExRBr{)}% \def\GlossSep{.33em plus .4em minus .2em}} \iflx@legacy \let\resetExdefaults\lx@defaults@legacy \else \let\resetExdefaults\lx@defaults@lazy \fi % \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} \iflx@legacy % main level: label box recomputed from the number; text margin % \Exindent + \Exlabelwidth + \Exlabelsep, as in the default mode \def\lx@geom@main{\lx@calc@Exlabelwidth \labelwidth\Exlabelwidth \labelsep\Exlabelsep \leftmargin\dimexpr\Exindent+\Exlabelwidth+\Exlabelsep\relax \if@noftnote\else\addtolength{\topsep}{-.5\topsep}\fi} % sub-levels: the text margin IS the parameter, the label sits flush % left inside it \def\lx@geom@sub{\leftmargin\SubExleftmargin \labelwidth\SubExleftmargin \labelsep\z@ \topsep.3\Extopsep} \def\lx@geom@subsub{\leftmargin\SubSubExleftmargin \labelwidth\SubSubExleftmargin \labelsep\z@ \topsep\z@} \else \def\lx@geom@main{% \lx@ol@set{lxOLdecimal}% \labelwidth\Exlabelwidth \labelsep\Exlabelsep \leftmargin\dimexpr\Exindent+\Exlabelwidth+\Exlabelsep\relax} \def\lx@geom@sub{\lx@ol@set{lxOLalpha}% \labelwidth\SubExlabelwidth \labelsep\Exlabelsep \leftmargin\dimexpr\SubExlabelwidth+\Exlabelsep\relax \topsep\z@} \def\lx@geom@subsub{\lx@ol@set{lxOLroman}% \labelwidth\SubSubExlabelwidth \labelsep\Exlabelsep \leftmargin\dimexpr\SubSubExlabelwidth+\Exlabelsep\relax \topsep\z@} \fi %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% 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. \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. \ExplSyntaxOn \tl_new:N \l__lx_judge_tl \tl_new:N \l__lx_judge_cont_tl % scan judgments, then execute #1 (the collected marks are in % \lx@judgeprint; \lx@emitjudge hangs them via \jdg if nonempty) \cs_new_protected:Npn \lx@scanjudgeto #1 { \tl_clear:N \l__lx_judge_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: } { \tl_use:N \l__lx_judge_cont_tl } } } } } } \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} } %% 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 } } \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 } } } \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 \def\ex{\@ifnextchar.{\lx@ex@dot}{\lx@ex@nodot}} \iflx@lazy \def\lx@ex@dot.{\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 \def\lx@ex@nodot{% \iflx@inexe \expandafter\lx@gbex \else \lx@gbex@err \expandafter\lx@gbex \fi} \def\lx@gbex@err{% \PackageError{linguexx}{\string\ex\space outside exe/xlist}{Put \string\ex\space inside \string\begin{exe} ... \string\end{exe}\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} \long\def\lx@gbex@opt[#1]#2{% \ifcase\lx@subdepth \lx@main@core \or \lx@subitem@core \or \lx@subsubitem@core \fi \lx@setjudge{#1}% \lx@makeitem#2} \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 \cs_new_protected:Npn \lx@collectbody { \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: } \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). \cs_new_protected:Npn \__lx_body_cs:n #1 { \str_if_eq:eeTF {#1} { \exp_not:N \par } { \peek_analysis_map_break:n { \lx@runbody } } { \str_if_eq:eeTF {#1} { \exp_not:N \ex } { % inside a \begin{...}\end{...} pair within the body (an xlist, % say), \ex is a legitimate item command and is collected; at % environment depth 0 it can only be a forgotten blank line \int_compare:nNnTF { \l__lx_body_env_int } > { 0 } { \tl_put_right:Ne \l__lx_body_tl {#1} } { \peek_analysis_map_break:n { \lx@body@info{\string\ex.}\lx@runbody \ex } } } { \str_if_eq:eeTF {#1} { \exp_not:N \exg } { \int_compare:nNnTF { \l__lx_body_env_int } > { 0 } { \tl_put_right:Ne \l__lx_body_tl {#1} } { \peek_analysis_map_break:n { \lx@body@info{\string\exg.}\lx@runbody \exg } } } { \str_if_eq:eeTF {#1} { \exp_not:N \begin } { \int_incr:N \l__lx_body_env_int \tl_put_right:Ne \l__lx_body_tl {#1} } { \str_if_eq:eeTF {#1} { \exp_not:N \end } { \int_compare:nNnTF { \l__lx_body_env_int } > { 0 } { \int_decr:N \l__lx_body_env_int \tl_put_right:Ne \l__lx_body_tl {#1} } { \peek_analysis_map_break:n { \lx@body@info{\string\end}\lx@runbody \end } } } { \str_if_eq:eeTF {#1} { \exp_not:N \begingroup } { \int_incr:N \l__lx_body_grp_int \tl_put_right:Ne \l__lx_body_tl {#1} } { \str_if_eq:eeTF {#1} { \exp_not:N \endgroup } { \int_compare:nNnTF { \l__lx_body_grp_int } > { 0 } { \int_decr:N \l__lx_body_grp_int \tl_put_right:Ne \l__lx_body_tl {#1} } { \peek_analysis_map_break:n { \lx@body@info{\string\endgroup}\lx@runbody \endgroup } } } { \str_if_eq:eeTF {#1} { \exp_not:N \color@begingroup } { \int_incr:N \l__lx_body_col_int \tl_put_right:Ne \l__lx_body_tl {#1} } { \str_if_eq:eeTF {#1} { \exp_not:N \color@endgroup } { \int_compare:nNnTF { \l__lx_body_col_int } > { 0 } { \int_decr:N \l__lx_body_col_int \tl_put_right:Ne \l__lx_body_tl {#1} } { \peek_analysis_map_break:n { \lx@body@info{\string\color@endgroup}\lx@runbody \color@endgroup } } } { \tl_put_right:Ne \l__lx_body_tl {#1} } } } } } } } } } } \cs_new_protected:Npn \lx@runbody { \exp_args:NV \lx@run@ex \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} } \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 } , } } \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}} %% 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. \def\lx@listdefaults{% \itemindent\z@ \listparindent\z@ \parsep\z@ \itemsep\z@ \partopsep\z@ \parskip\z@ \let\makelabel\lx@flushlabel} \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} %% 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. \long\def\lx@run@ex#1{% \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 \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} %% 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@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}} \def\lx@exstart@opt[#1]{\def\lx@itemlabel{#1}\lx@mainlist\lx@scanjudge} \def\lx@main@core{% \if@noftnote \refstepcounter{ExNo}\def\lx@itemlabel{\theExNo}% \else \refstepcounter{FnExNo}\def\lx@itemlabel{\theFnExNo}% \fi} \def\lx@exstart@normal{\lx@main@core\lx@mainlist\lx@scanjudge} \def\lx@mainitem{\lx@main@core\lx@scanjudge} % 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]{#1}} \def\lx@makeitem{\lx@kernel@item[\lx@itemlabel]\lx@emitjudge\ignorespaces} %% 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@subpush} \def\lx@dot@b.{\lx@subnext} \def\lx@dot@c.{\lx@subnext} \def\lx@dot@d.{\lx@subnext} \def\lx@dot@e.{\lx@subnext} \def\lx@dot@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}}} \def\lx@claim@shorthands{% \lx@claim@shorthand{z}{\lx@zpop}% \lx@claim@shorthand{exg}{\ex.\lx@glosshead}% \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} \def\lx@zpop@one{\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}} \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} \def\lx@subpush@i@go{% \begingroup\advance\lx@subdepth\@ne \lx@letters@local \lx@sublist@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{% \begingroup\advance\lx@subdepth\@ne \lx@letters@local \lx@sublist@ii \lx@subsubitem} \def\lx@subpush@err{% \PackageError{linguexx}{Only two sub-example levels are supported}% {Remove the third \string\a. level.}} \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.}} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% 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}} %% 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}{} {\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@subenv@begin \lx@inexetrue} {\lx@glt@langend \lx@closelist} \fi % iflx@gbfour \def\lx@subitem@core{% \refstepcounter{SubExNo}% \def\lx@itemlabel{\SubExLBr\Exalph{SubExNo}\SubExRBr}} \def\lx@subitem{\lx@subitem@core\lx@scanjudge} \def\lx@subsubitem@core{% \refstepcounter{SubSubExNo}% \def\lx@itemlabel{\SubSubExLBr\Exroman{SubSubExNo}\SubSubExRBr}} \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} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% Relative references: \Next \Last \NNext \LLast \TextNext %%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \newcommand\lx@fmtEx[1]{\theExLBr#1\theExRBr} \newcommand\lx@fmtFnEx[1]{\theFnExLBr#1\theFnExRBr} \newcommand\Last{{\if@noftnote \lx@fmtEx{\arabic{ExNo}}% \else\ifnum\value{FnExNo}>\z@ \lx@fmtFnEx{\roman{FnExNo}}% \else \lx@fmtEx{\arabic{ExNo}}% \fi\fi}} \newcommand\Next{{\if@noftnote \lx@fmtEx{\the\numexpr\value{ExNo}+1\relax}% \else \lx@fmtFnEx{\romannumeral\numexpr\value{FnExNo}+1\relax}% \fi}} \newcommand\NNext{{\if@noftnote \lx@fmtEx{\the\numexpr\value{ExNo}+2\relax}% \else \lx@fmtFnEx{\romannumeral\numexpr\value{FnExNo}+2\relax}% \fi}} \newcommand\LLast{{\if@noftnote \lx@fmtEx{\the\numexpr\value{ExNo}-1\relax}% \else\ifnum\value{FnExNo}>\@ne \lx@fmtFnEx{\romannumeral\numexpr\value{FnExNo}-1\relax}% \else \lx@fmtEx{\arabic{ExNo}}% \fi\fi}} \newcommand\TextNext{{\lx@fmtEx{\the\numexpr\value{ExNo}+1\relax}}} % parenthesis-free twins \newcommand\pref[1]{{\parenstrue\ref{#1}}} \newcommand\pLast{{\parenstrue\Last}} \newcommand\pNext{{\parenstrue\Next}} \newcommand\pNNext{{\parenstrue\NNext}} \newcommand\pLLast{{\parenstrue\LLast}} \newcommand\pTextNext{{\parenstrue\TextNext}} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% 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. %% 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 } % 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 \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 , } %% 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 } { \prop_gput:Nnn \g_lx_lpzg_prop {#1} {#2} \seq_if_in:NeF \g__lx_lpzg_declared_seq { \tl_to_str:n {#1} } { \seq_gput_right:Ne \g__lx_lpzg_declared_seq { \tl_to_str:n {#1} } } } %% \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} } } } \NewDocumentCommand \lpzg { m } { \lx@lpzg@build {#1} \bool_if:NTF \l__lx_lpzg_any_bool { \lx@tag@span@exp:nn { tag = Span , E = { \l__lx_lpzg_exp_tl } } { \textsc {#1} } } { \textsc {#1} } } %% \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 normalised to a STRING before it is stored or compared. %% It has to be: a label reaches \lpzg as letters, but the piece peeled %% off a compound one comes out of a str variable (catcode 12), and the %% keys read back from the .aux are letters again -- three spellings of %% "sg" that a seq would take for three different entries. \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 \str_new:N \l__lx_lpzg_key_str \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 { \tl_to_str: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:Nn \l__lx_lpzg_key_str {#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:Nn \l__lx_lpzg_key_str {#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:Nn \l__lx_lpzg_key_str {##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: } } } } %% One entry, expansion looked up (empty for an unknown key kept on request). \cs_new_protected:Npn \lx@lpzglist@item #1 { \prop_get:NnNTF \g_lx_lpzg_prop {#1} \l__lx_lpzg_tl { \exp_args:NnV \lpzglistentry {#1} \l__lx_lpzg_tl } { \lpzglistentry {#1} { } } } %% 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. \cs_new_protected:Npn \lx@lpzglist@widest { \dim_zero:N \l__lx_lpzglist_wd_dim \seq_map_inline:Nn \l__lx_lpzglist_seq { \hbox_set:Nn \l__lx_lpzglist_box { \textsc {##1} } \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 { \tl_to_str: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 STRINGS (catcode 12); the ignore list %% arrives as letters, 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 { \tl_to_str: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 characters that count %% (default: the judgment marks * ? # % and the openers ( [ < ). \altg %% alternative columns are not covered. \bool_new:N \l__lx_gl_phantom_bool \iflx@phantomalign \bool_set_true:N \l__lx_gl_phantom_bool \fi \str_new:N \l__lx_gl_phantomchars_str % The set must hold BARE "#" and "%": writing them as \#\% in \str_set:Nn % would stringify to "\#\%" and leave literal backslashes in the set, so any % object word beginning with a control sequence would then match "\" and be % padded. \c_hash_str / \c_percent_str are the catcode-12 characters. \str_set:Ne \l__lx_gl_phantomchars_str { *?([< \c_hash_str \c_percent_str } \tl_new:N \l__lx_gl_prefix_tl \tl_new:N \l__lx_gl_obj_tl \str_new:N \l__lx_gl_word_str \str_new:N \l__lx_gl_head_str \prg_generate_conditional_variant:Nnn \str_if_in:Nn { NV } { T } \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 } \cs_new_protected:Npn \GlossPhantomChars #1 { \str_set:Nn \l__lx_gl_phantomchars_str {#1} } % 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 phantom characters off word #1 into % \l__lx_gl_prefix_tl (as a str; catcode-12 punctuation, safe in \phantom). \cs_new_protected:Npn \__lx_gl_leadprefix:N #1 { \tl_clear:N \l__lx_gl_prefix_tl \str_set:Nx \l__lx_gl_word_str { \tl_to_str:N #1 } \__lx_gl_leadprefix_loop: } \cs_new_protected:Npn \__lx_gl_leadprefix_loop: { \str_if_empty:NF \l__lx_gl_word_str { \str_set:Nx \l__lx_gl_head_str { \str_head:N \l__lx_gl_word_str } \str_if_in:NVT \l__lx_gl_phantomchars_str \l__lx_gl_head_str { \tl_put_right:NV \l__lx_gl_prefix_tl \l__lx_gl_head_str \str_set:Nx \l__lx_gl_word_str { \str_tail:N \l__lx_gl_word_str } \__lx_gl_leadprefix_loop: } } } \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 \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 } \__lx_split:cn { l__lx_gl_tier_ \int_use:N \l__lx_gl_ntiers_int _seq } {##1} \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 } } } } \leavevmode \group_begin: \raggedright \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 characters, 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. f-expansion evaluates \seq_item only. \exp_args:NNf \tl_set:Nn \l__lx_gl_obj_tl { \seq_item:cn { 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 } \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. } } \hskip \GlossSep \relax } \unskip \par \group_end: } \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). \long\def\gll#1\\#2\\{\lx@gloss@multi{#1\\#2}} \long\def\glll#1\\#2\\#3\\{\lx@gloss@multi{#1\\#2\\#3}} \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 } } \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{% \par\nobreak \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. \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 \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 } { \int_compare:nNnTF { \l__lxp_alt_n_int } = { 2 } { \tl_gput_right:Nn \g__lxp_alt_build_tl { ~or~ } } { \tl_gput_right:Nn \g__lxp_alt_build_tl { ,~or~ } } } { \tl_gput_right:Nn \g__lxp_alt_build_tl { ,~ } } } \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. \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 { \\ } } \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. \cs_new_protected:Npn \__lxp_brace:nnn #1#2#3 { \begin { tikzpicture } [ baseline = 0pt ] \useasboundingbox ( 0pt , #2 ) rectangle ( \AltBraceWidth , #3 ) ; \str_if_eq:nnTF {#1} { L } { \draw [ decorate , decoration = { brace , amplitude = \AltBraceAmplitude } ] ( \dim_eval:n { \AltBraceWidth - 1pt } , #2 ) -- ( \dim_eval:n { \AltBraceWidth - 1pt } , #3 ) ; } { \draw [ decorate , decoration = { brace , amplitude = \AltBraceAmplitude } ] ( 1pt , #3 ) -- ( 1pt , #2 ) ; } \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: } %% 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_setstack:NNnn \l__lxp_alt_box \l__lxp_alt_seq { \str_case:VnF \l__lxp_alt_align_tl { {l}{l} {r}{r} } {c} } { } \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 printed object (brace + stack + brace), no tagging. The outer %% gap (\AltBraceOuterSep) clears the decoration's outward bulge from %% whatever precedes/follows; the inner gap (\AltBraceSep) just %% separates the brace from the stack it braces. \cs_new_protected:Npn \__lxp_alt_emit: { \leavevmode \hspace{\AltBraceOuterSep} \__lxp_alt_brace:n { L } \hspace{\AltBraceSep} \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 } \hspace{\AltBraceSep} \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 TikZ brace decoration % bulges outward past its own bounding box (the curl reaches further % than the box declared for it) only on the outer side -- the inner % side has no such overflow to clear, so it can sit much closer. \newcommand\AltBraceWidth{0.9ex} \newcommand\AltBraceAmplitude{4pt} \newcommand\AltBraceSep{0.15em} \newcommand\AltBraceOuterSep{0.7em} \newcommand\AltBraceRaise{0pt} % Alias \altn -> \lxAltn unless somebody already owns \altn. \AtBeginDocument{% \@ifundefined{altn}% {\global\let\altn\lxAltn}% {\PackageInfo{linguexx}% {\string\altn\space is already defined;\MessageBreak only \string\lxAltn\space is available}}} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% 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 \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 \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. \cs_new_protected:Npn \__lxp_altg_setstack:n #1 { \__lxp_setstack:NNnn \l__lxp_altg_stack_box \l__lxp_altg_seq { l } { #1 \renewcommand \lpzg [1] { \lx@lpzg@build {##1} \textsc {##1} } } \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 } } %% 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 } \hspace { \AltBraceSep } \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 } } \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 } \hspace { \AltBraceSep } \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:n { } \__lxp_altg_emit:n { \__lxp_altg_shape_obj: } } { % 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:n { \AltgTransFont } \__lxp_altg_emit:n { \__lxp_altg_shape_gloss: } } } { % solo, outside a gloss \__lxp_altg_setstack:n { } \__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. \AtBeginDocument{% \@ifundefined{altg}% {\global\let\altg\lxAltg}% {\PackageInfo{linguexx}% {\string\altg\space is already defined;\MessageBreak only \string\lxAltg\space is available}}} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% Explicit judgment markers: \jdg %%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \newlength{\JdgSep}% value set in the defaults block (Layout section) %% \jdg[]{} hangs in the margin. With an explicit %% (or a known default for ), 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=]{\cmd}{} names a mark. The %% spoken phrase, if given, is both attached to \cmd and registered for %% so a leading scanned 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} {}:~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{}{} 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})} \newcommand\Refrange[2]{\ref{#1}\rangedash\ref{#2}} %% 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@@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}} %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% 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 %%%% EOF