math-noa.lmt /size: 106 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['math-noa'] = {
2    version   = 1.001,
3    optimize  = true,
4    comment   = "companion to math-ini.mkiv",
5    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
6    copyright = "PRAGMA ADE / ConTeXt Development Team",
7    license   = "see context related readme files"
8}
9
10-- TODO: SET CLASSES !
11
12-- if specials and (specials[1] == "char" or specials[1] == "font") then -- can we avoid this
13-- ... better create a reverse mapping from the already present vectors
14
15-- beware: this is experimental code and there will be a more generic (attribute value
16-- driven) interface too but for the moment this is ok (sometime in 2015-2016 i will
17-- start cleaning up as by then the bigger picture is clear and code has been used for
18-- years; the main handlers will get some extensions)
19--
20-- we will also make dedicated processors (faster)
21--
22-- beware: names will change as we wil make noads.xxx.handler i.e. xxx
23-- subnamespaces
24
25-- 20D6 -> 2190
26-- 20D7 -> 2192
27
28-- todo: most is mathchar_code so we can have simple dedicated loops
29
30-- nota bene: uunderdelimiter uoverdelimiter etc are radicals (we have 5 types)
31
32local next, tonumber = next, tonumber
33local formatters, gmatch, match = string.formatters, string.gmatch, string.match
34local insert, remove, concat, sortedhash = table.insert, table.remove, table.concat, table.sortedhash
35local div, round = math.div, math.round
36
37local fonts              = fonts
38local nodes              = nodes
39local node               = node
40local mathematics        = mathematics
41
42local privateattribute   = attributes.private
43local registertracker    = trackers.register
44local registerdirective  = directives.register
45local logreporter        = logs.reporter
46local setmetatableindex  = table.setmetatableindex
47
48local texgetmode         = tex.getmode
49local mathmode_code      = tex.modelevels.math
50
51local colortracers       = nodes.tracers.colors
52
53-- most trace/report will move into the closures
54
55local trace_remapping    = false  registertracker("math.remapping",    function(v) trace_remapping    = v end)
56local trace_processing   = false  registertracker("math.processing",   function(v) trace_processing   = v end)
57local trace_analyzing    = false  registertracker("math.analyzing",    function(v) trace_analyzing    = v end)
58local trace_normalizing  = false  registertracker("math.normalizing",  function(v) trace_normalizing  = v end)
59local trace_collapsing   = false  registertracker("math.collapsing",   function(v) trace_collapsing   = v end)
60local trace_goodies      = false  registertracker("math.goodies",      function(v) trace_goodies      = v end)
61
62local check_coverage     = true   registerdirective("math.checkcoverage", function(v) check_coverage   = v     end)
63local use_math_goodies   = true   registerdirective("math.nogoodies",     function(v) use_math_goodies = not v end)
64
65local report_processing  = logreporter("mathematics","processing")
66local report_remapping   = logreporter("mathematics","remapping")
67local report_normalizing = logreporter("mathematics","normalizing")
68local report_collapsing  = logreporter("mathematics","collapsing")
69local report_goodies     = logreporter("mathematics","goodies")
70
71local a_mathrendering    <const> = privateattribute("mathrendering")
72local a_exportstatus     <const> = privateattribute("exportstatus")
73
74local nuts               = nodes.nuts
75local nodepool           = nuts.pool
76local tonut              = nuts.tonut
77local nutstring          = nuts.tostring
78
79local setfield           = nuts.setfield
80local setlink            = nuts.setlink
81local setlist            = nuts.setlist
82local setnext            = nuts.setnext
83local setprev            = nuts.setprev
84local setchar            = nuts.setchar
85local setfam             = nuts.setfam
86local setsubtype         = nuts.setsubtype
87local setattr            = nuts.setattr
88local setattrlist        = nuts.setattrlist
89local setwidth           = nuts.setwidth
90local setheight          = nuts.setheight
91local setdepth           = nuts.setdepth
92local setdelimiter       = nuts.setdelimiter
93local setclass           = nuts.setclass
94
95local getfield           = nuts.getfield
96local getnext            = nuts.getnext
97local getprev            = nuts.getprev
98local getboth            = nuts.getboth
99local isnext             = nuts.isnext
100local isprev             = nuts.isprev
101local isboth             = nuts.isboth
102local getid              = nuts.getid
103local getsubtype         = nuts.getsubtype
104local getchar            = nuts.getchar
105local getfont            = nuts.getfont
106local getfam             = nuts.getfam
107local getcharspec        = nuts.getcharspec
108local getattr            = nuts.getattr
109local getattrs           = nuts.getattrs
110local getlist            = nuts.getlist
111local getwidth           = nuts.getwidth
112local getheight          = nuts.getheight
113local getdepth           = nuts.getdepth
114local getwhd             = nuts.getwhd
115local getdelimiter       = nuts.getdelimiter
116local getleftdelimiter   = nuts.getleftdelimiter
117local getrightdelimiter  = nuts.getrightdelimiter
118local gettopdelimiter    = nuts.gettopdelimiter
119local getbottomdelimiter = nuts.getbottomdelimiter
120local getnumerator       = nuts.getnumerator
121local getdenominator     = nuts.getdenominator
122local getdegree          = nuts.getdegree
123local gettop             = nuts.gettop
124local getmiddle          = nuts.getmiddle
125local getbottom          = nuts.getbottom
126local getchoice          = nuts.getchoice
127
128local getnucleus         = nuts.getnucleus
129local getsub             = nuts.getsub
130local getsup             = nuts.getsup
131local getsubpre          = nuts.getsubpre
132local getsuppre          = nuts.getsuppre
133local getprime           = nuts.getprime
134
135local setnucleus         = nuts.setnucleus
136local setsub             = nuts.setsub
137local setsup             = nuts.setsup
138local setsubpre          = nuts.setsubpre
139local setsuppre          = nuts.setsuppre
140local setprime           = nuts.setprime
141
142local getoffsets         = nuts.getoffsets
143local setoffsets         = nuts.setoffsets
144
145local getscripts         = nuts.getscripts
146local setscripts         = nuts.setscripts
147
148local getoptions         = nuts.getoptions
149local setoptions         = nuts.setoptions
150
151local setprop            = nuts.setprop
152
153local flushnode          = nuts.flush
154local copy_node          = nuts.copy
155local slide_nodes        = nuts.slide
156local set_visual         = nuts.setvisual
157
158local mlisttohlist       = nuts.mlisttohlist
159
160local new_kern           = nodepool.kern
161local new_submlist       = nodepool.submlist
162local new_noad           = nodepool.noad
163local new_delimiter      = nodepool.delimiter
164local new_fence          = nodepool.fence
165
166local fonthashes         = fonts.hashes
167local fontdata           = fonthashes.identifiers
168local fontcharacters     = fonthashes.characters
169local fontitalics        = fonthashes.italics
170local fontparameters     = fonthashes.parameters
171
172local variables          = interfaces.variables
173local texsetattribute    = tex.setattribute
174local texgetattribute    = tex.getattribute
175local getfontoffamily    = tex.getfontoffamily
176local unsetvalue         <const> = attributes.unsetvalue
177local implement          = interfaces.implement
178
179local v_reset            <const> = variables.reset
180local v_small            <const> = variables.small
181local v_medium           <const> = variables.medium
182local v_big              <const> = variables.big
183local v_line             <const> = variables.line
184
185local chardata           = characters.data
186
187noads                    = noads or { }  -- todo: only here
188local noads              = noads
189
190noads.processors         = noads.processors or { }
191local processors         = noads.processors
192
193noads.handlers           = noads.handlers   or { }
194local handlers           = noads.handlers
195
196local tasks              = nodes.tasks
197local enableaction       = tasks.enableaction
198local setaction          = tasks.setaction
199
200local nodecodes          = nodes.nodecodes
201----- noadcodes          = nodes.noadcodes
202----- fencecodes         = nodes.fencecodes
203local classes            = mathematics.classes -- or nodes.noadcodes
204
205local ordinary_class     <const> = classes.ordinary
206local operator_class     <const> = classes.operator
207local binary_class       <const> = classes.binary
208local relation_class     <const> = classes.relation
209local open_class         <const> = classes.open
210local close_class        <const> = classes.close
211local middle_class       <const> = classes.middle
212local punctuation_class  <const> = classes.punctuation
213local fenced_class       <const> = classes.fenced
214local fraction_class     <const> = classes.fraction
215local radical_class      <const> = classes.radical
216local accent_class       <const> = classes.accent
217local numbergroup_class  <const> = classes.numbergroup
218local digit_class        <const> = classes.digit
219
220local noad_code          <const> = nodecodes.noad
221local accent_code        <const> = nodecodes.accent
222local radical_code       <const> = nodecodes.radical
223local fraction_code      <const> = nodecodes.fraction
224local subbox_code        <const> = nodecodes.subbox
225local submlist_code      <const> = nodecodes.submlist
226local mathchar_code      <const> = nodecodes.mathchar
227local mathtextchar_code  <const> = nodecodes.mathtextchar
228local delimiter_code     <const> = nodecodes.delimiter
229----- style_code         <const> = nodecodes.style
230----- parameter_code     <const> = nodecodes.parameter
231local math_choice        <const> = nodecodes.choice
232local fence_code         <const> = nodecodes.fence
233
234-- this initial stuff is tricky as we can have removed and new nodes with the same address
235-- the only way out is a free-per-page list of nodes (not bad anyway)
236
237-- local gf = getfield local gt = setmetatableindex("number") getfield = function(n,f)   gt[f] = gt[f] + 1 return gf(n,f)   end mathematics.GETFIELD = gt
238-- local sf = setfield local st = setmetatableindex("number") setfield = function(n,f,v) st[f] = st[f] + 1        sf(n,f,v) end mathematics.SETFIELD = st
239
240-- TODO : get rid of done
241
242local function process(start,what,n,parent)
243
244    if n then
245        n = n + 1
246    else
247        n = 0
248    end
249    --
250    local initial = start
251    --
252--     slide_nodes(start) -- we still miss a prev in noads -- fences test code
253    --
254    while start do
255        local noad = nil
256        local id   = getid(start)
257        if trace_processing then
258            if id == noad_code then
259                report_processing("%w%S, class %a",n*2,nutstring(start),classes[getsubtype(start)])
260            elseif id == mathchar_code then
261                local char, font, fam = getcharspec(start)
262                report_processing("%w%S, family %a, font %a, char %a, shape %c",n*2,nutstring(start),fam,font,char,char)
263            else
264                report_processing("%w%S",n*2,nutstring(start))
265            end
266        end
267        local proc = what[id]
268        if proc then
269         -- report_processing("start processing")
270            local done, newstart, newinitial = proc(start,what,n,parent) -- prev is bugged:  or getprev(start)
271            if newinitial then
272                initial = newinitial -- temp hack .. we will make all return head
273                if newstart then
274                    start = newstart
275                 -- report_processing("stop processing (new start)")
276                else
277                 -- report_processing("quit processing (done)")
278                    break
279                end
280            else
281                if newstart then
282                    start = newstart
283                 -- report_processing("stop processing (new start)")
284                else
285                 -- report_processing("stop processing")
286                end
287            end
288        elseif id == noad_code then
289            -- single characters are like this
290            noad = getnucleus(start)        if noad then process(noad,what,n,start) end -- list
291            noad = getsup(start)            if noad then process(noad,what,n,start) end -- list
292            noad = getsub(start)            if noad then process(noad,what,n,start) end -- list
293            noad = getsuppre(start)         if noad then process(noad,what,n,start) end -- list
294            noad = getsubpre(start)         if noad then process(noad,what,n,start) end -- list
295            noad = getprime(start)          if noad then process(noad,what,n,start) end -- list
296        elseif id == mathchar_code or id == mathtextchar_code or id == delimiter_code then
297            break
298        elseif id == subbox_code or id == submlist_code then
299            noad = getlist(start)            if noad then process(noad,what,n,start) end -- list (not getlist !)
300        elseif id == fraction_code then
301            noad = getnumerator(start)       if noad then process(noad,what,n,start) end -- list
302            noad = getdenominator(start)     if noad then process(noad,what,n,start) end -- list
303            noad = getleftdelimiter(start)   if noad then process(noad,what,n,start) end -- delimiter
304            noad = getdelimiter(start)       if noad then process(noad,what,n,start) end -- delimiter
305            noad = getrightdelimiter(start)  if noad then process(noad,what,n,start) end -- delimiter
306        elseif id == fence_code then
307            noad = getdelimiter(start)       if noad then process(noad,what,n,start) end -- delimiter
308            noad = gettop(start)             if noad then process(noad,what,n,start) end -- list
309            noad = getbottom(start)          if noad then process(noad,what,n,start) end -- list
310        elseif id == radical_code then
311            noad = getnucleus(start)         if noad then process(noad,what,n,start) end -- list
312            noad = getsup(start)             if noad then process(noad,what,n,start) end -- list
313            noad = getsub(start)             if noad then process(noad,what,n,start) end -- list
314            noad = getsuppre(start)          if noad then process(noad,what,n,start) end -- list
315            noad = getsubpre(start)          if noad then process(noad,what,n,start) end -- list
316            noad = getprime(start)           if noad then process(noad,what,n,start) end -- delimiter
317            noad = getleftdelimiter(start)   if noad then process(noad,what,n,start) end -- delimiter
318            noad = getrightdelimiter(start)  if noad then process(noad,what,n,start) end -- delimiter
319            noad = gettopdelimiter(start)    if noad then process(noad,what,n,start) end -- delimiter
320            noad = getbottomdelimiter(start) if noad then process(noad,what,n,start) end -- delimiter
321            noad = getdegree(start)          if noad then process(noad,what,n,start) end -- list
322        elseif id == accent_code then
323            noad = getnucleus(start)         if noad then process(noad,what,n,start) end -- list
324            noad = getsup(start)             if noad then process(noad,what,n,start) end -- list
325            noad = getsub(start)             if noad then process(noad,what,n,start) end -- list
326            noad = getsuppre(start)          if noad then process(noad,what,n,start) end -- list
327            noad = getsubpre(start)          if noad then process(noad,what,n,start) end -- list
328            noad = getprime(start)           if noad then process(noad,what,n,start) end -- list
329            noad = gettop(start)             if noad then process(noad,what,n,start) end -- list
330            noad = getmiddle(start)          if noad then process(noad,what,n,start) end -- list
331            noad = getbottom(start)          if noad then process(noad,what,n,start) end -- list
332        elseif id == math_choice then
333            noad = getchoice(start,1)        if noad then process(noad,what,n,start) end -- list
334            noad = getchoice(start,2)        if noad then process(noad,what,n,start) end -- list
335            noad = getchoice(start,3)        if noad then process(noad,what,n,start) end -- list
336            noad = getchoice(start,4)        if noad then process(noad,what,n,start) end -- list
337     -- elseif id == style_code then
338     --     -- has a next
339     -- elseif id == parameter_code then
340     --     -- has a next
341     -- else
342     --     -- glue, penalty, etc
343        end
344        start = getnext(start)
345    end
346    if not parent then
347        return initial -- only first level -- for now
348    end
349end
350
351local function processnested(current,what,n)
352    local noad = nil
353    local id   = getid(current)
354    if id == noad_code then
355        noad = getnucleus(current)         if noad then process(noad,what,n,current) end -- list
356        noad = getsup(current)             if noad then process(noad,what,n,current) end -- list
357        noad = getsub(current)             if noad then process(noad,what,n,current) end -- list
358        noad = getsuppre(current)          if noad then process(noad,what,n,current) end -- list
359        noad = getsubpre(current)          if noad then process(noad,what,n,current) end -- list
360        noad = getprime(current)           if noad then process(noad,what,n,current) end -- list
361    elseif id == subbox_code or id == submlist_code then
362        noad = getlist(current)            if noad then process(noad,what,n,current) end -- list (not getlist !)
363    elseif id == fraction_code then
364        noad = getnumerator(current)       if noad then process(noad,what,n,current) end -- list
365        noad = getdenominator(current)     if noad then process(noad,what,n,current) end -- list
366        noad = getleftdelimiter(current)   if noad then process(noad,what,n,current) end -- delimiter
367        noad = getdelimiter(current)       if noad then process(noad,what,n,current) end -- delimiter
368        noad = getrightdelimiter(current)  if noad then process(noad,what,n,current) end -- delimiter
369    elseif id == fence_code then
370        noad = getdelimiter(current)       if noad then process(noad,what,n,current) end -- delimiter
371        noad = gettop(current)             if noad then process(noad,what,n,current) end -- list
372        noad = getbottom(current)          if noad then process(noad,what,n,current) end -- list
373    elseif id == radical_code then
374        noad = getnucleus(current)         if noad then process(noad,what,n,current) end -- list
375        noad = getsup(current)             if noad then process(noad,what,n,current) end -- list
376        noad = getsub(current)             if noad then process(noad,what,n,current) end -- list
377        noad = getsuppre(current)          if noad then process(noad,what,n,current) end -- list
378        noad = getsubpre(current)          if noad then process(noad,what,n,current) end -- list
379        noad = getprime(current)           if noad then process(noad,what,n,current) end -- list
380        noad = getleftdelimiter(current)   if noad then process(noad,what,n,current) end -- delimiter
381        noad = getrightdelimiter(current)  if noad then process(noad,what,n,current) end -- delimiter
382        noad = gettopdelimiter(current)    if noad then process(noad,what,n,current) end -- delimiter
383        noad = getbottomdelimiter(current) if noad then process(noad,what,n,current) end -- delimiter
384        noad = getdegree(current)          if noad then process(noad,what,n,current) end -- list
385    elseif id == accent_code then
386        noad = getnucleus(current)         if noad then process(noad,what,n,current) end -- list
387        noad = getsup(current)             if noad then process(noad,what,n,current) end -- list
388        noad = getsub(current)             if noad then process(noad,what,n,current) end -- list
389        noad = getsuppre(current)          if noad then process(noad,what,n,current) end -- list
390        noad = getsubpre(current)          if noad then process(noad,what,n,current) end -- list
391        noad = getprime(current)           if noad then process(noad,what,n,current) end -- list
392        noad = gettop(current)             if noad then process(noad,what,n,current) end -- list
393        noad = getmiddle(current)          if noad then process(noad,what,n,current) end -- list
394        noad = getbottom(current)          if noad then process(noad,what,n,current) end -- list
395    elseif id == math_choice then
396        noad = getchoice(start,1)          if noad then process(noad,what,n,current) end -- list
397        noad = getchoice(start,2)          if noad then process(noad,what,n,current) end -- list
398        noad = getchoice(start,3)          if noad then process(noad,what,n,current) end -- list
399        noad = getchoice(start,4)          if noad then process(noad,what,n,current) end -- list
400    end
401end
402
403local function processstep(current,process,n,id)
404    local noad = nil
405    local id   = id or getid(current)
406    if id == noad_code then
407        noad = getnucleus(current)         if noad then process(noad,n,current) end -- list
408        noad = getsup(current)             if noad then process(noad,n,current) end -- list
409        noad = getsub(current)             if noad then process(noad,n,current) end -- list
410        noad = getsuppre(current)          if noad then process(noad,n,current) end -- list
411        noad = getsubpre(current)          if noad then process(noad,n,current) end -- list
412        noad = getprime(current)           if noad then process(noad,n,current) end -- list
413    elseif id == subbox_code or id == submlist_code then
414        noad = getlist(current)            if noad then process(noad,n,current) end -- list (not getlist !)
415    elseif id == fraction_code then
416        noad = getnumerator(current)       if noad then process(noad,n,current) end -- list
417        noad = getdenominator(current)     if noad then process(noad,n,current) end -- list
418        noad = getleftdelimiter(current)   if noad then process(noad,n,current) end -- delimiter
419        noad = getdelimiter(current)       if noad then process(noad,n,current) end -- delimiter
420        noad = getrightdelimiter(current)  if noad then process(noad,n,current) end -- delimiter
421    elseif id == fence_code then
422        noad = getdelimiter(current)       if noad then process(noad,n,current) end -- delimiter
423        noad = gettop(current)             if noad then process(noad,n,current) end -- list
424        noad = getbottom(current)          if noad then process(noad,n,current) end -- list
425    elseif id == radical_code then
426        noad = getnucleus(current)         if noad then process(noad,n,current) end -- list
427        noad = getsup(current)             if noad then process(noad,n,current) end -- list
428        noad = getsub(current)             if noad then process(noad,n,current) end -- list
429        noad = getsuppre(current)          if noad then process(noad,n,current) end -- list
430        noad = getsubpre(current)          if noad then process(noad,n,current) end -- list
431        noad = getprime(current)           if noad then process(noad,n,current) end -- delimiter
432        noad = getleftdelimiter(current)   if noad then process(noad,n,current) end -- delimiter
433        noad = getrightdelimiter(current)  if noad then process(noad,n,current) end -- delimiter
434        noad = gettopdelimiter(current)    if noad then process(noad,n,current) end -- delimiter
435        noad = getbottomdelimiter(current) if noad then process(noad,n,current) end -- delimiter
436        noad = getdegree(current)          if noad then process(noad,n,current) end -- list
437    elseif id == accent_code then
438        noad = getnucleus(current)         if noad then process(noad,n,current) end -- list
439        noad = getsup(current)             if noad then process(noad,n,current) end -- list
440        noad = getsub(current)             if noad then process(noad,n,current) end -- list
441        noad = getsuppre(current)          if noad then process(noad,n,current) end -- list
442        noad = getsubpre(current)          if noad then process(noad,n,current) end -- list
443        noad = getprime(current)           if noad then process(noad,n,current) end -- list
444        noad = gettop(current)             if noad then process(noad,n,current) end -- list
445        noad = getmiddle(current)          if noad then process(noad,n,current) end -- list
446        noad = getbottom(current)          if noad then process(noad,n,current) end -- list
447    elseif id == math_choice then
448        noad = getchoice(start,1)          if noad then process(noad,n,current) end -- list
449        noad = getchoice(start,2)          if noad then process(noad,n,current) end -- list
450        noad = getchoice(start,3)          if noad then process(noad,n,current) end -- list
451        noad = getchoice(start,4)          if noad then process(noad,n,current) end -- list
452    end
453end
454
455local function processnoads(head,actions,banner)
456    if trace_processing then
457        report_processing("start %a",banner)
458        head = process(head,actions)
459        report_processing("stop %a",banner)
460    else
461        head = process(head,actions)
462    end
463    return head
464end
465
466noads.process       = processnoads
467noads.processnested = processnested
468noads.processouter  = process
469
470-- experiment (when not present fall back to fam 0) -- needs documentation
471
472local unknowns = setmetatableindex("table")
473local checked  = setmetatableindex("table")
474local cached   = setmetatableindex("table") -- complex case
475
476local tracked  = false  trackers.register("fonts.missing", function(v) tracked = v end)
477
478local variantselectors = {
479    [0xFE00] = true,
480    [0xFE01] = true
481}
482
483local function errorchar(font,char)
484    if variantselectors[char] then
485        return char
486    end
487    local done = unknowns[font]
488    done[char] = (done[char] or 0) + 1
489    if tracked then
490        -- slower as we check each font too and we always replace as math has
491        -- more demands than text
492        local fake = cached[font][char]
493        if fake then
494            return fake
495        else
496            local kind, fake = fonts.checkers.placeholder(font,char)
497            if not fake or kind ~= "char" then -- Also check for "with" here?
498                fake = 0x3F
499            end
500            cached[font][char] = fake
501            return fake
502        end
503    else
504        -- only simple checking, report at the end so one should take
505        -- action anyway ... we can miss a few checks but that is ok
506        -- as there is at least one reported
507        if not checked[font][char] then
508            if trace_normalizing then
509                report_normalizing("character %C is not available",char)
510            end
511            checked[font][char] = true
512        end
513        return 0x3F
514    end
515end
516
517-- 0-2 regular
518-- 3-5 bold
519-- 6-8 pseudobold
520
521-- this could best be integrated in the remapper, and if we run into problems, we
522-- might as well do this
523
524do
525
526    local families     = { }
527    local a_mathfamily <const> = privateattribute("mathfamily")
528    local boldmap      = mathematics.boldmap
529
530    local trace_families  = false  registertracker("math.families", function(v) trace_families = v end)
531    local report_families = logreporter("mathematics","families")
532
533    local familymap = { [0] =
534        "regular",
535        "regular",
536        "regular",
537        "bold",
538        "bold",
539        "bold",
540        "pseudobold",
541        "pseudobold",
542        "pseudobold",
543    }
544
545    families[noad_code] = function(pointer,what,n,parent)
546        local a = getattr(pointer,a_mathfamily)
547        if a and a >= 0 then
548            if a > 0 then
549                setattr(pointer,a_mathfamily,0)
550                if a > 5 then
551                    a = a - 3
552                end
553            end
554            setfam(pointer,a)
555        end
556        processnested(pointer,families,n+1)
557    end
558
559    function mathematics.familyname(n)
560        return familymap[n] or "regular"
561    end
562
563    families[fraction_code] = families[noad_code]
564    families[accent_code]   = families[noad_code] -- added 2023-07
565    families[radical_code]  = families[noad_code] -- added 2023-07
566    families[fence_code]    = families[noad_code] -- added 2023-07
567
568    families[mathchar_code] = function(pointer)
569        if getfam(pointer) == 0 then
570            local a = getattr(pointer,a_mathfamily)
571            if a and a > 0 then
572                setattr(pointer,a_mathfamily,0)
573                if a > 5 then
574                    local char = getchar(pointer)
575                    local bold = boldmap[char]
576                    local newa = a - 3
577                    if not bold then
578                        if trace_families then
579                            report_families("no bold replacement for %C, family %s with remap %s becomes %s with remap %s",char,a,familymap[a],newa,familymap[newa])
580                        end
581                        setfam(pointer,newa)
582                    elseif not fontcharacters[getfontoffamily(newa)][bold] then
583                        if trace_families then
584                            report_families("no bold character for %C, family %s with remap %s becomes %s with remap %s",char,a,familymap[a],newa,familymap[newa])
585                        end
586                        if newa > 3 then
587                            setfam(pointer,newa-3)
588                        end
589                    else
590                        setattr(pointer,a_exportstatus,char)
591                        setchar(pointer,bold)
592                        if trace_families then
593                            report_families("replacing %C by bold %C, family %s with remap %s becomes %s with remap %s",char,bold,a,familymap[a],newa,familymap[newa])
594                        end
595                        setfam(pointer,newa)
596                    end
597                else
598                    local char = getchar(pointer)
599                    if not fontcharacters[getfontoffamily(a)][char] then
600                        if trace_families then
601                            report_families("no bold replacement for %C",char)
602                        end
603                    else
604                        if trace_families then
605                            report_families("family of %C becomes %s with remap %s",char,a,familymap[a])
606                        end
607                        setfam(pointer,a)
608                    end
609                end
610            end
611        end
612    end
613
614    -- has become:
615
616    families[delimiter_code] = function(pointer)
617        if getfam(pointer) == 0 then
618            local a = getattr(pointer,a_mathfamily)
619            if a and a > 0 then
620                setattr(pointer,a_mathfamily,0)
621                if a > 5 then
622                    -- no bold delimiters in unicode
623                    a = a - 3
624                end
625                local char = getchar(pointer)
626                local okay = fontcharacters[getfontoffamily(a)][char]
627                if okay then
628                    setfam(pointer,a)
629                elseif a > 2 then
630                    setfam(pointer,a-3)
631                end
632            else
633                setfam(pointer,0)
634            end
635        end
636    end
637
638    families[mathtextchar_code] = families[mathchar_code]
639
640    function handlers.families(head,style,penalties)
641        processnoads(head,families,"families")
642    end
643
644end
645
646-- character remapping
647
648do
649
650    local a_mathalphabet       <const> = privateattribute("mathalphabet")
651    local a_mathgreek          <const> = privateattribute("mathgreek")
652
653    local relocate             = { }
654
655    local remapalphabets       = mathematics.remapalphabets
656    local fallbackstyleattr    = mathematics.fallbackstyleattr
657    local fallbackalphabetattr = mathematics.fallbackalphabetattr
658
659    local setnodecolor = colortracers.set
660
661    local function report_remap(tag,id,old,new,extra)
662        if new then
663            report_remapping("remapping %s in font (%s,%s) from %C to %C%s",
664                tag,id,fontdata[id].properties.fontname or "",old,new,extra)
665        else
666            -- error !
667            report_remapping("remapping %s in font (%s,%s) from %C to ?",
668                tag,id,fontdata[id].properties.fontname or "",old)
669        end
670    end
671
672    local function checked(pointer)
673        local char, font = getcharspec(pointer)
674        local data = fontcharacters[font]
675        if not data[char] then
676            local specials = chardata[char].specials
677            if specials and (specials[1] == "char" or specials[1] == "font") then
678                local newchar = specials[#specials]
679                if trace_remapping then
680                    report_remap("fallback",font,char,newchar)
681                end
682                if trace_analyzing then
683                    setnodecolor(pointer,"font:isol")
684                end
685                setattr(pointer,a_exportstatus,char) -- testcase: exponentiale
686                setchar(pointer,newchar)
687                return true
688            end
689        end
690    end
691
692    -- We can optimize this if we really think that math is a bottleneck which it never
693    -- really is. Beware: the font is the text font in the family, so we only check the
694    -- text font here.
695
696    relocate[mathchar_code] = function(pointer)
697        local greek, alpha = getattrs(pointer,a_mathgreek,a_mathalphabet)
698        local char, font, fam = getcharspec(pointer)
699        local characters = fontcharacters[font]
700        if not alpha then alpha = 0 end
701        if not greek then greek = 0 end
702        if alpha > 0 or greek > 0 then
703            if alpha > 0 then
704                -- not really critital but we could use properties
705                setattr(pointer,a_mathgreek,0)
706            end
707            if greek > 0 then
708                -- not really critital but we could use properties
709                setattr(pointer,a_mathalphabet,0)
710            end
711            local newchar = remapalphabets(char,alpha,greek)
712            if newchar then
713                local newchardata = characters[newchar]
714                if newchardata then
715                    if trace_remapping then
716                        report_remap("char",font,char,newchar,newchardata.commands and " (virtual)" or "")
717                    end
718                    if trace_analyzing then
719                        setnodecolor(pointer,"font:isol")
720                    end
721                    setchar(pointer,newchar)
722                    return true
723                else
724                    local fallback = fallbackstyleattr(alpha)
725                    if fallback then
726                        local newchar = remapalphabets(char,fallback,greek)
727                        if newchar then
728                            if characters[newchar] then
729                                if trace_remapping then
730                                    report_remap("char",font,char,newchar," (fallback remapping used)")
731                                end
732                                if trace_analyzing then
733                                    setnodecolor(pointer,"font:isol")
734                                end
735                                setchar(pointer,newchar)
736                                return true
737                            elseif trace_remapping then
738                                report_remap("char",font,char,newchar," fails (no fallback character)")
739                            end
740                        elseif trace_remapping then
741                            report_remap("char",font,char,newchar," fails (no fallback remap character)")
742                        end
743                    elseif trace_remapping then
744                        report_remap("char",font,char,newchar," fails (no fallback style)")
745                    end
746                end
747            elseif trace_remapping then
748                local chardata = characters[char]
749                if chardata and chardata.commands then
750                    report_remap("char",font,char,char," (virtual)")
751                end
752            end
753        end
754        if not characters[char] then
755            local fnt = getfontoffamily(fam,1)
756            setchar(pointer,errorchar(font,char))
757            if font ~= fnt then
758                errorchar(fnt,char)
759                errorchar(getfontoffamily(fam,2),char)
760            end
761        end
762        if trace_analyzing then
763            setnodecolor(pointer,"font:medi")
764        end
765        if check_coverage then
766            return checked(pointer)
767        end
768    end
769
770    relocate[mathtextchar_code] = function(pointer)
771        if trace_analyzing then
772            setnodecolor(pointer,"font:init")
773        end
774    end
775
776    relocate[delimiter_code] = function(pointer)
777        if trace_analyzing then
778            setnodecolor(pointer,"font:fina")
779        end
780    end
781
782    function handlers.relocate(head,style,penalties)
783        processnoads(head,relocate,"relocate")
784    end
785
786end
787
788-- rendering (beware, not exported)
789
790do
791
792    local render     = { }
793
794    local rendersets = mathematics.renderings.numbers or { } -- store
795
796    render[mathchar_code] = function(pointer)
797        local attr = getattr(pointer,a_mathrendering)
798        if attr and attr > 0 then
799            local char, font = getcharspec(pointer)
800            local renderset = rendersets[attr]
801            if renderset then
802                local newchar = renderset[char]
803                if newchar then
804                    local characters = fontcharacters[font]
805                    if characters and characters[newchar] then
806                        setchar(pointer,newchar)
807                        setattr(pointer,a_exportstatus,char) -- yes or no
808                    end
809                end
810            end
811        end
812    end
813
814    function handlers.render(head,style,penalties)
815        processnoads(head,render,"render")
816    end
817
818end
819
820-- -- We now use the engine feature and a bit of different lua interfacing to it but
821-- -- that code is located elsewhere.
822--
823-- do
824--
825--     local a_mathsize <const> = privateattribute("mathsize") -- this might move into other fence code
826--     local resize     = { }
827--
828--     resize[fence_code] = function(pointer)
829--         local subtype = getsubtype(pointer)
830--      -- if subtype == leftfence_code or subtype == rightfence_code then
831--             local a = getattr(pointer,a_mathsize)
832--             if a and a > 0 then
833--                 local method = div(a,100)
834--                 local size   = a % 100
835--                 setattr(pointer,a_mathsize,0)
836--                 if size ~= 0 then
837--                     local delimiter = getdelimiter(pointer)
838--                     if delimiter then
839--                         local oldchar, font, fam = getcharspec(delimiter)
840--                         if oldchar > 0 and font > 0 then
841--                             local ht         = getheight(pointer)
842--                             local dp         = getdepth(pointer)
843--                             local data       = fontdata[font]
844--                             local characters = data.characters
845--                             local olddata    = characters[oldchar]
846--                             if olddata then
847--                                 local template  = olddata.varianttemplate
848--                                 local newchar   = mathematics.big(data,template or oldchar,size,method)
849--                                 local newdata   = characters[newchar]
850--                                 local newheight = newdata.height or 0
851--                                 local newdepth  = newdata.depth or 0
852--                                 if template then
853--                                     setheight(pointer,newheight)
854--                                     setdepth(pointer,newdepth)
855--                                     if not olddata.extensible then
856--                                         -- check this on bonum and antykwa
857--                                         setoptions(pointer,0)
858--                                     end
859--                                     setoptions(pointer,getoptions(pointer)| tex.noadoptioncodes.scale                        )
860--                                     if trace_fences then
861--                                         report_fences("replacing %C using method %a, size %a and template %C",newchar,method,size,template)
862--                                     end
863--                                 else
864--                                     -- 1 scaled point is a signal, for now
865--                                     if ht == 1 then
866--                                         setheight(pointer,newheight)
867--                                     end
868--                                     if dp == 1 then
869--                                         setdepth(pointer,newdepth)
870--                                     end
871--                                     setchar(delimiter,newchar)
872--                                     if trace_fences then
873--                                         report_fences("replacing %C by %C using method %a and size %a",oldchar,char,method,size)
874--                                     end
875--                                 end
876--                             elseif trace_fences then
877--                                 report_fences("not replacing %C using method %a and size %a",oldchar,method,size)
878--                             end
879--                         end
880--                     end
881--                 end
882--             end
883--      -- end
884--     end
885--
886--     function handlers.resize(head,style,penalties)
887--         processnoads(head,resize,"resize")
888--     end
889--
890-- end
891
892-- still not perfect:
893
894do
895
896    local trace_fences     = false  registertracker("math.fences", function(v) trace_fences = v end)
897    local report_fences    = logreporter("mathematics","fences")
898
899    local a_autofence      <const> = privateattribute("mathautofence")
900    local autofences       = { }
901    local dummyfencechar   = 0x2E
902
903    local fencecodes       = nodes.fencecodes
904    local leftfence_code   <const> = fencecodes.left
905    local middlefence_code <const> = fencecodes.middle
906    local rightfence_code  <const> = fencecodes.right
907
908    local function makefence(what,char,template)
909        local d = new_delimiter()
910        local f = new_fence()
911        if char then
912            local sym = getnucleus(char)
913            local chr, fnt, fam = getcharspec(sym)
914            if chr == dummyfencechar then
915                chr = 0
916            end
917            setchar(d,chr)
918            setfam(d,fam)
919            flushnode(sym)
920        end
921        setattrlist(d,template)
922        setattrlist(f,template)
923        setsubtype(f,what)
924        setdelimiter(f,d)
925        setclass(f,-1) -- tex itself does this, so not fenceclasses[what]
926        return f
927    end
928
929    local function show(where,pointer)
930        print("")
931        local i = 0
932        for n in nuts.traverse(pointer) do
933            i = i + 1
934            print(i,where,nuts.tonode(n))
935        end
936        print("")
937    end
938
939    local function makelist(middle,noad,f_o,o_next,c_prev,f_c)
940-- report_fences(
941--     "middle %s, noad %s, open %s, opennext %s, closeprev %s, close %s",
942--     middle or "?",
943--     noad   or "?",
944--     f_o    or "?",
945--     o_next or "?",
946--     c_prev or "?",
947--     f_c    or "?"
948-- )
949        local list = new_submlist()
950        setsubtype(noad,fenced_class)
951        setnucleus(noad,list)
952        setattrlist(list,noad)
953        setlist(list,f_o)
954        setlink(f_o,o_next) -- prev of list is nil
955        setlink(c_prev,f_c) -- next of list is nil
956-- show("list",f_o)
957        if middle and next(middle) then
958            local prev    = f_o
959            local current = o_next
960            while current ~= f_c do
961                local midl = middle[current]
962                local next = getnext(current)
963                if midl then
964                    local fence = makefence(middlefence_code,current,current)
965                    setnucleus(current)
966                    flushnode(current)
967                    middle[current] = nil
968                    -- replace_node
969                    setlink(prev,fence,next)
970                    prev = fence
971                else
972                    prev = current
973                end
974                current = next
975            end
976        end
977        return noad
978    end
979
980    -- relinking is now somewhat overdone
981
982    local function convert_both(open,close,middle)
983        local o_next = getnext(open)
984        if o_next == close then
985            return close
986        else
987            local c_prev, c_next = getboth(close)
988            local f_o = makefence(leftfence_code,open,open)
989            local f_c = makefence(rightfence_code,close,close)
990            makelist(middle,open,f_o,o_next,c_prev,f_c)
991            setnucleus(close)
992            flushnode(close)
993            -- open is now a list
994            setlink(open,c_next)
995            return open
996        end
997    end
998
999    local function convert_open(open,last,middle) -- last is really last (final case)
1000        local f_o = makefence(leftfence_code,open,open)
1001        local f_c = makefence(rightfence_code,nil,open)
1002        local o_next = getnext(open)
1003        makelist(middle,open,f_o,o_next,last,nil)
1004        -- open is now a list
1005        setlink(open,l_next)
1006        return open
1007    end
1008
1009    local function convert_close(first,close,middle)
1010        local f_o = makefence(leftfence_code,nil,close)
1011        local f_c = makefence(rightfence_code,close)
1012        local c_prev = getprev(close)
1013        local f_next = getnext(first)
1014        makelist(middle,close,f_o,f_next,c_prev,f_c)
1015        -- close is now a list : inner but should be fenced
1016        if c_prev ~= first then
1017            setlink(first,close)
1018        end
1019        return close
1020    end
1021
1022    local stacks = setmetatableindex("table")
1023
1024    -- 1=open 2=close 3=middle 4=both : todo check both
1025
1026    local function processfences(pointer,n,parent)
1027        local current = pointer
1028        local last    = pointer
1029        local start   = pointer
1030        local done    = false
1031        local initial = pointer
1032        local stack   = nil
1033        local middle  = nil -- todo: use properties
1034        while current do
1035-- show("before",pointer)
1036            local id = getid(current)
1037            if id == noad_code then
1038                local a = getattr(current,a_autofence)
1039                if a and a > 0 then
1040                    local stack = stacks[n]
1041                    setattr(current,a_autofence,0) -- hm, better use a property
1042                    local level = #stack
1043                    if a == 1 then
1044                        if trace_fences then
1045                            report_fences("%2i: level %i, handling %s, action %s",n,level,"open","open")
1046                        end
1047                        insert(stack,current)
1048                    elseif a == 2 then
1049                        local open = remove(stack)
1050                        if open then
1051                            if trace_fences then
1052                                report_fences("%2i: level %i, handling %s, action %s",n,level,"close","both")
1053                            end
1054                            local prime, sup, sub, presup, presub = getscripts(current)
1055                            setscripts(current)
1056                            current = convert_both(open,current,middle)
1057                            setscripts(current,prime, sup, sub, presup, presub)
1058                         elseif current == start then
1059                            if trace_fences then
1060                                report_fences("%2i: level %i, handling %s, action %s",n,level,"close","skip")
1061                            end
1062                        else
1063                            if trace_fences then
1064                                report_fences("%2i: level %i, handling %s, action %s",n,level,"close","close")
1065                            end
1066                            local prime, sup, sub, presup, presub = getscripts(current)
1067                            setscripts(current)
1068                            current = convert_close(initial,current,middle)
1069                            setscripts(current,prime, sup, sub, presup, presub)
1070                            if not parent then
1071                                initial = current
1072                            end
1073                        end
1074                    elseif a == 3 then
1075                        if trace_fences then
1076                            report_fences("%2i: level %i, handling %s, action %s",n,level,"middle","middle")
1077                        end
1078                        if middle then
1079                            middle[current] = last
1080                        else
1081                            middle = { [current] = last }
1082                        end
1083                    elseif a == 4 then
1084                        if not stack or #stack == 0 then
1085                            if trace_fences then
1086                                report_fences("%2i: level %i, handling %s, action %s",n,level,"both","open")
1087                            end
1088                            insert(stack,current)
1089                        else
1090                            local open = remove(stack)
1091                            if open then
1092                                if trace_fences then
1093                                    report_fences("%2i: level %i, handling %s, action %s",n,level,"both","both")
1094                                end
1095                                current = convert_both(open,current,middle)
1096                            elseif current == start then
1097                                if trace_fences then
1098                                    report_fences("%2i: level %i, handling %s, action %s",n,level,"both","skip")
1099                                end
1100                            else
1101                                if trace_fences then
1102                                    report_fences("%2i: level %i, handling %s, action %s",n,level,"both","close")
1103                                end
1104                                current = convert_close(initial,current,middle)
1105                                if not parent then
1106                                    initial = current
1107                                end
1108                            end
1109                        end
1110                    end
1111                    done = true
1112                else
1113                    processstep(current,processfences,n+1,id)
1114                end
1115            else
1116                -- next at current level
1117                processstep(current,processfences,n,id)
1118            end
1119-- show("after",pointer)
1120            last    = current
1121            current = getnext(current)
1122        end
1123        if done then
1124            local stack = stacks[n]
1125            local s = #stack
1126            if s > 0 then
1127                for i=1,s do
1128                    local open = remove(stack)
1129                    if trace_fences then
1130                        report_fences("%2i: level %i, handling %s, action %s",n,#stack,"flush","open")
1131                    end
1132                    last = convert_open(open,last,middle)
1133                end
1134-- show("done",pointer)
1135            end
1136        end
1137    end
1138
1139    -- we can have a first changed node .. an option is to have a leading dummy node in math
1140    -- lists like the par node as it can save a  lot of mess
1141
1142    local enabled = false
1143
1144    implement {
1145        name     = "enableautofences",
1146        onlyonce = true,
1147        actions  = function()
1148            enableaction("math","noads.handlers.autofences")
1149            enabled = true
1150        end
1151    }
1152
1153    function handlers.autofences(head,style,penalties)
1154        if enabled then -- tex.modes.c_math_fences_auto
1155         -- inspect(nodes.totree(head))
1156            processfences(head,1)
1157         -- inspect(nodes.totree(head))
1158        end
1159    end
1160
1161end
1162
1163-- normalize scripts
1164
1165do
1166
1167    local unscript     = { }  noads.processors.unscript = unscript
1168    local superscripts = characters.superscripts
1169    local subscripts   = characters.subscripts
1170    local fractions    = characters.fractions
1171    local replaced     = { }
1172
1173    local function replace(pointer,what,n,parent)
1174        pointer = parent -- we're following the parent list (chars trigger this)
1175        local next = getnext(pointer)
1176        local start_super, stop_super, start_sub, stop_sub
1177        local mode = "unset"
1178        while next and getid(next) == noad_code do
1179            local nextnucleus = getnucleus(next)
1180            if nextnucleus and getid(nextnucleus) == mathchar_code and not getsub(next) and not getsup(next) then
1181                local char = getchar(nextnucleus)
1182                local s = superscripts[char]
1183                if s then
1184                    if not start_super then
1185                        start_super = next
1186                        mode = "super"
1187                    elseif mode == "sub" then
1188                        break
1189                    end
1190                    stop_super = next
1191                    next = getnext(next)
1192                    setchar(nextnucleus,s)
1193                    replaced[char] = (replaced[char] or 0) + 1
1194                    if trace_normalizing then
1195                        report_normalizing("superscript %C becomes %C",char,s)
1196                    end
1197                else
1198                    local s = subscripts[char]
1199                    if s then
1200                        if not start_sub then
1201                            start_sub = next
1202                            mode = "sub"
1203                        elseif mode == "super" then
1204                            break
1205                        end
1206                        stop_sub = next
1207                        next = getnext(next)
1208                        setchar(nextnucleus,s)
1209                        replaced[char] = (replaced[char] or 0) + 1
1210                        if trace_normalizing then
1211                            report_normalizing("subscript %C becomes %C",char,s)
1212                        end
1213                    else
1214                        break
1215                    end
1216                end
1217            else
1218                break
1219            end
1220        end
1221        if start_super then
1222            if start_super == stop_super then
1223                setsup(pointer,getnucleus(start_super))
1224            else
1225                local list = new_submlist()
1226                setattrlist(list,pointer)
1227                setlist(list,start_super)
1228                setsup(pointer,list)
1229            end
1230            if mode == "super" then
1231                setnext(pointer,getnext(stop_super))
1232            end
1233            setnext(stop_super)
1234        end
1235        if start_sub then
1236            if start_sub == stop_sub then
1237                setsub(pointer,getnucleus(start_sub))
1238            else
1239                local list = new_submlist()
1240                setattrlist(list,pointer)
1241                setlist(list,start_sub)
1242                setsub(pointer,list)
1243            end
1244            if mode == "sub" then
1245                setnext(pointer,getnext(stop_sub))
1246            end
1247            setnext(stop_sub)
1248        end
1249        -- we could return stop
1250    end
1251
1252    unscript[mathchar_code] = replace -- not noads as we need to recurse
1253
1254    function handlers.unscript(head,style,penalties)
1255        processnoads(head,unscript,"unscript")
1256    end
1257
1258end
1259
1260do
1261
1262    local unstack   = { }    noads.processors.unstack = unstack
1263    local enabled   = false
1264    local a_unstack <const> = privateattribute("mathunstack")
1265
1266    local trace_unstacking  = false  registertracker("math.unstack", function(v) trace_unstacking  = v end)
1267    local report_unstacking = logreporter("mathematics","unstack")
1268
1269    local indexedsubscript_code   <const> = tex.noadoptioncodes.indexedsubscript
1270    local indexedsuperscript_code <const> = tex.noadoptioncodes.indexedsuperscript
1271
1272    unstack[noad_code] = function(pointer)
1273        local a = getattr(pointer,a_unstack)
1274        if a then
1275            local sup = getsup(pointer)
1276            local sub = getsub(pointer)
1277            if sup and sub then
1278             -- if trace_unstacking then
1279             --     report_unstacking() -- todo ... what to show ...
1280             -- end
1281                if a == 1 then
1282                    a = indexedsubscript_code
1283                elseif a == 2 then
1284                    a = indexedsuperscript_code
1285                else
1286                    a = 0
1287                end
1288                setoptions(pointer,getoptions(pointer) | a)
1289            end
1290        end
1291    end
1292
1293    function handlers.unstack(head,style,penalties)
1294        if enabled then
1295            processnoads(head,unstack,"unstack")
1296
1297        end
1298    end
1299
1300    implement {
1301        name     = "enablescriptunstacking",
1302        onlyonce = true,
1303        actions  = function()
1304            enableaction("math","noads.handlers.unstack")
1305            enabled = true
1306        end
1307    }
1308
1309end
1310
1311do
1312
1313    local function collected(fontlist)
1314        if fontlist and next(fontlist) then
1315            local properties = fonts.hashes.properties
1316            local result     = { }
1317            for font, list in sortedhash(fontlist) do
1318                if list and next(list) then
1319                    local n, t = 0, { }
1320                    for k, v in sortedhash(list) do
1321                        n = n + 1
1322                        t[n] = formatters["%C"](k)
1323                    end
1324                    local p = properties[font]
1325                    result[#result+1] = formatters["%s : %i : [ % t ]"](p and p.fontname or "unknown",n,t)
1326                end
1327            end
1328            if #result > 0 then
1329                return concat(result, ", ")
1330            end
1331        end
1332    end
1333
1334    statistics.register("math script replacements", function()
1335        return collected(replaced)
1336    end)
1337
1338    statistics.register("unknown math characters", function()
1339        return collected(unknowns)
1340    end)
1341
1342end
1343
1344-- math alternates: (in xits     lgf: $ABC$ $\cal ABC$ $\mathalternate{cal}\cal ABC$)
1345-- math alternates: (in lucidaot lgf: $ABC \mathalternate{italic} ABC$)
1346
1347-- todo: set alternate for specific symbols
1348-- todo: no need to do this when already loaded
1349-- todo: use a fonts.hashes.mathalternates
1350
1351do
1352
1353    local trace_alternates  = false  registertracker("math.alternates", function(v) trace_alternates = v end)
1354    local report_alternates = logreporter("mathematics","alternates")
1355
1356    local last = 0
1357
1358    local known = setmetatableindex(function(t,k)
1359        local v = 0 | 2^last
1360        t[k] = v
1361        last = last + 1
1362        return v
1363    end)
1364
1365    local defaults = {
1366        dotless = { feature = 'dtls', value = 1, comment = "Mathematical Dotless Forms" },
1367     -- zero    = { feature = 'zero', value = 1, comment = "Slashed or Dotted Zero" }, -- in no math font (yet)
1368    }
1369
1370    local function initializemathalternates(tfmdata)
1371        if use_math_goodies then
1372
1373            local goodies  = tfmdata.goodies
1374            local autolist = defaults -- table.copy(defaults)
1375
1376            local function setthem(newalternates)
1377                local resources      = tfmdata.resources -- was tfmdata.shared
1378                local mathalternates = resources.mathalternates
1379                local alternates, attributes, registered, presets
1380                if mathalternates then
1381                    alternates = mathalternates.alternates
1382                    attributes = mathalternates.attributes
1383                    registered = mathalternates.registered
1384                else
1385                    alternates, attributes, registered = { }, { }, { }
1386                    mathalternates = {
1387                        attributes = attributes,
1388                        alternates = alternates,
1389                        registered = registered,
1390                        presets    = { },
1391                        resets     = { },
1392                        hashes     = setmetatableindex("table")
1393                    }
1394                    resources.mathalternates = mathalternates
1395                end
1396                --
1397                for name, data in sortedhash(newalternates) do
1398                    if alternates[name] then
1399                        -- ignore
1400                    else
1401                        local attr = known[name]
1402                        attributes[attr] = data
1403                        alternates[name] = attr
1404                        registered[#registered+1] = attr
1405                    end
1406                end
1407            end
1408
1409            if goodies then
1410                local done = { }
1411                for i=1,#goodies do
1412                    -- first one counts
1413                    -- we can consider sharing the attributes ... todo (only once scan)
1414                    local mathgoodies = goodies[i].mathematics
1415                    local alternates  = mathgoodies and mathgoodies.alternates
1416                    if alternates then
1417                        if trace_goodies then
1418                            report_goodies("loading alternates for font %a",tfmdata.properties.name)
1419                        end
1420                        for k, v in next, autolist do
1421                            if not alternates[k] then
1422                                alternates[k] = v
1423                            end
1424                        end
1425                        setthem(alternates)
1426                        return
1427                    end
1428                end
1429            end
1430
1431            if trace_goodies then
1432                report_goodies("loading default alternates for font %a",tfmdata.properties.name)
1433            end
1434            setthem(autolist)
1435        end
1436
1437    end
1438
1439    local otf                = fonts.handlers.otf
1440    local registerotffeature = fonts.constructors.features.otf.register
1441    ----- getalternate       = otf.getalternate (runtime new method so ...)
1442
1443    registerotffeature {
1444        name        = "mathalternates",
1445        description = "additional math alternative shapes",
1446        initializers = {
1447            base = initializemathalternates,
1448            node = initializemathalternates,
1449        }
1450    }
1451
1452    -- todo: not shared but copies ... one never knows
1453
1454    local a_mathalternate <const> = privateattribute("mathalternate")
1455    local alternate       = { } -- processors.alternate = alternate
1456    local fontdata        = fonts.hashes.identifiers
1457    local fontresources   = fonts.hashes.resources
1458
1459    local function getalternate(fam,tag,current)
1460        -- fam is always zero, so we assume a very consistent setup
1461        local resources = fontresources[getfontoffamily(fam)]
1462        local attribute = unsetvalue
1463        if resources then
1464            local mathalternates = resources.mathalternates
1465            if mathalternates then
1466                local presets = mathalternates.presets
1467                if presets then
1468                    local resets = mathalternates.resets
1469                    attribute = presets[tag]
1470                    if not attribute then
1471                        attribute = 0
1472                        local alternates = mathalternates.alternates
1473                        for s in gmatch(tag,"[^, ]+") do
1474                            local n, s = match(s,"^([+-]*)(.*)$")
1475                            if s == v_reset then
1476                                resets[tag] = true
1477                                current = unsetvalue
1478                            else
1479                                local a = alternates[s] -- or known[s]
1480                                if a then
1481                                    if n == "-" then
1482                                        attribute = attribute & ~a
1483                                    else
1484                                        attribute = attribute | a
1485                                    end
1486                                end
1487                            end
1488                        end
1489                        if attribute == 0 then
1490                            attribute = unsetvalue
1491                        end
1492                        presets[tag] = attribute
1493                    elseif resets[tag] then
1494                        current = unsetvalue
1495                    end
1496                end
1497            end
1498        end
1499        if attribute > 0 and current and current > 0 then
1500            return current | attribute
1501        else
1502            return attribute
1503        end
1504    end
1505
1506    implement {
1507        name      = "presetmathfontalternate",
1508        arguments = "argument",
1509        public    = true,
1510        protected = true,
1511        actions   = function(tag)
1512            if texgetmode() == mathmode_code then
1513                texsetattribute(a_mathalternate,getalternate(0,tag))
1514            end
1515        end,
1516    }
1517
1518    implement {
1519        name      = "setmathfontalternate",
1520        arguments = "argument",
1521        public    = true,
1522        protected = true,
1523        actions   = function(tag)
1524            if texgetmode() == mathmode_code then
1525                texsetattribute(a_mathalternate,getalternate(0,tag,texgetattribute(a_mathalternate)))
1526            end
1527        end,
1528    }
1529
1530    alternate[mathchar_code] = function(pointer) -- slow
1531        local a = getattr(pointer,a_mathalternate)
1532        if a and a > 0 then
1533            setattr(pointer,a_mathalternate,0)
1534            local fontid    = getfont(pointer)
1535            local resources = fontresources[fontid]
1536            if resources then
1537                local mathalternates = resources.mathalternates
1538                if mathalternates then
1539                    local attributes = mathalternates.attributes
1540                    local registered = mathalternates.registered
1541                    local hashes     = mathalternates.hashes
1542                    local newchar    = nil
1543                    for i=1,#registered do
1544                        local r = registered[i]
1545                        if (a & r) ~= 0 then
1546                            local char = newchar or getchar(pointer)
1547                            local alt  = hashes[i][char]
1548                            if alt == nil then
1549                                local what = attributes[r]
1550                                local list = what.list
1551                                if not list or list[char] then
1552                                alt = otf.getalternate(fontdata[fontid],char,what.feature,what.value) or false
1553                                    if alt == char then
1554                                        alt = false
1555                                    end
1556                                    hashes[i][char] = alt
1557                                end
1558                            end
1559                            if alt then
1560                                if trace_alternates then
1561                                    local what = attributes[r]
1562                                    report_alternates("alternate %a, value %a, replacing glyph %U by glyph %U",
1563                                        tostring(what.feature),tostring(what.value),char,alt)
1564                                end
1565                             -- setchar(pointer,alt)
1566                             -- break
1567                                newchar = alt
1568                            end
1569                        end
1570                    end
1571                    if newchar then
1572                        setchar(pointer,newchar)
1573                    end
1574                end
1575            end
1576        end
1577    end
1578
1579    alternate[delimiter_code] = alternate[mathchar_code]
1580
1581    function handlers.alternates(head,style,penalties)
1582        processnoads(head,alternate,"alternate")
1583    end
1584
1585end
1586
1587-- Italic correction control has been removed here. You can find it in the files
1588-- for \MKIV. The left-over code deals with text-math boundaries.
1589
1590do
1591
1592    local enable = function()
1593        if trace_italics then
1594            report_italics("enabling math italics")
1595        end
1596        -- we enable math (unless already enabled elsewhere)
1597        typesetters.italics.enablemath()
1598        enable = false
1599    end
1600
1601    implement {
1602        name      = "initializemathitalics",
1603        actions   = enable,
1604        onlyonce  = true,
1605    }
1606
1607end
1608
1609do
1610
1611    -- math kerns (experiment) in goodies:
1612    --
1613    -- mathematics = {
1614    --     kernpairs = {
1615    --         [0x1D44E] = {
1616    --             [0x1D44F] = 400, -- 𝑎𝑏
1617    --         }
1618    --     },
1619    -- }
1620
1621    local a_kernpairs <const> = privateattribute("mathkernpairs")
1622    local kernpairs   = { }
1623
1624    local trace_kernpairs  = false  registertracker("math.kernpairs", function(v) trace_kernpairs = v end)
1625    local report_kernpairs = logreporter("mathematics","kernpairs")
1626
1627    local function enable()
1628        enableaction("math", "noads.handlers.kernpairs")
1629        if trace_kernpairs then
1630            report_kernpairs("enabling math kern pairs")
1631        end
1632        enable = false
1633    end
1634
1635    implement {
1636        name      = "initializemathkernpairs",
1637        actions   = enable,
1638        onlyonce  = true,
1639    }
1640
1641    local hash = setmetatableindex(function(t,font)
1642        local g = fontdata[font].goodies
1643        local m = g and g[1] and g[1].mathematics
1644        local k = m and m.kernpairs
1645        t[font] = k
1646        return k
1647    end)
1648
1649    -- no correction after prime because that moved to a superscript
1650
1651    kernpairs[mathchar_code] = function(pointer,what,n,parent)
1652        if getattr(pointer,a_kernpairs) == 1 then
1653            local first, firstfont = getcharspec(pointer)
1654            local list = hash[firstfont]
1655            if list then
1656                local found = list[first]
1657                if found then
1658                    local next = isnext(parent,noad_code)
1659                    if next then
1660--                         pointer = getnucleus(next)
1661--                         if pointer then
1662                            local second, secondfont = getcharspec(pointer)
1663                            if secondfont == firstfont then
1664                                local kern = found[second]
1665                                if kern then
1666                                    kern = kern * fontparameters[firstfont].hfactor
1667                                    if trace_kernpairs then
1668                                        report_kernpairs("adding %p kerning between %C and %C",kern,first,second)
1669                                    end
1670                                    kern = new_kern(kern)
1671                                    setlink(parent,kern,getnext(parent))
1672                                    setattrlist(kern,pointer)
1673                                end
1674                            end
1675--                         end
1676                    end
1677                end
1678            end
1679        end
1680    end
1681
1682    function handlers.kernpairs(head,style,penalties)
1683        if use_math_goodies then
1684            processnoads(head,kernpairs,"kernpairs")
1685        end
1686    end
1687
1688end
1689
1690do
1691
1692    local a_numbers <const> = privateattribute("mathnumbers")
1693    local a_spacing <const> = privateattribute("mathspacing")
1694    local a_fencing <const> = privateattribute("mathfencing")
1695
1696    local numbers = { }
1697    local spacing = { }
1698    local fencing = { }
1699
1700    local separators = {
1701        [0x2E] = { 0x2E, 0x2C, 0x002E, 0x002C, 0x2008, 0x2008 }, -- . -- punctuationspace
1702        [0x2C] = { 0x2C, 0x2E, 0x2008, 0x2008, 0x002E, 0x002C }, -- ,
1703    }
1704
1705    local digits = {
1706        [0x30] = true, [0x31] = true,
1707        [0x32] = true, [0x33] = true,
1708        [0x34] = true, [0x35] = true,
1709        [0x36] = true, [0x37] = true,
1710        [0x38] = true, [0x39] = true,
1711    }
1712
1713    local snoloc = {
1714        [punctuation_class] = 0x003A,
1715        [relation_class]    = 0x2236,
1716    }
1717
1718    local colons = {
1719        [0x003A] = snoloc,
1720        [0x2236] = snoloc,
1721    }
1722
1723    local middles = {
1724        [0x007C] = true,
1725        [0x2016] = true,
1726        [0x2980] = true,
1727    }
1728
1729    local singles = {
1730        0x007C,
1731        0x2016,
1732        0x2980,
1733    }
1734
1735    local followedbyspace_code <const> = tex.noadoptioncodes.followedbyspace
1736
1737    local function followedbyspace(n)
1738        return n and (getoptions(n) & followedbyspace_code == followedbyspace_code)
1739    end
1740
1741    local function followbyspace(n)
1742        setoptions(n,getoptions(n) | followedbyspace_code)
1743    end
1744
1745    numbers[mathchar_code] = function(pointer,what,n,parent)
1746        local alternative = getattr(pointer,a_numbers)
1747        if alternative then
1748            local oldchar = getcharspec(pointer)
1749            local found   = separators[oldchar]
1750            if found then
1751                local prev, next = isboth(parent,noad_code)
1752                if prev and next then
1753                 -- local lc = getcharspec(getnucleus(prev))
1754                    local lc = getcharspec(prev)
1755                    if digits[lc] then
1756                     -- local rc = getcharspec(getnucleus(next))
1757                        local rc = getcharspec(next)
1758                        if digits[rc] then
1759                            local newchar = found[alternative]
1760                            local class   = followedbyspace(parent) and punctuation_class or ordinary_class
1761                            -- noad class == subtype
1762--                             setsubtype(parent,class)
1763setclass(parent,class)
1764                            if newchar ~= oldchar then
1765                                setchar(pointer,newchar)
1766                            end
1767                         -- if trace_numbers then
1768                         --     report_numbers("digit separator digit")
1769                         -- end
1770                        end
1771                    end
1772                end
1773                return
1774            end
1775            found = digits[oldchar]
1776            if found then
1777                if followedbyspace(parent) then
1778                    local next = isnext(parent,noad_code)
1779                    if next then
1780                 --     local rc = getcharspec(getnucleus(next))
1781                        local rc = getcharspec(next)
1782                        if rc and digits[rc] then
1783                            local n = new_noad(numbergroup_class)
1784                            local s = new_submlist()
1785                            setnucleus(n,s)
1786                            setattrlist(n,pointer)
1787                            setattrlist(s,pointer)
1788                            setlink(parent,n,next)
1789                         -- if trace_numbers then
1790                         --     report_numbers("digit spacer digit")
1791                         -- end
1792                        end
1793                    end
1794                end
1795                return
1796            end
1797        end
1798    end
1799
1800    spacing[mathchar_code] = function(pointer,what,n,parent)
1801        if getattr(pointer,a_spacing) then
1802            local oldchar = getcharspec(pointer)
1803            local found   = colons[oldchar]
1804            if found then
1805                local prev = isprev(parent,noad_code)
1806                if prev then
1807                    local class   = followedbyspace(prev) and relation_class or punctuation_class
1808                    local newchar = found[class]
1809--                     setsubtype(parent,class)
1810setclass(parent,class)
1811                    if newchar ~= oldchar then
1812                        setchar(pointer,newchar)
1813                    end
1814                 -- if trace_spacing then
1815                 --     report_spacing("spacer colon")
1816                 -- end
1817                end
1818                return
1819            end
1820        end
1821    end
1822
1823    -- we can share code, see earlier
1824
1825    local function makefence(chr,fam,subtype,class,template)
1826        local f = new_fence()
1827        local d = new_delimiter()
1828        setchar(d,chr)
1829        setfam(d,fam)
1830        setattrlist(d,template)
1831        setattrlist(f,template)
1832        setsubtype(f,subtype)
1833        setdelimiter(f,d)
1834        setclass(f,class) -- tex itself does this, so not fenceclasses[what]
1835        return f
1836    end
1837
1838    -- we loose scripts so maybe also copy these
1839
1840    fencing[mathchar_code] = function(pointer,what,n,parent)
1841        if getattr(pointer,a_fencing) and pointer == getnucleus(parent) then
1842            local oldchar = getcharspec(pointer)
1843            local found   = middles[oldchar]
1844            if found then
1845                local prev, next = getboth(parent)
1846                if getcharspec(next) == oldchar and not followedbyspace(parent) then
1847                    local nextnext = getnext(next)
1848                    -- we need to preserve the followed property
1849                    if getcharspec(nextnext) == oldchar and not followedbyspace(next) then
1850                        oldchar = singles[3]
1851                        prev, parent = nuts.remove(prev,parent,true)
1852                        prev, parent = nuts.remove(prev,parent,true)
1853                    else
1854                        oldchar = singles[2]
1855                        prev, parent = nuts.remove(prev,parent,true)
1856                    end
1857                    next = getnext(parent)
1858                    pointer = getnucleus(parent)
1859                    setchar(pointer,oldchar)
1860                end
1861                if followedbyspace(prev) and followedbyspace(parent) then
1862                    local chr, fnt, fam = getcharspec(pointer)
1863                    local f1 = makefence(0,0,0,0,pointer)
1864                    local f2 = makefence(chr,fam,middlefence_code,middle_class,pointer)
1865                    setlink(prev,f1,f2,next)
1866                    flushnode(parent)
1867                    followbyspace(f1)
1868                    followbyspace(f2)
1869                    return true, f2
1870                else
1871                    return true, parent
1872                end
1873            end
1874        end
1875    end
1876
1877    -- numbers
1878
1879    function handlers.numbers(head,style,penalties)
1880        processnoads(head,numbers,"numbers")
1881    end
1882
1883    local enable = function()
1884        enableaction("math", "noads.handlers.numbers")
1885     -- if trace_numbers then
1886     --     report_numbers("enabling math numbers")
1887     -- end
1888        enable = false
1889    end
1890
1891    implement {
1892        name     = "initializemathnumbers",
1893        actions  = enable,
1894        onlyonce = true,
1895    }
1896
1897    -- spacing
1898
1899    function handlers.spacing(head,style,penalties)
1900        processnoads(head,spacing,"spacing")
1901    end
1902
1903    local enable = function()
1904        enableaction("math", "noads.handlers.spacing")
1905     -- if trace_spacing then
1906     --     report_spacing("enabling math spacing")
1907     -- end
1908        enable = false
1909    end
1910
1911    implement {
1912        name     = "initializemathspacing",
1913        actions  = enable,
1914        onlyonce = true,
1915    }
1916
1917    -- fences
1918
1919    function handlers.fencing(head,style,penalties)
1920        processnoads(head,fencing,"fencing")
1921    end
1922
1923    local enable = function()
1924        enableaction("math", "noads.handlers.fencing")
1925     -- if trace_fencing then
1926     --     report_fencing("enabling math fencing")
1927     -- end
1928        enable = false
1929    end
1930
1931    implement {
1932        name     = "initializemathfencing",
1933        actions  = enable,
1934        onlyonce = true,
1935    }
1936
1937end
1938
1939-- primes and such
1940
1941do
1942
1943    -- is validpair stil needed? why not always now?
1944
1945    local a_mathcollapsing <const> = privateattribute("mathcollapsing")
1946    local collapse         = { }
1947    local mathlists        = characters.mathlists
1948    local validpair        = {
1949        [ordinary_class]    = true,
1950        [operator_class]    = true,
1951        [binary_class]      = true, -- new
1952        [relation_class]    = true,
1953        [open_class]        = true, -- new
1954        [middle_class]      = true, -- new
1955        [close_class]       = true, -- new
1956        [punctuation_class] = true, -- new
1957        [fraction_class]    = true,
1958        [accent_class]      = true,
1959    }
1960
1961    local reported = setmetatableindex("table")
1962
1963    -- We could move primes to its own handler but we need to run this one anyway
1964    -- so for now we keep it here.
1965
1966    mathlists[39] = { [39] = { [39] = { enforced = 0x2034, [39] = { enforced = 0x2057 } }, enforced = 0x2033 }, enforced = 0x2032 }
1967    mathlists[96] = { [96] = { [96] = { enforced = 0x2037                               }, enforced = 0x2036 }, enforced = 0x2035 }
1968
1969    local getchardict  = nuts.getchardict
1970    local setchardict  = nuts.setchardict
1971
1972    local groups       = mathematics.dictionaries.groups
1973
1974    collapse[mathchar_code] = function(pointer,what,n,parent)
1975        if parent and mathlists[getchar(pointer)] then
1976            local found, last, lucleus, lsup, lsub, lprime, category
1977            local tree    = mathlists
1978            local current = parent
1979            while current and validpair[getsubtype(current)] do
1980                local nucleus, prime, sup, sub = getnucleus(current,true)
1981                local char = getchar(nucleus)
1982                if char then
1983                    local match = tree[char]
1984                    if match then
1985                        local method = getattr(current,a_mathcollapsing)
1986                        if method and method > 0 and method <= 3 then
1987                            local enforced = match.enforced
1988                            local specials = match.specials
1989                            local mathlist = match.mathlist
1990                            local ligature
1991                            if method == 0 then
1992                                ligature = enforced
1993                            elseif method == 1 then
1994                                ligature = enforced or specials
1995                            elseif method == 2 then
1996                                ligature = enforced or specials or mathlist
1997                            else -- 3
1998                                ligature = enforced or mathlist or specials
1999                            end
2000                            if ligature then
2001                                category = mathlist and "mathlist" or "specials"
2002                                found    = ligature
2003                                last     = current
2004                                lucleus  = nucleus
2005                                lsup     = sup
2006                                lsub     = sub
2007                                lprime   = prime
2008                            end
2009                            tree = match
2010                            if sub or sup or prime then
2011                                break
2012                            else
2013                                current = getnext(current)
2014                            end
2015                        else
2016                            break
2017                        end
2018                    else
2019                        break
2020                    end
2021                else
2022                    break
2023                end
2024            end
2025            if found and last and lucleus then
2026                local id         = getfont(lucleus)
2027                local characters = fontcharacters[id]
2028                local replace    = characters and characters[found]
2029                if not replace then
2030                    if not reported[id][found] then
2031                        reported[id][found] = true
2032                        report_collapsing("%s ligature %C from %s","ignoring",found,category)
2033                    end
2034                elseif trace_collapsing then
2035                    report_collapsing("%s ligature %C from %s","creating",found,category)
2036                end
2037-- we need to use the chardict of the replacement
2038
2039local properties, group = getchardict(pointer)
2040local c = chardata[found]
2041if c then
2042    local g = c.mathgroup
2043    if g then
2044        group = groups[g] or 0xFFFF
2045    else
2046        group = 0xFFFF
2047    end
2048else
2049    group = 0xFFFF
2050end
2051setchardict(pointer,properties,group,found)
2052
2053                setchar(pointer,found)
2054                local l = getnext(last)
2055                local c = getnext(parent)
2056                if lsub then
2057                    setsub(parent,lsub)
2058                    setsub(last)
2059                end
2060                if lsup then
2061                    setsup(parent,lsup)
2062                    setsup(last)
2063                end
2064                if lprime then
2065                    setprime(parent,lprime)
2066                    setprime(last)
2067                end
2068                while c ~= l do
2069                    local n = getnext(c)
2070                    flushnode(c)
2071                    c = n
2072                end
2073                setlink(parent,l)
2074            end
2075        end
2076    end
2077
2078    function noads.handlers.collapse(head,style,penalties)
2079        processnoads(head,collapse,"collapse")
2080    end
2081
2082    local enable = function()
2083        enableaction("math", "noads.handlers.collapse")
2084        if trace_collapsing then
2085            report_collapsing("enabling math collapsing")
2086        end
2087        enable = false
2088    end
2089
2090    implement {
2091        name     = "initializemathcollapsing",
2092        actions  = enable,
2093        onlyonce = true,
2094    }
2095
2096end
2097
2098do
2099
2100    local variants  = { }
2101    ----- chardata  = characters.data
2102    local a_variant <const> = privateattribute("mathvariant")
2103
2104    local trace_variants  = false  registertracker("math.variants", function(v) trace_variants = v end)
2105    local report_variants = logreporter("mathematics","variants")
2106
2107    local function setvariant(pointer,selector,char)
2108        local tfmdata      = fontdata[getfont(pointer)]
2109        local mathvariants = tfmdata.resources.variants -- and variantdata / can be a hash
2110        if mathvariants then
2111            mathvariants = mathvariants[selector]
2112            if mathvariants then
2113                local variant = mathvariants[char]
2114                if variant then
2115                    setchar(pointer,variant)
2116                    setattr(pointer,a_exportstatus,char) -- we don't export the variant as it's visual markup
2117                    if trace_variants then
2118                        report_variants("variant (%U,%U) replaced by %U",char,selector,variant)
2119                    end
2120                else
2121                    if trace_variants then
2122                        report_variants("no variant (%U,%U)",char,selector)
2123                    end
2124                end
2125            end
2126        end
2127    end
2128
2129    variants[mathchar_code] = function(pointer,what,n,parent) -- also set export value
2130        local char = getchar(pointer)
2131        local data = chardata[char]
2132        if data then
2133            local variants = data.variants
2134            if variants then
2135                local next = isnext(parent,noad_code)
2136                if next then
2137                    local nucleus = getnucleus(next)
2138                    if nucleus and getid(nucleus) == mathchar_code then
2139                        local selector = getchar(nucleus)
2140                        if variants[selector] then
2141                            setvariant(pointer,selector,char)
2142                            setprev(next,pointer)
2143                            setnext(parent,getnext(next))
2144                            flushnode(next)
2145                        end
2146                    end
2147                end
2148                local selector = getattr(pointer,a_variant)
2149                if selector and variants[selector] then
2150                    setvariant(pointer,selector,char)
2151                end
2152            end
2153        end
2154    end
2155
2156    function mathematics.addvariant(tfmdata,char,variant,selector)
2157        if char and variant and selector then
2158            local data = chardata[char]
2159            if data then
2160                local variants = data.variants
2161                if variants and variants[selector] then
2162                    local resources = tfmdata.resources
2163                    local variants  = resources.variants -- and variantdata
2164                    if not variants then
2165                        variants           = { }
2166                        resources.variants = variants
2167                    end
2168                    local selectors = variants[selector]
2169                    if not selectors then
2170                        selectors          = { }
2171                        variants[selector] = selectors
2172                    end
2173                    selectors[char] = variant
2174                    return true
2175                end
2176            end
2177        end
2178        return false
2179    end
2180
2181    function handlers.variants(head,style,penalties)
2182        processnoads(head,variants,"unicode variant")
2183    end
2184
2185    local valid = {
2186        calligraphic = 0xFE00,
2187        calligraphy  = 0xFE00,
2188        script       = 0xFE01,
2189        handwriting  = 0xFE01,
2190    }
2191
2192    function mathematics.setvariant(s)
2193        texsetattribute(a_variant,valid[s] or unsetvalue)
2194    end
2195
2196    implement {
2197        name      = "setmathvariant",
2198        public    = true,
2199        protected = true,
2200        arguments = "argument",
2201        actions   = mathematics.setvariant,
2202    }
2203
2204end
2205
2206-- for manuals
2207
2208do
2209
2210    -- Given the amount of classes this no longer makes much sense or we need to
2211    -- extend it.
2212
2213    local classes = { }
2214    local colors  = {
2215        [relation_class]    = "trace:dr",
2216        [ordinary_class]    = "trace:db",
2217        [binary_class]      = "trace:dg",
2218        [open_class]        = "trace:dm",
2219        [middle_class]      = "trace:dm",
2220        [close_class]       = "trace:dm",
2221        [punctuation_class] = "trace:dc",
2222    }
2223
2224    local setcolor   = colortracers.set
2225    local resetcolor = colortracers.reset
2226
2227    classes[mathchar_code] = function(pointer,what,n,parent)
2228        local color = colors[getsubtype(parent)]
2229        if color then
2230            setcolor(pointer,color)
2231        else
2232            resetcolor(pointer)
2233        end
2234    end
2235
2236    function handlers.classes(head,style,penalties)
2237        processnoads(head,classes,"classes")
2238    end
2239
2240    registertracker("math.classes",function(v)
2241        setaction("math","noads.handlers.classes",v)
2242    end)
2243
2244end
2245
2246do
2247
2248    local traversehlist = nuts.traversers.hlist
2249
2250    local getshift      = nuts.getshift
2251    local setwhd        = nuts.setwhd
2252    local setshift      = nuts.setshift
2253
2254    -- normalizer: can become engine feature (native tex loves shifts)
2255
2256    local function normalize(h)
2257        for n, s in traversehlist, h do
2258            if s > 0 then
2259                local sh = getshift(n)
2260                local ox, oy = getoffsets(n)
2261                if sh ~= 0 then
2262                    local w, h, d = getwhd(n)
2263                    h = h - sh
2264                    d = d + sh
2265                    setshift(n)
2266                    setwhd(n,w,h > 0 and h or 0,d > 0 and d or 0)
2267                    setoffsets(n,ox,oy - sh)
2268                end
2269            end
2270            local l = getlist(l)
2271            if l then
2272                normalize(l)
2273            end
2274        end
2275    end
2276
2277    function handlers.normalize(h)
2278        return normalize(h)
2279    end
2280
2281end
2282
2283do
2284
2285    local traversehlist = nuts.traversers.hlist
2286
2287    local texgetdimen   = tex.getdimen
2288    local texgetcount   = tex.getcount
2289
2290    local newrule       = nuts.pool.outlinerule
2291    local newkern       = nuts.pool.kern
2292    local setcolor      = nodes.tracers.colors.set
2293
2294    local a_mathsnap    <const> = privateattribute("mathsnap")
2295
2296    local d_mathstrutht <const> = tex.isdimen("mathstrutht")
2297    local d_mathstrutdp <const> = tex.isdimen("mathstrutdp")
2298    local c_mathnesting <const> = tex.iscount("mathnestinglevel")
2299
2300    local trace_snapping  = false  registertracker("math.snapping", function(v) trace_snapping = v end)
2301    local report_snapping = logreporter("mathematics","snapping")
2302
2303    function handlers.snap(h,_,_,_,_,level)
2304     -- if not level or level == 0 then
2305        if texgetcount(c_mathnesting) == 1 then
2306            local trace_color
2307            if trace_snapping == "frame" then
2308                trace_color = "darkgray"
2309            elseif type(trace_snapping) == "string" then
2310                trace_color = trace_snapping
2311            else
2312                trace_color = false
2313            end
2314            local ht, dp, dd, hs, ds, hd
2315            for n, s in traversehlist, h do
2316                local step = getattr(n,a_mathsnap)
2317                if step then
2318                    local done = false
2319                    if not dd then
2320                        ht = texgetdimen(d_mathstrutht)
2321                        dp = texgetdimen(d_mathstrutdp)
2322                        hd = ht + dp
2323                        -- lineskip can be large in alignments
2324                     -- dd = hd / 12
2325                        dd = hd / 6
2326                        if step == 0xFFFF then
2327                            hs = dd
2328                            ds = dd
2329                        else
2330                            hs = ht/step
2331                            ds = dp/step
2332                        end
2333                    end
2334                    local w, h, d = getwhd(n)
2335                    -- snap to line
2336                  ::height::
2337                    if h-dd < ht then
2338                        if trace_snapping == true then
2339                            report_snapping("adapting ht: old %p, new %p, lineskip %p",h,ht,dd)
2340                        end
2341                        done = true
2342                        setheight(n,ht)
2343                        goto depth
2344                    end
2345                    if h > ht then
2346--                         while ht < (h-dd) do
2347                        while ht < h do
2348                            ht = round(ht + hs)
2349                        end
2350                        if h ~= ht then
2351                            setheight(n,ht)
2352                            if trace_snapping == true then
2353                                report_snapping("enlarging ht: old %p, new %p, step %p",h,ht,hs)
2354                            end
2355                            done = true
2356                        end
2357                    end
2358                  ::depth::
2359                    if d-dd < dp then
2360                        if trace_snapping == true then
2361                            report_snapping("adapting dp: old %p, new %p, lineskip %p",d,dp,dd)
2362                        end
2363                        setdepth(n,dp)
2364                        done = true
2365                        goto done
2366                    end
2367                    if d > dp then
2368--                         while dp < (d-dd) do
2369                        while dp < d do
2370                            dp = round(dp + ds)
2371                        end
2372                        if d ~= dp then
2373                            setdepth(n,dp)
2374                            if trace_snapping == true then
2375                                report_snapping("enlarging dp: old %p, new %p, step %p",d,dp,ds)
2376                            end
2377                            done = true
2378                        end
2379                    end
2380                 ::done::
2381                    if done and trace_color then
2382                     -- w, h, d = getwhd(n)
2383                     -- local r = newrule(w,h,d,65536)
2384                     -- setcolor(r,trace_color)
2385                     -- setlink(r,newkern(-w),getlist(n))
2386                     -- setlist(n,r)
2387
2388                        local old = newrule(w,h,d,65536)
2389                        setcolor(old,"middlegray")
2390                        w, h, d = getwhd(n)
2391                        local new = newrule(w,h,d,65536/4)
2392                        setcolor(new,trace_color)
2393                        setlink(old,newkern(-w),new,newkern(-w),getlist(n))
2394                        local ox, oy = getoffsets(n)
2395                        setoffsets(old,-ox,-oy)
2396                        setoffsets(new,-ox,-oy)
2397                        setlist(n,old)
2398                    end
2399                end
2400            end
2401        end
2402    end
2403
2404    local valid = {
2405        [v_reset]  = unsetvalue,
2406        [v_line]   = 0xFFFF,
2407        [v_small]  = 8,
2408        [v_medium] = 4,
2409        [v_big]    = 2,
2410    }
2411
2412    function mathematics.setsnapping(s)
2413        if not enabled then
2414            enableaction("math", "noads.handlers.snap")
2415            enabled = true
2416        end
2417        texsetattribute(a_mathsnap,valid[s] or unsetvalue)
2418    end
2419
2420    implement {
2421        name      = "setmathsnapping",
2422        public    = true,
2423        protected = true,
2424        arguments = "argument",
2425        actions   = mathematics.setsnapping,
2426    }
2427
2428end
2429
2430-- experimental : replaced by dictionaries but for now we keep the code
2431--
2432-- do
2433--
2434--  -- mathematics.registerdomain {
2435--  --     name       = "foo",
2436--  --     parents    = { "bar" },
2437--  --     characters = {
2438--  --         [0x123] = { char = 0x234, class = binary },
2439--  --     },
2440--  -- }
2441--
2442--     local trace_domains  = false  registertracker("math.domains", function(v) trace_domains = v end)
2443--     local report_domains = logreporter("mathematics","domains")
2444--
2445--     local domains       = { }
2446--     local categories    = { }
2447--     local numbers       = { }
2448--     local a_mathdomain  <const> = privateattribute("mathdomain")
2449--     mathematics.domains = categories
2450--     local permitted     = {
2451--         ordinary    = ordinary_class,
2452--         binary      = binary_class,
2453--         relation    = relation_class,
2454--         punctuation = punctuation_class,
2455--         inner       = innernoad_code,
2456--         fenced      = fenced_class,
2457--      -- fraction    = fraction_class,
2458--      -- radical     = radical_class,
2459--     }
2460--
2461--     function mathematics.registerdomain(data)
2462--         local name = data.name
2463--         if not name then
2464--             return
2465--         end
2466--         local attr       = #numbers + 1
2467--         categories[name] = data
2468--         numbers[attr]    = data
2469--         data.attribute   = attr
2470--         -- we delay hashing
2471--         return attr
2472--     end
2473--
2474--     local enable
2475--
2476--     enable = function()
2477--         enableaction("math", "noads.handlers.domains")
2478--         if trace_domains then
2479--             report_domains("enabling math domains")
2480--         end
2481--         enable = false
2482--     end
2483--
2484--     function mathematics.setdomain(name)
2485--         if enable then
2486--             enable()
2487--         end
2488--         local data = name and name ~= v_reset and categories[name]
2489--         texsetattribute(a_mathdomain,data and data.attribute or unsetvalue)
2490--     end
2491--
2492--     function mathematics.getdomain(name)
2493--         if enable then
2494--             enable()
2495--         end
2496--         local data = name and name ~= v_reset and categories[name]
2497--         context(data and data.attribute or unsetvalue)
2498--     end
2499--
2500--     implement {
2501--         name      = "initializemathdomain",
2502--         actions   = enable,
2503--         onlyonce  = true,
2504--     }
2505--
2506--     implement {
2507--         name      = "setmathdomain",
2508--         arguments = "string",
2509--         actions   = mathematics.setdomain,
2510--     }
2511--
2512--     implement {
2513--         name      = "getmathdomain",
2514--         arguments = "string",
2515--         actions   = mathematics.getdomain,
2516--     }
2517--
2518--     local function makehash(data)
2519--         local hash    = { }
2520--         local parents = data.parents
2521--         if parents then
2522--             local function merge(name)
2523--                 if name then
2524--                     local c = categories[name]
2525--                     if c then
2526--                         local hash = c.hash
2527--                         if not hash then
2528--                             hash = makehash(c)
2529--                         end
2530--                         for k, v in next, hash do
2531--                             hash[k] = v
2532--                         end
2533--                     end
2534--                 end
2535--             end
2536--             if type(parents) == "string" then
2537--                 merge(parents)
2538--             elseif type(parents) == "table" then
2539--                 for i=1,#parents do
2540--                     merge(parents[i])
2541--                 end
2542--             end
2543--         end
2544--         local characters = data.characters
2545--         if characters then
2546--             for k, v in next, characters do
2547--              -- local chr = n.char
2548--                 local cls = v.class
2549--                 if cls then
2550--                     v.code = permitted[cls]
2551--                 else
2552--                     -- invalid class
2553--                 end
2554--                 hash[k] = v
2555--             end
2556--         end
2557--         data.hash = hash
2558--         return hash
2559--     end
2560--
2561--     domains[mathchar_code] = function(pointer,what,n,parent)
2562--         local attr = getattr(pointer,a_mathdomain)
2563--         if attr then
2564--             local domain = numbers[attr]
2565--             if domain then
2566--                 local hash = domain.hash
2567--                 if not hash then
2568--                     hash = makehash(domain)
2569--                 end
2570--                 local char = getchar(pointer)
2571--                 local okay = hash[char]
2572--                 if okay then
2573--                     local chr = okay.char
2574--                     local cls = okay.code
2575--                     if chr and chr ~= char then
2576--                         setchar(pointer,chr)
2577--                     end
2578--                     if cls and cls ~= getsubtype(parent) then
2579--                         setsubtype(parent,cls)
2580--                     end
2581--                 end
2582--             end
2583--         end
2584--     end
2585--
2586--     function handlers.domains(head,style,penalties)
2587--         processnoads(head,domains,"domains")
2588--     end
2589--
2590-- end
2591
2592-- just for me
2593
2594function handlers.showtree(head,style,penalties)
2595    inspect(nodes.totree(tonut(head)))
2596end
2597
2598registertracker("math.showtree",function(v)
2599    setaction("math","noads.handlers.showtree",v)
2600end)
2601
2602-- also for me
2603
2604do
2605
2606    local applyvisuals = nuts.applyvisuals
2607    local visual       = false
2608
2609    function handlers.makeup(head)
2610        applyvisuals(head,visual)
2611    end
2612
2613    registertracker("math.makeup",function(v)
2614        visual = v
2615        setaction("math","noads.handlers.makeup",v)
2616    end)
2617
2618end
2619
2620-- Musical timestamp: August 2022 with "Meditation by Cory Wong (Live @ Brooklyn
2621-- Steel FEB 2022). Seen live earlier that year and its gets better and better!
2622--
2623-- As we also try to do here:
2624
2625do
2626
2627    local trace_dictionaries  = false  registertracker("math.dictionaries",         function(v) trace_dictionaries = v end)
2628    local trace_details       = false  registertracker("math.dictionaries.details", function(v) trace_details      = v end)
2629
2630    local report_dictionaries = logreporter("mathematics","dictionaries")
2631
2632    local setnodecolor = colortracers.set
2633    local getchardict  = nuts.getchardict
2634    local setchardict  = nuts.setchardict
2635
2636    local dictionaries = { }  noads.processors.dictionaries = dictionaries
2637    local groups       = mathematics.dictionaries.groups
2638    local sets         = mathematics.dictionaries.sets
2639    local variants     = mathematics.dictionaries.variants
2640    local defaults     = mathematics.dictionaries.defaults
2641
2642    local function check(pointer,group,index)
2643        local v = variants[index]
2644        if v then
2645            local c = v[group]
2646            if c then
2647                return group, c
2648            end
2649        end
2650        return 1
2651    end
2652
2653    dictionaries[mathchar_code] = function(pointer,what,n,parent)
2654        local properties, oldgroup, index, font, char = getchardict(pointer)
2655        local newgroup = 1
2656        local newclass = false
2657--         local oldclass = getsubtype(pointer)
2658        local oldclass = getsubtype(parent)
2659        if (properties & 0x1) == 0x1 then
2660            newclass = oldclass
2661            newgroup = oldgroup
2662        else
2663            local set = sets[oldgroup]
2664            if set then
2665                local groups    = set.groups
2666                local nofgroups = groups and #groups
2667                if nofgroups > 0 then
2668                    for i=1,nofgroups do
2669                        local group = groups[i]
2670                        local real, class = check(pointer,group,index)
2671                        if real ~= 1 then
2672                            newclass = class
2673                            newgroup = group
2674                            goto done
2675                        end
2676                    end
2677                end
2678            else
2679                newgroup, newclass = check(pointer,oldgroup,index)
2680            end
2681          ::done::
2682            if newgroup == 1 then
2683                newgroup = defaults[index] or 1
2684            end
2685            setchardict(pointer,properties,newgroup,index)
2686            if type(newclass) == "number" then
2687-- print(newgroup,newclass,oldclass)
2688                setsubtype(parent,newclass)
2689--                 setsubtype(pointer,newclass)
2690            else
2691                newclass = oldclass
2692            end
2693        end
2694        if trace_dictionaries or trace_details then
2695            if newgroup > 1 then
2696                local groupname = groups[newgroup]
2697                if groupname then
2698                    setnodecolor(pointer,"dictionary:"..groupname)
2699                end
2700            end
2701            if trace_details then
2702                report_dictionaries("properties 0x%02X, group 0x%02X -> 0x%02X, class 0x%02X -> 0x%02X, index %05X, %U %c",properties,oldgroup,newgroup,oldclass,newclass,index,char,char)
2703            end
2704        end
2705    end
2706
2707    function handlers.dictionaries(head,style,penalties)
2708        processnoads(head,dictionaries,"dictionaries")
2709    end
2710
2711end
2712
2713do
2714
2715    local trace_suspicious  = false  registertracker("math.suspicious", function(v) trace_suspicious = v end)
2716    local report_suspicious = logreporter("mathematics","suspicious")
2717
2718    local suspicious = { }  noads.processors.suspicious = suspicious
2719
2720    local candidates = {
2721        [classes.maybeordinary] = "maybeordinary",
2722        [classes.mayberelation] = "mayberelation",
2723        [classes.maybebinary  ] = "maybebinary",
2724    }
2725    local registered = setmetatableindex("table")
2726
2727    suspicious[mathchar_code] = function(pointer,what,n,parent)
2728        local class = getsubtype(pointer)
2729        local found = candidates[class]
2730        if found then
2731            local char = getchar(pointer)
2732            if not registered[class][char] then
2733                report_suspicious("class %a, %U %c",found,char,char)
2734                registered[class][char] = true
2735            end
2736        end
2737    end
2738
2739    function handlers.suspicious(head,style,penalties)
2740        if trace_suspicious then
2741            processnoads(head,suspicious,"suspicious")
2742        end
2743    end
2744
2745end
2746
2747do  -- we don't need to support nesting here unless we run over sublists
2748
2749    local trace_intervals  = false  registertracker("math.intervals", function(v) trace_intervals = v end)
2750    local report_intervals = logreporter("mathematics","intervals")
2751
2752    local a_mathintervals <const> = privateattribute("mathintervals")
2753
2754    local intervals = { }  noads.processors.intervals = intervals
2755
2756    -- ] a,b [ | [ a,b ] | ] a, b ] | [ a, b [
2757
2758    local firstclass   = false
2759    local firstparent  = false
2760    local firstpointer = false
2761    local punctuation  = false
2762
2763    local ignored = {
2764        [relation_class] = true,
2765        [middle_class]   = true,
2766        [fenced_class]   = true,
2767        [fraction_class] = true,
2768        [radical_class]  = true,
2769        [accent_class]   = true,
2770    }
2771
2772    intervals[mathchar_code] = function(pointer,what,n,parent)
2773        local class = getsubtype(pointer)
2774        if class == open_class or class == close_class then
2775            local c = getchar(pointer)
2776            if c == 0x5B or c == 0x5D then -- c = 0x27E6 or c = 0x27E7 double ones
2777                local a = getattr(pointer,a_mathintervals)
2778                if not a then
2779                    firstclass  = false
2780                    punctuation = false
2781                elseif firstclass then
2782                    if not punctuation then
2783                        --
2784                    else
2785                        if firstclass ~= open_class then
2786                            setclass(firstparent, open_class)
2787                            setprop(firstpointer,"swappedclass",open_class)
2788                            if trace_intervals then
2789                                report_intervals("setting class of %C to %a",getchar(firstpointer),"open")
2790                            end
2791                        end
2792                        if class ~= close_class then
2793                            setclass(parent, close_class)
2794                            setprop(pointer,"swappedclass",close_class)
2795                            if trace_intervals then
2796                                report_intervals("setting class of %C to %a",getchar(pointer),"close")
2797                            end
2798                        end
2799                    end
2800                    firstclass  = false
2801                    punctuation = false
2802                else
2803                    firstclass   = class
2804                    firstpointer = pointer
2805                    firstparent  = parent
2806                end
2807            else
2808                firstclass  = false
2809                punctuation = false
2810            end
2811        elseif class == punctuation_class then
2812            local c = getchar(pointer)
2813            if c == 0x2C or c == 0x3A or c == 0x3B then -- , : ;
2814                punctuation = true
2815            else
2816                punctuation = false
2817            end
2818        elseif ignored[class] then
2819            firstclass  = false
2820            punctuation = false
2821        end
2822    end
2823
2824    function handlers.intervals(head,style,penalties)
2825        processnoads(head,intervals,"intervals")
2826    end
2827
2828end
2829
2830-- the normal builder
2831
2832-- do
2833--
2834--     local force_penalties = false
2835--
2836--     function builders.kernel.mlisttohlist(head,style,penalties)
2837--         return mlisttohlist(head,style,force_penalties or penalties)
2838--     end
2839--
2840--     implement {
2841--         name      = "setmathpenalties",
2842--         arguments = "integer",
2843--         actions   = function(p)
2844--             force_penalties = p > 0
2845--         end,
2846--     }
2847--
2848-- end
2849
2850builders.kernel.mlisttohlist = mlisttohlist
2851
2852local actions = tasks.actions("math") -- head, style, penalties
2853
2854local starttiming, stoptiming = statistics.starttiming, statistics.stoptiming
2855
2856function processors.mlisttohlist(head,style,penalties,beginclass,endclass,level,style)
2857    starttiming(noads)
2858    head = actions(head,style,penalties,beginclass,endclass,level,style)
2859    stoptiming(noads)
2860    return head
2861end
2862
2863callbacks.register("mlist_to_hlist",processors.mlisttohlist,"convert a noad list into a node list")
2864
2865-- tracing
2866
2867statistics.register("math processing time", function()
2868    return statistics.elapsedseconds(noads)
2869end)
2870