1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
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
|