summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/tex/lualatex
diff options
context:
space:
mode:
authorKarl Berry <karl@freefriends.org>2024-03-25 20:00:05 +0000
committerKarl Berry <karl@freefriends.org>2024-03-25 20:00:05 +0000
commit102cc82a2aa27744578f87fe381e22229b67a320 (patch)
tree5db872eb68367806bfc740811cc92c085ecf4b09 /Master/texmf-dist/tex/lualatex
parent0cc5bd9658065e6002752e3d926cecc7066d7b5a (diff)
longmath (25mar24)
git-svn-id: svn://tug.org/texlive/trunk@70765 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/tex/lualatex')
-rw-r--r--Master/texmf-dist/tex/lualatex/longmath/longmath.lua428
-rw-r--r--Master/texmf-dist/tex/lualatex/longmath/longmath.sty223
2 files changed, 651 insertions, 0 deletions
diff --git a/Master/texmf-dist/tex/lualatex/longmath/longmath.lua b/Master/texmf-dist/tex/lualatex/longmath/longmath.lua
new file mode 100644
index 00000000000..56c64c8cdb1
--- /dev/null
+++ b/Master/texmf-dist/tex/lualatex/longmath/longmath.lua
@@ -0,0 +1,428 @@
+--
+-- longmath.lua is part of longmath version 0.1.
+--
+-- (c) 2024 Hans-Jürgen Matschull
+--
+-- This work may be distributed and/or modified under the
+-- conditions of the LaTeX Project Public License, either version 1.3
+-- 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.
+--
+-- This work has the LPPL maintenance status 'maintained'.
+--
+-- The Current Maintainer of this work is Hans-Jürgen Matschull
+--
+-- see README for a list of files belonging to longmath.
+--
+
+-- Attribute numbers.
+local attr_info = luatexbase.registernumber( 'longmath@info' )
+local attr_limits = luatexbase.registernumber( 'longmath@limits' )
+
+-- Character for tagging parent groups.
+local parent = '+'
+
+
+-- Check for specific node types.
+local ntypes = node.types()
+local function is_noad( nd ) return nd and ntypes[ nd.id ] == 'noad' and nd.subtype end
+local function is_fence( nd ) return nd and ntypes[ nd.id ] == 'fence' and nd.subtype end
+local function is_hlist( nd ) return nd and ntypes[ nd.id ] == 'hlist' and nd.subtype end
+local function is_style( nd ) return nd and ntypes[ nd.id ] == 'style' and nd.subtype end
+local function is_whatsit( nd ) return nd and ntypes[ nd.id ] == 'whatsit' and nd.subtype end
+
+-- Read a command whatsit.
+local function get_comm( nd )
+ if is_whatsit( nd ) == 3 and node.has_attribute( nd, attr_info ) then
+ local comm, data = nd.data:match( '^([^:]+):?(.*)$' )
+ if data == '' then data = nil end
+ return comm, data, node.get_attribute( nd, attr_info )
+ end
+end
+
+-- Iterator over all fields of a node to be copied.
+-- Returns the field name and a boolean indicating if a deep copy is needed.
+-- If the node already has fixed dimensions, its head field is ignored.
+-- If the limits attribute is set on a large operator, scripts are ignored in display style.
+local function fields( nd, disp )
+ local list, oper = {}, is_noad( nd )
+ for _, fld in ipairs( node.fields( nd.id, nd.subtype ) ) do
+ list[fld] = not not node.is_node( nd[fld] )
+ end
+ list.id, list.subtype, list.attr = nil, nil, list.attr and false
+ if list.height ~= nil and list.depth ~= nil then list.head = nil end
+ if ( disp and oper == 1 or oper == 2 ) and node.has_attribute( nd, attr_limits ) then
+ list.sub, list.sup = nil, nil
+ end
+ return pairs( list )
+end
+
+-- Tables representing special delimiters.
+local delim_null = { small_fam = 0, small_char = 0, large_fam = 0, large_char = 0 }
+local delim_auto = { small_fam = 0xF, small_char = 0xEF, large_fam = 0xE, large_char = 0xFE }
+
+-- Check if two delimiters (either real nodes or tables) are equal.
+local function delim_eq( da, db )
+ for i in pairs( delim_null ) do if da[i] ~= db[i] then return false end end
+ return true
+end
+
+-- Make the delimiter node or table da equal to db.
+local function delim_set( da, db )
+ for i in pairs( delim_null ) do da[i] = db[i] end
+ return da
+end
+
+-- Metatable for delimiter sequences. Internalises the overflow check.
+local meta_delims = {}
+function meta_delims.__index( tab, ix )
+ if type( ix ) ~= 'number' then return nil end
+ if ix > #tab then ix = #tab end
+ if ix < 1 then ix = 1 end
+ return rawget( tab, ix )
+end
+
+-- Stack to store auto delimiters.
+-- The last entry in this table is the current one.
+-- It contains two tables "opn" and "cls".
+-- Each contains a sequence of delimiter data for each level.
+local delimiters = {}
+
+-- Scans a sample of auto delimiters.
+-- Inserts the opening and closing delimites into the "opn" and "cls" tables in "tab".
+local function scan_sample( head, tab )
+ local opn, cls
+ for pos in node.traverse( head ) do
+ if is_fence( pos ) == 1 then opn = delim_set( {}, pos.delim )
+ elseif is_fence( pos ) == 3 then cls = delim_set( {}, pos.delim )
+ elseif is_noad( pos ) == 9 then scan_sample( pos.nucleus.head, tab ) end
+ end
+ if opn and cls then table.insert( tab.opn, opn ) table.insert( tab.cls, cls ) end
+end
+
+-- This function is triggered by a "set" command.
+-- The "sample" is a node containing a nested list if delimiter groups.
+-- Push a new item on the stack and scan the sample.
+local function set_auto( sample )
+ local tab = { opn = setmetatable( {}, meta_delims ), cls = setmetatable( {}, meta_delims ) }
+ table.insert( delimiters, tab )
+ scan_sample( sample, tab )
+end
+
+-- This function is triggered by a "res" command.
+-- Remove the topmost item from the stack.
+local function res_auto()
+ table.remove( delimiters )
+end
+
+-- Adapt a fake delimiter group to the given data.
+-- "head" is the node containing the "\math___{}" object.
+-- "level" is the nesting level to be used if this is an auto delimiter.
+-- "ht" and "dp" are the height and depth of the content of the group.
+-- Returns the level if this was an auto delimiter
+-- or a fixed one equal to the auto delimiter of some level.
+local function set_delim( head, level, ht, dp )
+ local type, delim, hbox = head.subtype == 6 and 'opn' or head.subtype == 7 and 'cls'
+ while is_noad( head ) do
+ node.unset_attribute( head, attr_info ) head = head.nucleus
+ node.unset_attribute( head, attr_info ) head = head.head
+ end
+ for pos in node.traverse( head ) do
+ node.unset_attribute( pos, attr_info )
+ if is_fence( pos ) == 3 then delim = pos.delim end
+ if is_noad( pos ) == 0 then hbox = pos.nucleus.head end
+ end
+ if not ( delim and hbox ) then return end
+ local auto = delim_eq( delim, delim_auto )
+ local brks = delimiters[#delimiters]
+ if brks then brks = brks[type] end
+ if type and auto and brks then
+ delim_set( delim, brks[level] )
+ elseif type and brks then
+ level = nil
+ for l = 1, #brks do if delim_eq( brks[l], delim ) then level = l break end end
+ else
+ level = nil
+ end
+ local scale = node.get_attribute( hbox, attr_info )
+ hbox.height, hbox.depth = ht * scale // 1000, dp * scale // 1000
+ if delim_eq( delim, delim_null ) then hbox.width = hbox.width * 2 end
+ return level
+end
+
+-- This creates a deep copy of the node list from start to stop (inclusive).
+-- Ignores whatsits and the content of nodes that have fixed dimension.
+-- Ignores limits of large operators if flagged and "disp" is true.
+-- Ignores nodes that have the info attribute set. These are unprocessed delimiters.
+-- Tries to keep track of the current math style.
+local function copy_list( start, stop, disp )
+ local old, new, copy, last = start
+ while old do
+ if is_style( old ) then disp = old.style:match( 'display' ) end
+ local ign = is_noad( old ) and node.has_attribute( old, attr_info ) or is_whatsit( old )
+ if not ign then
+ new = node.new( old.id, old.subtype )
+ for field, deep in fields( old, disp ) do
+ if deep then
+ local disp = disp and ( field == 'nucleus' or field == 'head' or field == 'display' )
+ new[field] = copy_list( old[field], nil, disp )
+ else
+ new[field] = old[field]
+ end
+ end
+ if not copy then last, copy = new, new
+ else last, copy = new, node.insert_after( copy, last, new ) end
+ end
+ if old == stop then break end
+ old = node.next( old )
+ end
+ return copy
+end
+
+-- Creates a math style node.
+local function style_node( style )
+ local nd = node.new( "style" )
+ nd.style = style
+ return nd
+end
+
+-- Copies the node list from "start" to "stop" and packs it into a temporary hbox.
+-- Returns the height and depth of that box when typeset in "style".
+local function dimensions( start, stop, style )
+ local disp = style == 'display' or style == 0 or style == 1
+ local copy = copy_list( start, stop, disp )
+ if not copy then return 0, 0 end
+ if style then copy = node.insert_before( copy, copy, style_node( style ) ) end
+ local box = node.mlist_to_hlist( copy, 'text', false )
+ if not box then return 0, 0 end
+ local wd, ht, dp = node.dimensions( box )
+ node.flush_list( box )
+ return ht, dp
+end
+
+-- Table containing information read from the aux file.
+local oldgroups = {}
+-- Table containing information to be written to the aux file.
+local newgroups = {}
+-- Table containing tables of tags that are synonyms for the same group.
+local equals = {}
+
+-- Merge information of a group with the inforation from the aux file
+-- and store the new information to be written to the aux file.
+-- A group table contains the following information:
+-- "tags": table of tags attached to the group (as keys with value "true").
+-- "ht", "dp": dimensions
+-- "lv": the maximal level of automatic delimiters used for subgroups.
+local function max( a, b ) return a and b and math.max( a, b ) or a or b end
+local function merge( group )
+ local tags = group.tags
+ if not tags or not next( tags ) then return end
+ if next( tags, ( next( tags ) ) ) then table.insert( equals, tags ) end
+ for tag in pairs( tags ) do
+ local ngrp = newgroups[tag] or {}
+ newgroups[tag] = ngrp
+ ngrp.ht = max( ngrp.ht, group.ht )
+ ngrp.dp = max( ngrp.dp, group.dp )
+ ngrp.lv = max( ngrp.lv, group.lv )
+ end
+ for tag in pairs( tags ) do
+ local ogrp = oldgroups[tag]
+ if ogrp then
+ group.ht = max( group.ht, ogrp.ht )
+ group.dp = max( group.dp, ogrp.dp )
+ group.lv = max( group.lv, ogrp.lv )
+ end
+ end
+end
+
+
+-- Parses a math list and process all delimiters.
+-- "pargrp" is the parent group.
+-- "head" is the head of the math list.
+-- "open" is the opening delimiter if this is a recursive call.
+-- "pos" is the position where to start the scan.
+-- "style" is the current math style, if known.
+-- This will otherwise be set when th first command is detected.
+-- "group" is the group table for this group.
+-- Returns a modified head, and sets information in the parent group.
+-- "delims" collects all delimiters belonging to this group.
+-- When a longmath special is detected, it is removed and the next node is processed:
+-- set/res: set or reset auto delimiters.
+-- opn: an opening delimiter follows. The function is called recursively.
+-- cls: a closing delimiter follows. The current group ends
+-- mid: an inner delimiter follows.
+-- For every other node in the list, its subnodes are parsed recursively.
+local function parse( pargrp, head, open, pos, style, group )
+ if not head then return end
+ local delims, subgrp = { open }
+ group = group or {}
+ pos = pos or head
+ while pos do
+ local comm, data, info = get_comm( pos )
+ if comm then head, pos = node.remove( head, pos )
+ if comm == 'set' then set_auto( pos ) return head end
+ if comm == 'res' then res_auto() return head end
+ style = style or info
+ group.tags = group.tags or {}
+ if comm == 'opn' then
+ local subgrp = { tags = {} }
+ if data then subgrp.tags[data] = true end
+ head, pos = parse( group, head, pos, node.next( pos ), info, subgrp )
+ elseif comm == 'mid' then
+ if data then group.tags[data] = true end
+ table.insert( delims, pos )
+ pos = node.next( pos )
+ elseif comm == 'cls' then
+ if data then group.tags[data] = true end
+ table.insert( delims, pos )
+ break
+ end
+ else
+ for field, deep in fields( pos ) do if deep then
+ pos[field] = parse( group, pos[field] )
+ end end
+ pos = node.next( pos )
+ end
+ end
+ -- if this is an actual delimiter group, measure its demensions.
+ if group.tags then group.ht, group.dp = dimensions( open or head, pos, style ) end
+ -- merge information with aux file
+ merge( group )
+ -- finally set the delimiters and parse any scripts attached to them.
+ local level, auto = group.lv or 0
+ for _, del in ipairs( delims ) do
+ auto = set_delim( del, level+1, group.ht, group.dp ) or auto
+ del.sub = parse( group, del.sub )
+ del.sup = parse( group, del.sup )
+ end
+ if pos and not open then
+ -- this group started without an opening delimiter.
+ -- we use a tail call and proceed as if there was one and this function was called from a parent.
+ pargrp = { lv = auto or group.lv, tags = {} }
+ if group.tags then for tag in pairs( group.tags ) do pargrp.tags[tag..parent] = true end end
+ return parse( {}, head, nil, node.next( pos ), style, pargrp )
+ else
+ -- inform the parent about the auto delimiter level and any tags used in subgroups.
+ pargrp.lv = max( pargrp.lv, auto or group.lv )
+ if pargrp.tags and group.tags then for tag in pairs( group.tags ) do pargrp.tags[tag..parent] = true end end
+ return head, pos
+ end
+end
+
+-- Callback that scans a math list.
+local function scan( head, style, pen )
+ head = parse( {}, head, nil, nil, style )
+ return node.mlist_to_hlist( head, style, true )
+end
+luatexbase.add_to_callback( 'mlist_to_hlist', scan, 'longmath parse' )
+
+-- Creates a glue node.
+local function glue_node( wd )
+ local nd = node.new( "glue", 8 )
+ nd.width = wd
+ return nd
+end
+
+-- Applies a stepwise shift to the hlists in a vlist if "extra" is non-zero.
+local function shift( head )
+ local extra = tex.dimen['longmath@extra']
+ if extra == 0 then return true end
+ local lines = {}
+ for nd in node.traverse( head ) do
+ if is_hlist( nd ) == 1 then table.insert( lines, nd ) end
+ end
+ local n = #lines - 1
+ if n > 0 then
+ for i, nd in ipairs( lines ) do
+ nd.width = nd.width + extra
+ nd.head = node.insert_before( nd.head, nd.head, glue_node( (i-1) * extra / n ) )
+ nd.head = node.insert_after( nd.head, node.tail( nd.head ), glue_node( (n-i+1) * extra / n ) )
+ end
+ end
+ return true
+end
+luatexbase.add_to_callback( 'post_linebreak_filter', shift, 'longmath shift' )
+
+-- Table for functions to be called from TeX.
+longmath = {}
+
+-- Read data for a collection of identical groups.
+function longmath.read_group( tags, tab )
+ for _, tag in ipairs( tags ) do oldgroups[tag] = tab end
+end
+
+-- Check if a tag is not already in a set.
+-- If a subgroup or parent is in the set, inserts it into a the "loops" table.
+local loops = {}
+local loop_patt = '[' .. parent .. ']*$'
+local function is_new( tag, set )
+ if set[tag] then return false end
+ tag = tag:gsub( loop_patt, '' )
+ for tagg in pairs( set ) do if tag == tagg:gsub( loop_patt, '' ) then
+ loops[ tag:gsub( '@$', '' ) ] = true
+ return false
+ end end
+ return true
+end
+
+-- Given a set of tags attached to the same group,
+-- add all other tags attached to the same group to the set.
+local function find_eq( set )
+ local new = {}
+ for tag in pairs( set ) do
+ local app = ''
+ while true do
+ for _, w in ipairs( equals ) do if w[tag] then for tagg in pairs( w ) do
+ if is_new( tagg..app, set ) then new[tagg..app] = true end
+ end end end
+ if tag:sub(-1) ~= parent then break end
+ tag, app = tag:sub(1,-2), app .. parent
+ end
+ end
+ if not next( new ) then return set end
+ for tag in pairs( new ) do set[tag] = true end
+ return find_eq( set )
+end
+
+local loop_mess = '\\PackageWarningNoLine{longmath}{Cyclic delimiter group%s %s detected}'
+local data_mess = '\\PackageWarningNoLine{longmath}{Delimiters may have changed. Rerun to get them right}'
+
+-- Save all groups to the aux file.
+-- Tags that belong to the same group are collected into a single entry.
+function longmath.save_groups( aux )
+ local check, done = true, {}
+ for tag, grp in pairs( newgroups ) do if not done[tag] then
+ local tags, vals, eqs = {}, {}, find_eq( { [tag] = true } )
+ for tagg in pairs( eqs ) do
+ check, done[tagg] = check and oldgroups[tagg], true
+ table.insert( tags, string.format( '%q', tagg ) )
+ while tagg do
+ if newgroups[tagg] then
+ grp.ht, grp.dp = max( grp.ht, newgroups[tagg].ht ), max( grp.dp, newgroups[tagg].dp )
+ grp.lv = max( grp.lv, newgroups[tagg].lv )
+ end
+ tagg = tagg:match( '^(.*)[' .. parent .. ']$' )
+ end
+ end
+ for k, v in pairs( grp ) do
+ table.insert( vals, string.format( '%s=%d', k, v ) )
+ for tagg in pairs( eqs ) do check = check and v == oldgroups[tagg][k] end
+ end
+ tags, vals = table.concat( tags, ',' ), table.concat( vals, ',' )
+ texio.write( aux, string.format( '\\longmath@group{%s}{%s}\n', tags, vals ) )
+ end end
+ local lps = {}
+ for l in pairs( loops ) do
+ if #lps > 5 then table.insert( lps, '...' ) break end
+ table.insert( lps, l )
+ end
+ if #lps > 0 then
+ tex.print( string.format( loop_mess, #lps > 1 and 's' or '', table.concat( lps, ', ' ) ) )
+ end
+ if not check then tex.print( data_mess ) end
+end
+
+return
diff --git a/Master/texmf-dist/tex/lualatex/longmath/longmath.sty b/Master/texmf-dist/tex/lualatex/longmath/longmath.sty
new file mode 100644
index 00000000000..2b4848db3c0
--- /dev/null
+++ b/Master/texmf-dist/tex/lualatex/longmath/longmath.sty
@@ -0,0 +1,223 @@
+%%
+%% longmath.sty is part of longmath version 0.1.
+%%
+%% (c) 2024 Hans-Jürgen Matschull
+%%
+%% This work may be distributed and/or modified under the
+%% conditions of the LaTeX Project Public License, either version 1.3
+%% 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.
+%%
+%% This work has the LPPL maintenance status 'maintained'.
+%%
+%% The Current Maintainer of this work is Hans-Jürgen Matschull
+%%
+%% see README for a list of files belonging to longmath.
+%%
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{longmath}[2024/03/25]
+
+\RequirePackage{luatexbase}
+
+% generic attribute used to mark special objects and send mathstyle to Lua.
+\newattribute\longmath@info
+
+% attribute to tag large operators.
+\newattribute\longmath@limits
+\def\ignorelimits{\setattribute{\longmath@limits}{1}}
+\def\includelimits{\unsetattribute{\longmath@limits}}
+
+% Save original primitives.
+\let \longmath@left \left
+\let \longmath@middle \middle
+\let \longmath@right \right
+
+\directlua{ require( 'longmath.lua' ) }
+
+% Registers.
+\newcount\delimiterscale \delimiterscale 902
+\newdimen\longmathlinesep \longmathlinesep 3pt
+
+\newif\iflongmath@star
+\newcount\longmath@lines
+\newdimen\longmath@extra
+\newdimen\longmath@width
+\newcount\longmath@xscale
+\newcount\longmath@yscale
+\newbox\longmath@box
+
+% The prefix for tags.
+\def\longmath@prefix{}
+\def\delimiterprefix#1{\def\longmath@prefix{#1}}
+
+% User macros
+\def\lleft {\longmath@delimA \mathopen {1000}{opn}}
+\def\rright {\longmath@delimA \mathclose {1000}{cls}}
+\def\mleft {\longmath@delimA \mathopen {1000}{mid}}
+\def\mright {\longmath@delimA \mathclose {1000}{mid}}
+\def\mmord {\longmath@delimA \mathord \delimiterscale {mid}}
+\def\mmbin {\longmath@delimA \mathbin \delimiterscale {mid}}
+\def\mmrel {\longmath@delimA \mathrel \delimiterscale {mid}}
+\def\mmpunct{\longmath@delimA \mathpunct \delimiterscale {mid}}
+\def\mmiddle{\longmath@delimA \mathinner \delimiterscale {mid}}
+\def\mmop {\longmath@delimA \mathop \delimiterscale {mid}}
+
+% This sets the math atom type, the default scale factor, and the command to be send to Lua,
+% then checks if a tag in {} is following.
+\def\longmath@delimA #1#2#3{\begingroup
+ \let\longmath@type #1 \longmath@xscale = #2 \relax \edef\longmath@comm{#3}
+ \futurelet \longmath@token \longmath@delimB }
+
+% If a tag is following, read it. Otherwise, skip this step.
+\def\longmath@delimB{\ifx\longmath@token\bgroup \expandafter \longmath@delimC
+ \else \expandafter \longmath@delimD \fi }
+
+% Read the tag, prepend the prefix and append a @, then add it to the command.
+\def\longmath@delimC #1{\edef\longmath@comm{\longmath@comm:\longmath@prefix#1@} \longmath@delimD }
+
+% Read the optional scale factor. If none is given, the scale is set to 0.
+\def\longmath@delimD #1{\afterassignment\longmath@delimE \longmath@yscale = 0#1}
+
+% This creates a \special whatsit with the command as string argument, followed by
+% a fake delimiter group with the requested delimiter always being the right one.
+% For the delimiter nodes, the info attribute is set to the current math style.
+% For the empty \hbox, it is set to the requested scale factor.
+\def\longmath@delimE{
+ \setattribute\longmath@info{\mathstyle} \special{\longmath@comm}
+ \longmath@type\bgroup \longmath@left . \aftergroup\egroup \aftergroup\endgroup
+ \begingroup \setattribute\longmath@info{\ifnum \longmath@yscale < 1 \longmath@xscale \else \longmath@yscale \fi}
+ \hbox to -\nulldelimiterspace{} \endgroup
+ \longmath@right }
+
+
+% magic number to identify the variable delimiter.
+\delcode`* = "FEFEFE %"
+
+% scanner to read the delimiter sample and create a nested \left-\right-group.
+\def\longmath@autoL #1#2\relax{\ifx *#1 \longmath@autoR #2\relax
+ \else \longmath@left #1 \longmath@autoL #2\relax \fi }
+\def\longmath@autoR #1#2\relax{\ifx *#1
+ \else \longmath@right #1 \longmath@autoR #2\relax \fi}
+
+% Read the sample provided by the user, insert a set command, and typeset it into a box.
+\def\autodelimiters #1{\begingroup \longmath@info = 0
+ \setbox\longmath@box\hbox{$ \special{set} \longmath@autoL #1***\relax $}
+ \endgroup\aftergroup\longmath@autoreset}
+
+% To make the setting local to the current TeX group, send a reset command at the end of the group.
+\def\longmath@autoreset{\begingroup \longmath@info = 0
+ \setbox\longmath@box\hbox{$ \special{res} {} $}
+ \endgroup}
+
+
+% Counter for automatic tag generation.
+\newcount\longmath@tagnum
+
+% Stack to store pushed tags, separated by spaces.
+\def\longmath@stack{}
+
+% Create a unique tag.
+\def\longmath@unique{
+ \global\advance\longmath@tagnum\@ne
+ \global\edef\longmath@stack{@\the\longmath@tagnum\space\longmath@stack}
+}
+
+% Create an invisible closing delimiter.
+\def\pushdelimiter{\longmath@unique
+ \expandafter\longmath@push\longmath@stack\relax}
+
+\def\longmath@push #1 #2\relax{\ifmmode\begingroup
+ \def\longmath@comm{mid:#1} \let\longmath@type\mathclose \longmath@yscale=1\relax
+ \longmath@delimE . \fi}
+
+% Create an invisible opening delimiter.
+\def\pulldelimiter{\ifx\longmath@stack\empty \longmath@unique \fi
+ \expandafter\longmath@pull\longmath@stack\relax}
+
+\def\longmath@pull #1 #2\relax{\global\edef\longmath@stack{#2}\ifmmode\begingroup
+ \def\longmath@comm{mid:#1} \let\longmath@type\mathopen \longmath@yscale=1\relax
+ \longmath@delimE . \fi}
+
+
+% Read width argument for the longmath environment.
+% Expects to numbers or dimensions terminted by +.
+% Numbers are multplied by \hsize.
+\def\longmath@gobble #1\relax{}
+\def\longmath@setwidth #1+#2+#3\relax{
+ \afterassignment\longmath@gobble \longmath@width = #1 \hsize \relax
+ \afterassignment\longmath@gobble \longmath@extra = #2 \hsize \relax }
+
+% Alignment for the longmath box.
+\let\longmath@align@t@\vtop
+\let\longmath@align@b@\vbox
+\let\longmath@align@c@\vcenter
+
+% Save and restore the current math style.
+\newcount\longmath@style
+\def\longmath@getstyle{\longmath@style = \mathstyle \relax}
+\def\longmath@setstyle{\ifcase\longmath@style \relax \displaystyle \or \crampeddisplaystyle \or
+ \textstyle \or \crampedtextstyle \or \scriptstyle \or \crampedscriptstyle \or
+ \scriptscriptstyle \or \crampedscriptscriptstyle \else \textstyle \fi }
+
+% Begin of longmath environment.
+% Set alignment and width and save the current math style.
+% Then start a horizontal box in math mode.
+\def\longmath@begin #1#2{\begingroup
+ \expandafter\let\expandafter\longmath@align\csname longmath@align@#1@\endcsname
+ \ifx \longmath@align\relax \let\longmath@align\longmath@align@c@ \fi
+ \longmath@getstyle \longmath@setwidth #2+0+\relax
+ %\rlap{\textcolor{green}{\rule{\longmath@width}{0.1pt}}\textcolor{yellow}{\rule{\longmath@extra}{0.1pt}}}
+ \setbox\longmath@box\hbox\bgroup $ \mathpenaltiesmode = 1 \longmath@setstyle
+}
+
+% End of longmath environment.
+% If the width of the box is smaller than the requsted width, just use it as is.
+% Otherwise, modify the requested width if the * version was used.
+% Then \unhbox the content into a paragraph with the requested width.
+% \box0 and \box1 are used to adapt the baselineskip to the math style.
+% \leftskip and \rightskip are set such that lines are centered but not justified.
+% The glue is removed from the beginning of the first line with \hskip,
+% and from the end of the last line with \parfilskip.
+% If \longmath@extra is non-zero, Lua will stack the lines of the paragraph.
+\def\longmath@end{$\egroup
+ \ifdim \wd\longmath@box < \dimexpr \longmath@width + \longmath@extra \relax
+ \longmath@extra = \z@
+ \box\longmath@box
+ \else
+ \iflongmath@star
+ \longmath@lines = \dimexpr ( 1.1\wd\longmath@box + 0.5\longmath@width ) / \longmath@width \relax
+ \longmath@width = \dimexpr ( 1.1\wd\longmath@box / \longmath@lines )
+ \fi
+ \setbox0\hbox{$1$}
+ \setbox1\hbox{$\longmath@setstyle 1$}
+ \longmath@align{\hsize\longmath@width
+ \baselineskip \dimexpr \baselineskip * \ht1 / \ht0 \relax \lineskip\z@ \lineskiplimit\z@
+ \openup \longmathlinesep
+ \leftskip 0pt plus 0.6\longmath@width
+ \rightskip 0pt plus 0.6\longmath@width
+ \parfillskip-\rightskip \noindent\hskip-\leftskip \unhbox\longmath@box}
+ \longmath@extra = \z@
+ \fi \endgroup
+}
+
+\newenvironment{longmath}[2][c]
+ {\longmath@starfalse\longmath@begin{#1}{#2}}{\longmath@end}
+\newenvironment{longmath*}[2][c]
+ {\longmath@startrue\longmath@begin{#1}{#2}}{\longmath@end}
+
+% Read the dimensions of tagged groups from the aux file.
+\def\longmath@group#1#2{\directlua{longmath.read_group({#1},{#2})}}
+\def\longmath@skip#1#2{}
+
+% Save the dimensions of the tagged groups at the end of the document.
+\def\longmath@save{\let\longmath@group\longmath@skip\directlua{longmath.save_groups(\the\@auxout)}}
+\AtEndDocument{\longmath@save}
+
+
+
+
+
+