%% This file is luabidi.sty %% This is part of the luabidi package %% %% Copyright © 2009 Vafa Khalighi, 2013, 2019--2026 Arthur Reutenauer, %% 2019--2026 Jürgen Spitzmüller, 2026 Udi Fogiel %% %%%% It may be distributed and/or modified under the %% conditions of the LaTeX Project Public License, either version 1.3c %% of this license or (at your option) any later version. \NeedsTeXFormat{LaTeX2e} \ProvidesPackage{luabidi}[2026/04/10 v1.0 Bidirectional typesetting in LuaTeX] % % General command and switches % \newif\if@RTL \newif\if@RTLmain \protected\def\setRTLmain{% \@RTLmaintrue \pagedirection \@ne \chardef\luabidi@currvlist@dir \@ne \setRTL } % If the direction of the current vertical list % (\luabidi@currvlist@dir) is different from \pardir, then % \shapemode needs to be set to mirror \parshape in lists % and hangindent in @hangfrom. A value of 3 mirrors both; % -3 is persistent between paragraphs. \chardef\luabidi@currvlist@dir\z@ \protected\def\luabidi@update@shapemode{% \ifnum\luabidi@currvlist@dir=\pardirection \shapemode \z@ \else \shapemode=-3\relax \fi } \let\luabidi@everyvbox\everyvbox \newtoks\everyvbox \everyvbox\expandafter{\the\luabidi@everyvbox} \luabidi@everyvbox{% \chardef\luabidi@currvlist@dir\bodydirection \luabidi@update@shapemode \the\everyvbox} % Most direction registers only serve internally as registers that are read % in different parts of LuaTeX's code, but \textdirection/\linedirection has two % additional effects: it updates a special direction stack (a stack that is used % to respect grouping and to correct the direction in line breaks), and if used % in horizontal mode it also creates dir nodes. To keep the stack simple and % avoid creating unnecessary nodes, we should probably update \textdirection only % when needed. \def\luabidi@setdir#1#2{\ifnum #1=#2\else #1=#2\fi} \protected\def\setRTL{% \@RTLtrue \bodydirection \@ne \pardirection \@ne \luabidi@setdir \textdirection \@ne \luabidi@update@shapemode } \protected\def\setLTR{% \@RTLfalse \bodydirection \z@ \pardirection \z@ \luabidi@setdir \textdirection \z@ \luabidi@update@shapemode } \protected\def\RTL{\par\setRTL} \protected\def\endRTL{\par} \protected\def\LTR{\par\setLTR} \protected\def\endLTR{\par} % \LRE/\RLE are defined this way to avoid reading the text % as an argument, so that category code changes can be made inside % (to use verbatim, for example). Similarly to how plain TeX defines \footnote. \let\n@xt=\ \protected\def\LRE{\afterassignment\luabidi@LRE \let\n@xt} \protected\def\RLE{\afterassignment\luabidi@RLE \let\n@xt} \def\luabidi@bracetext#1{\ifx\n@xt\bgroup\else \PackageError{luabidi}{Missing left brace after \string#1 has been substituted}{}\fi\bgroup} \def\luabidi@LRE{\leavevmode\luabidi@bracetext\LRE\@RTLfalse\luabidi@setdir\linedirection\z@} \def\luabidi@RLE{\leavevmode\luabidi@bracetext\RLE\@RTLtrue\luabidi@setdir\linedirection\@ne} \protected\def\hboxR#1#{\hbox bdir\@ne #1\bgroup\@RTLtrue\let\n@xt} \protected\def\vboxR#1#{\vbox bdir\@ne #1\bgroup\@RTLtrue\let\n@xt} \protected\def\vtopR#1#{\vtop bdir\@ne #1\bgroup\@RTLtrue\let\n@xt} \protected\def\hboxL#1#{\hbox bdir\z@ #1\bgroup\@RTLfalse\let\n@xt} \protected\def\vboxL#1#{\vbox bdir\z@ #1\bgroup\@RTLfalse\let\n@xt} \protected\def\vtopL#1#{\vtop bdir\z@ #1\bgroup\@RTLfalse\let\n@xt} % Aliases \let\setRL=\setRTL \let\setLR=\setLTR \let\unsetRTL=\setLTR \let\unsetLTR=\setRTL \let\LR=\LRE \let\RL=\RLE % % by default \leqno and \eqno are influenced by \predisplaydirection, which is set to -1 if % \pardir and \mathdir differ at the start of display, so to make sure \eqno is on the right % we need to test \predisplaydirection (remove if will be in kernel https://github.com/latex3/latex2e/issues/1956) % \let\luabidi@eqno=\@kernel@eqno \let\luabidi@leqno=\@kernel@leqno \protected\def\@kernel@eqno{\ifnum\predisplaydirection<\z@ \luabidi@leqno \else \luabidi@eqno \fi} \protected\def\@kernel@leqno{\ifnum\predisplaydirection<\z@ \luabidi@eqno \else \luabidi@leqno \fi} % % The following registers are set to 1 to fix some bugs in the engine. See sections 3.3.3 and 7.5.3 of LuaTeX's manual. % \matheqdirmode \@ne \breakafterdirmode \@ne \mathemptydisplaymode \@ne \fixupboxesmode \@ne % % A helper macro for patching % \newcatcodetable\luabidi@cctab \ExplSyntaxOn \savecatcodetable\luabidi@cctab \ExplSyntaxOff \def\luabidi@replmacro #1{\expandafter\luabidi@replmacroA\expandafter#1\meaning#1\relax} \def\luabidi@replmacroA #1#2:#3->#4\relax#5#6{% \expandafter\luabidi@replmacroB\expandafter#1\directlua{ local prefixes="\luaescapestring{#2}" local params="\luaescapestring{#3}" local body="\luaescapestring{#4}" local find = "\luaescapestring{\unexpanded{#5}}" local replace = "\luaescapestring{\unexpanded{#6}}" prefixes = string.gsub(prefixes,"macro","") body = string.gsub(body, find, replace, 1) tex.sprint(\the\luabidi@cctab, "{"..prefixes.."}{"..params.."}{"..body.."}") }% } \long\def\luabidi@replmacroB #1#2#3#4{\@firstofone#2\def#1#3{#4}} % % patches to math, tabulars and graphics % \luabidi@replmacro\equation\hb@xt@{\hbox bdir\mathdirection to} \ExpandArgs{c}\luabidi@replmacro{[ }\hb@xt@{\hbox bdir\mathdirection to} \AddToHook{package/amstext/after}{\luabidi@replmacro\textdef@\hbox{\hbox bdir\if@RTL\@ne\else\z@\fi}} \AddToHook{package/amsmath/after}{% \luabidi@replmacro\intertext@\vbox{\vbox bdir\if@RTL\@ne\else\z@\fi} \AddToHook{env/align*/end}{\iftag@\else\tag*{}\fi}% \AddToHook{env/alignat*/end}{\iftag@\else\tag*{}\fi}% } \AtBeginDocument{ \luabidi@replmacro\@tabular{\hbox\bgroup} {\hbox\bgroup\etokspre\everyhbox{% \everyhbox{\the\everyhbox}% \everyvbox{\the\everyvbox}% \mathdirection=\the\mathdirection}% \etokspre\everyvbox{% \everyhbox{\the\everyhbox}% \everyvbox{\the\everyvbox}% \mathdirection=\the\mathdirection}% \mathdirection=\if@RTL\@ne\else\z@\fi}% } \ExplSyntaxOn \AddToHook{package/tabularray/after}{% \luabidi@replmacro \__tblr_get_vcenter_box:N {\tex_vcenter:D} {\bodydirection=\if@RTL\@ne\else\z@\fi\tex_vcenter:D} } \ExplSyntaxOff \ExpandArgs{c}\luabidi@replmacro{underline }\hbox{\hbox bdir\if@RTL\@ne\else\z@\fi} \AddToHook{package/graphics/after}{% \luabidi@replmacro\rotatebox{\setbox\z@\hbox{{#2}}} {\setbox\z@\hbox bdir0{\hbox bdir\if@RTL\@ne\else\z@\fi{#2}}}% } \AddToHook{package/graphicx/after}{% \luabidi@replmacro\Grot@box@std{\setbox\z@\hbox{{#2}}} {\setbox\z@\hbox bdir0{\hbox bdir\if@RTL\@ne\else\z@\fi{#2}}}% \luabidi@replmacro\Grot@box@kv{\@begin@tempboxa\hbox{#3}} {\@begin@tempboxa{\hbox bdir0}{\hbox bdir\if@RTL\@ne\else\z@\fi{#3}}}% \luabidi@replmacro\Grot@box{\setbox\z@\hbox}{\setbox\z@\hbox bdir0} } \AtBeginDocument{% \ifdefined\pgfpicture \luabidi@replmacro\pgfinterruptpicture${\if@RTL\setRTL\fi}% \luabidi@replmacro\pgfsys@typesetpicturebox\hbox{\hbox bdir\z@}% \luabidi@replmacro\pgf@picture\hbox{\hbox bdir\z@}% \fi } % % Footnotes % % These are defined in polyglossia. Provide simple fallbacks \ProvideDocumentCommand\localnumeral{s}{\IfBooleanTF{#1}\@@localnumeral\@localnumeral} \providecommand*\@localnumeral[1]{#1} \providecommand*\@@localnumeral[1]{\ExpandArgs{c}\@arabic{c@#1}} \let\footnotemarkLR\footnotemark \let\footnotemarkRL\footnotemark \DeclareDocumentCommand\LTRfootnote{o+m}{% \begingroup \IfNoValueTF{#1}% {\footnotemarkLR \renewcommand\thefootnote{\localnumeral*{footnote}}}% {\footnotemarkLR[#1]% \renewcommand\thefootnote{\localnumeral{#1}}}% \setLTR\footnotetext{#2}% \endgroup } \DeclareDocumentCommand\RTLfootnote{o+m}{% \begingroup \IfNoValueTF{#1}% {\footnotemarkRL \renewcommand\thefootnote{\localnumeral*{footnote}}}% {\footnotemarkRL[#1]% \renewcommand\thefootnote{\localnumeral{#1}}}% \setRTL\footnotetext{#2}% \endgroup } % backwards compatibility \let\Footnote\LTRfootnote % % Footnote rules % % adjustable rule length \newlength\footnoterulewidth \setlength\footnoterulewidth{.4\columnwidth} % The left, right and full width rules \chardef\luabidi@fnr@dir\z@ \def\luabidi@fnr{% \hbox bdir\luabidi@fnr@dir to \columnwidth {\vbox{\kern -3\p@ \hrule\@width\footnoterulewidth\kern2.6\p@}\hfil}} \def\luabidi@textwidth@fnr{% \kern-3\p@\hrule\@width\textwidth\kern2.6\p@} % auto rule % this simply traverse the node list of the footnote box, % and checks the direction of the first line. % TODO what if the lines are inside a box? \directlua{ local line_sub = table.swapped(node.subtypes("hlist")).line local luafnalloc = luatexbase.new_luafunction('luabidi@auto@fnr') token.set_lua('luabidi@auto@fnr', luafnalloc) lua.get_functions_table()[luafnalloc] = function() local fnbox = tex.box["footins"] for n, sb in node.traverse_id(node.id('hlist'),fnbox.list) do if sb == line_sub then token.set_char("luabidi@fnr@dir",n.direction) return token.unchecked_put_next({token.create("luabidi@fnr")}) end end end } % switches for the rule position \def\leftfootnoterule{\glet\footnoterule\luabidi@fnr\chardef\luabidi@fnr@dir\z@} \def\rightfootnoterule{\glet\footnoterule\luabidi@fnr\chardef\luabidi@fnr@dir\@ne} \def\textwidthfootnoterule{\glet\footnoterule\luabidi@textwidth@fnr} \def\autofootnoterule{\glet\footnoterule\luabidi@auto@fnr} % % Package options % \DeclareOption{arabmaths}{\mathdirection=\@ne} \DeclareOption{autofootnoterule}{\autofootnoterule} \DeclareOption{textwidthfootnoterule}{\textwidthfootnoterule} \ProcessOptions % % output routine % % When a page box is shipped, the box direction most likely, % needs to have the direction of \pagedirection, % otherwise most of the text will be outside of the media box (see the documentation). % Since the box is created in vertical mode, and its direction is not specified, % the direction is determined by \bodydirection. Also a horizontal box containing % several columns should probably have that direction, so that the columns will be % in the correct order. If there is a column break in the middle of a paragraph of a RTL % text, the value of \bodydirection will be 1 in the output routine, so we need % to set the correct value there as it might be wrong. \directlua{ local runtoks, putnext, currbodydir = tex.runtoks, token.unchecked_put_next local zero, one = token.create("z@"), token.create("@ne") local bodydir, pagedir = token.create("bodydir"), token.create("pagedir") local bodydirection = token.create("bodydirection") luatexbase.add_to_callback("pre_output_filter", function() currbodydir = tex.bodydirection runtoks(function() putnext({bodydir,pagedir}) end) return true end, "luabidi.pre_output") luatexbase.add_to_callback("buildpage_filter", function(info) if info \string~= "after_output" then return true end runtoks(function() local num = currbodydir == 0 and zero or one putnext({bodydirection,num}) end) return true end, "luabidi.post_output") } % % workaround LuaTeX bug (see https://mailman.ntg.nl/archives/list/dev-luatex@ntg.nl/thread/2PZX3W7322ZZZM4YB3WZVTO5EF4YKXHY/) % \directlua{ luatexbase.add_to_callback("pre_linebreak_filter", function(head) local stack = 0 for n, sb in node.traverse_id(node.id('dir'),head) do stack = stack + 1-2*sb if stack == -1 then head, n = node.remove(head,n) return head end end return head end, "luabidi.remove_spurious_enddir") } \endinput