diff options
author | Karl Berry <karl@freefriends.org> | 2022-04-19 20:10:27 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2022-04-19 20:10:27 +0000 |
commit | b12479292b3e2468c564619ee6b60dbc13eeb1be (patch) | |
tree | 01f444779592389c9650c7cfecfcd301ca6198db /Master | |
parent | 3aef115c3b639b7f73a9866cda39c98e9bc8649c (diff) |
luamathalign (19apr22)
git-svn-id: svn://tug.org/texlive/trunk@63081 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master')
-rw-r--r-- | Master/texmf-dist/doc/lualatex/luamathalign/README.md | 29 | ||||
-rw-r--r-- | Master/texmf-dist/doc/lualatex/luamathalign/luamathalign.pdf | bin | 0 -> 391838 bytes | |||
-rw-r--r-- | Master/texmf-dist/source/lualatex/luamathalign/luamathalign.dtx | 655 | ||||
-rw-r--r-- | Master/texmf-dist/tex/lualatex/luamathalign/build.lua | 21 | ||||
-rw-r--r-- | Master/texmf-dist/tex/lualatex/luamathalign/luamathalign-luacmd.lua | 19 | ||||
-rw-r--r-- | Master/texmf-dist/tex/lualatex/luamathalign/luamathalign-luaprop.lua | 42 | ||||
-rw-r--r-- | Master/texmf-dist/tex/lualatex/luamathalign/luamathalign.lua | 375 | ||||
-rw-r--r-- | Master/texmf-dist/tex/lualatex/luamathalign/luamathalign.sty | 98 | ||||
-rwxr-xr-x | Master/tlpkg/bin/tlpkg-ctan-check | 2 | ||||
-rwxr-xr-x | Master/tlpkg/libexec/ctan2tds | 2 | ||||
-rw-r--r-- | Master/tlpkg/tlpsrc/collection-luatex.tlpsrc | 1 | ||||
-rw-r--r-- | Master/tlpkg/tlpsrc/luamathalign.tlpsrc | 0 |
12 files changed, 1243 insertions, 1 deletions
diff --git a/Master/texmf-dist/doc/lualatex/luamathalign/README.md b/Master/texmf-dist/doc/lualatex/luamathalign/README.md new file mode 100644 index 00000000000..0eb7c4d06f7 --- /dev/null +++ b/Master/texmf-dist/doc/lualatex/luamathalign/README.md @@ -0,0 +1,29 @@ +# The `luamathalign` package for LuaLaTeX + +With `amsmath`'s `align` environment simple equations can be aligned easily, e.g. + +```tex +2 &=1+1\\ +2-1&=1 +``` + +but even just adding an underline makes this much more complicated. E.g. + +```tex +2 &=1+1\\ +\underline{2-1&=1} +``` + +is not allowed. Similarly alignment points surrounded by `\left` and `\right` +or points nested in other environments do not work. + +For LuaLaTeX this can be solved with luamathalign. It adds a command `\AlignHere` +which behaves mustly like `&` except that it's allowed almost anywhere. E.g. + +``` + 2 &= 1+1\\ +\underline{2-1 \AlignHere= 1} +``` + + +Licensed under the LPPL v1.3c or later. diff --git a/Master/texmf-dist/doc/lualatex/luamathalign/luamathalign.pdf b/Master/texmf-dist/doc/lualatex/luamathalign/luamathalign.pdf Binary files differnew file mode 100644 index 00000000000..d946d148943 --- /dev/null +++ b/Master/texmf-dist/doc/lualatex/luamathalign/luamathalign.pdf diff --git a/Master/texmf-dist/source/lualatex/luamathalign/luamathalign.dtx b/Master/texmf-dist/source/lualatex/luamathalign/luamathalign.dtx new file mode 100644 index 00000000000..401c8ef7eaf --- /dev/null +++ b/Master/texmf-dist/source/lualatex/luamathalign/luamathalign.dtx @@ -0,0 +1,655 @@ +% \iffalse meta-comment +% +%% Copyright (C) 2019--2022 by Marcel Krueger +%% +%% This file may be distributed and/or modified under the +%% conditions of the LaTeX Project Public License, either +%% version 1.3c of this license or (at your option) any later +%% version. The latest version of this license is in: +%% +%% http://www.latex-project.org/lppl.txt +%% +%% and version 1.3 or later is part of all distributions of +%% LaTeX version 2005/12/01 or later. +% +%<*batch> +%<*gobble> +\ifx\jobname\relax\let\documentclass\undefined\fi +\ifx\documentclass\undefined +\csname fi\endcsname +%</gobble> +\input l3docstrip.tex +\keepsilent +\preamble +\endpreamble +\generate{\file{luamathalign.sty}{\from{luamathalign.dtx}{package}}} +\begingroup +\def\MetaPrefix{--} +\preamble +\endpreamble +\postamble +\endpostamble +\generate{\file{luamathalign.lua}{\from{luamathalign.dtx}{lua}}} +\endgroup +\endbatchfile +%</batch> +%<*gobble> +\fi +\expandafter\ifx\csname @currname\endcsname\empty +\csname fi\endcsname +%</gobble> +%<*driver> +\documentclass[full]{l3doc} +\usepackage{luamathalign} +\usepackage{csquotes} +\MakeShortVerb{\|} +\setlength\IndexMin{100pt} +\begin{document} +\DocInput{luamathalign.dtx} +\PrintIndex +\PrintChanges +\end{document} +%</driver> +%<*gobble> +\fi +%</gobble> +% \fi +% +% \GetFileInfo{luamathalign.dtx} +% \title{The \pkg{luamathalign} package\thanks{This document +% corresponds to \pkg{luamathalign}~v0.1, dated~2022-04-18.}} +% \author{Marcel Kr\"uger \\ \href{mailto:tex@2krueger.de}{tex@2krueger.de}} +% +% \maketitle +% +% \begin{documentation} +% \section{The problem} +% In most cases, |amsmath| makes it simple to align multiple equations in a +% |align| environment. But sometimes, special requirements come up. +% +% Maybe one of your alignment points is in an exponent, or in a radical? +% The first attempts for such alignments often fail. For example, assume that +% you want to align the following radicals like this (at the $x^3$ term): +% \begin{align*} +% \sqrt{1-3x+3x^2+(\AlignHere x-1)^3}\\ +% =\sqrt{1-3x+3x^2+{\AlignHere x}^3-3x^2+3x-1}\\ +% =\sqrt{\AlignHere x^3} +% \end{align*} +% \enquote{Just adding \texttt{\&} at the alignment points} doesn't work: +% \begin{verbatim} +% \begin{align*} +% \sqrt{1-3x+3x^2+(&x-1)^3}\\ +% =\sqrt{1-3x+3x^2+&x^3-3x^2+3x-1}\\ +% =\sqrt{&x^3} +% \end{align*} +% \end{verbatim} +% fails with +% \begin{verbatim} +% ! Missing } inserted. +% <inserted text> +% } +% l.73 \end{align*} +% \end{verbatim} +% +% Another problem are nested alignments. Take this sample from \href{https://tex.stackexchange.com/questions/68547/alignment-across-nested-aligned-environments}{anonymous on \TeX\ -- \LaTeX\ StackExchange}: We want alignment like +% \begin{align*} +% aaaa &= 1 &&\text{for $X$} \\ +% bbbb &= 1 &&\text{for $Y$} \\ +% \left. \begin{aligned} +% c \SetAlignmentPoint1 &= 1 \\ +% d &= 12 +% \end{aligned} \right\}\ExecuteAlignment1 &&\text{for $Z$} +% \end{align*} +% but in +% \begin{verbatim} +% \begin{align*} +% aaaa &= 1 &&\text{for $X$} \\ +% bbbb &= 1 &&\text{for $Y$} \\ +% \left. \begin{aligned} +% c &= 1 \\ +% d &= 12 +% \end{aligned} \right\}&&&\text{for $Z$} +% \end{align*} +% \end{verbatim} +% there is not obvious way to align the equal signs in the nested |aligned| with the outer signs. +% \section{The solution} +% \pkg{luamathalign} provides solutions for both problems under Lua\LaTeX: +% \begin{function}{\AlignHere} +% The most important new macro is \cmd\AlignHere: +% It generates an alignment point like \texttt{\&}, +% but it can be used almost everywhere. +% +% So problems like our first example can be implemented by just using \cmd\AlignHere\ instead of \texttt{\&}: +% \begin{verbatim} +% \begin{align*} +% \sqrt{1-3x+3x^2+(\AlignHere x-1)^3}\\ +% =\sqrt{1-3x+3x^2+{\AlignHere x}^3-3x^2+3x-1}\\ +% =\sqrt{\AlignHere x^3} +% \end{align*} +% \end{verbatim} +% \begin{align*} +% \sqrt{1-3x+3x^2+(\AlignHere x-1)^3}\\ +% =\sqrt{1-3x+3x^2+{\AlignHere x}^3-3x^2+3x-1}\\ +% =\sqrt{\AlignHere x^3} +% \end{align*} +% \end{function} +% Sadly, this doesn't really help with the nested alignment problem: +% Even if we use \cmd\AlignHere\ in the \env{aligned} environment, the alignment points +% would be inserted in the inner and not in the outer alignment. +% For such cases, there is a variant which allows to specify at which level the alignment should happen: +% \begin{function}{\SetAlignmentPoint,\ExecuteAlignment} +% The primary command for this is \cmd\SetAlignmentPoint\meta{number}. When called with a negative number it specifies the nesting level. +% For example when \meta{number} is -1 it is the same as \cmd\AlignHere, while for -2 it is aligning one level higher and so on. +% +% For example, our nested alignment above wanted to align the inner \env{aligned} and the outer \env{align*} at the same point, +% so |\SetAlignmentPoint-2| is used directly next to a inner alignment point (here |&|, \cmd\AlignHere\ would work too). +% Then the \cmd\ExecuteAlignment\ has to appear in the context of the outer \env{align*}, so it can be written e.g. directly before the next |&| of the outer \env{align*}: +% \begin{verbatim} +% \begin{align*} +% aaaa &= 1 &&\text{for $X$} \\ +% bbbb &= 1 &&\text{for $Y$} \\ +% \left. \begin{aligned} +% c \SetAlignmentPoint-2 &= 1 \\ +% d &= 12 +% \end{aligned} \right\}&&\text{for $Z$} +% \end{align*} +% \end{verbatim} +% \begin{align*} +% aaaa &= 1 &&\text{for $X$} \\ +% bbbb &= 1 &&\text{for $Y$} \\ +% \left. \begin{aligned} +% c \SetAlignmentPoint-2 &= 1 \\ +% d &= 12 +% \end{aligned} \right\}&&\text{for $Z$} +% \end{align*} +% +% If you do not want to keep track of the right nesting level you can explicitly mark a level and refer to it. +% To do so, use a non-negative \meta{number}. +% When \cmd\SetAlignmentPoint\ is used with a non-negative \meta{number} then \cmd\ExecuteAlignment\meta{number} +% must be executed afterwards with the same \meta{number} at a point where adding a |&| would add a valid alignment point at the right level. +% +% Our example above could therefore also be written as +% \begin{verbatim} +% \begin{align*} +% aaaa &= 1 &&\text{for $X$} \\ +% bbbb &= 1 &&\text{for $Y$} \\ +% \left. \begin{aligned} +% c \SetAlignmentPoint0 &= 1 \\ +% d &= 12 +% \end{aligned} \right\}\ExecuteAlignment0 &&\text{for $Z$} +% \end{align*} +% \end{verbatim} +% \begin{align*} +% aaaa &= 1 &&\text{for $X$} \\ +% bbbb &= 1 &&\text{for $Y$} \\ +% \left. \begin{aligned} +% c \SetAlignmentPoint0 &= 1 \\ +% d &= 12 +% \end{aligned} \right\}\ExecuteAlignment0 &&\text{for $Z$} +% \end{align*} +% +% This variant is also useful when working with custom alignment environment not prepared to work with luamathalign. +% By default \cmd\SetAlignmentPoint\meta{number} with negative numbers (and therefore also \cmd\AlignHere) only work with +% amsmath's \texttt{\{align\}}, \texttt{\{aligned\}} and their variants. +% If you have another environment which also follows similar alignment rules then you can either restrict yourself to +% non-negative \meta{number}s in combination with \cmd\ExecuteAlignment\ or patch these environments similar to what luamathalign does for amsmath. +% \end{function} +% \end{documentation} +% \begin{implementation} +% \section{The implementation} +% \subsection{Lua} +% \iffalse +%<*gobble> +\RequirePackage{docstrip-luacode} +\begin{docstrip-luacode}{luamathalign} +%</gobble> +%<*lua> +% \fi +% \begin{macrocode} +local luacmd = require'luamathalign-luacmd' +local luaprop = require'luamathalign-luaprop'('mathalign') +local hlist = node.id'hlist' +local vlist = node.id'vlist' +local whatsit = node.id'whatsit' +local kern = node.id'kern' +local user_defined = node.subtype'user_defined' +local whatsit_id = luatexbase.new_whatsit'mathalign' +local node_cmd = token.command_id'node' +local ampersand = token.new(38, 4) +-- We might want to add y later +local function is_marked(mark, list) + for n in node.traverse(list) do + local id = n.id + if id == hlist or id == vlist then + if is_marked(mark, n.head) then return true end + elseif id == whatsit and n.subtype == user_defined + and n.user_id == whatsit_id and n.value == mark then + return true + end + end + return false +end +local function assert_unmarked(mark, list, ...) + local marked = is_marked(mark, list) + if marked then + tex.error("Multiple alignment marks", "I found multiple alignment marks \z + of type " .. mark .. " in an alignment where I already had an \z + alignment mark of that type. You should look at both of them and \z + decide which one is right. I will continue with the first one for now.") + end + return ... +end +local measure do + local vmeasure + local function hmeasure(mark, list) + local x, last = 0, list.head + for n in node.traverse(last) do + local id = n.id + if id == hlist then + local w, h, d = node.rangedimensions(list, last, n) + x, last = x + w, n + local dx = hmeasure(mark, n) + if dx then return assert_unmarked(mark, n.next, dx + x) end + elseif id == vlist then + local w, h, d = node.rangedimensions(list, last, n) + x, last = x + w, n + local dx = vmeasure(mark, n) + if dx then return assert_unmarked(mark, n.next, dx + x) end + elseif id == whatsit and n.subtype == user_defined + and n.user_id == whatsit_id and n.value == mark then + local w, h, d = node.rangedimensions(list, last, n) + local after + list.head, after = node.remove(list.head, n) + return assert_unmarked(mark, after, x + w) + end + end + end + function vmeasure(mark, list) + for n in node.traverse(list.head) do + local id = n.id + if id == hlist then + local dx = hmeasure(mark, n) + if dx then return assert_unmarked(mark, n.next, dx + n.shift) end + elseif id == vlist then + local dx = vmeasure(mark, n) + if dx then return assert_unmarked(mark, n.next, dx + n.shift) end + elseif id == whatsit and n.subtype == user_defined + and n.user_id == whatsit_id and n.value == mark then + local after + list.head, after = node.remove(list.head, n) + return assert_unmarked(mark, after, 0) + end + end + end + function measure(mark, head) + local x, last = 0, head + for n in node.traverse(last) do + local id = n.id + if id == hlist then + local w, h, d = node.dimensions(last, n) + x, last = x + w, n + local dx = hmeasure(mark, n) + if dx then return assert_unmarked(mark, n.next, head, dx + x) end + elseif id == vlist then + local w, h, d = node.dimensions(last, n) + x, last = x + w, n + local dx = vmeasure(mark, n) + if dx then return assert_unmarked(mark, n.next, head, dx + x) end + elseif id == whatsit and n.subtype == user_defined + and n.user_id == whatsit_id and n.value == mark then + local w, h, d = node.dimensions(last, n) + local after + head, after = node.remove(head, n) + return assert_unmarked(mark, after, head, x + w) + end + end + return head + end +end + +local isolate do + local visolate + local function hisolate(list, offset) + local x, last = 0, list.head + local newhead, newtail = nil, nil + local n = last + while n do + local id = n.id + if id == hlist then + local w, h, d = node.rangedimensions(list, last, n) + x, last = x + w, n + local inner_head, inner_tail, new_offset = hisolate(n, offset - x) + if inner_head then + if newhead then + newtail.next, inner_head.prev = inner_head, newtail + else + newhead = inner_head + end + newtail = inner_tail + offset = x + new_offset + end + n = n.next + elseif id == vlist then + local w, h, d = node.rangedimensions(list, last, n) + x, last = x + w, n + local inner_head, inner_tail, new_offset = visolate(n, offset - x) + if inner_head then + if newhead then + newtail.next, inner_head.prev = inner_head, newtail + else + newhead = inner_head + end + newtail = inner_tail + offset = x + new_offset + end + n = n.next + elseif id == whatsit and n.subtype == user_defined + and n.user_id == whatsit_id then + local w, h, d = node.rangedimensions(list, last, n) + x = x + w + list.head, last = node.remove(list.head, n) + if x ~= offset then + local k = node.new(kern) + k.kern, offset = x - offset, x + newhead, newtail = node.insert_after(newhead, newtail, k) + end + newhead, newtail = node.insert_after(newhead, newtail, n) + n = last + else + n = n.next + end + end + return newhead, newtail, offset + end + function visolate(list, offset) + local newhead, newtail = nil, nil + local n = list.head + while n do + local id = n.id + if id == hlist then + if dx then return assert_unmarked(mark, n.next, dx + n.shift) end + local inner_head, inner_tail, new_offset = hisolate(n, offset) + if inner_head then + if newhead then + newtail.next, inner_head.prev = inner_head, newtail + else + newhead = inner_head + end + newtail = inner_tail + offset = new_offset + end + n = n.next + elseif id == vlist then + if dx then return assert_unmarked(mark, n.next, dx + n.shift) end + local inner_head, inner_tail, new_offset = visolate(n, offset) + if inner_head then + if newhead then + newtail.next, inner_head.prev = inner_head, newtail + else + newhead = inner_head + end + newtail = inner_tail + offset = new_offset + end + n = n.next + elseif id == whatsit and n.subtype == user_defined + and n.user_id == whatsit_id then + local after + list.head, after = node.remove(list.head, n) + if 0 ~= offset then + local k = node.new(kern) + k.kern, offset = -offset, 0 + newhead, newtail = node.insert_after(newhead, newtail, k) + end + newhead, newtail = node.insert_after(newhead, newtail, n) + n = last + else + n = n.next + end + end + return newhead, newtail, offset + end + function isolate(head) + local x, last = 0, head + local newhead, newtail, offset = nil, nil, 0 + local n = last + while n do + local id = n.id + if id == hlist then + local w, h, d = node.dimensions(last, n) + x, last = x + w, n + local inner_head, inner_tail, new_offset = hisolate(n, offset - x) + if inner_head then + if newhead then + newtail.next, inner_head.prev = inner_head, newtail + else + newhead = inner_head + end + newtail = inner_tail + offset = x + new_offset + end + n = n.next + elseif id == vlist then + local w, h, d = node.dimensions(last, n) + x, last = x + w, n + local inner_head, inner_tail, new_offset = visolate(n, offset - x) + if inner_head then + if newhead then + newtail.next, inner_head.prev = inner_head, newtail + else + newhead = inner_head + end + newtail = inner_tail + offset = x + new_offset + end + n = n.next + elseif id == whatsit and n.subtype == user_defined + and n.user_id == whatsit_id then + local w, h, d = node.dimensions(last, n) + x = x + w + head, last = node.remove(head, n) + if x ~= offset then + local k = node.new(kern) + k.kern, offset = x - offset, x + newhead, newtail = node.insert_after(newhead, newtail, k) + end + newhead, newtail = node.insert_after(newhead, newtail, n) + n = last + else + n = n.next + end + end + return head, newhead + end +end + +local mark, afterkern +luatexbase.add_to_callback('post_mlist_to_hlist_filter', function(n) + if mark then + local x + n, x = measure(mark, n) + local k = node.new'kern' + local off = x - n.width + k.kern, afterkern.kern = off, -off + node.insert_after(n.head, nil, k) + n.width = x + mark, afterkern = nil, nil + end + return n +end, 'luamathalign') + +local function get_kerntoken(newmark) + assert(not mark) + mark, afterkern = newmark, node.new'kern' + return token.new(node.direct.todirect(afterkern), node_cmd) +end + +local function insert_whatsit(mark) + local n = node.new(whatsit, user_defined) + n.user_id, n.type, n.value = whatsit_id, string.byte'd', mark + node.write(n) +end +luacmd("SetAlignmentPoint", function() + local mark = token.scan_int() + if mark < 0 then + for i=tex.nest.ptr,0,-1 do + local t = tex.nest[i].head + if luaprop.query(t) ~= nil then + mark = mark + 1 + if mark == 0 then + luaprop.set(t, true) + return insert_whatsit(-i) + end + end + end + tex.error('No compatible alignment environment found', + 'This either means that \\SetAlignmentPoint was used outside\n\z + of an alignment or the used alignment is not setup for use with\n\z + luamathalign. In the latter case you might want to look at\n\z + non-negative alignment marks.') + else + return insert_whatsit(mark) + end +end, "protected") + +function handle_whatsit(mark) + token.put_next(ampersand, get_kerntoken(mark)) +end +luacmd("ExecuteAlignment", function() + return handle_whatsit(token.scan_int()) +end, "protected") + +luacmd("LuaMathAlign@begin", function() + local nest = tex.nest.top + luaprop.set(nest.head, false) +end, "protected") +luacmd("LuaMathAlign@end@early", function() + local t = tex.nest.top.head + if luaprop.set(t, nil) == true then + handle_whatsit(-tex.nest.ptr) + end +end, "protected") +local delayed +luacmd("LuaMathAlign@end", function() + local nest = tex.nest.top + local t = nest.head + if luaprop.set(t, nil) == true then + assert(not delayed) + delayed = {get_kerntoken(-tex.nest.ptr), ampersand} + end +end, "protected") +luatexbase.add_to_callback("hpack_filter", function(head, groupcode) + if delayed and groupcode == "align_set" then +-- HACK: token.put_next puts the tokens into the input stream after the cell +-- is fully read, before the next starts. This will act as if the content was +-- written as the first element of the next field. + token.put_next(delayed) + delayed = nil + end + return true +end, "luamathalign.delayed") + +luacmd("LuaMathAlign@IsolateAlignmentPoints", function() + local main = token.scan_int() + if not token.scan_keyword 'into' then + tex.error'Expected "into"' + end + local marks = token.scan_int() + local head, newhead = isolate(tex.box[main]) + tex.box[marks] = node.direct.tonode(node.direct.hpack( + newhead and node.direct.todirect(newhead) or 0)) +end, "protected") +% \end{macrocode} +% \subsection{LaTeX} +% \iffalse +%</lua> +%<*gobble> +\end{docstrip-luacode} +%</gobble> +%<*package> +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage + {luamathalign} + [2022-04-18 v0.1 additional math alignment tricks using Lua] +% \fi +% The actual \LaTeX\ package just loads the Lua module and patches \pkg{amsmath}: +% \begin{macrocode} +% \RequirePackage{scrlfile} +\directlua{require'luamathalign'} +\IfPackageLoadedTF{amsmath}{% + \@firstofone +}{% + \AddToHook{package/amsmath/after} +} +{% + \def\align@preamble{% + &\hfil + \strut@ + \setboxz@h{\@lign$\m@th\displaystyle{% + \LuaMathAlign@begin##\LuaMathAlign@end}$}% + \ifmeasuring@\savefieldlength@\fi + \set@field + \tabskip\z@skip + &\setboxz@h{\@lign$\m@th\displaystyle{{}##}$}% + \ifmeasuring@\savefieldlength@\fi + \set@field + \hfil + \tabskip\alignsep@ + } + \renewcommand{\start@aligned}[2]{% + \RIfM@\else + \nonmatherr@{\begin{\@currenvir}}% + \fi + \savecolumn@ % Assumption: called inside a group + \alignedspace@left + \if #1t\vtop \else \if#1b \vbox \else \vcenter \fi \fi \bgroup + \maxfields@#2\relax + \ifnum\maxfields@>\m@ne + \multiply\maxfields@\tw@ + \let\math@cr@@@\math@cr@@@alignedat + \alignsep@\z@skip + \else + \let\math@cr@@@\math@cr@@@aligned + \alignsep@\minalignsep + \fi + \Let@ \chardef\dspbrk@context\@ne + \default@tag + \spread@equation % no-op if already called + \global\column@\z@ + \ialign\bgroup + &\column@plus + \hfil + \strut@ + $\m@th\displaystyle{\LuaMathAlign@begin##\LuaMathAlign@end}$% + \tabskip\z@skip + &\column@plus + $\m@th\displaystyle{{}##}$% + \hfil + \tabskip\alignsep@ + \crcr + } + \edef\math@cr@@@alignedat{\LuaMathAlign@end@early + \unexpanded\expandafter{\math@cr@@@alignedat}} + \edef\math@cr{\LuaMathAlign@end@early + \unexpanded\expandafter{\math@cr}} + \edef\endaligned{\LuaMathAlign@end@early + \unexpanded\expandafter{\endaligned}} +} +\protected\def\AlignHere{\SetAlignmentPoint\m@ne} +\begingroup + \def\patch@finph@nt\setbox\tw@\null{% + \LuaMathAlign@IsolateAlignmentPoints\z@ into \tw@ + }% +\expanded{\endgroup% +\protected\def\noexpand\finph@nt{% + \unexpanded\expandafter\expandafter\expandafter{% + \expandafter\patch@finph@nt\finph@nt + }% +}} +\ExplSyntaxOff +% \end{macrocode} +% \iffalse +%</package> +% \fi +% \end{implementation} +\endinput diff --git a/Master/texmf-dist/tex/lualatex/luamathalign/build.lua b/Master/texmf-dist/tex/lualatex/luamathalign/build.lua new file mode 100644 index 00000000000..3ce83c670a6 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luamathalign/build.lua @@ -0,0 +1,21 @@ +module = "luamathalign" +typesetexe = "lualatex" +tdsroot = "lualatex" +checkengines = {"luatex"} +unpackfiles = {"*.dtx"} +installfiles = {"*.sty", "*.lua"} +sourcefiles = {"*.dtx", "*.lua"} + +uploadconfig = { + pkg = "luamathalign", + version = "v0.1", + author = "Marcel Krüger", + license = "lppl1.3c", + summary = "More flexible alignment in amsmath environments", + ctanPath = "/macros/luatex/latex/luamathalign/", + -- repository = "https://github.com/zauguin/luamathalign/" + description = "Allow aligning mathematical expressions on points where \z + where direcly using & is not possible, especially in nested macros or environments.", + topic = {'maths', 'luatex'}, + -- update = true, +} diff --git a/Master/texmf-dist/tex/lualatex/luamathalign/luamathalign-luacmd.lua b/Master/texmf-dist/tex/lualatex/luamathalign/luamathalign-luacmd.lua new file mode 100644 index 00000000000..a2ff4ed0679 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luamathalign/luamathalign-luacmd.lua @@ -0,0 +1,19 @@ +----Copyright (C) 2019--2022 by Marcel Krueger +--- +--- This file may be distributed and/or modified under the +--- conditions of the LaTeX Project Public License, either +--- version 1.3c of this license or (at your option) any later +--- version. The latest version of this license is in: +--- +--- http://www.latex-project.org/lppl.txt +--- +--- and version 1.3 or later is part of all distributions of +--- LaTeX version 2005/12/01 or later. +local func_tbl = lua.get_functions_table() +local set_lua = token.set_lua +local new_luafunction = luatexbase.new_luafunction +return function(name, func, ...) + local idx = new_luafunction(name) + set_lua(name, idx, ...) + func_tbl[idx] = func +end diff --git a/Master/texmf-dist/tex/lualatex/luamathalign/luamathalign-luaprop.lua b/Master/texmf-dist/tex/lualatex/luamathalign/luamathalign-luaprop.lua new file mode 100644 index 00000000000..3e10f0fc454 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luamathalign/luamathalign-luaprop.lua @@ -0,0 +1,42 @@ +----Copyright (C) 2019--2022 by Marcel Krueger +--- +--- This file may be distributed and/or modified under the +--- conditions of the LaTeX Project Public License, either +--- version 1.3c of this license or (at your option) any later +--- version. The latest version of this license is in: +--- +--- http://www.latex-project.org/lppl.txt +--- +--- and version 1.3 or later is part of all distributions of +--- LaTeX version 2005/12/01 or later. +return function(namespace) + return { + query = function(n) + local p = node.getproperty(n) + return p and p[namespace] + end, + get = function(n, ...) + local p = node.getproperty(n) + if not p then + p = {} + node.setproperty(n, p) + end + local n = p[namespace] + if nil == p[namespace] then + local d = select('#', ...) ~= 0 and ... or {} + n, p[namespace] = d, d + end + return n + end, + set = function(n, v) + local p = node.getproperty(n) + if not p then + p = {} + node.setproperty(n, p) + end + local old = p[namespace] + p[namespace] = v + return old + end, + } +end diff --git a/Master/texmf-dist/tex/lualatex/luamathalign/luamathalign.lua b/Master/texmf-dist/tex/lualatex/luamathalign/luamathalign.lua new file mode 100644 index 00000000000..aa8476b0553 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luamathalign/luamathalign.lua @@ -0,0 +1,375 @@ +-- +-- This is file `luamathalign.lua', +-- generated with the docstrip utility. +-- +-- The original source files were: +-- +-- luamathalign.dtx (with options: `lua') +-- +-- Copyright (C) 2019--2022 by Marcel Krueger +-- +-- This file may be distributed and/or modified under the +-- conditions of the LaTeX Project Public License, either +-- version 1.3c of this license or (at your option) any later +-- version. The latest version of this license is in: +-- +-- http://www.latex-project.org/lppl.txt +-- +-- and version 1.3 or later is part of all distributions of +-- LaTeX version 2005/12/01 or later. +local luacmd = require'luamathalign-luacmd' +local luaprop = require'luamathalign-luaprop'('mathalign') +local hlist = node.id'hlist' +local vlist = node.id'vlist' +local whatsit = node.id'whatsit' +local kern = node.id'kern' +local user_defined = node.subtype'user_defined' +local whatsit_id = luatexbase.new_whatsit'mathalign' +local node_cmd = token.command_id'node' +local ampersand = token.new(38, 4) +-- We might want to add y later +local function is_marked(mark, list) + for n in node.traverse(list) do + local id = n.id + if id == hlist or id == vlist then + if is_marked(mark, n.head) then return true end + elseif id == whatsit and n.subtype == user_defined + and n.user_id == whatsit_id and n.value == mark then + return true + end + end + return false +end +local function assert_unmarked(mark, list, ...) + local marked = is_marked(mark, list) + if marked then + tex.error("Multiple alignment marks", "I found multiple alignment marks \z + of type " .. mark .. " in an alignment where I already had an \z + alignment mark of that type. You should look at both of them and \z + decide which one is right. I will continue with the first one for now.") + end + return ... +end +local measure do + local vmeasure + local function hmeasure(mark, list) + local x, last = 0, list.head + for n in node.traverse(last) do + local id = n.id + if id == hlist then + local w, h, d = node.rangedimensions(list, last, n) + x, last = x + w, n + local dx = hmeasure(mark, n) + if dx then return assert_unmarked(mark, n.next, dx + x) end + elseif id == vlist then + local w, h, d = node.rangedimensions(list, last, n) + x, last = x + w, n + local dx = vmeasure(mark, n) + if dx then return assert_unmarked(mark, n.next, dx + x) end + elseif id == whatsit and n.subtype == user_defined + and n.user_id == whatsit_id and n.value == mark then + local w, h, d = node.rangedimensions(list, last, n) + local after + list.head, after = node.remove(list.head, n) + return assert_unmarked(mark, after, x + w) + end + end + end + function vmeasure(mark, list) + for n in node.traverse(list.head) do + local id = n.id + if id == hlist then + local dx = hmeasure(mark, n) + if dx then return assert_unmarked(mark, n.next, dx + n.shift) end + elseif id == vlist then + local dx = vmeasure(mark, n) + if dx then return assert_unmarked(mark, n.next, dx + n.shift) end + elseif id == whatsit and n.subtype == user_defined + and n.user_id == whatsit_id and n.value == mark then + local after + list.head, after = node.remove(list.head, n) + return assert_unmarked(mark, after, 0) + end + end + end + function measure(mark, head) + local x, last = 0, head + for n in node.traverse(last) do + local id = n.id + if id == hlist then + local w, h, d = node.dimensions(last, n) + x, last = x + w, n + local dx = hmeasure(mark, n) + if dx then return assert_unmarked(mark, n.next, head, dx + x) end + elseif id == vlist then + local w, h, d = node.dimensions(last, n) + x, last = x + w, n + local dx = vmeasure(mark, n) + if dx then return assert_unmarked(mark, n.next, head, dx + x) end + elseif id == whatsit and n.subtype == user_defined + and n.user_id == whatsit_id and n.value == mark then + local w, h, d = node.dimensions(last, n) + local after + head, after = node.remove(head, n) + return assert_unmarked(mark, after, head, x + w) + end + end + return head + end +end + +local isolate do + local visolate + local function hisolate(list, offset) + local x, last = 0, list.head + local newhead, newtail = nil, nil + local n = last + while n do + local id = n.id + if id == hlist then + local w, h, d = node.rangedimensions(list, last, n) + x, last = x + w, n + local inner_head, inner_tail, new_offset = hisolate(n, offset - x) + if inner_head then + if newhead then + newtail.next, inner_head.prev = inner_head, newtail + else + newhead = inner_head + end + newtail = inner_tail + offset = x + new_offset + end + n = n.next + elseif id == vlist then + local w, h, d = node.rangedimensions(list, last, n) + x, last = x + w, n + local inner_head, inner_tail, new_offset = visolate(n, offset - x) + if inner_head then + if newhead then + newtail.next, inner_head.prev = inner_head, newtail + else + newhead = inner_head + end + newtail = inner_tail + offset = x + new_offset + end + n = n.next + elseif id == whatsit and n.subtype == user_defined + and n.user_id == whatsit_id then + local w, h, d = node.rangedimensions(list, last, n) + x = x + w + list.head, last = node.remove(list.head, n) + if x ~= offset then + local k = node.new(kern) + k.kern, offset = x - offset, x + newhead, newtail = node.insert_after(newhead, newtail, k) + end + newhead, newtail = node.insert_after(newhead, newtail, n) + n = last + else + n = n.next + end + end + return newhead, newtail, offset + end + function visolate(list, offset) + local newhead, newtail = nil, nil + local n = list.head + while n do + local id = n.id + if id == hlist then + if dx then return assert_unmarked(mark, n.next, dx + n.shift) end + local inner_head, inner_tail, new_offset = hisolate(n, offset) + if inner_head then + if newhead then + newtail.next, inner_head.prev = inner_head, newtail + else + newhead = inner_head + end + newtail = inner_tail + offset = new_offset + end + n = n.next + elseif id == vlist then + if dx then return assert_unmarked(mark, n.next, dx + n.shift) end + local inner_head, inner_tail, new_offset = visolate(n, offset) + if inner_head then + if newhead then + newtail.next, inner_head.prev = inner_head, newtail + else + newhead = inner_head + end + newtail = inner_tail + offset = new_offset + end + n = n.next + elseif id == whatsit and n.subtype == user_defined + and n.user_id == whatsit_id then + local after + list.head, after = node.remove(list.head, n) + if 0 ~= offset then + local k = node.new(kern) + k.kern, offset = -offset, 0 + newhead, newtail = node.insert_after(newhead, newtail, k) + end + newhead, newtail = node.insert_after(newhead, newtail, n) + n = last + else + n = n.next + end + end + return newhead, newtail, offset + end + function isolate(head) + local x, last = 0, head + local newhead, newtail, offset = nil, nil, 0 + local n = last + while n do + local id = n.id + if id == hlist then + local w, h, d = node.dimensions(last, n) + x, last = x + w, n + local inner_head, inner_tail, new_offset = hisolate(n, offset - x) + if inner_head then + if newhead then + newtail.next, inner_head.prev = inner_head, newtail + else + newhead = inner_head + end + newtail = inner_tail + offset = x + new_offset + end + n = n.next + elseif id == vlist then + local w, h, d = node.dimensions(last, n) + x, last = x + w, n + local inner_head, inner_tail, new_offset = visolate(n, offset - x) + if inner_head then + if newhead then + newtail.next, inner_head.prev = inner_head, newtail + else + newhead = inner_head + end + newtail = inner_tail + offset = x + new_offset + end + n = n.next + elseif id == whatsit and n.subtype == user_defined + and n.user_id == whatsit_id then + local w, h, d = node.dimensions(last, n) + x = x + w + head, last = node.remove(head, n) + if x ~= offset then + local k = node.new(kern) + k.kern, offset = x - offset, x + newhead, newtail = node.insert_after(newhead, newtail, k) + end + newhead, newtail = node.insert_after(newhead, newtail, n) + n = last + else + n = n.next + end + end + return head, newhead + end +end + +local mark, afterkern +luatexbase.add_to_callback('post_mlist_to_hlist_filter', function(n) + if mark then + local x + n, x = measure(mark, n) + local k = node.new'kern' + local off = x - n.width + k.kern, afterkern.kern = off, -off + node.insert_after(n.head, nil, k) + n.width = x + mark, afterkern = nil, nil + end + return n +end, 'luamathalign') + +local function get_kerntoken(newmark) + assert(not mark) + mark, afterkern = newmark, node.new'kern' + return token.new(node.direct.todirect(afterkern), node_cmd) +end + +local function insert_whatsit(mark) + local n = node.new(whatsit, user_defined) + n.user_id, n.type, n.value = whatsit_id, string.byte'd', mark + node.write(n) +end +luacmd("SetAlignmentPoint", function() + local mark = token.scan_int() + if mark < 0 then + for i=tex.nest.ptr,0,-1 do + local t = tex.nest[i].head + if luaprop.query(t) ~= nil then + mark = mark + 1 + if mark == 0 then + luaprop.set(t, true) + return insert_whatsit(-i) + end + end + end + tex.error('No compatible alignment environment found', + 'This either means that \\SetAlignmentPoint was used outside\n\z + of an alignment or the used alignment is not setup for use with\n\z + luamathalign. In the latter case you might want to look at\n\z + non-negative alignment marks.') + else + return insert_whatsit(mark) + end +end, "protected") + +function handle_whatsit(mark) + token.put_next(ampersand, get_kerntoken(mark)) +end +luacmd("ExecuteAlignment", function() + return handle_whatsit(token.scan_int()) +end, "protected") + +luacmd("LuaMathAlign@begin", function() + local nest = tex.nest.top + luaprop.set(nest.head, false) +end, "protected") +luacmd("LuaMathAlign@end@early", function() + local t = tex.nest.top.head + if luaprop.set(t, nil) == true then + handle_whatsit(-tex.nest.ptr) + end +end, "protected") +local delayed +luacmd("LuaMathAlign@end", function() + local nest = tex.nest.top + local t = nest.head + if luaprop.set(t, nil) == true then + assert(not delayed) + delayed = {get_kerntoken(-tex.nest.ptr), ampersand} + end +end, "protected") +luatexbase.add_to_callback("hpack_filter", function(head, groupcode) + if delayed and groupcode == "align_set" then +-- HACK: token.put_next puts the tokens into the input stream after the cell +-- is fully read, before the next starts. This will act as if the content was +-- written as the first element of the next field. + token.put_next(delayed) + delayed = nil + end + return true +end, "luamathalign.delayed") + +luacmd("LuaMathAlign@IsolateAlignmentPoints", function() + local main = token.scan_int() + if not token.scan_keyword 'into' then + tex.error'Expected "into"' + end + local marks = token.scan_int() + local head, newhead = isolate(tex.box[main]) + tex.box[marks] = node.direct.tonode(node.direct.hpack( + newhead and node.direct.todirect(newhead) or 0)) +end, "protected") +-- +-- +-- End of file `luamathalign.lua'. diff --git a/Master/texmf-dist/tex/lualatex/luamathalign/luamathalign.sty b/Master/texmf-dist/tex/lualatex/luamathalign/luamathalign.sty new file mode 100644 index 00000000000..6a735b40120 --- /dev/null +++ b/Master/texmf-dist/tex/lualatex/luamathalign/luamathalign.sty @@ -0,0 +1,98 @@ +%% +%% This is file `luamathalign.sty', +%% generated with the docstrip utility. +%% +%% The original source files were: +%% +%% luamathalign.dtx (with options: `package') +%% +%% Copyright (C) 2019--2022 by Marcel Krueger +%% +%% This file may be distributed and/or modified under the +%% conditions of the LaTeX Project Public License, either +%% version 1.3c of this license or (at your option) any later +%% version. The latest version of this license is in: +%% +%% http://www.latex-project.org/lppl.txt +%% +%% and version 1.3 or later is part of all distributions of +%% LaTeX version 2005/12/01 or later. +\NeedsTeXFormat{LaTeX2e} +\ProvidesPackage + {luamathalign} + [2022-04-18 v0.1 additional math alignment tricks using Lua] +\directlua{require'luamathalign'} +\IfPackageLoadedTF{amsmath}{% + \@firstofone +}{% + \AddToHook{package/amsmath/after} +} +{% + \def\align@preamble{% + &\hfil + \strut@ + \setboxz@h{\@lign$\m@th\displaystyle{% + \LuaMathAlign@begin##\LuaMathAlign@end}$}% + \ifmeasuring@\savefieldlength@\fi + \set@field + \tabskip\z@skip + &\setboxz@h{\@lign$\m@th\displaystyle{{}##}$}% + \ifmeasuring@\savefieldlength@\fi + \set@field + \hfil + \tabskip\alignsep@ + } + \renewcommand{\start@aligned}[2]{% + \RIfM@\else + \nonmatherr@{\begin{\@currenvir}}% + \fi + \savecolumn@ % Assumption: called inside a group + \alignedspace@left + \if #1t\vtop \else \if#1b \vbox \else \vcenter \fi \fi \bgroup + \maxfields@#2\relax + \ifnum\maxfields@>\m@ne + \multiply\maxfields@\tw@ + \let\math@cr@@@\math@cr@@@alignedat + \alignsep@\z@skip + \else + \let\math@cr@@@\math@cr@@@aligned + \alignsep@\minalignsep + \fi + \Let@ \chardef\dspbrk@context\@ne + \default@tag + \spread@equation % no-op if already called + \global\column@\z@ + \ialign\bgroup + &\column@plus + \hfil + \strut@ + $\m@th\displaystyle{\LuaMathAlign@begin##\LuaMathAlign@end}$% + \tabskip\z@skip + &\column@plus + $\m@th\displaystyle{{}##}$% + \hfil + \tabskip\alignsep@ + \crcr + } + \edef\math@cr@@@alignedat{\LuaMathAlign@end@early + \unexpanded\expandafter{\math@cr@@@alignedat}} + \edef\math@cr{\LuaMathAlign@end@early + \unexpanded\expandafter{\math@cr}} + \edef\endaligned{\LuaMathAlign@end@early + \unexpanded\expandafter{\endaligned}} +} +\protected\def\AlignHere{\SetAlignmentPoint\m@ne} +\begingroup + \def\patch@finph@nt\setbox\tw@\null{% + \LuaMathAlign@IsolateAlignmentPoints\z@ into \tw@ + }% +\expanded{\endgroup% +\protected\def\noexpand\finph@nt{% + \unexpanded\expandafter\expandafter\expandafter{% + \expandafter\patch@finph@nt\finph@nt + }% +}} +\ExplSyntaxOff +\endinput +%% +%% End of file `luamathalign.sty'. diff --git a/Master/tlpkg/bin/tlpkg-ctan-check b/Master/tlpkg/bin/tlpkg-ctan-check index 08ead87ae28..26ffe182ec8 100755 --- a/Master/tlpkg/bin/tlpkg-ctan-check +++ b/Master/tlpkg/bin/tlpkg-ctan-check @@ -500,7 +500,7 @@ my @TLP_working = qw( luabibentry luabidi luacensor luacode luacolor luafindfont luahyphenrules luaimageembed luaindex luainputenc luaintro luakeys lualatex-doc lualatex-doc-de - lualatex-math lualatex-truncate lualibs + lualatex-math lualatex-truncate lualibs luamathalign luamesh luamplib luaotfload luapackageloader luaprogtable luapstricks luarandom luasseq luatex85 luatexbase luatexja luatexko luatextra diff --git a/Master/tlpkg/libexec/ctan2tds b/Master/tlpkg/libexec/ctan2tds index 082479e952a..bf24f1215f1 100755 --- a/Master/tlpkg/libexec/ctan2tds +++ b/Master/tlpkg/libexec/ctan2tds @@ -2121,6 +2121,7 @@ $standardtex 'luaintro', 'NULL', # doc pkg 'luakeys', '\.lua|luakeys\.tex|' . $standardtex, 'lualatex-math', '\.sty', # not phst-doc.cls + 'luamathalign','luam.*\.lua|' . $standardtex, # not build.lua 'luamesh', '\.sty', # not lltxdoc.cls 'luapackageloader', '\.lua|' . $standardtex, 'luaprogtable','\.lua|' . $standardtex, @@ -3082,6 +3083,7 @@ my $core_latex = "$Master/texmf-dist/tex/latex"; 'lua-typo' => 'etex', 'lua-ul' => 'etex-answer-y', # https://github.com/latex3/latex2e/issues/558 'luaindex' => 'lualatex --shell-escape', + 'luamathalign' => 'etex', 'luatexja' => 'lualatex', 'makelabels' => 'tex --8bit', 'mandi' => 'pdflatex-preserve-pdf', diff --git a/Master/tlpkg/tlpsrc/collection-luatex.tlpsrc b/Master/tlpkg/tlpsrc/collection-luatex.tlpsrc index f3a8569ef1f..40ed2ba2a05 100644 --- a/Master/tlpkg/tlpsrc/collection-luatex.tlpsrc +++ b/Master/tlpkg/tlpsrc/collection-luatex.tlpsrc @@ -44,6 +44,7 @@ depend lualatex-doc depend lualatex-math depend lualatex-truncate depend lualibs +depend luamathalign depend luamplib depend luaotfload depend luapackageloader diff --git a/Master/tlpkg/tlpsrc/luamathalign.tlpsrc b/Master/tlpkg/tlpsrc/luamathalign.tlpsrc new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/Master/tlpkg/tlpsrc/luamathalign.tlpsrc |