math-act.lmt /size: 181 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['math-act'] = {
2    version   = 1.001,
3    comment   = "companion to math-ini.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- Here we tweak some font properties (if needed). The commented sections
10-- have been removed (no longer viable) but can be found in the .lua variant.
11
12-- The tweaks here evolved from experiments with, discussions about and upgrades of
13-- the math subsystem, a project that Mikael Sundvist and I started end 2021 and
14-- that is still ongoing in 2023 (and probably beyond as we find new challenges as
15-- we go).
16
17local type, next, tonumber = type, next, tonumber
18local fastcopy, copytable, insert, remove, concat = table.fastcopy, table.copy, table.insert, table.remove, table.concat
19local formatters = string.formatters
20local byte = string.byte
21local max = math.max
22local setmetatableindex, sortedkeys, sortedhash  = table.setmetatableindex, table.sortedkeys, table.sortedhash
23local lpegmatch = lpeg.match
24
25local trace_defining   = false  trackers.register("math.defining",   function(v) trace_defining   = v end)
26local trace_collecting = false  trackers.register("math.collecting", function(v) trace_collecting = v end)
27local trace_tweaking   = false  trackers.register("math.tweaks",     function(v) trace_tweaking   = v end)
28
29local report_math      = logs.reporter("mathematics","initializing")
30local report_mathtweak = logs.reporter("mathematics","tweak")
31
32local getfontoffamily  = tex.getfontoffamily
33local texget           = tex.get
34local fontcharacters   = fonts.hashes.characters
35local chardata         = characters.data
36local extensibles      = mathematics.extensibles
37
38local context          = context
39local commands         = commands
40local mathematics      = mathematics
41local texsetdimen      = tex.setdimen
42local abs              = math.abs
43
44local blocks           = characters.blocks
45local stepper          = utilities.parsers.stepper
46
47local helpers          = fonts.helpers
48local prependcommands  = helpers.prependcommands
49
50local vfcommands       = helpers.commands
51local upcommand        = vfcommands.up
52local downcommand      = vfcommands.down
53local rightcommand     = vfcommands.right
54local leftcommand      = vfcommands.left
55local slotcommand      = vfcommands.slot
56local charcommand      = vfcommands.char
57local push             = vfcommands.push
58local pop              = vfcommands.pop
59
60local sequencers       = utilities.sequencers
61local appendgroup      = sequencers.appendgroup
62local appendaction     = sequencers.appendaction
63
64local fontchars        = fonts.hashes.characters
65local fontproperties   = fonts.hashes.properties
66
67local mathgaps         = mathematics.gaps
68
69local d_scratchleftoffset  <const> = tex.isdimen("scratchleftoffset")
70local d_scratchrightoffset <const> = tex.isdimen("scratchrightoffset")
71
72local use_math_goodies = true   directives.register("math.nogoodies",    function(v) use_math_goodies = not v end)
73local checkitalics     = false  trackers  .register("math.checkitalics", function(v) checkitalics     =     v end)
74
75local function registerdone(done,unicode)
76    if not trace_tweaking then
77        done = true
78    elseif done then
79        done[unicode] = true
80    else
81        done = { [unicode] = true }
82    end
83    return done
84end
85
86local mathfontparameteractions = sequencers.new {
87    name      = "mathparameters",
88    arguments = "target,original",
89}
90
91appendgroup("mathparameters","before") -- user
92appendgroup("mathparameters","system") -- private
93appendgroup("mathparameters","after" ) -- user
94
95function fonts.constructors.assignmathparameters(original,target) -- wrong way around
96    local runner = mathfontparameteractions.runner
97    if runner then
98        runner(original,target)
99    end
100end
101
102-- we need a better reset because the following will scale
103
104local undefined <const> = 0x3FFFFFFF -- maxdimen or undefined_math_parameter
105
106function mathematics.initializeparameters(target,original,nodimensions)
107    local mathparameters = original.mathparameters
108    if mathparameters and next(mathparameters) then
109        if nodimensions ~= "noscale" then
110            mathparameters = mathematics.dimensions(mathparameters)
111        end
112        --
113        -- defaults : times unit or fraction
114        --
115     -- if not mathparameters.MinConnectorOverlap               then mathparameters.MinConnectorOverlap               = undefined end
116        if not mathparameters.SubscriptShiftDownWithSuperscript then mathparameters.SubscriptShiftDownWithSuperscript = mathparameters.SubscriptShiftDown * 1.5 end
117     -- if not mathparameters.FractionDelimiterSize             then mathparameters.FractionDelimiterSize             = undefined end
118     -- if not mathparameters.FractionDelimiterDisplayStyleSize then mathparameters.FractionDelimiterDisplayStyleSize = undefined end
119     -- if not mathparameters.SkewedDelimiterTolerance          then mathparameters.SkewedDelimiterTolerance          = undefined end
120        -- some more can be undefined:
121        if not mathparameters.SpaceAfterScript                  then mathparameters.SpaceAfterScript                  = 0 end
122        if not mathparameters.SpaceBeforeScript                 then mathparameters.SpaceBeforeScript                 = mathparameters.SpaceAfterScript end
123        if not mathparameters.SpaceBetweenScript                then mathparameters.SpaceBetweenScript                = mathparameters.SpaceAfterScript end
124        if not mathparameters.PrimeRaisePercent                 then mathparameters.PrimeRaisePercent                 = 0 end
125        if not mathparameters.PrimeRaiseComposedPercent         then mathparameters.PrimeRaiseComposedPercent         = 0 end
126        if not mathparameters.PrimeShiftUp                      then mathparameters.PrimeShiftUp                      = mathparameters.SuperscriptShiftUp end
127        if not mathparameters.PrimeBaselineDropMax              then mathparameters.PrimeBaselineDropMax              = mathparameters.SuperscriptBaselineDropMax end
128        if not mathparameters.PrimeShiftUpCramped               then mathparameters.PrimeShiftUpCramped               = mathparameters.SuperscriptShiftUpCramped end
129        if not mathparameters.PrimeSpaceAfter                   then mathparameters.PrimeSpaceAfter                   = mathparameters.SpaceAfterScript end
130        if not mathparameters.NoLimitSupFactor                  then mathparameters.NoLimitSupFactor                  = 0 end
131        if not mathparameters.NoLimitSubFactor                  then mathparameters.NoLimitSubFactor                  = 0 end
132        if not mathparameters.AccentTopShiftUp                  then mathparameters.AccentTopShiftUp                  = 0 end
133        if not mathparameters.AccentBottomShiftDown             then mathparameters.AccentBottomShiftDown             = 0 end
134        if not mathparameters.FlattenedAccentTopShiftUp         then mathparameters.AccentTopShiftUp                  = 0 end
135        if not mathparameters.FlattenedAccentBottomShiftDown    then mathparameters.AccentBottomShiftDown             = 0 end
136        if not mathparameters.AccentBaseDepth                   then mathparameters.AccentBaseDepth                   = 0 end
137        if not mathparameters.AccentFlattenedBaseDepth          then mathparameters.AccentFlattenedBaseDepth          = 0 end
138        if not mathparameters.AccentTopOvershoot                then mathparameters.AccentTopOvershoot                = 0 end
139        if not mathparameters.AccentBottomOvershoot             then mathparameters.AccentBottomOvershoot             = 0 end
140        if not mathparameters.AccentSuperscriptDrop             then mathparameters.AccentSuperscriptDrop             = 0 end
141        if not mathparameters.AccentSuperscriptPercent          then mathparameters.AccentSuperscriptPercent          = 0 end
142        if not mathparameters.AccentExtendMargin                then mathparameters.AccentExtendMargin                = 50 end
143        if not mathparameters.DelimiterPercent                  then mathparameters.DelimiterPercent                  = 100 end
144        if not mathparameters.DelimiterShortfall                then mathparameters.DelimiterShortfall                = 0 end
145        if not mathparameters.DelimiterDisplayPercent           then mathparameters.DelimiterDisplayPercent           = 100 end
146        if not mathparameters.DelimiterDisplayShortfall         then mathparameters.DelimiterDisplayShortfall         = 0 end
147        if not mathparameters.RadicalKernAfterExtensible        then mathparameters.RadicalKernAfterExtensible        = 0 end
148        if not mathparameters.RadicalKernBeforeExtensible       then mathparameters.RadicalKernBeforeExtensible       = 0 end
149        if not mathparameters.SuperscriptSnap                   then mathparameters.SuperscriptSnap                   = 750 end
150        if not mathparameters.SubscriptSnap                     then mathparameters.SubscriptSnap                     = 250 end
151        --
152-- if mathparameters.RadicalDisplayStyleVerticalGap then
153--     mathparameters.RadicalDisplayStyleVerticalGap = mathparameters.RadicalDisplayStyleVerticalGap - 100
154-- end
155-- if mathparameters.RadicalVerticalGap then
156--     mathparameters.RadicalVerticalGap = mathparameters.RadicalVerticalGap - 100
157-- end
158        --
159        target.mathparameters = mathparameters
160    end
161end
162
163sequencers.appendaction("mathparameters","system","mathematics.initializeparameters")
164
165local how = {
166 -- RadicalKernBeforeDegree         = "horizontal",
167 -- RadicalKernAfterDegree          = "horizontal",
168    ScriptPercentScaleDown          = "unscaled",
169    ScriptScriptPercentScaleDown    = "unscaled",
170    RadicalDegreeBottomRaisePercent = "unscaled",
171    NoLimitSupFactor                = "unscaled",
172    NoLimitSubFactor                = "unscaled",
173    PrimeRaisePercent               = "unscaled",
174    PrimeRaiseComposedPercent       = "unscaled",
175    AccentTopOvershoot              = "unscaled",
176    AccentBottomOvershoot           = "unscaled",
177    AccentSuperscriptPercent        = "unscaled",
178    DelimiterPercent                = "unscaled",
179    DelimiterDisplayPercent         = "unscaled",
180    --
181    RadicalRuleThickness            = "vertical",
182    OverbarRuleThickness            = "vertical",
183    FractionRuleThickness           = "vertical",
184    UnderbarRuleThickness           = "vertical",
185}
186
187local function scaleparameters(mathparameters,parameters)
188    if mathparameters and next(mathparameters) and parameters then
189        local factor  = parameters.factor
190        local hfactor = parameters.hfactor
191        local vfactor = parameters.vfactor
192        for name, value in next, mathparameters do
193            local h = how[name]
194            if h == "unscaled" then
195                -- kept
196            elseif h == "horizontal" then
197                value = value * hfactor
198            elseif h == "vertical" then
199                value = value * vfactor
200            else
201                value = value * factor
202            end
203            mathparameters[name] = value
204        end
205    end
206end
207
208function mathematics.scaleparameters(target,original)
209    if not target.properties.math_is_scaled then
210        scaleparameters(target.mathparameters,target.parameters)
211        target.properties.math_is_scaled = true
212    end
213end
214
215function mathematics.overloadparameters(target,original)
216    if use_math_goodies then
217        local mathparameters = target.mathparameters
218        if mathparameters and next(mathparameters) then
219            local goodies = target.goodies
220            if goodies then
221                for i=1,#goodies do
222                    local goodie = goodies[i]
223                    local mathematics = goodie.mathematics
224                    if mathematics then
225                        local parameters = mathematics.parameters
226                        local bigslots   = mathematics.bigslots or mathematics.bigs
227                        local scales     = mathematics.scales
228                        if parameters then
229                            if trace_defining then
230                                report_math("overloading math parameters in %a @ %p",target.properties.fullname,target.parameters.size)
231                            end
232                            for name, value in next, parameters do
233                                local tvalue   = type(value)
234                                local oldvalue = mathparameters[name]
235                                local newvalue = oldvalue
236                                if tvalue == "number" then
237                                    newvalue = value
238                                elseif tvalue == "string" then
239                                    -- delay till all set
240                                elseif tvalue == "function" then
241                                    newvalue = value(oldvalue,target,original)
242                                elseif not tvalue then
243                                    newvalue = nil
244                                end
245                                if trace_defining and oldvalue ~= newvalue then
246                                    report_math("overloading math parameter %a: %S => %S",name,oldvalue or 0,newvalue)
247                                end
248                                mathparameters[name] = newvalue
249                            end
250                            for name, value in next, parameters do
251                                local tvalue = type(value)
252                                if tvalue == "string" then
253                                    local newvalue = mathparameters[value]
254                                    if not newvalue then
255                                        local code = loadstring("return " .. value,"","t",mathparameters)
256                                        if type(code) == "function" then
257                                            local okay, v = pcall(code)
258                                            if okay then
259                                                newvalue = v
260                                            end
261                                        end
262                                    end
263                                    if newvalue then
264                                        -- split in number and string
265                                        mathparameters[name] = newvalue
266                                    elseif trace_defining then
267                                        report_math("ignoring math parameter %a: %S",name,value)
268                                    end
269                                end
270                            end
271                        end
272                        if bigslots then
273                            target.bigslots = bigslots
274                        end
275                        if scales then
276                            local feature  = scales.feature
277                            local features = target.specification.features.normal
278                            if feature == nil or features[feature] then
279                                target.scriptxscale       = scales.scriptxscale
280                                target.scriptyscale       = scales.scriptyscale
281                                target.scriptweight       = scales.scriptweight
282                                target.scriptscriptxscale = scales.scriptscriptxscale
283                                target.scriptscriptyscale = scales.scriptscriptyscale
284                                target.scriptscriptweight = scales.scriptscriptweight
285                            end
286                        end
287                    end
288                end
289            end
290        end
291    end
292end
293
294-- a couple of predefined tweaks:
295
296local datasets       = { }
297local mathtweaks     = { datasets = datasets }
298mathematics.tweaks   = mathtweaks
299
300-- can be a common helper:
301
302local f_u = formatters["%U"]
303
304local function unicodecharlist(t)
305    local r = { }
306    local n = 0
307    for u in sortedhash(t) do
308        n = n + 1 ; r[n] = f_u(u)
309    end
310    return concat(r," ")
311end
312
313local function report_tweak(fmt,target,original,...)
314    if fmt then
315        local metadata   = (original and original.shared.rawdata.metadata) or
316                           (target   and target  .shared.rawdata.metadata)
317        local parameters = target.parameters
318        if parameters then
319            report_mathtweak(
320                "%a, size %P, math size %i, %s",
321                metadata and metadata.fontname or "unknown",
322                parameters.size or 655360,
323                parameters.mathsize or 1,
324                formatters[fmt](...)
325            )
326        else
327            print("something is wrong")
328        end
329    else
330        report_mathtweak("")
331    end
332end
333
334local function feedback_tweak(tweak,target,original,done)
335    if not done or (type(done) == "table" and not next(done)) then
336if trace_tweaking then -- for now
337        report_tweak("no need for %a",target,original,tweak)
338end
339    elseif trace_tweaking then
340        report_tweak("tweak %a applied to: %s",target,original,tweak,unicodecharlist(done))
341    end
342end
343
344mathtweaks.subsets = {
345    acenorsuvxz      = { 0x1D44E, 0x1D450, 0x1D452, 0x1D45B, 0x1D45C, 0x1D45F, 0x1D460, 0x1D462, 0x1D463, 0x1D465, 0x1D467 },
346    bhklt            = { 0x1D44F, 0x1D455, 0x1D458, 0x1D459, 0x1D461 },
347    d                = { 0x1D451 },
348    f                = { 0x1D453 },
349    gjqy             = { 0x1D454, 0x1D457, 0x1D45E, 0x1D466 },
350    i                = { 0x1D456 },
351    mw               = { 0x1D45A, 0x1D464 },
352    p                = { 0x1D45D },
353    letterlike       = { 0x02202 },
354    dotless          = { 0x00049, 0x0004A, 0x00131, 0x00237, 0x1D6A4, 0x1D6A5 },
355    integrals        = { 0x0222B, 0x0222C, 0x0222D, 0x0222E, 0x0222F, 0x02230, 0x02231, 0x02232, 0x02233, 0x02A0B, 0x02A0C, 0x02A0D, 0x02A0E, 0x02A0F, 0x02A10, 0x02A11, 0x02A12, 0x02A13, 0x02A14, 0x02A15, 0x02A16, 0x02A17, 0x02A18, 0x02A19, 0x02A1A, 0x02A1B, 0x02A1C, 0x02320, 0x02321 },
356    horizontalfences = { 0x0203E, 0x023B4, 0x023B5, 0x023DC, 0x023DD, 0x023DE, 0x023DF, 0x023E0, 0x023E1 }, -- not really used
357}
358
359local function getalso(target,original)
360    local also = target.tweakalso -- maybe
361    if not also then
362        also = { }
363     -- for k, v in sortedhash(target.characters) do
364        for k, v in next, target.characters do
365            local u = v.unicode
366            if u and k ~= u then
367                local a = also[u]
368                if a then
369                    a[#a+1] = k
370                else
371                    also[u] = { k }
372                end
373            end
374        end
375        target.tweakalso = also
376    end
377    return also
378end
379
380-- {
381--     tweak = "dimensions",
382--     list  = {
383--         ["lowercasegreeksansserifbolditalic"] = {
384--          -- delta    = 0x003B1 - 0x1D7AA,
385--             slant    = -0.2,
386--             line     = 0.1,
387--             mode     = 1,
388--             width    = 0.675,
389--          -- scale    = 0.975,
390--             squeeze  = 0.975,
391--             extend   = .7,
392--         },
393--     },
394-- },
395
396 -- ["0x7C.variants.*"] = { squeeze = 0.10, height = 0.10, depth = 0.10 },
397
398local detail  do
399
400    local splitter = lpeg.tsplitat(".")
401    local private  = fonts.helpers.privateslot
402
403    detail = function(characters,k)
404        if type(k) == "string" then
405            local t = lpegmatch(splitter,k)
406            local n = #t
407            if n > 0 then
408                local slot = t[1]
409                local base = tonumber(slot) or tonumber(slot,16) or private(slot)
410                if base then
411                    local c = characters[base]
412                    if c and n > 1 then
413                        local list = t[2]
414                        if list == "parts" then
415                            local nxt = c.next
416                            while nxt do
417                                c = characters[nxt]
418                                nxt = c.next
419                            end
420                            c = c.parts
421                            if c then
422                                local index = t[3]
423                                if index == "*" then
424                                    return t
425                                else
426                                    if index == "top" then
427                                        index = #c
428                                    elseif index == "bottom" then
429                                        index = 1
430                                    else
431                                        index = tonumber(index)
432                                    end
433                                    if index then
434                                        c = c[index]
435                                        if c then
436                                            return c.glyph
437                                        end
438                                    end
439                                end
440                            end
441                        elseif list == "variants" then
442                            local index = t[3]
443                            if index == "*" then
444                                local t = { }
445                                local nxt = c.next
446                                while nxt do
447                                    t[#t+1] = nxt
448                                    c = characters[nxt]
449                                    nxt = c.next
450                                end
451                                return t
452                            else
453                                index = tonumber(index)
454                                if index then
455                                    local nxt = c.next
456                                    while nxt and index > 1 do
457                                        c = characters[nxt]
458                                        nxt = c.next
459                                        index = index - 1
460                                    end
461                                    return nxt
462                                end
463                            end
464                        elseif list == "flataccent" then
465                            return c.flataccent
466                        end
467                    end
468                end
469            end
470        else
471            return k
472        end
473    end
474
475end
476
477-- This temporary tweak was used when we (MS & HH) were fixing the Latin Modern
478-- parameters that relate to script placement. We started from the original cmr
479-- ratios combined with the formal specification and ended up with the following
480-- values. In the end we rejected this tweak and setteled for checking and fixing:
481--
482-- SubscriptShiftDown
483-- SubscriptShiftDownWithSuperscript
484-- SuperscriptShiftUp
485-- SuperscriptShiftUpCramped
486--
487-- because it looks a bit arbitrary what values are set. We keep the code below
488-- as documentation.
489
490-- do
491--     -- modern
492--     --
493--     -- local factors = {
494--     --     scripts = {
495--     --         SubscriptBaselineDropMin          = 0.116,
496--     --         SubscriptShiftDown                = 0.348,
497--     --         SubscriptShiftDownWithSuperscript = 0.573,
498--     --         SubscriptTopMax                   = 0.800,
499--     --         SuperscriptBaselineDropMax        = 0.896,
500--     --         SuperscriptBottomMaxWithSubscript = 0.800,
501--     --         SuperscriptBottomMin              = 0.250,
502--     --         SuperscriptShiftUp                = 0.958,
503--     --         SuperscriptShiftUpCramped         = 0.958,
504--     --     }
505--     -- }
506--
507--     -- -- cambria
508--     --
509--     -- local factors = {
510--     --     scripts = {
511--     --         SubscriptBaselineDropMin          = 0.279,
512--     --         SubscriptShiftDown                = 0.364,
513--     --         SubscriptShiftDownWithSuperscript = 0.547,
514--     --         SubscriptTopMax                   = 0.662,
515--     --         SuperscriptBaselineDropMax        = 0.401,
516--     --         SuperscriptBottomMaxWithSubscript = 0.667,
517--     --         SuperscriptBottomMin              = 0.208,
518--     --         SuperscriptShiftUp                = 0.654,
519--     --         SuperscriptShiftUpCramped         = 0.654,
520--     --     }
521--     -- }
522--
523--     -- after some tests and inspection
524--     --
525--
526--     local factors = {
527--         scripts = {
528--             SubscriptBaselineDropMin          = 0.100, -- harmless but small (seldom triggered)
529--             SubscriptShiftDown                = 0.400, -- by inspection in several files
530--             SubscriptShiftDownWithSuperscript = 0.400, -- as above
531--             SubscriptTopMax                   = 0.800, -- Microsoft recommendation
532--             SuperscriptBaselineDropMax        = 0.100, -- see SubscriptBaselineDropMin
533--             SuperscriptBottomMaxWithSubscript = 0.800, -- Microsoft recommendation
534--             SuperscriptBottomMin              = 0.250, -- Microsoft recommendation
535--             SuperscriptShiftUp                = 0.650, -- by inspection, but also a bit gamble
536--             SuperscriptShiftUpCramped         = 0.650, -- see above, non-TeX
537--         }
538--     }
539--
540--     datasets.fixparameters = factors
541--
542--     function mathtweaks.fixparameters(target,original,parameters)
543--         local mathparameters = target.mathparameters
544--         if mathparameters and next(mathparameters) then
545--             local xheight = target.parameters.xheight
546--             -- todo : options
547--             for k, v in next, factors.scripts do
548--                 mathparameters[k] = v * xheight
549--             end
550--         end
551--     end
552--
553-- end
554
555do
556
557    local stepper     = utilities.parsers.stepper
558    local count       = 0
559    local toeffect    = fonts.toeffect
560    local privateslot = fonts.helpers.privateslot
561
562    local function adapt(list,target,original,targetcharacters,originalcharacters,k,v,compact,n)
563        k = mathgaps[k] or k
564        local character = targetcharacters[k]
565        if character then
566         -- if not character.tweaked then -- todo: add a force
567                local t = type(v)
568                if t == "number" then
569                    v = list[v]
570                    t = type(v)
571                end
572                if t == "table" and next(v) then
573
574                    local axis = tonumber(v.axis)
575                    if axis then
576                        axis = target.mathparameters.AxisHeight * axis
577                    end
578
579                    local factor = v.factor
580                    if factor then
581                        local m = v
582                        v = setmetatableindex({
583                            width   = factor,
584                            height  = factor,
585                            depth   = factor,
586                            squeeze = factor,
587                            extend  = factor,
588                        }, v)
589                    end
590                    local originalslot = v.original
591                    if not originalslot then
592                        local delta = v.delta
593                        if delta then
594                            originalslot = k + delta
595                        end
596                    end
597                    if originalslot then
598                        originalslot = mathgaps[originalslot] or originalslot
599                        local data = targetcharacters[originalslot]
600                        if data then
601                            data = copytable(data)
602                            data.unicode = originalslot
603                            targetcharacters[k] = data
604                            character = data
605                        else
606                            report_mathtweak("no slot %U",originalslot)
607                            return
608                        end
609                    end
610                    --
611                    local width        = character.width
612                    local height       = character.height
613                    local depth        = character.depth
614                    local italic       = character.italic
615                    local topanchor    = character.topanchor
616                    local bottomanchor = character.bottomanchor
617                    --
618                    local widthfactor   = v.width
619                    local heightfactor  = v.height
620                    local depthfactor   = v.depth
621                    local italicfactor  = v.italic
622                    local anchorfactor  = v.anchor
623                    local advancefactor = v.advance
624                    local xoffsetfactor = v.xoffset
625                    local yoffsetfactor = v.yoffset
626                    local scalefactor   = v.scale
627                    local total = (height or 0) + (depth or 0)
628                    if scalefactor ~= 1 then
629                        character.scale = scalefactor
630                    end
631                    if width and width ~= 0 then
632                        if advancefactor then
633                            character.advance = advancefactor * width
634                        else
635                            character.advance = character.advance or width -- so advance is oldwidth
636                        end
637                        if widthfactor then
638                            character.width = widthfactor * width
639                        end
640                        if xoffsetfactor then
641                            character.xoffset = xoffsetfactor * width
642                        end
643                    end
644                    if height and height ~= 0 then
645                        if heightfactor then
646                            character.height  = heightfactor * height
647                        end
648                    end
649                    if depth and depthfactor then
650                        character.depth = depthfactor * depth
651                    end
652                    if yoffsetfactor then
653                        character.yoffset = yoffsetfactor * total
654                    end
655
656                    if axis then
657                        character.height  = (character.height  or 0) - axis
658                        character.depth   = (character.depth   or 0) + axis
659                        character.yoffset = (character.yoffset or 0) + axis
660                    end
661
662                    if italicfactor then
663                        if italic then
664                            character.italic = italicfactor * italic
665                        elseif width and italicfactor ~= 1 then
666                            character.italic = italicfactor * width
667                        end
668                    end
669                    if anchorfactor then
670                        character.topanchor = anchorfactor * (topanchor or width)
671                    end
672                 -- if anchorfactor then
673                 --     character.bottomaccent = anchorfactor * (bottomanchor or width)
674                 -- end
675                 -- begin experiment
676                    local line = v.wline
677                    if line then
678                        local parameters = target.parameters
679                        v.line = parameters.hfactor * line / parameters.units
680                    end
681                 -- end experiment
682                    character.effect = toeffect(v) -- todo: move wline test inside here
683                 -- begin experiment
684                    v.line = line
685                 -- end experiment
686                    if trace_tweaking then
687                        report_tweak("adapting dimensions of %U ",target,original,k)
688                    end
689                     -- missing when private
690                    local originaldata = originalcharacters[k] -- or targetcharacters[k]
691                    local smaller = originaldata and originaldata.smaller
692                    if compact and smaller and smaller ~= k then
693                        adapt(list,target,original,targetcharacters,originalcharacters,smaller,v,compact,n+1)
694                    end
695                    count = count + 1
696                else
697                    report_mathtweak("invalid dimension entry %U",k)
698                end
699             -- character.tweaked = true
700                if v.all then
701                    local nxt = character.next
702                    if nxt then
703                        adapt(list,target,original,targetcharacters,originalcharacters,nxt,v,compact,n)
704                    else
705                        local parts = character.parts
706                        if parts then
707                            for i=1,#parts do
708                                adapt(list,target,original,targetcharacters,originalcharacters,parts[i],v,compact,n)
709                            end
710                        end
711                    end
712                end
713         -- end
714        else
715            report_tweak("no character %U",target,original,k)
716        end
717    end
718
719    function mathtweaks.dimensions(target,original,parameters)
720        local list = parameters.list
721        if list then
722            local targetcharacters   = target.characters
723            local originalcharacters = original.characters
724            local compact = target.parameters.textscale and true or false
725            count = 0
726            for k, v in sortedhash(list) do
727                local t = type(k)
728                if t == "number" then
729                    adapt(list,target,original,targetcharacters,originalcharacters,k,v,compact,1)
730                elseif t == "string" then
731                    local d = privateslot(k) or detail(targetcharacters,k) -- watch the private here
732                    local t = type(d)
733                    if t == "table" then
734                        for i=1,#d do
735                            adapt(list,target,original,targetcharacters,originalcharacters,d[i],v,compact,1)
736                        end
737                    elseif t == "number" then
738                        adapt(list,target,original,targetcharacters,originalcharacters,d,v,compact,1)
739                    elseif d then
740                        -- some kind of error
741                    else
742                        local r = blocks[k]
743                        if r then
744                            local done = false
745                            for i=r.first,r.last do
746                                adapt(list,target,original,targetcharacters,originalcharacters,i,v,compact,1)
747                            end
748                        else
749                            stepper(k,function(n)
750                                adapt(list,target,original,targetcharacters,originalcharacters,n,v,compact,1)
751                            end)
752                        end
753                    end
754             -- elseif t == "table" then
755             --     for i=1,#t do
756             --         adapt(list,target,original,targetcharacters,originalcharacters,t[i],v,compact,1)
757             --     end
758                end
759            end
760            if trace_tweaking and count > 0 then
761                report_mathtweak("%i dimensions adapted",count)
762            end
763        end
764    end
765
766end
767
768do
769
770    function mathtweaks.message(target,original,parameters)
771        report_mathtweak(parameters.text or "no message")
772    end
773
774    function mathtweaks.showinfo(target,original,parameters)
775        local mathparameters = target.mathparameters
776        for k, v in sortedhash(mathparameters) do
777            report_mathtweak("%s : %s",k,v)
778        end
779    end
780
781end
782
783do
784
785    function mathtweaks.wipevariants(target,original,parameters)
786        local list = parameters.list
787        if list then
788            local targetcharacters   = target.characters
789         -- local originalcharacters = original.characters
790            local count = 0
791         -- local also  = getalso(target,original)
792            local done  = false
793            for k, v in sortedhash(list) do
794                local ori = targetcharacters[k]
795                local nxt = ori.next
796                local cnt = v
797                if nxt then
798                    local prt = nil
799                    local pro = nil
800                    local lst = { }
801                    while nxt do
802                        local chr = targetcharacters[nxt]
803                        lst[#lst+1] = chr
804                        nxt = chr.next
805                        if not nxt then
806                            prt = chr.parts
807                            pro = chr.partsorientation
808                            break
809                        end
810                    end
811                    if prt then
812                        count = count + 1
813                        if cnt ~= "*" then
814                            if #lst < cnt then
815                                cnt = #lst
816                            end
817                            ori = lst[cnt]
818                        end
819                        ori.parts = prt
820                        ori.partsorientation = pro
821                    end
822                    done = registerdone(done,k)
823                end
824            end
825            feedback_tweak("wipevariants",target,original,done)
826        end
827    end
828
829end
830
831do
832
833    -- This is a horrible tweak for lm that has bars inconsistent with other fences.
834
835    local function find(targetcharacters,slot,n)
836        local chr = targetcharacters[slot]
837        while chr do
838            slot = chr.next
839            if not slot then
840                break
841            elseif n == 1 then
842                return slot
843            end
844            n = n - 1
845            chr = targetcharacters[slot]
846        end
847        return nil
848    end
849
850    function mathtweaks.fixvariants(target,original,parameters)
851        local list = parameters.list
852        if list then
853            local targetcharacters   = target.characters
854            local done  = false
855            for k, v in sortedhash(list) do
856                local tmp = v.template
857                local idx = v.index
858                local dy  = v.yoffset or 0
859                if tmp and idx then
860                    local olduni = find(targetcharacters,k,idx)
861                    local tmpuni = find(targetcharacters,tmp,idx)
862                    local axis   = target.mathparameters.AxisHeight
863                    if axis then
864                        while olduni and tmpuni do
865                            local oldchr  = targetcharacters[olduni]
866                            local tmpchr  = targetcharacters[tmpuni]
867                            local oldht   = oldchr.height or 0
868                            local olddp   = oldchr.depth  or 0
869                            local tmpht   = tmpchr.height or 0
870                            local tmpdp   = tmpchr.depth  or 0
871                            local scale   = (tmpht + tmpdp) / (oldht + olddp)
872                            local total   = (oldht - olddp) * scale
873                            local yoffset = axis - total/2
874                            oldchr.effect = { squeeze = scale }
875                            oldchr.height  = scale * oldht + yoffset
876                            oldchr.depth   = scale * olddp - yoffset
877                            oldchr.yoffset = yoffset
878                            olduni = oldchr.next
879                            tmpuni = tmpchr.next
880                        end
881                        done = registerdone(done,k)
882                    end
883                end
884            end
885            feedback_tweak("addvariants",target,original,done)
886        end
887    end
888
889end
890
891do
892
893    function mathtweaks.replace(target,original,parameters)
894        local list = parameters.list
895        if list then
896            local targetcharacters   = target.characters
897            local originalcharacters = original.characters
898            local unicodes = original.resources.unicodes
899            if unicodes then
900                local count = 0
901                for k, v in sortedhash(list) do
902                    if type(v) == "string" then
903                        v = unicodes[v]
904                    end
905                    if type(v) == "number" then
906                        targetcharacters[mathgaps[k] or k] = targetcharacters[mathgaps[v] or v]
907                        count = count + 1
908                    end
909                end
910                if trace_tweaking and count > 0 then
911                    report_tweak("%i permanent replacements",target,original,count)
912                end
913            end
914        end
915    end
916
917    function mathtweaks.substitute(target,original,parameters)
918        local list = parameters.list
919        if list then
920            local targetcharacters   = target.characters
921            local originalcharacters = original.characters
922            local getsubstitution    = fonts.handlers.otf.getsubstitution
923            local count              = 0
924            for k, v in next, list do -- no need for sortedhash(list) unless we report
925                local sub = getsubstitution(original,k,v,true)
926                if sub then
927                    targetcharacters[mathgaps[k] or k] = targetcharacters[mathgaps[sub] or sub]
928                    count = count + 1
929                end
930            end
931            if trace_tweaking and count > 0 then
932                report_tweak("%i permanent substitutions",target,original,count)
933            end
934        end
935    end
936
937end
938
939do
940
941    -- maybe we'll have a different name
942
943    function mathtweaks.kernpairs(target,original,parameters)
944        local list = parameters.list
945        if list then
946            local targetcharacters   = target.characters
947            local originalcharacters = original.characters
948            local done               = false
949
950            local function add(v,n)
951                local chardata = targetcharacters[mathgaps[n] or n]
952                if chardata then
953                    local width = chardata.width
954                    if width then
955                        local kerns = chardata.kerns or { }
956                        for kk, vv in next, v do
957                            stepper(kk,function(nn) -- todo: also make stepper accept a table
958                                local t = mathgaps[nn] or nn
959                                if t then
960                                    kerns[t] = vv * width
961                                    done = registerdone(done,t)
962                                end
963                            end)
964                        end
965                        chardata.kerns = kerns
966                    end
967                end
968            end
969
970            for k, v in next, list do -- no need for sortedhash(list) unless we report
971                stepper(k,function(n) -- todo: also make stepper accept a table
972                    add(v,n)
973                end)
974            end
975
976         -- for k, v in next, list do -- no need for sortedhash(list) unless we report
977         --     local chardata = targetcharacters[mathgaps[k] or k]
978         --     if chardata then
979         --         local width = chardata.width
980         --         if width then
981         --             local kerns = chardata.kerns or { }
982         --             for kk, vv in next, v do
983         --                 local t = mathgaps[kk] or kk
984         --                 if t then
985         --                     kerns[t] = vv * width
986         --                     count = count + 1
987         --                 end
988         --             end
989         --             chardata.kerns = kerns
990         --         end
991         --     end
992         -- end
993
994            feedback_tweak("kernpairs",target,original,done)
995        end
996    end
997
998end
999
1000do
1001
1002    local list = {
1003        { 0x2032, 1 },
1004        { 0x2033, 2, 0x2032 },
1005        { 0x2034, 3, 0x2032 },
1006        { 0x2057, 4, 0x2032 },
1007        { 0x2035, 1 },
1008        { 0x2036, 2, 0x2035 },
1009        { 0x2037, 3, 0x2035 },
1010    }
1011
1012    datasets.fixprimes = list
1013
1014    function mathtweaks.fixprimes(target,original,parameters)
1015        local targetcharacters = target.characters
1016        local factor = parameters.factor or 1
1017        local fake   = tonumber(parameters.fake)
1018        for i=1,#list do
1019            local entry   = list[i]
1020            local unicode = entry[1]
1021            local count   = entry[2]
1022            local used    = fonts.handlers.otf.getsubstitution(target,unicode,"ssty",true,"math","dflt") or unicode
1023            local data    = targetcharacters[used]
1024            if data then
1025                targetcharacters[unicode] = data
1026                local oldheight = data.height or 0
1027                local newheight = factor * oldheight
1028                data.yoffset = newheight - (oldheight or 0)
1029                data.height  = newheight
1030                data.smaller = nil
1031            elseif not fake then
1032                report_tweak("missing %i prime %U",target,original,count,unicode)
1033            end
1034        end
1035        if fake then
1036            for i=1,#list do
1037                local entry = list[i]
1038                local count = entry[2]
1039                if count > 1 then
1040                    local unicode  = entry[1]
1041                    local original = entry[3]
1042                    local data     = targetcharacters[original]
1043                    if data then
1044                        local oldwidth = data.width
1045                        local xoffset  = fake * oldwidth
1046                        local newwidth = oldwidth + (count - 1) * xoffset
1047                        targetcharacters[unicode] = {
1048                            width    = newwidth,
1049                            height   = data.height,
1050                            unicode  = unicode,
1051                            commands = {
1052                                              { "offset",           0, 0, original },
1053                                              { "offset",     xoffset, 0, original },
1054                                count > 2 and { "offset", 2 * xoffset, 0, original } or nil,
1055                                count > 3 and { "offset", 3 * xoffset, 0, original } or nil,
1056                            },
1057                        }
1058                    end
1059                end
1060            end
1061        end
1062    end
1063
1064end
1065
1066do
1067
1068    local nps = fonts.helpers.newprivateslot
1069
1070    local privates = {
1071        [0x2212] = nps("unary minus"),
1072        [0x002B] = nps("unary plus"),
1073        [0x00B1] = nps("unary plus minus"),
1074        [0x2213] = nps("unary minus plus"),
1075    }
1076
1077    -- these are the values tested with texgyre-bonum
1078
1079    local predefined  = {
1080        ["unary minus"] = {
1081            original = 0x2212,
1082            extend   = .5,
1083            width    = .5,
1084            unicode  = 0x002D, -- hyphen minus
1085        },
1086        ["unary plus"] = {
1087            original = 0x002B,
1088            extend   = .5,
1089            squeeze  = .5,
1090            width    = .5,
1091            height   = .5,
1092            yoffset  = .2,
1093            mode     = 2,
1094            wline    = .5,
1095            unicode  = 0x002B,
1096        },
1097        ["unary plus minus"] = {
1098            original = 0x00B1,
1099            extend   = .5,
1100            squeeze  = .5,
1101            width    = .5,
1102            height   = .5,
1103            yoffset  = .2,
1104            mode     = 2,
1105            wline    = .5,
1106        },
1107        ["unary minus plus"] = {
1108            original = 0x2213,
1109            extend   = .5,
1110            squeeze  = .5,
1111            width    = .5,
1112            height   = .5,
1113            yoffset  = .2,
1114            mode     = 2,
1115            wline    = .5,
1116        },
1117    }
1118
1119 -- {
1120 --     tweak = "addprivates",
1121 --     list  = {
1122 --         -- for specific parameters see act file
1123 --         ["unary minus"]      = { preset = "unary minus"      },
1124 --         ["unary plus"]       = { preset = "unary plus"       },
1125 --         ["unary plus minus"] = { preset = "unary plus minus" },
1126 --         ["unary minus plus"] = { preset = "unary minus plus" },
1127 --     },
1128 -- },
1129
1130    function mathtweaks.addprivates(target,original,parameters)
1131        local list = parameters.list or predefined
1132        if list then
1133            local targetcharacters   = target.characters
1134            local targetparameters   = target.parameters
1135            local originalcharacters = original.characters
1136            local processedprivates  = { }
1137            for name, v in sortedhash(list) do
1138                if type(v) == "table" then
1139                    local preset = v.preset
1140                    if preset then
1141                        local p = predefined[preset]
1142                        if p then
1143                            v = table.combine(p,v)
1144                            p.preset = nil
1145                        else
1146                            goto next
1147                        end
1148                    end
1149                    local charslot = v.original
1150                    if charslot then
1151                        local chardata = targetcharacters[charslot]
1152                        if chardata then
1153                            local clonedata = copytable(chardata)
1154                            local cloneslot = nps(name)
1155                            local unicode   = v.unicode or clonedata.unicode
1156                            clonedata.uncode = unicode
1157                            targetcharacters[cloneslot] = clonedata
1158                            if trace_tweaking then
1159                                report_tweak("cloning %a from %C into %U with tounicode %U",target,original,name,charslot,cloneslot,unicode)
1160                            end
1161                        end
1162                        processedprivates[name] = v
1163                    end
1164                  ::next::
1165                end
1166            end
1167            mathtweaks.dimensions(target,original,{
1168                tweak = parameters.tweak,
1169                list  = processedprivates,
1170            })
1171        end
1172    end
1173
1174end
1175
1176-- do
1177--
1178--     function mathtweaks.fixanchors(target,original,parameters)
1179--         local targetcharacters= target.characters
1180--         local factor = tonumber(parameters.factor) or 0
1181--         if factor ~= 0 then
1182--             local done = false
1183--             for k, v in next, targetcharacters do
1184--                 local a = v.topanchor
1185--                 if a and a > 0 then
1186--                     v.topanchor = a * factor
1187--                     count = count + 1
1188--                     done = registerdone(done,u)
1189--                 end
1190--             end
1191--         end
1192--         feedback_tweak("fixanchors",target,original,done)
1193--     end
1194--
1195-- end
1196
1197-- do
1198--
1199--     -- actually this should be a an engine feature driven by category because we don't
1200--     -- want this in display mode .. only a test for MS and HH
1201--
1202--     local issymbol = characters.is_symbol
1203--
1204--     function mathtweaks.oldstylemath(target,original,parameters)
1205--         local chardata = characters.data
1206--         local characters = target.characters
1207--         local axis       = target.mathparameters.AxisHeight
1208--         local delta      = (parameters.factor or .1) * axis
1209--         target.mathparameters.AxisHeight = (axis - delta)
1210--         for k, v in sortedhash(characters) do
1211--             if issymbol[k] then -- quick hack, engine knows
1212--                 v.yoffset = -delta
1213--                 v.height  = (v.height or 0) - delta
1214--                 v.depth   = (v.depth  or 0) - delta
1215--             end
1216--         end
1217--     end
1218--
1219--     function mathtweaks.oldstylemath(target,original,parameters)
1220--         -- not relevant
1221--     end
1222--
1223-- end
1224
1225do
1226
1227    function mathtweaks.simplifykerns(target,original,parameters)
1228        local characters = target.characters
1229        local done       = false
1230     -- for u, v in sortedhash(characters) do
1231        for u, v in next, characters do
1232            local mathkerns = v.mathkerns
1233            if mathkerns then
1234                local k = mathkerns.topleft
1235                if k then
1236                    k = k[#k].kern
1237                    if k then
1238                        v.topleft = k
1239                    end
1240                end
1241                local k = mathkerns.topright
1242                if k then
1243                    k = k[#k].kern
1244                    if k then
1245                        v.topright = k
1246                    end
1247                end
1248                local k = mathkerns.bottomleft
1249                if k then
1250                    k = k[1].kern -- todo get value at baseline
1251                    if k then
1252                        v.bottomleft = k
1253                    end
1254                end
1255                local k = mathkerns.bottomright
1256                if k then
1257                    k = k[1].kern -- todo get value at baseline
1258                    if k then
1259                        v.bottomright = k
1260                    end
1261                end
1262                v.mathkerns = nil
1263                done = registerdone(done,u)
1264            end
1265        end
1266        feedback_tweak("simplifykerns",target,original,done)
1267    end
1268
1269end
1270
1271
1272-- In TeX The Program we find in section 543 a remark about the second use of math
1273-- italic: it is always added to the width except with respect to the position of
1274-- the subscript. That paragraph also mentions that the number of different widths
1275-- is normally small so they can be shared (there is an eight bit index into a width
1276-- array). Actually the limits of at most 16 heights and depths has as side effect
1277-- that we get some snapping of sizes. Now, because many math characters have an
1278-- italic correction, the saving on shared widths is negated by the amount of (at
1279-- most 64) italics. So in practice there is no gain and the italic correction could
1280-- have served as bottom kern. We think that the following approach (that we actualy
1281-- came to by a different reasonsing: inconsistent open type fonts) is quite valid
1282-- and robust. It's just that the opentype math fonts should never have gone that
1283-- TeX italic route. Italic usage is more clear from section 543 than from the math
1284-- rendering code.
1285
1286do
1287
1288     local function wipe(whatever,target,original,parameters,field,move,integrals)
1289        local targetcharacters   = target.characters
1290        local targetdescriptions = target.descriptions
1291        local factor             = target.parameters.factor
1292        local correct            = parameters.correct
1293        local done               = false
1294        local function getllx(u)
1295            local d = targetdescriptions[u]
1296            if d then
1297                local b = d.boundingbox
1298                if b then
1299                    local llx = b[1]
1300                    if llx < 0 then
1301                        return - llx
1302                    end
1303                end
1304            end
1305            return false
1306        end
1307        local function step(s)
1308            while s do
1309                local u = mathgaps[s] or s
1310                local c = targetcharacters[u]
1311                if c then
1312                    if field == "topanchor" then
1313                        if c.topanchor then
1314                            c.topanchor = nil
1315                        else
1316                            goto smaller
1317                        end
1318                    else
1319                        local okay   = false
1320                        local italic = c.italic
1321                        if move and not c.advance then -- advance check prevents double move
1322                            local width  = c.width or 0
1323                            c.advance = width
1324                            if correct then
1325                                local llx = getllx(u)
1326                                if llx then
1327                                    local topanchor = c.topanchor
1328                                    llx   = llx * factor
1329                                    width = width + llx
1330                                    c.xoffset = llx
1331                                    if topanchor then
1332                                        c.topanchor = topanchor + llx
1333                                    end
1334                                    -- too bad (schola e^x):
1335                                 -- c.bottomleft = (c.bottomleft or 0) - llx
1336                                 -- c.topleft    = (c.topleft    or 0) - llx
1337                                    okay = true
1338                                end
1339                            end
1340                            if italic and italic ~= 0 then
1341                                c.width       = width + italic
1342                                c.bottomright = - italic
1343                                okay = true
1344                            else
1345                                c.width = width
1346                            end
1347                            c.bottomanchor = width/2 -- maybe optional
1348                        end
1349                        if italic then
1350                            c.italic = nil
1351                            okay = true
1352                        end
1353                        if okay then
1354                            done = registerdone(done,u)
1355                        else
1356                            goto smaller
1357                        end
1358                    end
1359                    goto smaller
1360                  ::smaller::
1361                    s = c.smaller
1362                  ::variants::
1363                    -- no italics here anyway but we could check them some day
1364                else
1365                    break
1366                end
1367            end
1368        end
1369        local list = parameters.list -- todo: ranges
1370        if list == "letters" or parameters.letters then
1371            local chardata = characters.data
1372         -- for k, v in sortedhash(targetcharacters) do
1373            for k, v in next, targetcharacters do
1374                if v.italic then
1375                    local d = chardata[v.unicode]
1376                    local c = d and d.category
1377                    if c == "ll" or c == "lu" then
1378                        step(k)
1379                    end
1380                end
1381            end
1382            goto done
1383        end
1384        if not list or list == "all" or list == true or parameters.all then
1385            list = sortedkeys(targetcharacters)
1386        elseif type(list) == "string" then
1387            list = { list }
1388        end
1389        for i=1,#list do
1390            local l = list[i]
1391            local t = type(l)
1392            if not l then
1393                -- can be false
1394            elseif t == "table" then
1395                for i=1,#l do
1396                    step(l[i])
1397                end
1398            elseif t == "number" then
1399                step(l)
1400            else
1401                local r = blocks[l]
1402                if r then
1403                    for i=r.first,r.last do
1404                        step(i)
1405                    end
1406                else
1407                    stepper(l,step)
1408                end
1409            end
1410        end
1411       ::done::
1412        feedback_tweak(whatever,target,original,done)
1413    end
1414
1415    function mathtweaks.wipeanchors(target,original,parameters)
1416        wipe("wipeanchors",target,original,parameters,"topanchor")
1417    end
1418
1419    function mathtweaks.wipeitalics(target,original,parameters)
1420        if not checkitalics then
1421            wipe("wipeitalics",target,original,parameters,"italic")
1422        end
1423    end
1424
1425    function mathtweaks.moveitalics(target,original,parameters)
1426        wipe("moveitalics",target,original,parameters,"italic",true)
1427    end
1428
1429 -- function mathtweaks.fixdigits(target,original,parameters)
1430 --     mathtweaks.fixanchors(target,original,{ list = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } })
1431 -- end
1432
1433end
1434
1435do
1436
1437    function mathtweaks.topanchors(target,original,parameters)
1438        local characters = target.characters
1439        local list       = parameters.list
1440        if list then
1441            local done = false
1442            for u, v in sortedhash(list) do
1443                local c = characters[k]
1444                if c then
1445                    local w = c.width
1446                    if w and w ~= 0 then
1447                        c.topanchor = v * w
1448                        done = registerdone(done,u)
1449                    end
1450                end
1451            end
1452            feedback_tweak("topanchors",target,original,done)
1453        end
1454    end
1455
1456    function mathtweaks.movelimits(target,original,parameters)
1457        local characters = target.characters
1458        local list       = parameters.list
1459        if list then
1460            local factor = parameters.factor or 1
1461            local also   = getalso(target,original)
1462            local done   = { }
1463            local function relocate(u,factor)
1464                if done[u] then
1465                    return
1466                end
1467                done[u] = true
1468                local c = characters[u]
1469                if c then
1470                    local italic = c.italic
1471                    if italic then
1472                        if italic ~= 0 then
1473                            local width = c.width or 0
1474                            local half  = (italic/2) * factor
1475                            c.topanchor    = width/2 + half
1476                            c.bottomanchor = width/2 - half
1477                            c.bottomright  = - italic * (parameters.icfactor or 1)
1478                            if trace_tweaking then
1479                                -- todo
1480                            end
1481                        end
1482                        c.anchored     = true
1483                        c.italic       = nil
1484                    end
1485                    local s = c.smaller
1486                    if s then
1487                        relocate(s,factor)
1488                    end
1489                    local n = c.next
1490                    if n then
1491                        relocate(n,factor)
1492                    end
1493                    -- Kind of tricky: we configure the engine to use the vitalic
1494                    -- so when we tweak we need to set that to zero.
1495                    local parts  = c.parts
1496                    local italic = c.partsitalic
1497                    if parts and italic then
1498                        if italic ~= 0 then
1499                            local tchar = characters[parts[#parts].glyph]
1500                            local bchar = characters[parts[1].glyph]
1501                            local width = tchar.width or 0
1502                            local half  = (italic/2) * factor
1503                            tchar.topanchor    = width/2 + half
1504                            bchar.bottomanchor = width/2 - half
1505                            bchar.bottomright  = - italic
1506                            if trace_tweaking then
1507                                -- todo
1508                            end
1509                            tchar.italic   = nil
1510                            bchar.italic   = nil
1511                            tchar.anchored = true
1512                            bchar.anchored = true
1513                        end
1514                        c.vitalic = nil
1515                    end
1516                    if also then
1517                        local a = also[u]
1518                        if a then
1519                            for i=1,#a do
1520                                relocate(a[i],factor)
1521                            end
1522                        end
1523                    end
1524                end
1525            end
1526            if #list > 0 then
1527                for i=1,#list do
1528                    relocate(list[i],factor)
1529                end
1530            else
1531                for k, v in sortedhash(list) do
1532                    relocate(k,tonumber(v) or factor)
1533                end
1534            end
1535            feedback_tweak("movelimits",target,original,done)
1536        end
1537    end
1538
1539end
1540
1541do
1542
1543    -- musical timestamp: March 2022, Antonio Sanches (Bad Hombre), live performance in NL
1544
1545    function mathtweaks.kerns(target,original,parameters)
1546        local kerns = parameters.list
1547        if kerns then
1548            local characters = target.characters
1549            local done       = false
1550            local function setone(uc,data)
1551                local function set(unicode)
1552                    unicode = mathgaps[unicode] or unicode
1553                    local chardata = characters[unicode]
1554                    if chardata then
1555                        local width = chardata.width  or 0
1556                        local k = data.topleft     ; if k and k ~= 0 then chardata.topleft     = k * width end
1557                        local k = data.topright    ; if k and k ~= 0 then chardata.topright    = k * width end
1558                        local k = data.bottomleft  ; if k and k ~= 0 then chardata.bottomleft  = k * width end
1559                        local k = data.bottomright ; if k and k ~= 0 then chardata.bottomright = k * width end
1560                        done = registerdone(done,unicode)
1561                    end
1562                end
1563                local unicode  = detail(characters,uc)
1564                if type(unicode) == "table" then
1565                    for i=1,#unicode do
1566                        set(unicode[i])
1567                    end
1568                elseif unicode then
1569                    set(unicode)
1570                end
1571            end
1572            for unicode, data in next, kerns do
1573                setone(unicode,data) -- withscriptcode(tfmdata,unicode,data,kernone)
1574                -- also smaller
1575            end
1576            feedback_tweak("kerns",target,original,done)
1577        end
1578    end
1579
1580end
1581
1582do
1583
1584    function mathtweaks.margins(target,original,parameters)
1585        local kerns = parameters.list
1586        if kerns then
1587            local characters = target.characters
1588            local done       = false
1589            local function setone(uc,data)
1590                local function set(unicode)
1591                    unicode = mathgaps[unicode] or unicode
1592                    local chardata = characters[unicode]
1593                    if chardata then
1594                        local width = chardata.width  or 0
1595                        local k = data.left   ; if k and k ~= 0 then chardata.leftmargin   = k * width end
1596                        local k = data.right  ; if k and k ~= 0 then chardata.rightmargin  = k * width end
1597                        local k = data.top    ; if k and k ~= 0 then chardata.topmargin    = k * width end
1598                        local k = data.bottom ; if k and k ~= 0 then chardata.bottommargin = k * width end
1599                        done = registerdone(done,unicode)
1600                    end
1601                end
1602                local unicode  = detail(characters,uc)
1603                if type(unicode) == "table" then
1604                    for i=1,#unicode do
1605                        set(unicode[i])
1606                    end
1607                elseif unicode then
1608                    set(unicode)
1609                end
1610            end
1611            for unicode, data in next, kerns do
1612                setone(unicode,data) -- withscriptcode(tfmdata,unicode,data,kernone)
1613                -- also smaller
1614            end
1615            feedback_tweak("margins",target,original,done)
1616        end
1617    end
1618
1619
1620end
1621
1622do
1623
1624    -- musical timestamp: June 2022, Porcupine Tree - Rats Return
1625
1626    -- we can actually share these and flag them as being tweaked
1627
1628    local function scale(t,width,total)
1629        local r = { }
1630        for i=1,#t do
1631            local ti = t[i]
1632            local kern   = ti.kern
1633            local height = ti.height
1634            if kern then
1635                kern = width * kern
1636            end
1637            if height then
1638                height = total * height
1639            end
1640            r[i] = {
1641                kern   = kern or 0,
1642                height = height or 0,
1643            }
1644        end
1645        return r
1646    end
1647
1648    function mathtweaks.staircase(target,original,parameters)
1649        local kerns = parameters.list
1650        if kerns then
1651            local characters = target.characters
1652            local function kernone(unicode,data)
1653                local chardata = characters[mathgaps[unicode] or unicode]
1654                local total = (chardata.height or 0) + (chardata.depth or 0)
1655                local width = chardata.width or 0
1656                if data then
1657                    local tl = data.topleft     ; if tl then tl = scale(tl,width,total) end
1658                    local tr = data.topright    ; if tr then tr = scale(tr,width,total) end
1659                    local bl = data.bottomleft  ; if bl then bl = scale(bl,width,total) end
1660                    local br = data.bottomright ; if br then br = scale(br,width,total) end
1661                    chardata.mathkerns = {
1662                        topleft     = tl,
1663                        ropright    = tr,
1664                        bottomleft  = bl,
1665                        bottomright = br,
1666                    }
1667                else
1668                    chardata.mathkerns = nil
1669                end
1670            end
1671            for unicode, data in next, kerns do
1672                kernone(unicode,data) -- withscriptcode(tfmdata,unicode,data,kernone)
1673                -- also smaller
1674            end
1675        end
1676    end
1677
1678end
1679
1680do
1681
1682 -- local list = {
1683 --     [0x203E] = { factor = .4 }, -- overbar
1684 --     [0x203E] = { factor = .7 }, -- underbar
1685 --     [0x23DE] = { factor = .4 }, -- overbrace
1686 --     [0x23DF] = { factor = .7 }, -- underbrace
1687 --     [0x23DC] = { factor = .4 }, -- overparent
1688 --     [0x23DD] = { factor = .7 }, -- underparent
1689 --     [0x23B4] = { factor = .4 }, -- overbracket
1690 --     [0x23B5] = { factor = .7 }, -- underbracket
1691 -- }
1692
1693    -- We can patch the dimensions in-place or we can use additional characters in
1694    -- the private namespace.
1695
1696    -- local addprivate = fonts.helpers.addprivate
1697    -- local newnextglyph = addprivate(target,formatters["M-N-%H"](nextglyph),newnextdata)
1698
1699    local nps = fonts.helpers.newprivateslot
1700
1701    local umbracepiece = nps("um brace piece") -- will be created
1702    local lmbracepiece = nps("lm brace piece") -- will be created
1703    local cmbracepiece = nps("cm brace piece") -- will be created : center piece for brace builder hack
1704
1705    local ulbracepiece = nps("ul brace piece")
1706    local urbracepiece = nps("ur brace piece")
1707    local llbracepiece = nps("ll brace piece")
1708    local lrbracepiece = nps("lr brace piece")
1709
1710    local over  = { factor = "over"   }
1711    local under = { factor = "under"  }
1712
1713    local candidates = {
1714        over = {
1715            [0x203E] = over,  -- overbar
1716            [0x23DE] = over,  -- overbrace
1717            [0x23DC] = over,  -- overparent
1718            [0x23B4] = over,  -- overbracket
1719        },
1720        under = {
1721            [0x23DF] = under, -- underbrace
1722            [0x23DD] = under, -- underparent
1723            [0x23B5] = under, -- underbracket
1724        },
1725        accent = {
1726            [0x0300] = over,  -- widegrave
1727            [0x0308] = over,  -- wideddot
1728            [0x0304] = over,  -- macron (bar)
1729            [0x0305] = over,  -- widebar
1730            [0x0301] = over,  -- wideacute
1731            [0x0302] = over,  -- widehat
1732            [0x030C] = over,  -- widecheck
1733            [0x0306] = over,  -- widebreve
1734            [0x0307] = over,  -- widedot
1735            [0x030A] = over,  -- widering
1736            [0x0303] = over,  -- widetilde
1737            [0x20DB] = over,  -- widedddot
1738        },
1739    }
1740
1741    datasets.accentdimensions = candidates
1742
1743    local function adapt(c,factor,baseheight,basedepth)
1744        if not c.tweaked then
1745            local height  = c.height or 0
1746            local depth   = c.depth  or 0
1747            local yoffset = 0
1748            if factor == "over" then
1749                local h = height - baseheight
1750                yoffset = h - height
1751                height  = h
1752                depth   = depth - baseheight
1753            elseif factor == "under" then
1754                local d = depth - basedepth
1755                yoffset = depth - d
1756                depth   = d
1757                height  = height - baseheight
1758            elseif height > 0 then
1759                local h = tonumber(factor) * height
1760                yoffset = h - height
1761                height  = h
1762            elseif depth > 0 then
1763                local d = tonumber(factor) * depth
1764                yoffset = depth - d
1765                depth   = d
1766            end
1767            c.yoffset = yoffset ~= 0 and yoffset or nil
1768            c.height  = height   > 0 and height  or nil
1769            c.depth   = depth    > 0 and depth   or nil
1770            c.tweaked = true
1771        end
1772    end
1773
1774    local function process(target,original,characters,list,baseheight,basedepth)
1775        if list then
1776            for k, v in sortedhash(list) do -- sort for tracing
1777                local c = characters[k]
1778                if c and not c.yoffset then
1779                    local factor = v.factor
1780                    if factor then
1781                        adapt(c,factor,baseheight,basedepth)
1782                        local nc  = c.next
1783                        local nv = 0
1784                        local ns = 0
1785                        while nc do
1786                            local c = characters[nc]
1787                            if c then
1788                                adapt(c,factor,baseheight,basedepth)
1789                                nv = nv + 1
1790                                nc = c.next
1791                                if not nc then
1792                                    local hv = c.parts
1793                                    if hv then
1794                                        for i=1,#hv do
1795                                            local c = characters[hv[i].glyph]
1796                                            if c then
1797                                                adapt(c,factor,baseheight,basedepth)
1798                                                ns = ns + 1
1799                                            end
1800                                        end
1801                                    end
1802                                    break
1803                                end
1804                            else
1805                                break
1806                            end
1807                        end
1808                        if trace_tweaking then
1809                            report_tweak("adapting extensible (%i sizes, %i parts) %U",target,original,k,nv,ns)
1810                        end
1811                    end
1812                end
1813            end
1814        end
1815    end
1816
1817    function mathtweaks.accentdimensions(target,original,parameters)
1818        local list = parameters.list or { "over", "under" }
1819        if list then
1820            local characters = target.characters
1821            local baseheight = target.mathparameters.AccentBaseHeight or 0
1822            local basedepth  = target.mathparameters.AccentBaseDepth  or 0
1823            for k, v in sortedhash(list) do -- sort for tracing
1824                local t = type(v)
1825                if t == "string" then
1826                    v = candidates[v]
1827                    t = type(v)
1828                end
1829                if t == "table" then
1830                    process(target,original,characters,v,baseheight,basedepth)
1831                end
1832            end
1833        end
1834    end
1835
1836end
1837
1838do
1839
1840    local addprivate     = fonts.helpers.addprivate
1841    local privateslot    = fonts.helpers.privateslot
1842    local newprivateslot = fonts.helpers.newprivateslot
1843
1844    -- no checking for present extensibles
1845
1846    function mathtweaks.addrules(target,original,parameters)
1847        local characters = target.characters
1848        local thickness  = target.mathparameters.OverbarRuleThickness
1849        local width      = target.parameters.quad / 3
1850        local step       = width / 2
1851        local quarter    = thickness / 4
1852        local half       = thickness / 2
1853        local double     = thickness * 2
1854        local done       = false
1855        characters[0x203E] = { -- middle used for all kind
1856            width    = width,
1857            height   = half,
1858            depth    = half,
1859            yoffset  = - half,
1860            unicode  = 0x203E,
1861            commands = { { "rule", thickness, width } },
1862            parts    = {
1863                { advance = width, ["end"] = step, glyph = 0x203E, start = 0 },
1864                { advance = width, ["end"] = 0,    glyph = 0x203E, start = step, extender = 1 },
1865            },
1866            partsorientation = "horizontal",
1867        }
1868        local function build(target,leftarrow,rightarrow)
1869            if leftarrow and rightarrow then
1870                -- actually the same sort of code as we have for antykwa
1871                local left  = leftarrow.parts
1872                local right = rightarrow.parts
1873                if left and right then
1874                    local leftline   = right[1].glyph
1875                    local rightline  = left[#left].glyph
1876                    local leftdata   = characters[leftline]
1877                    local rightdata  = characters[rightline]
1878                    local leftwidth  = leftdata.width
1879                    local rightwidth = rightdata.width
1880                    local result     = characters[target] -- copytable(leftdata)
1881                    if not result or result.width == 0 then
1882                        result = {
1883                            height   = leftdata.height,
1884                            depth    = leftdata.depth,
1885                            width    = 0.9*leftwidth + rightwidth,
1886                            unicode  = target,
1887                            commands = {
1888                                slotcommand[0][leftline],
1889                                leftcommand[0.1*leftwidth],
1890                                slotcommand[0][rightline],
1891                            },
1892                        }
1893                        characters[target] = result
1894                    end
1895                    result.parts = {
1896                        { advance = leftwidth, glyph = leftline, ["end"] = .9*leftwidth, start = 0 },
1897                        { advance = rightwidth, glyph = rightline, ["end"] = .1*leftwidth, start = .9*rightwidth, extender = 1 },
1898                    }
1899                    result.partsorientation = "horizontal"
1900                    done = registerdone(done,target)
1901                end
1902            end
1903        end
1904        build(0x305,characters[0x20D6],characters[0x20D7]) -- overbar  accent arrows
1905        build(0x332,characters[0x20EE],characters[0x20EF]) -- underbar accent arrows
1906        --
1907        -- lucida lacks them ...
1908        --
1909        if not characters[0x23B4] then
1910            local depth  = 0
1911            local height = 5 * thickness
1912            local tpiece = addprivate(target,"bracket-piece-top",{
1913                width    = thickness,
1914                height   = height,
1915                depth    = depth,
1916                commands = { upcommand[thickness], { "rule", 4 * thickness, thickness } },
1917            })
1918            local mpiece = addprivate(target,"bracket-piece-top-middle",{
1919                width    = width,
1920                height   = height,
1921                depth    = depth,
1922                commands = { upcommand[4*thickness], { "rule", thickness, width } },
1923            })
1924            characters[0x23B4] = { -- over
1925                width      = double + width,
1926                height     = height,
1927                depth      = depth,
1928                unicode    = 0x23B4,
1929                extensible = false, -- needs checking
1930                commands = {
1931                    slotcommand[0][tpiece],
1932                    slotcommand[0][mpiece],
1933                    slotcommand[0][tpiece],
1934                },
1935                parts    = {
1936                    { advance = thickness, glyph = tpiece, ["end"] = 0, start = half },
1937                    { advance = width,     glyph = mpiece, ["end"] = step, start = step, extender = 1 },
1938                    { advance = thickness, glyph = tpiece, ["end"] = half, start = 0 },
1939                },
1940                partsorientation = "horizontal",
1941            }
1942            done = registerdone(done,0x23B4)
1943        end
1944        if not characters[0x23B5] then
1945            local depth  = 0
1946            local height = 5 * thickness
1947            local bpiece = addprivate(target,"bracket-piece-bottom",{
1948                width    = thickness,
1949                height   = height,
1950                depth    = depth,
1951                yoffset  = depth,
1952                commands = { { "rule", 4 * thickness, thickness } },
1953            })
1954            local mpiece = addprivate(target,"bracket-piece-bottom-middle",{
1955                width    = width,
1956                height   = height,
1957                depth    = depth,
1958                commands = { { "rule", thickness, width } },
1959            })
1960            characters[0x23B5] = { -- under
1961                width      = double + width,
1962                height     = height,
1963                depth      = depth,
1964                unicode    = 0x23B5,
1965                extensible = false, -- needs checking
1966                commands = {
1967                    slotcommand[0][bpiece],
1968                    slotcommand[0][mpiece],
1969                    slotcommand[0][bpiece],
1970                },
1971                parts    = {
1972                    { advance = thickness, glyph = bpiece, ["end"] = 0, start = half },
1973                    { advance = width,     glyph = mpiece, ["end"] = step, start = step, extender = 1 },
1974                    { advance = thickness, glyph = bpiece, ["end"] = half, start = 0 },
1975                },
1976                partsorientation = "horizontal",
1977            }
1978            done = registerdone(done,0x23B5)
1979        end
1980        --
1981        feedback_tweak("rules",target,original,done)
1982    end
1983
1984    -- vfmath.builders.extension(target)
1985
1986    local rbe = newprivateslot("radical bar extender")
1987    local fbe = newprivateslot("fraction bar extender")
1988
1989    local frp = {
1990        newprivateslot("flat rule left piece"),
1991        newprivateslot("flat rule middle piece"),
1992        newprivateslot("flat rule right piece"),
1993    }
1994
1995    local rrp = {
1996        newprivateslot("radical rule left piece"),
1997        newprivateslot("radical rule middle piece"),
1998        newprivateslot("radical rule right piece"),
1999    }
2000
2001    local mrp = {
2002        newprivateslot("minus rule left piece"),
2003        newprivateslot("minus rule middle piece"),
2004        newprivateslot("minus rule right piece"),
2005    }
2006
2007    local forceextensible_tag <const> = tex.charactertagcodes.forceextensible
2008
2009    local function useminus(target,unicode,characters,parameters,skipfirst,what,tounicode)
2010        local minus   = characters[0x2212]
2011        local parts   = minus.parts
2012        if parameters == true then
2013            parameters = { }
2014        end
2015        if parts then
2016            minus.tag = forceextensible_tag
2017            what  = copytable(what)
2018            parts = copytable(parts)
2019            local xscale   = parameters.xscale  or 1 -- why not applied to width ?
2020            local yscale   = parameters.yscale  or 1
2021            local mwidth   = minus.width
2022            local mheight  = minus.height
2023            local height   = (parameters.height  or 1) * mheight
2024            local yshift   = (parameters.yoffset or 0) * mheight
2025            local loverlap = parameters.leftoverlap  or 0
2026            local roverlap = parameters.rightoverlap or 0
2027            local loffset  = parameters.leftoffset   or 0
2028            local roffset  = parameters.rightoffset  or 0
2029            if skipfirst then
2030                remove(parts,1)
2031                remove(what,1)
2032            end
2033            height = height / 2
2034            yshift = yshift + height
2035            for i=1,#parts do
2036                local part   = parts[i]
2037                local glyph  = part.glyph
2038                local gdata  = characters[glyph]
2039                local width  = gdata.width
2040                local xshift = 0
2041                if i == 1 and loverlap ~= 0 then
2042                    xshift = loverlap * width
2043                    width  = width - xshift
2044                elseif i == #parts and roverlap ~= 0 then
2045                    width  = width - roverlap * width
2046                end
2047                characters[what[i]] = {
2048                    height   = height,
2049                    depth    = height,
2050                    width    = width,
2051                    advance  = gdata.width,
2052                    unicode  = 0xFFFD, -- we have no unicode ignore ... maybe zws
2053                    commands = {
2054                        leftcommand[xshift],
2055                        downcommand[yshift],
2056                        { "slot", 0, glyph, xscale, yscale },
2057                    },
2058                }
2059                -- we should overlap more ...
2060                if part["start"] >= width then
2061                    part["start"] = width
2062                end
2063                if part["end"] >= width then
2064                    part["end"] = width
2065                end
2066                part.advance = width
2067                part.glyph   = what[i]
2068            end
2069            characters[what[1]].unicode  = unicode -- nice for tagging
2070            xshift = loffset * mwidth + loverlap * mwidth
2071            width  = mwidth - xshift - roffset * mwidth - roverlap * mwidth
2072            characters[unicode] = {
2073                -- base character
2074                height   = height,
2075                depth    = height,
2076                width    = width,
2077                tag      = forceextensible_tag,
2078                -- not needed as we force:
2079                commands = {
2080                    leftcommand[xshift],
2081                    downcommand[yshift],
2082                    { "slot", 0, 0x2212, xscale, yscale },
2083                },
2084                unicode          = tounicode or unicode,
2085                -- extensibles
2086                parts            = parts,
2087                partsorientation = "horizontal",
2088            }
2089        end
2090    end
2091
2092    -- add minus parts of not there and create clipped clone
2093
2094    local function checkminus(target,unicode,characters,parameters,skipfirst,what,tounicode)
2095        local minus = characters[unicode]
2096        local parts = minus.parts
2097        if parameters == true then
2098            parameters = { }
2099        end
2100        local p_normal = 0
2101        local p_flat   = 0
2102        local mwidth   = minus.width
2103        local height   = minus.height
2104        local depth    = minus.depth
2105        local loffset  = parameters.leftoffset  or 0
2106        local roffset  = parameters.rightoffset or 0
2107        local lshift   = mwidth * loffset
2108        local rshift   = mwidth * roffset
2109        local width    = mwidth - lshift - rshift
2110        if parts then
2111         -- print("minus has parts")
2112            if lshift ~= 0 or width ~= mwidth then
2113                parts = copytable(parts)
2114                for i=1,#parts do
2115                    local part    = parts[i]
2116                    local glyph   = part.glyph
2117                    local gdata   = characters[glyph]
2118                    local width   = gdata.width
2119                    local advance = part.advance
2120                    local lshift = 0
2121                    if i == 1 and left ~= 0 then
2122                        lshift  = loffset * width
2123                        width   = width - lshift
2124                        advance = advance - lshift
2125                    elseif i == #parts and roffset ~= 0 then
2126                        width   = width - rshift
2127                        advance = advance - rshift
2128                    end
2129                    characters[what[i]] = {
2130                        height   = height,
2131                        depth    = depth,
2132                        width    = width,
2133                        commands = {
2134                         -- { "offset", lshift, 0, glyph },
2135                            leftcommand[lshift],
2136                            slotcommand[0][glyph],
2137                        },
2138                    }
2139                    part.glyph   = what[i]
2140                    part.advance = advance
2141                end
2142                minus.parts = parts
2143                minus.partsorientation = "horizontal"
2144
2145            end
2146        else
2147            local f_normal = formatters["M-NORMAL-%H"](unicode)
2148         -- local p_normal = hasprivate(main,f_normal)
2149            p_normal = addprivate(target,f_normal,{
2150                height   = height,
2151                width    = width,
2152                commands = {
2153                 -- { "offset", lshift, 0, unicode },
2154                    push,
2155                    leftcommand[lshift],
2156                    slotcommand[0][unicode],
2157                    pop,
2158                },
2159            })
2160         -- local step = width/2
2161            local step = .8*width
2162            minus.parts = {
2163                { extender = 0, glyph = p_normal, ["end"] = step, start = 0,    advance = width },
2164                { extender = 1, glyph = p_normal, ["end"] = step, start = step, advance = width },
2165                { extender = 0, glyph = p_normal, ["end"] = 0,    start = step, advance = width },
2166            }
2167            minus.partsorientation = "horizontal"
2168        end
2169        minus.unicode = tounicode or unicode
2170    end
2171
2172    function mathtweaks.replacerules(target,original,parameters)
2173        local characters = target.characters
2174        local minus      = parameters.minus
2175        local fraction   = parameters.fraction
2176        local radical    = parameters.radical
2177        local stacker    = parameters.stacker
2178        if minus then
2179            checkminus(target,0x2212,characters,minus,false,mrp)
2180        end
2181        if fraction then
2182            useminus(target,fbe,characters,fraction,false,frp,0x2044) -- division slash
2183        end
2184        if radical and not characters[rbe] then
2185            local skipfirst = true
2186            if radical.skipfirst == false then -- explicit
2187                skipfirst = false
2188            end
2189            useminus(target,rbe,characters,radical,skipfirst,rrp,0x2061)   -- apply function
2190        end
2191        if stacker then
2192            useminus(target,0x203E,characters,stacker,false,frp)
2193        end
2194    end
2195
2196    local force = false  experiments.register("math.arrows", function(v) force = v end)
2197
2198    local function tighten(target,unicode,left,right,squeeze,yoffset)
2199        local name = formatters["math tightened %U %.3N %.3N %.3N %.3N"](unicode,left,right,squeeze,yoffset)
2200        local slot = privateslot(target,name)
2201        if not slot then
2202            local characters = target.characters
2203            local data   = copytable(characters[unicode])
2204            local width  = data.width
2205            data.advance = width
2206            data.width   = width * (1-left-right)
2207            data.xoffset = width * -left
2208            if squeeze ~= 1 then
2209                data.effect  = { squeeze = squeeze }
2210            end
2211            if yoffset ~= 0 then
2212                data.yoffset = (data.height or 0) * yoffset
2213            end
2214            slot = addprivate(target,name,data)
2215        end
2216        return slot
2217    end
2218
2219    local function create(target,unicode,list,overloads)
2220        local characters = target.characters
2221        local chardata   = characters[unicode]
2222        if chardata then
2223            local endpoint = unicode
2224            while chardata.next do
2225                chardata = characters[chardata.next]
2226            end
2227            if chardata and (force or overloads[unicode] == false or not chardata.parts) then
2228                if not list then
2229                 -- chardata.parts = nil -- when we test
2230                else
2231                    local overload = overloads[unicode]
2232                    local parts    = { }
2233                    for i=1,#list do
2234                        local part    = list[i]
2235                        local glyph   = part.glyph or unicode
2236                        local check   = overloads[glyph]
2237                        local left    = (check and check.left   ) or part.left    or 0
2238                        local right   = (check and check.right  ) or part.right   or 0
2239                        local squeeze =  check and check.squeeze or 1
2240                        local yoffset =  check and check.yoffset or 0
2241                        if left~= 0 or right ~= 0 or squeeze ~= 1 or yoffset ~= 0 then
2242                            glyph = tighten(target,glyph,left,right,squeeze,yoffset)
2243                        end
2244                        local width = characters[glyph].width
2245                        local step  = width/2
2246                        if part.extensible then
2247                            parts[#parts+1] = {
2248                                advance  = width,
2249                                glyph    = glyph,
2250                                ["end"]  = step,
2251                                start    = step,
2252                                extender = 1,
2253                            }
2254                        else
2255                            parts[#parts+1] = {
2256                                advance = width,
2257                                glyph   = glyph,
2258                                ["end"] = 0,
2259                                start   = step,
2260                            }
2261                        end
2262                    end
2263                    if #parts == #list then
2264                        chardata.parts            = parts
2265                        chardata.partsorientation = "horizontal"
2266                    end
2267                end
2268            end
2269        end
2270    end
2271
2272    -- Unicode math lacks the arrow snippet while it does have fence snippets. Also, some
2273    -- fonts have a relbar that doesn't match the double arrow.
2274    --
2275    -- {
2276    --     tweak = "addarrows",
2277    --     list  = { [0x3D] = { squeeze = .85, yoffset = .0975 } }
2278    -- },
2279    --
2280    -- We have no begin and end snippet, so I played with centering and rules at the edges
2281    --
2282    -- [0x21A9] = { -- hookleftarrow
2283    --     { glyph = 0x2212, left = slack, extensible = true },
2284    --     { glyph = 0x21A9, right = slack },
2285    --     { glyph = 0x2212, right = slack, extensible = true },
2286    -- }
2287    --
2288    -- but in the end rejected it.
2289
2290    local function initialize(left, right, slack)
2291        -- We save some space with locals. When no glyph is given the unicode itself is
2292        -- used which also saves some.
2293        local single = { glyph = 0x2212, left = slack, right = slack, extensible = true }
2294        local double = { glyph = 0x003D, left = slack, right = slack, extensible = true }
2295        local triple = { glyph = 0x2261, left = slack, right = slack, extensible = true }
2296        ----- spacer = { glyph = 0x0020, left = slack, right = slack, extensible = true }
2297        local slackslack  = { left = slack, right = slack }
2298        local leftslack   = { left = left,  right = slack }
2299        local slackright  = { left = slack, right = right }
2300        ----- centered    = { spacer, { }, spacer }
2301        local centered    = false -- the luametatex engine does this
2302        local singleright = { single, slackright }
2303        local leftsingle  = { leftslack,  single }
2304        return {
2305            --
2306            [0x002D] = { { left = slack, right = slack, glyph = 0x2212 }, single }, -- rel
2307         -- [0x2212] = { { left = slack, right = slack, glyph = 0x2212 }, single }, -- rel
2308            --
2309            [0x2190] = leftsingle, -- leftarrow
2310            [0x219E] = leftsingle, -- twoheadleftarrow
2311            [0x21BC] = leftsingle, -- leftharpoonup
2312            [0x21BD] = leftsingle, -- leftharpoondown
2313            --
2314            [0x2192] = singleright, -- rightarrow
2315            [0x21A0] = singleright, -- twoheadrightarrow
2316            [0x21C0] = singleright, -- rightharpoonup
2317            [0x21C1] = singleright, -- rightharpoondown
2318            --
2319            [0x003D] = { slackslack, double }, -- equaltext
2320            [0x2261] = { slackslack, triple }, -- triplerel
2321            [0x27F8] = { leftslack,  double }, -- Leftarrow
2322            [0x27F9] = { double, slackright }, -- Rightarrow
2323            --
2324            [0x21A9] = centered, -- hookleftarrow
2325            [0x21AA] = centered, -- hookrightarrow
2326            [0x21CB] = centered, -- leftrightharpoons
2327            [0x21CC] = centered, -- rightleftharpoons
2328            [0x21C4] = centered, -- rightoverleftarrow
2329            [0x21C6] = centered, -- leftoverrightarrow
2330            [0x21A6] = centered, -- mapsto
2331            --
2332            [0x203E] = { slackslack, { left = slack, right = slack, extensible = true } }, -- bar
2333            --
2334            [0x27F7] = { { glyph = 0x2190, left = left, right = slack }, single, { glyph = 0x2192, left = slack, right = right } }, -- leftrightarrow rightleftarrow
2335            [0x27FA] = { { glyph = 0x27F8, left = left, right = slack }, double, { glyph = 0x27F9, left = slack, right = right } }, -- Leftrightarrow Rightleftarrow
2336        }
2337    end
2338
2339    datasets.addarrows = { }
2340
2341    function mathtweaks.addarrows(target,original,parameters)
2342        local overloads = parameters.list  or { } -- { [unicode] = { left = .1, right = .1 } }
2343        local left      = parameters.left  or 0.05
2344        local right     = parameters.right or 0.05
2345        local slack     = parameters.slack or 0.1
2346        local arrows    = initialize(left,right,slack)
2347         -- inspect(arrows)
2348        for unicode, list in sortedhash(arrows) do
2349            create(target,unicode,list,overloads)
2350        end
2351        datasets.addarrows = sortedkeys(arrows)
2352        if trace_tweaking then
2353            report_tweak("arrows added",target,original)
2354        end
2355
2356    end
2357
2358end
2359
2360
2361
2362do -- this could be combined with the previous
2363
2364    function mathtweaks.addparts(target,original,parameters)
2365        local characters = target.characters
2366        local list       = parameters.list
2367        if list then
2368            for unicode, data in sortedhash(list) do
2369                local template = data.template
2370                if template then
2371                    local source = characters[template]
2372                    local target = characters[unicode]
2373                    if source and target then
2374                        local sequence = data.sequence
2375                        if sequence then
2376                            -- we should follow the next chain first
2377                            local parts = source.parts
2378                            if parts then
2379                                local p = { }
2380                                for i=1,#sequence do
2381                                    local step  = sequence[i]
2382                                    local glyph = step.glyph
2383                                    if glyph == "first" or glyph == "last" then
2384                                        local g = glyph == "first" and 1 or #parts
2385                                        local c = fastcopy(parts[g])
2386                                        local f = step.factor
2387                                        if f then
2388                                            c["end"]  = f * (c["end"] or 0)
2389                                            c.start   = f * (c.start  or 0)
2390                                        end
2391                                        p[#p+1] = c
2392                                    else
2393                                        local c = characters[glyph]
2394                                        if c then
2395                                            p[#p+1] = {
2396                                                glyph   = glyph,
2397                                                advance = c.width,
2398                                                start   = 0,
2399                                                ["end"] = 0,
2400                                            }
2401                                        end
2402                                    end
2403                                end
2404                                if #p > 0 then
2405                                    target.parts = p
2406                                    if not data.horizontal then
2407                                        target.partsorientation = "vertical"
2408                                    end
2409                                end
2410                            end
2411                        end
2412                    end
2413                end
2414            end
2415        end
2416    end
2417
2418end
2419
2420function mathtweaks.action(target,original,parameters)
2421    local action = parameters.action
2422    if type(action) == "function" then
2423        action(target,original,parameters)
2424    end
2425end
2426
2427do
2428
2429    local list = {
2430        { 0x00A0, "s", 1         }, -- nbsp
2431        { 0x2000, "q", 1/2       }, -- enquad
2432        { 0x2001, "q", 1         }, -- emquad
2433        { 0x2002, "q", 1/2       }, -- enspace
2434        { 0x2003, "q", 1         }, -- emspace
2435        { 0x2004, "q", 1/3       }, -- threeperemspace
2436        { 0x2005, "q", 1/4       }, -- fourperemspace
2437        { 0x2006, "q", 1/6       }, -- sixperemspace
2438        { 0x2007, "c", byte('0') }, -- figurespace
2439        { 0x2008, "c", byte('.') }, -- punctuationspace
2440        { 0x2009, "q", 1/8       }, -- breakablethinspace
2441        { 0x200A, "q", 1/8       }, -- hairspace
2442        { 0x200B, "q", 0         }, -- zerowidthspace
2443        { 0x202F, "q", 1/8       }, -- narrownobreakspace
2444        { 0x205F, "s", 1/2       }, -- math thinspace
2445    }
2446
2447    datasets.checkspacing = list
2448
2449    function mathtweaks.checkspacing(target,original,parameters)
2450        local characters = target.characters
2451        local parameters = target.parameters
2452        for i=1,#list do
2453            local entry   = list[i]
2454            local unicode = entry[1]
2455            local data    = characters[unicode]
2456            if not data then
2457                local method   = entry[2]
2458                local fraction = entry[3]
2459                local width    = 0
2460                local height   = 0
2461             -- local depth    = 0
2462                if method == "c" then
2463                    local template = characters[fraction]
2464                    width  = template.width
2465                    height = template.height
2466                 -- depth  = template.depth
2467                elseif method == "s" then
2468                    width  = fraction * parameters.space -- space
2469                    height = 0
2470                 -- depth  = 0
2471                else
2472                    width = fraction * parameters.quad  -- quad
2473                    height = 0
2474                 -- depth  = 0
2475                end
2476                if trace_tweaking then
2477                    report_tweak("setting width of %U to %p",target,original,unicode,width)
2478                end
2479                characters[unicode] = {
2480                    width    = width,
2481                 -- advance  = width,
2482                    height   = height,
2483                 -- depth    = depth,
2484                    unicode  = unicode,
2485                    commands = {
2486                     -- { "slot", 0, 32 },
2487                    },
2488                }
2489            end
2490        end
2491    end
2492
2493end
2494
2495do
2496
2497    -- mirror
2498    -- smaller
2499
2500    local nps = fonts.helpers.newprivateslot
2501
2502    local radicalbarextender = nps("radical bar extender") -- we reserve it here
2503
2504    local list = {
2505        0x221A,
2506    }
2507
2508    local function fix(target,original,characters,unicode)
2509        local data = characters[unicode]
2510        if data then
2511            local height = data.height or 0
2512            local depth  = data.depth or 0
2513            if depth > height then
2514                if trace_tweaking then
2515                    report_tweak("swapping height and depth of radical %U",target,original,unicode)
2516                end
2517                if data.rorrim then
2518                    -- the original does the magic
2519                else
2520                    data.height  = (data.height or 0) + (data.depth or 0)
2521                    data.yoffset = data.depth
2522                    data.depth   = 0
2523                end
2524            end
2525            local smaller = data.smaller
2526            if smaller then
2527                fix(target,original,characters,smaller)
2528            end
2529         -- local mirror = data.mirror
2530         -- if mirror then
2531         --     fix(target,original,characters,mirror)
2532         -- end
2533            local next = data.next
2534            if next then
2535                fix(target,original,characters,next)
2536            else
2537             -- -- assume no depth
2538             -- local parts = data.parts
2539             -- if parts then
2540             --     fix(target,original,characters,parts[1].glyph)
2541             -- end
2542            end
2543        end
2544    end
2545
2546    function mathtweaks.fixradicals(target,original,parameters)
2547        local characters = target.characters
2548        for i=1,#list do
2549            local unicode = list[i]
2550            fix(target,original,characters,unicode)
2551        end
2552    end
2553
2554    local function fix(target,original,characters,u,l)
2555        local data = characters[u]
2556        if data then
2557         -- data.innerlocation = l.location == "right" and 2 or 1
2558            data.innerlocation = l.location == "right" and "right" or "left"
2559            data.innerxoffset  = (l.hfactor or 1) *  (data.width  or 0)
2560            data.inneryoffset  = (l.vfactor or 1) * ((data.height or 0) + (data.depth or 0))
2561        end
2562    end
2563
2564    function mathtweaks.radicaldegreeanchors(target,original,parameters)
2565        local list = parameters.list
2566        if list then
2567            local characters = target.characters
2568            for unicode, l in sortedhash(list) do -- resolve variants
2569                local u = detail(characters,unicode) or unicode
2570                if type(u) == "table" then
2571                    for i=1,#u do
2572                        fix(target,original,characters,u[i],l)
2573                    end
2574                else
2575                    fix(target,original,characters,u,l)
2576                end
2577            end
2578        end
2579    end
2580
2581    local function fix(target,original,characters,u,l)
2582        local data = characters[u]
2583        if data then
2584            data.leftmargin  = (l.left  or 1) * (data.width  or 0)
2585            data.rightmargin = (l.right or 1) * (data.width  or 0)
2586        end
2587    end
2588
2589    function mathtweaks.radicalbodymargins(target,original,parameters)
2590        local list = parameters.list
2591        if list then
2592            local characters = target.characters
2593            for unicode, l in sortedhash(list) do -- resolve variants
2594                local u = detail(characters,unicode) or unicode
2595                if type(u) == "table" then
2596                    for i=1,#u do
2597                        fix(target,original,characters,u[i],l)
2598                    end
2599                else
2600                    fix(target,original,characters,u,l)
2601                end
2602            end
2603        end
2604    end
2605
2606end
2607
2608do
2609
2610    local done = nil
2611
2612    local function fix(target,original,characters,unicode,axis)
2613        if done[unicode] then
2614            return
2615        end
2616        done[unicode] = true
2617        local data = characters[unicode]
2618        if data then
2619            local height = data.height or 0
2620            local depth  = data.depth or 0
2621            if trace_tweaking then
2622                report_tweak("swapping height and depth of %U",target,original,unicode)
2623            end
2624            local half = (height + depth)/2
2625            if data.rorrim then
2626                -- the original does the magic
2627            else
2628                data.yoffset = depth - (half - axis)
2629            end
2630            height = half + axis
2631            depth  = half - axis
2632            data.height  = height
2633            data.depth   = depth
2634            local smaller = data.smaller
2635            if smaller then
2636                fix(target,original,characters,smaller,axis)
2637            end
2638            local mirror = data.mirror
2639            if mirror then
2640                fix(target,original,characters,mirror,axis)
2641            end
2642            local next = data.next
2643            if next then
2644                fix(target,original,characters,next,axis)
2645            end
2646        end
2647    end
2648
2649    function mathtweaks.fixoldschool(target,original,parameters)
2650        local characters = target.characters
2651        local list       = mathtweaks.subsets.integrals
2652        local also       = getalso(target,original)
2653        local axis       = target.mathparameters.AxisHeight
2654        done = { }
2655        for i=1,#list do
2656            local unicode = list[i]
2657            fix(target,original,characters,unicode,axis)
2658        end
2659        if also then
2660            local a = also[u]
2661            if a then
2662                for i=1,#a do
2663                    fix(target,original,characters,a[i],axis)
2664                end
2665            end
2666        end
2667        done = nil
2668    end
2669
2670    -- After the next one I rewarded myself by (again) watching Joe Parrish interpretation
2671    -- of Shostakovich 10 Mvmt. II - Metal several times (video on yt, track on bandcamp)
2672    -- ... timestamp: awaiting the new Albion (Official) single; their work comes in parts.
2673
2674    function mathtweaks.fixintegrals(target,original,parameters)
2675        local characters = target.characters
2676        local integral   = characters[0x222B]
2677        while integral do
2678            local next = integral.next
2679            if next then
2680                integral = characters[next]
2681            else
2682                break
2683            end
2684        end
2685        if integral and not integral.parts then
2686            local top = characters[0x2320]
2687            local mid = characters[0x23AE]
2688            local bot = characters[0x2321]
2689            if top and mid and bot then
2690                top = top.height
2691                mid = mid.height
2692                bot = bot.height
2693                integral.partsitalic = integral.italic
2694                integral.parts       = {
2695                    { advance = bot, ["end"] = bot/3, glyph = 0x2321, start = bot/3  },
2696                    { advance = mid, ["end"] = mid/2, glyph = 0x23AE, start = mid/2, extender = 1 },
2697                    { advance = top, ["end"] = top/3, glyph = 0x2320, start = top/3  },
2698                }
2699                integral.partsorientation = "vertical"
2700                if trace_tweaking then
2701                    report_tweak("fixing the integral extensible",target,original)
2702                end
2703            end
2704        else
2705            report_tweak("no need to fix the integral extensible",target,original)
2706        end
2707    end
2708
2709end
2710
2711do
2712
2713    local list = { 0x2061, 0x2062, 0x2063, 0x2064 }
2714
2715    datasets.wipecues = list
2716
2717    function mathtweaks.wipecues(target,original,parameters)
2718        local characters = target.characters
2719        local tobewiped  = parameters.list or list
2720        local done       = false
2721        for i=1,#tobewiped do
2722            local unicode = tobewiped[i]
2723            characters[unicode] = {
2724                width   = 0,
2725                height  = 0,
2726                depth   = 0,
2727                unicode = unicode,
2728                commands = {
2729                    { "rule" , 0, 0 }
2730                }
2731            }
2732            done = registerdone(done,unicode)
2733        end
2734        feedback_tweak("wipecues",target,original,done)
2735    end
2736
2737end
2738
2739do
2740
2741    local mapping = {
2742        [0x002F] = 0x2044,
2743    }
2744
2745    datasets.fixslashes = mapping
2746
2747    function mathtweaks.fixslashes(target,original,parameters)
2748        local characters = target.characters
2749     -- local done       = false
2750        for normal, weird in sortedhash(mapping) do
2751            local normalone  = characters[normal]
2752            local weirdone   = characters[weird]
2753            if normalone and weirdone and not normalone.next then
2754                normalone.next = weirdone.next
2755             -- done = registerdone(done,normal)
2756            end
2757            weirdone = copytable(normalone)
2758            characters[weird] = weirdone
2759            weirdone.unicode = weird
2760        end
2761     -- feedback_tweak("fixslashes",target,original,done)
2762        if trace_tweaking then
2763            report_tweak("slashes fixed",target,original)
2764        end
2765     end
2766
2767end
2768
2769do -- see pagella for an extensive example
2770
2771    local nps = fonts.helpers.newprivateslot
2772
2773    local mapping = {
2774        [0x0300] = { 0x0060, false },
2775        [0x0308] = { 0x00A8, false },
2776        [0x0304] = { 0x00AF, false }, -- 305
2777        [0x0301] = { 0x00B4, false },
2778        [0x0302] = { 0x02C6, true  },
2779        [0x030C] = { 0x02C7, true  },
2780        [0x0306] = { 0x02D8, false },
2781        [0x0307] = { 0x02D9, false },
2782        [0x030A] = { 0x02DA, false },
2783        [0x0303] = { 0x02DC, true  },
2784        [0x20DB] = { 0x20DB, false },
2785-- [0x20EF] = { 0x20EF, false },
2786    }
2787
2788    datasets.fixaccents     = mapping
2789    datasets.extendaccents  = mapping
2790    datasets.flattenaccents = mapping
2791    datasets.copyaccents    = mapping
2792
2793    -- local flat = stretchingdata.flataccent
2794    -- if flat then
2795    --     -- Nasty! xoffset needed. Check this when we patch vf.
2796    --     local flatdata = characters[flat]
2797    --     flatdata.width     = width
2798    --     flatdata.advance   = 0
2799    --     flatdata.topanchor = topanchor
2800    --     flatdata.xoffset   = width + topanchor
2801    -- end
2802
2803local cdata = characters.data
2804
2805    function mathtweaks.fixaccents(target,original,parameters)
2806        local characters = target.characters
2807        local done       = false
2808        for stretching, entry in sortedhash(mapping) do
2809            local alias  = entry[1]
2810            local stretchingdata = characters[stretching]
2811            if stretchingdata and stretchingdata.width == 0 then
2812             -- if false then
2813             --     local b = target.descriptions[stretching].boundingbox
2814             --     if b then
2815             --         local llx = b[1] * target.parameters.hfactor
2816             --         local urx = b[3] * target.parameters.hfactor
2817             --         local width = urx - llx
2818             --         stretchingdata.width        = width
2819             --         stretchingdata.xoffset      = - llx
2820             --         stretchingdata.advance      = urx
2821             --         if not stretchingdata.anchored then -- safeguard
2822             --             stretchingdata.topanchor    = width/2
2823             --             stretchingdata.bottomanchor = width/2
2824             --         end
2825             --     end
2826             -- else
2827                    local topanchor = stretchingdata.topanchor or 0
2828                    local width     = -topanchor
2829                    topanchor = width/2
2830                    stretchingdata.width     = width
2831                    stretchingdata.advance   = 0
2832                    if not stretchingdata.anchored then -- safeguard
2833                        stretchingdata.topanchor = topanchor
2834                    end
2835                    stretchingdata.commands  = { rightcommand[width + topanchor], charcommand[stretching] }
2836             -- end
2837                done = registerdone(done,stretching)
2838            end
2839        end
2840        feedback_tweak("fixaccents",target,original,done)
2841    end
2842
2843    function mathtweaks.checkaccents(target,original,parameters)
2844        local characters = target.characters
2845        local done       = false
2846        local factor     = target.parameters.hfactor
2847        for unicode, data in sortedhash(characters) do
2848            local width = data.width
2849            if width == 0 then
2850                local d = chardata[data.unicode or unicode]
2851                if d then
2852                    local c = d and d.category
2853                    if c == "mn" or c == "sk" or c == "lm" then -- can probably can go
2854                        local b = target.descriptions[unicode]
2855                        if b then
2856                            b = b.boundingbox
2857                        end
2858                        if b and not d.anchored then
2859                            local topanchor = data.topanchor or 0
2860                            local llx       = b[1] * factor
2861                            local urx       = b[3] * factor
2862                            data.advance = data.width
2863                            if true then
2864                             -- width         = - topanchor
2865                                width = 2 * (topanchor - llx)
2866                             -- data.commands = {
2867                             --  -- rightcommand[width+width/2],
2868                             --     rightcommand[-llx],
2869                             --     slotcommand[0][unicode]
2870                             -- }
2871                                data.xoffset = -llx
2872                            else
2873                                width         = urx - llx
2874                                data.commands = {
2875                                    leftcommand[llx],
2876                                    slotcommand[0][unicode]
2877                                }
2878                            end
2879                            data.width        = width
2880                            data.topanchor    = width/2
2881                            data.bottomanchor = width/2
2882                        end
2883                        done = registerdone(done,unicode)
2884                    end
2885                end
2886            end
2887        end
2888        feedback_tweak("checkaccents",target,original,done)
2889    end
2890
2891    -- all  true|number  false
2892
2893    function mathtweaks.extendaccents(target,original,parameters)
2894        local characters = target.characters
2895        local all        = parameters.all
2896        local count      = tonumber(all)
2897        local done       = false
2898        for stretching, entry in sortedhash(mapping) do
2899            local extend = entry[2]
2900            if extend then
2901                local last = characters[stretching]
2902                local cnt  = 1
2903                local okay = false
2904                while last do
2905                    if all or (count and cnt > count) then
2906                        last.extensible = true
2907                        local flataccent = last.flataccent
2908                        if flataccent then
2909                            characters[flataccent].extensible = true
2910                            okay = true
2911                        end
2912                    end
2913                    local n = last.next
2914                    if n then
2915                        last = characters[n]
2916                    else
2917                        last.extensible = true
2918                        local flataccent = last.flataccent
2919                        if flataccent then
2920                            characters[flataccent].extensible = true
2921                            okay = true
2922                        end
2923                        break
2924                    end
2925                    cnt = cnt + 1
2926                end
2927                if okay then
2928                    done = registerdone(done,stretching)
2929                end
2930            end
2931        end
2932        feedback_tweak("extendaccents",target,original,done)
2933    end
2934
2935    -- force     true     false
2936    -- height    factor   0.8
2937    -- offset    factor   0.9|calculated
2938    -- squeeze   factor   0.1|calculated
2939
2940    local f_flat = formatters["flat accent %05X"]
2941
2942    function mathtweaks.flattenaccents(target,original,parameters)
2943        local characters = target.characters
2944        local force   = parameters.force
2945        local squeeze = parameters.squeeze or 0.85
2946        local ofactor = parameters.offset  or (squeeze/8.5)
2947        local hfactor = parameters.height  or 0.95 -- (1 - ofactor)
2948        local done    = false
2949        for stretching, entry in sortedhash(mapping) do
2950            local code  = stretching
2951            local last  = characters[stretching]
2952            while last do
2953                if force or not last.flataccent then
2954                    local slot   = nps(f_flat(code))
2955                    local height = last.height or 0
2956                    characters[slot] = {
2957                        width        = last.width,
2958                        depth        = last.depth,
2959                        height       = last.height * hfactor,
2960                        topanchor    = last.topanchor,
2961                        bottomanchor = last.bottomanchor,
2962                        commands     = { { "offset", 0, ofactor * height, code, 1, squeeze } },
2963                     -- commands     = { slotcommand[0][code] },
2964                     -- effect       = { squeeze = squeeze },
2965                     -- next         = last.next,
2966                        unicode      = last.unicode,
2967                    }
2968
2969                    last.flataccent  = slot
2970                    done = registerdone(done,stretching)
2971                end
2972                code = last.next
2973                if code then
2974                    last = characters[code]
2975                else
2976                    break
2977                end
2978            end
2979        end
2980        feedback_tweak("flattenaccents",target,original,done)
2981    end
2982
2983    function mathtweaks.copyaccents(target,original,parameters)
2984        local characters = target.characters
2985        local done       = false
2986        for stretching, entry in sortedhash(mapping) do
2987            local alias = entry[1]
2988            if alias ~= stretching then
2989                local stretchingdata = characters[stretching]
2990                if stretchingdata then
2991                    -- we need to nil [x|y]offsets
2992                    characters[alias] = {
2993                        width     = stretchingdata.width,
2994                        height    = stretchingdata.height,
2995                        depth     = stretchingdata.depth,
2996                        next      = stretchingdata.next,
2997                     -- commands  = { charcommand[stretching] },
2998                        commands  = stretchingdata.commands or { charcommand[stretching] },
2999                        topanchor = stretchingdata.topanchor,
3000                     -- unicode   = stretching,  -- when we alias to combiners
3001                        unicode   = alias, -- when we keep the original
3002                    }
3003                    done = registerdone(done,stretching)
3004                end
3005            end
3006        end
3007        feedback_tweak("copyaccents",target,original,done)
3008    end
3009
3010    function mathtweaks.keepbases(target,original,parameters)
3011        local characters = target.characters
3012        local done       = false
3013        local list       = parameters.list
3014        if list == "default" then
3015            list = sortedkeys(mapping)
3016        end
3017        if list and #list > 0 then
3018            for i=1,#list do -- assumes sorting
3019                local unicode  = list[i]
3020                local chardata = characters[unicode]
3021                if chardata then
3022                    chardata.keepbase = true
3023                    done = registerdone(done,unicode)
3024                end
3025            end
3026        else
3027            -- maybe also hash
3028        end
3029        feedback_tweak("keepbases",target,original,done)
3030    end
3031
3032end
3033
3034do
3035
3036    function mathtweaks.inspect(target,original,parameters)
3037        local characters = target.characters
3038        local slot = parameters.slot or parameters.unicode
3039        if slot then
3040            -- todo: show unicode
3041            report_math(formatters["%C data:"](slot))
3042            inspect(characters[slot])
3043        end
3044    end
3045
3046end
3047
3048-- do
3049--
3050--     local single <const> = 0x003D
3051--     local double <const> = 0x2A75
3052--     local triple <const> = 0x2A76
3053--
3054--     function mathtweaks.addequals(target,original,parameters)
3055--         local characters = target.characters
3056--         local basechar   = characters[single]
3057--         local width      = basechar.width
3058--         local height     = basechar.height
3059--         local depth      = basechar.depth
3060--         local advance    = (parameters.advance or 1/20) * width
3061--         local char       = charcommand[single]
3062--         local left       = leftcommand[advance]
3063--         characters[double] = {
3064--             unicode  = double,
3065--             width    = 2*width - 1*advance,
3066--             height   = height,
3067--             depth    = depth,
3068--             commands = { char, left, char },
3069--         }
3070--         characters[triple] = {
3071--             unicode  = triple,
3072--             width    = 3*width - 2*advance,
3073--             height   = height,
3074--             depth    = depth,
3075--             commands = { char, left, char, left, char },
3076--         }
3077--         if trace_tweaking then
3078--             report_tweak("double %U and triple %U equals added",target,original,double,triple)
3079--         end
3080--     end
3081--
3082-- end
3083
3084do
3085
3086    local function jointwo(characters,force,unicode,ds,u1,d12,u2)
3087        if force or not characters[unicode] then
3088            local c1 = characters[u1]
3089            local c2 = characters[u2]
3090            if c1 and c2 then
3091                local w1 = c1.width
3092                local w2 = c2.width
3093                local width
3094                if d12 == false then
3095                    d12   = 0
3096                    width = w2
3097                elseif d12 < 0 then
3098                    d12   = d12 * w2
3099                    width = w2
3100                else
3101                    d12   = d12 * ds
3102                    width = w1 + w2 - d12
3103                end
3104                characters[unicode] = {
3105                    unicode     = unicode,
3106                    width       = width,
3107                    height      = max(c1.height or 0, c2.height or 0),
3108                    depth       = max(c1.depth  or 0, c2.depth  or 0),
3109                    keepvirtual = true, -- needs testing
3110                    commands    = {
3111                     -- { "inspect" },
3112                     -- { "trace" },
3113                        slotcommand[0][u1],
3114                     -- { "trace" },
3115                        d12 ~= 0 and leftcommand[d12] or false,
3116                        slotcommand[0][u2],
3117                     -- { "trace" },
3118                    },
3119                }
3120            end
3121        end
3122    end
3123
3124    local function jointhree(characters,force,unicode,ds,u1,d12,u2,d23,u3)
3125        if force or not characters[unicode] then
3126            local c1 = characters[u1]
3127            local c2 = characters[u2]
3128            local c3 = characters[u3]
3129            if c1 and c2 and c3 then
3130                local w1 = c1.width
3131                local w2 = c2.width
3132                local w3 = c3.width
3133                d12 = d12 * ds
3134                d23 = d23 * ds
3135                characters[unicode] = {
3136                    unicode  = unicode,
3137                    width    = w1 + w2 + w3 - d12 - d23,
3138                    height   = max(c1.height or 0, c2.height or 0, c3.height or 0),
3139                    depth    = max(c1.depth  or 0, c2.depth  or 0, c3.depth  or 0),
3140                    commands = {
3141                        slotcommand[0][u1],
3142                        d12 ~= 0 and leftcommand[d12] or false,
3143                        slotcommand[0][u2],
3144                        d23 ~= 0 and leftcommand[d23] or false,
3145                        slotcommand[0][u3],
3146                    }
3147                }
3148            end
3149        end
3150    end
3151
3152    function mathtweaks.addequals(target,original,parameters)
3153        local characters = target.characters
3154        local step       = target.parameters.size/18
3155        local force      = parameters.force
3156force = true
3157        jointwo  (characters,force,0x2254,step,0x03A,0,0x03D)         -- :=
3158        jointhree(characters,force,0x2A74,step,0x03A,0,0x03A,0,0x03D) -- ::=
3159        jointwo  (characters,force,0x2A75,step,0x03D,0,0x03D)         -- ==
3160        jointhree(characters,force,0x2A76,step,0x03D,0,0x03D,0,0x03D) -- ===
3161    end
3162
3163end
3164
3165do
3166
3167    -- If we really want, we can have variants that also match radicals but in practice
3168    -- radicals and actuarians are never seen together. We could also have a smaller
3169    -- extender.
3170
3171    local nps = fonts.helpers.newprivateslot
3172
3173    local radical             <const> = 0x0221A
3174    local actuarianrightlong  <const> = nps("delimited right annuity long") -- 0x020E7
3175    local actuarianrightshort <const> = nps("delimited right annuity short")
3176    local actuarianleftlong   <const> = nps("delimited left annuity long" )
3177    local actuarianleftshort  <const> = nps("delimited left annuity short" )
3178    local placehold           <const> = nps("delimited ghost annuity")
3179
3180    local actuarianbottomrightlong  <const> = nps("delimited bottom right annuity long") -- 0x020E7
3181    local actuarianbottomrightshort <const> = nps("delimited bottom right annuity short")
3182    local actuarianbottomleftlong   <const> = nps("delimited bottom left annuity long" )
3183    local actuarianbottomleftshort  <const> = nps("delimited bottom left annuity short" )
3184
3185    function mathtweaks.addactuarian(target,original,parameters)
3186        local characters = target.characters
3187        local parameters = target.parameters
3188        local linewidth  = target.MathConstants.RadicalRuleThickness -- make option
3189        local basechar   = characters[radical]
3190        local baseheight = (basechar.height or 0)/2
3191        local basedepth  = (basechar.depth  or 0)/2
3192        local basetotal  = baseheight + basedepth
3193        local used       = baseheight
3194        --
3195        characters[placehold] = {
3196            width    = 2*linewidth,
3197            height   = baseheight,
3198            depth    = basedepth,
3199            unicode  = actuarian, -- whatever
3200            callback = "devirtualize",
3201            commands = {
3202                rightcommand[linewidth],
3203                downcommand[basedepth],
3204                { "rule", basetotal, 0 },
3205            },
3206        }
3207        --
3208        characters[0x020E7] = {
3209            width    = 6*linewidth,
3210            height   = baseheight,
3211            depth    = basedepth,
3212            unicode  = actuarian,
3213            callback = "devirtualize",
3214            commands = {
3215                upcommand[baseheight-4*linewidth],
3216                { "rule", linewidth, 4*linewidth },
3217                downcommand[basetotal/2-linewidth],
3218                { "rule", basetotal/2, linewidth },
3219            },
3220        }
3221        --
3222        characters[actuarianrightlong] = {
3223            width    = 2*linewidth,
3224            height   = baseheight,
3225            depth    = basedepth,
3226            unicode  = actuarian,
3227            callback = "devirtualize",
3228            commands = {
3229                downcommand[basedepth],
3230                { "rule", basetotal, linewidth },
3231            },
3232            parts = {
3233                {
3234                    advance  = basetotal,
3235                    ["end"]  = used,
3236                    glyph    = actuarianrightlong,
3237                    start    = 0,
3238                },
3239                {
3240                    advance  = basetotal,
3241                    ["end"]  = 0,
3242                    extender = 1,
3243                    glyph    = actuarianrightlong,
3244                    start    = used,
3245                },
3246            }
3247        }
3248        characters[actuarianbottomrightlong] =
3249            characters[actuarianrightlong]
3250        characters[actuarianrightshort] = {
3251            width    = 2*linewidth,
3252            height   = baseheight,
3253            depth    = basedepth,
3254            unicode  = actuarian,
3255            callback = "devirtualize",
3256            commands = {
3257                upcommand[baseheight-4*linewidth],
3258                { "rule", 4*linewidth, linewidth },
3259            },
3260            parts = {
3261                {
3262                    advance  = basetotal,
3263                    ["end"]  = used,
3264                    glyph    = actuarianrightshort,
3265                    start    = 0,
3266                },
3267                {
3268                    advance  = basetotal,
3269                    ["end"]  = 0,
3270                    extender = 1,
3271                    glyph    = actuarianrightshort,
3272                    start    = used,
3273                },
3274            }
3275        }
3276        characters[actuarianbottomrightshort] = {
3277            width    = 2*linewidth,
3278            height   = linewidth,
3279            depth    = basedepth,
3280            unicode  = actuarian,
3281            callback = "devirtualize",
3282            commands = {
3283                downcommand[basedepth],
3284                { "rule", 4*linewidth, linewidth },
3285            },
3286            parts = {
3287                {
3288                    advance  = basetotal,
3289                    ["end"]  = used,
3290                    glyph    = actuarianrightshort,
3291                    start    = 0,
3292                },
3293                {
3294                    advance  = basetotal,
3295                    ["end"]  = 0,
3296                    extender = 1,
3297                    glyph    = actuarianrightshort,
3298                    start    = used,
3299                },
3300            }
3301        }
3302
3303        characters[actuarianleftlong] = {
3304            width    = 2*linewidth,
3305            height   = baseheight,
3306            depth    = basedepth,
3307            unicode  = actuarian, -- whatever
3308            callback = "devirtualize",
3309            commands = {
3310                rightcommand[linewidth],
3311                downcommand[basedepth],
3312                { "rule", basetotal, linewidth },
3313            },
3314            parts = {
3315                {
3316                    advance  = basetotal,
3317                    ["end"]  = used,
3318                    extender = 1,
3319                    glyph    = placehold,
3320                    start    = 0,
3321                },
3322                {
3323                    advance  = basetotal,
3324                    ["end"]  = 0,
3325                    glyph    = actuarianleftlong,
3326                    start    = used,
3327                },
3328            }
3329        }
3330        characters[actuarianbottomleftlong] =
3331            characters[actuarianleftlong]
3332        characters[actuarianleftshort] = {
3333            width    = 2*linewidth,
3334            height   = baseheight,
3335            depth    = basedepth,
3336            unicode  = actuarian, -- whatever
3337            callback = "devirtualize",
3338            commands = {
3339                rightcommand[linewidth],
3340                upcommand[baseheight-4*linewidth],
3341                { "rule", 4*linewidth, linewidth },
3342            },
3343            parts = {
3344                {
3345                    advance  = basetotal,
3346                    ["end"]  = used,
3347                    extender = 1,
3348                    glyph    = placehold,
3349                    start    = 0,
3350                },
3351                {
3352                    advance  = basetotal,
3353                    ["end"]  = 0,
3354                    glyph    = actuarianleftshort,
3355                    start    = used,
3356                },
3357            }
3358        }
3359        characters[actuarianbottomleftshort] = {
3360            width    = 2*linewidth,
3361         -- height   = baseheight,
3362            height   = linewidth,
3363            depth    = basedepth,
3364            unicode  = actuarian, -- whatever
3365            callback = "devirtualize",
3366            commands = {
3367                rightcommand[linewidth],
3368                downcommand[basedepth],
3369                { "rule", 4*linewidth, linewidth },
3370            },
3371            parts = {
3372                {
3373                    advance  = basetotal,
3374                    ["end"]  = used,
3375                    extender = 1,
3376                    glyph    = placehold,
3377                    start    = 0,
3378                },
3379                {
3380                    advance  = basetotal,
3381                    ["end"]  = 0,
3382                    glyph    = actuarianleftshort,
3383                    start    = used,
3384                },
3385            }
3386        }
3387        --
3388     -- if trace_tweaking then
3389     --     report_tweak("actuarian %U added",target,original,actuarian) -- wrong parameter
3390     -- end
3391    end
3392
3393end
3394
3395do
3396
3397    -- todo: make callback because we can delay it but then we need to stack
3398    -- callbacks
3399
3400    local nps = fonts.helpers.newprivateslot
3401
3402    local list = {
3403     -- { 0x0300, nps("delimited right grave"),   nps("delimited ghost grave")   },
3404        { 0x0308, nps("delimited right ddot"),    nps("delimited ghost ddot")    },
3405        { 0x0304, nps("delimited right bar"),     nps("delimited ghost bar")     },
3406     -- { 0x0301, nps("delimited right acute"),   nps("delimited ghost acute")   },
3407        { 0x0302, nps("delimited right hat"),     nps("delimited ghost hat")     },
3408        { 0x030C, nps("delimited right check"),   nps("delimited ghost check")   },
3409        { 0x0306, nps("delimited right breve"),   nps("delimited ghost breve")   },
3410        { 0x0307, nps("delimited right dot"),     nps("delimited ghost dot")     },
3411        { 0x030A, nps("delimited right ring"),    nps("delimited ghost ring")    },
3412        { 0x0303, nps("delimited right tilde"),   nps("delimited ghost tilde")   },
3413        { 0x20DB, nps("delimited right dddot"),   nps("delimited ghost dddot")   },
3414
3415        { 0x2032, nps("delimited right prime"),   nps("delimited ghost prime"),   false, 1 },
3416        { 0x2033, nps("delimited right dprime"),  nps("delimited ghost dprime"),  false, 1 },
3417        { 0x2034, nps("delimited right tprime"),  nps("delimited ghost tprime"),  false, 1 },
3418        { 0x2057, nps("delimited right qprime"),  nps("delimited ghost qprime"),  false, 1 },
3419        { 0x2035, nps("delimited right rprime"),  nps("delimited ghost rprime"),  false, 1 },
3420        { 0x2036, nps("delimited right drprime"), nps("delimited ghost rdprime"), false, 1 },
3421        { 0x2037, nps("delimited right dtprime"), nps("delimited ghost rtprime"), false, 1 },
3422
3423        { 0x231C, nps("delimited left upper corner"),  nps("delimited ghost upper corner") },
3424        { 0x231D, nps("delimited right upper corner"), nps("delimited ghost upper corner") },
3425        { 0x231E, nps("delimited left lower corner"),  nps("delimited ghost lower corner"), true  },
3426        { 0x231F, nps("delimited right lower corner"), nps("delimited ghost lower corner"), true  },
3427
3428        -- If needed we can have an installer:
3429
3430        { 0x2020, nps("delimited right dagger"),  nps("delimited ghost dagger")  },
3431        { 0x2021, nps("delimited right ddagger"), nps("delimited ghost ddagger") },
3432        { 0x2217, nps("delimited right ast"),     nps("delimited ghost ast")     },
3433        { 0x22C6, nps("delimited right star"),    nps("delimited ghost star")    },
3434
3435        { 0x2020, nps("delimited right dagger 1"),  nps("delimited ghost dagger 1"),  false, 1 },
3436        { 0x2021, nps("delimited right ddagger 1"), nps("delimited ghost ddagger 1"), false, 1 },
3437        { 0x2217, nps("delimited right ast 1"),     nps("delimited ghost ast 1"),     false, 1 },
3438        { 0x22C6, nps("delimited right star 1"),    nps("delimited ghost star 1"),    false, 1 },
3439    }
3440
3441    function mathtweaks.addfourier(target,original,parameters)
3442        local characters = target.characters
3443        for i=1,#list do
3444            local entry        = list[i]
3445            local unicode      = entry[1]
3446            local basecode     = unicode
3447            local fouriercode  = entry[2]
3448            local movecode     = entry[3]
3449            local reverse      = entry[4]
3450            local size         = entry[5] or 0
3451            local basechar     = characters[basecode]
3452            local compactscale = 1
3453            -- bah ...
3454            if basechar and target.properties.compactmath and size > 0 then
3455                compactscale = target.parameters[size > 1 and "scriptscriptscale" or "scriptscale"] / 1000
3456                for i=1,size do
3457                    basecode = basechar.smaller or basecode
3458                    basechar = characters[basecode]
3459                end
3460            end
3461            if basechar then
3462                local scale   = (parameters.scale or 1) * compactscale
3463                local variant = parameters.variant
3464                if variant then
3465                    for i=1,variant do
3466                        local okay = basechar.next
3467                        if okay then
3468                            basecode = okay
3469                            basechar = characters[basecode]
3470                        else
3471                            break
3472                        end
3473                    end
3474                end
3475                local baseheight = scale * (basechar.height or 0)
3476                local basedepth  = scale * (basechar.depth  or 0)
3477                local basewidth  = scale * (basechar.width  or 0)
3478                local used       = baseheight/2
3479                local total      = baseheight + basedepth
3480                if reverse then
3481                    used = total / 2 -- basedepth / 2
3482                end
3483                characters[movecode] = {
3484                    width    = basewidth,
3485                    height   = used,
3486                    unicode  = 0xFFFD,
3487                    commands = {
3488                        downcommand[used],
3489                        { "rule", used, 0 },
3490                    },
3491                }
3492                local parts = {
3493                    {
3494                        advance  = used,
3495                        ["end"]  = used,
3496                        extender = 1,
3497                        glyph    = movecode, -- bottom
3498                        start    = used,
3499                    },
3500                    {
3501                        advance  = total,
3502                        ["end"]  = 0,
3503                        glyph    = fouriercode, -- top
3504                        start    = total,
3505                    },
3506                }
3507                if reverse then
3508                    parts[1], parts[2] = parts[2], parts[1]
3509                end
3510                characters[fouriercode] = {
3511                    width    = basewidth,
3512                    height   = baseheight, -- somehow no \primed antykwa (unless we double the height)
3513                    depth    = basedepth,
3514                    unicode  = unicode,
3515                    commands = {
3516                        scale == 1 and charcommand[basecode] or { "slot", 0, basecode, scale, scale },
3517                    },
3518                    partsorientation = "vertical",
3519                    parts            = parts,
3520--                     keepvirtual      = basechar.commands and true or false,
3521        --             callback = "devirtualize",
3522                }
3523                if trace_tweaking then
3524                    report_tweak("fourier %U added using %U",target,original,basecode,fouriercode)
3525                end
3526            end
3527        end
3528    end
3529
3530end
3531
3532do
3533
3534    -- \im{\left\Uchar"007C \frac{1}{2} \right\Uchar"007C}
3535    -- \im{\left\Uchar"2016 \frac{1}{2} \right\Uchar"2016}
3536    -- \im{\left\Uchar"2980 \frac{1}{2} \right\Uchar"2980}
3537
3538    local single <const> = 0x007C
3539    local double <const> = 0x2016
3540    local triple <const> = 0x2980
3541
3542    local function variantlist(unicode,chardata,total,used)
3543        chardata.varianttemplate = 0x0028
3544     -- chardata.next = nil -- better use wipe variants but for testign we keep them
3545        chardata.parts = {
3546            {
3547                advance  = total,
3548                ["end"]  = used,
3549                glyph    = unicode,
3550                start    = 0,
3551            },
3552            {
3553                advance  = total,
3554             -- ["end"]  = 0,
3555             -- ["end"]  = used/5,   -- prevents small gap with inward curved endpoints
3556                ["end"]  = 4*used/5, -- is this better?
3557                extender = 1,
3558                glyph    = unicode,
3559                start    = used,
3560            },
3561        }
3562        chardata.partsorientation = "vertical"
3563    end
3564
3565    -- This is such an inconsistent mess that we cannot simply copy variants so we
3566    -- just accept some middle ground for the missing ones. See goodie files for when
3567    -- 'check' and/or 'variant' is used.
3568
3569    function mathtweaks.addbars(target,original,parameters)
3570        local characters = target.characters
3571        local template   = single
3572        local basechar   = characters[template]
3573        local tempchar   = basechar
3574        local check      = parameters.check
3575     -- local variant    = false -- alas ... too large: tonumber(parameters.variant)
3576     -- if variant then
3577     --     for i=1,variant do
3578     --         local n = tempchar.next
3579     --         if n then
3580     --             template = n
3581     --             tempchar = characters[template]
3582     --         else
3583     --             break
3584     --         end
3585     --     end
3586     -- end
3587        local width   = tempchar.width
3588        local height  = tempchar.height
3589        local depth   = tempchar.depth
3590        local advance = (parameters.advance or 1/10) * width
3591        local used    = 1.2*height -- large overlap because no smaller pieces
3592        local total   = height + depth
3593        --
3594        if not check or not basechar.parts then
3595            variantlist(template,basechar,total,used)
3596        end
3597        if not check or not characters[double] then
3598            basechar = {
3599                unicode  = double,
3600                width    = 2*width - 1*advance,
3601                height   = height,
3602                depth    = depth,
3603             -- callback = "devirtualize",
3604                commands = {
3605                    charcommand[template],
3606                    leftcommand[advance],
3607                    charcommand[template],
3608                },
3609            }
3610            characters[double] = basechar
3611        else
3612            basechar = characters[double]
3613        end
3614        if not check or not basechar.parts then
3615            variantlist(double,basechar,total,used)
3616        end
3617        if not check or not characters[triple] then
3618            basechar = {
3619                unicode  = triple,
3620                width    = 3*width - 2*advance,
3621                height   = height,
3622                depth    = depth,
3623             -- callback = "devirtualize",
3624                commands = {
3625                    charcommand[template],
3626                    leftcommand[advance],
3627                    charcommand[template],
3628                    leftcommand[advance],
3629                    charcommand[template],
3630                },
3631            }
3632            characters[triple] = basechar
3633        else
3634            basechar = characters[triple]
3635        end
3636        if not check or not basechar.parts then
3637            variantlist(triple,basechar,total,used)
3638        end
3639        --
3640        if trace_tweaking then
3641            report_tweak("single, double and triple bars added",target,original)
3642        end
3643    end
3644
3645end
3646
3647do
3648
3649    -- this will move to char-prv.lmt
3650
3651    local nps = fonts.helpers.newprivateslot
3652
3653    local slot = nps("minus one composite")
3654
3655    local data = {
3656        category    = "lu",
3657        description = "MATHEMATICAL PRE IMAGE",
3658        direction   = "l",
3659        linebreak   = "al",
3660        mathspec    = {
3661            {
3662                class   = mathematics.classes.prime,
3663                group   = "postfix operator",
3664                meaning = "preimage",
3665                name    = "preimagesymbol",
3666            },
3667            {
3668                class   = mathematics.classes.prime,
3669                group   = "postfix operator",
3670                meaning = "inverse",
3671                name    = "inversesymbol",
3672            },
3673        },
3674        unicode     = slot,
3675        recipe      = { 0x2212, 0x31 },
3676        specials    = { "char", 0x2212, 0x31 },
3677        unicodeslot = slot,
3678    }
3679
3680    characters.data[slot] = data
3681
3682    for i,d in next,data.mathspec do
3683        mathematics.setmathsymbol( -- could take a array
3684            d.name,
3685            d.class,
3686            0, -- family,
3687            data.unicodeslot,
3688            nil, -- stretch,
3689            d.group,
3690            d.meaning
3691        )
3692    end
3693
3694    -- till here
3695
3696    local specifications = {
3697        data, -- characters.data[fonts.helpers.privateslot("minus one composite")]
3698    }
3699
3700    function mathtweaks.addcomposites(target,original,parameters)
3701        local characters = target.characters
3702        for i=1,#specifications do
3703            local spec     = specifications[i]
3704            local private  = spec.unicodeslot
3705            local unicode  = spec.recipe
3706            local commands = { }
3707            local width    = 0
3708            local height   = 0
3709            local depth    = 0
3710            for i=1,#unicode do
3711                local uc = unicode[i]
3712                local cd = characters[uc]
3713                local wd = cd.width or 0
3714                local ht = cd.height or 0
3715                local dp = cd.depth or 0
3716                commands[#commands+1] = charcommand[uc]
3717                width = width + wd
3718                if ht > height then
3719                    height = ht
3720                end
3721                if dp > depth then
3722                    depth = dp
3723                end
3724            end
3725            -- tounicode
3726            characters[private] = {
3727                unicode  = unicode,
3728                commands = commands,
3729                width    = width,
3730                height   = height,
3731                depth    = depth,
3732            }
3733         -- if trace_tweaking then
3734         --     report_tweak("composite %U added",target,original) -- useless
3735         -- end
3736        end
3737    end
3738
3739end
3740
3741do
3742
3743    -- lucida: \im{\cdot \ldot \cdots \ldots}
3744
3745    local snormal <const> = 0x002E
3746    local sraised <const> = 0x22C5
3747
3748    local tnormal <const> = 0x2026
3749    local traised <const> = 0x22EF
3750
3751    function mathtweaks.fixellipses(target,original,parameters)
3752        local characters = target.characters
3753        local function fix(normal,raised)
3754            local normaldata = characters[normal]
3755            if normaldata then
3756                local raiseddata = copytable(normaldata)
3757                characters[raised] = raiseddata
3758                raiseddata.unicode = raised
3759                local height  = raiseddata.height
3760                local yoffset = (parameters.yoffset or 2) * height
3761                raiseddata.yoffset = yoffset
3762                raiseddata.height  = height + yoffset
3763                if trace_tweaking then
3764                    report_tweak("taking %U from %U",target,original,raised,normal)
3765                end
3766            end
3767        end
3768        fix(snormal,sraised)
3769        fix(tnormal,traised)
3770    end
3771
3772end
3773
3774do
3775
3776    -- For Ton, who needs the high minus and plus for calculator signs in Dutch
3777    -- school math books.
3778
3779    local list = {
3780        { 0x207A, 0x002B, true  },
3781        { 0x207B, 0x2212, true  },
3782        { 0x208A, 0x002B, false },
3783        { 0x208B, 0x2212, false },
3784    }
3785
3786    datasets.addscripts = list
3787
3788    local function add(target,original,characters,unicode,template,super,baseheight,scale)
3789        if not characters[unicode] then
3790            local origdata = characters[template]
3791            if origdata then
3792                local width  = scale * (origdata.width  or 0)
3793                local height = scale * (origdata.height or 0)
3794                local depth  = scale * (origdata.depth  or 0)
3795                local half   = - (height + depth) / 2
3796                local offset = super and baseheight/2 or -baseheight/4
3797                characters[unicode] = {
3798                    width    = width,
3799                    height   = height + offset,
3800                    depth    = depth  - offset,
3801                    unicode  = unicode,
3802                    commands = {
3803                        { "offset", 0, offset, template, scale, scale }
3804                    },
3805                }
3806                if trace_tweaking then
3807                    report_tweak("adding script %U scaled %0.3f",target,original,unicode,scale)
3808                end
3809                -- no need for smaller
3810            end
3811        end
3812    end
3813
3814    function mathtweaks.addscripts(target,original,parameters)
3815        local characters = target.characters
3816        local baseheight = target.mathparameters.AccentBaseHeight
3817        local scaledown  = parameters.scale or target.mathparameters.ScriptScriptPercentScaleDown / 100
3818        for i=1,#list do
3819            local entry = list[i]
3820            if entry then
3821                add(target,original,characters,entry[1],entry[2],entry[3],baseheight,scaledown)
3822            end
3823        end
3824    end
3825
3826end
3827
3828do
3829
3830    function mathtweaks.sortvariants(target,original,parameters)
3831        local list = parameters.list
3832        if list then
3833            local characters = target.characters
3834            local horizontal = parameters.orientation == "horizontal"
3835            for i=1,#list do
3836                local u = list[i]
3837                local c = characters[u]
3838                if c then
3839                    local t = { }
3840                    while true do
3841                        local n = c.next
3842                        if n then
3843                            c = characters[n]
3844                        end
3845                        if c and not c.parts then
3846                            if horizontal then
3847                                t[c.width or 0] = n
3848                            else
3849                                t[(c.height or 0) + (c.depth or 0)] = n
3850                            end
3851                        else
3852                            break
3853                        end
3854                    end
3855                    local c = characters[u]
3856                    for k, v in sortedhash(t) do
3857                        c.next = v
3858                        c = characters[v]
3859                    end
3860                end
3861            end
3862        end
3863    end
3864
3865end
3866
3867do
3868
3869    -- We started with the list that xits has in rtlm but most of them can be derived from
3870    -- the database, and others need to be added.
3871
3872    -- Checked while watching/listening to Dave Matthews Band: The Central Park Concert
3873    -- (with superb solos by Warren Haynes), a DVD I bought around when we started with the
3874    -- LUATEX advanture.
3875
3876    local mirrors = {
3877
3878        [0x0002F] = true, -- slashes
3879        [0x0005C] = true,
3880        [0x000F7] = true,
3881        [0x02044] = true,
3882        [0x02215] = true,
3883
3884        [0x02032] = true, -- primes
3885        [0x02033] = true,
3886        [0x02034] = true,
3887        [0x02057] = true,
3888        [0x02035] = true,
3889        [0x02036] = true,
3890        [0x02037] = true,
3891
3892        [0x0221A] = true, -- radicals
3893        [0x0221B] = true,
3894        [0x0221C] = true,
3895        [0x0221D] = true,
3896
3897        [0x0222B] = true, -- integrals
3898        [0x0222C] = true,
3899        [0x0222D] = true,
3900        [0x0222E] = true,
3901        [0x0222F] = true,
3902        [0x02230] = true,
3903        [0x02231] = true,
3904        [0x02232] = true,
3905        [0x02233] = true,
3906
3907        [0x02A0A] = true, -- seen in xits (to be checked)
3908        [0x02A0B] = true,
3909        [0x02A0C] = true,
3910        [0x02A0D] = true,
3911        [0x02A0E] = true,
3912
3913        [0x02140] = true,
3914        [0x02201] = true,
3915        [0x02202] = true,
3916        [0x02203] = true,
3917        [0x02204] = true,
3918        [0x02211] = true,
3919
3920        [0x02239] = true,
3921        [0x0225F] = true,
3922        [0x0228C] = true,
3923        [0x022A7] = true,
3924        [0x022AA] = true,
3925        [0x022AC] = true,
3926        [0x022AD] = true,
3927        [0x022AE] = true,
3928        [0x022AF] = true,
3929        [0x022F5] = true,
3930        [0x022F8] = true,
3931        [0x022F9] = true,
3932        [0x022FF] = true,
3933        [0x02320] = true,
3934        [0x02321] = true,
3935        [0x027C0] = true,
3936        [0x029DC] = true,
3937        [0x029F4] = true,
3938
3939        [0x02A0F] = true,
3940        [0x02A10] = true,
3941        [0x02A11] = true,
3942        [0x02A12] = true,
3943        [0x02A13] = true,
3944        [0x02A14] = true,
3945        [0x02A15] = true,
3946        [0x02A16] = true,
3947        [0x02A17] = true,
3948        [0x02A18] = true,
3949        [0x02A19] = true,
3950        [0x02A1A] = true,
3951        [0x02A1B] = true,
3952        [0x02A1C] = true,
3953        [0x02A20] = true,
3954
3955        [0x02A74] = true,
3956        [0x02AA3] = true,
3957        [0x02AE2] = true,
3958        [0x02AE6] = true,
3959        [0x1D715] = true,
3960    }
3961
3962    local new = fonts.helpers.newprivateslot
3963
3964    local function add(target,original,characters,unicode,what)
3965        local data = characters[unicode]
3966        if data then
3967            if not data.mirror then
3968                local slot      = new("mirror."..unicode)
3969                local mirror    = copytable(data)
3970                data.mirror     = slot
3971                mirror.rorrim   = unicode -- so we can check later
3972                mirror.commands = {
3973                    { "offset", data.width, 0, unicode, -1, 1 }
3974                }
3975                if trace_tweaking then
3976                    report_tweak("adding mirror %U (%s)",target,original,unicode,what)
3977                end
3978                characters[slot] = mirror
3979            elseif trace_tweaking then
3980                report_tweak("skipping mirror %U (%s)",target,original,unicode,what)
3981            end
3982            local parts = data.parts
3983            if parts then
3984                for i=1,#parts do
3985                    add(target,original,characters,parts[i],"hpart")
3986                end
3987            end
3988            local smaller = data.smaller
3989            if smaller then
3990                add(target,original,characters,"smaller")
3991            end
3992            local next = data.next
3993            if next then
3994                if next == unicode then
3995                    if unicode == 0xFDFFF then
3996                        -- can happen when we add privates
3997                    else
3998                        report_tweak("skipping cyclic %U (%s)",target,original,unicode,"next")
3999                    end
4000                else
4001                    add(target,original,characters,next,"next")
4002                end
4003            end
4004        end
4005    end
4006
4007    -- todo: also check the rtlm table if present
4008
4009    function mathtweaks.addmirrors(target,original,parameters)
4010        local characters = target.characters
4011     -- for unicode, detail in sortedhash(characters) do
4012        for unicode, detail in next, characters do
4013            local data = chardata[unicode]
4014            if data and data.mirror then
4015                add(target,original,characters,unicode,"mirror")
4016            end
4017        end
4018        for unicode, detail in sortedhash(mirrors) do
4019            if characters[unicode] then
4020                add(target,original,characters,unicode,"character")
4021            elseif trace_tweaking then
4022                report_tweak("ignoring mirror %U (%s)",target,original,unicode,what)
4023            end
4024        end
4025    end
4026
4027end
4028
4029do
4030
4031    local reported = { }
4032
4033    function mathtweaks.version(target,original,parameters)
4034        local metadata = original.shared.rawdata.metadata
4035        if metadata then
4036            local version = string.strip(metadata.version or "") -- some have trailing spaces
4037            if version then
4038                local expected = parameters.expected
4039                local fontname = metadata.fontname or false
4040                local message  = parameters.message
4041             -- version = tonumber(string.match(version,"%d+.%d+"))
4042                if version ~= expected and not reported[fontname] then
4043                    report_tweak("version %a found, version %a expected",target,original,version,expected)
4044                elseif trace_tweaking then
4045                    report_tweak("version %a found",target,original,version)
4046                end
4047                if message and message ~= "" and not reported[fontname] then
4048                    report_tweak()
4049                    report_tweak("%s",target,original,message)
4050                    report_tweak()
4051                end
4052                reported[fontname] = true
4053                target.tweakversion = version
4054            end
4055        end
4056    end
4057
4058end
4059
4060do
4061
4062    function mathtweaks.parameters(target,original,parameters)
4063        local newparameters = parameters.list
4064        local oldparameters = target.mathparameters
4065        if newparameters and oldparameters then
4066            newparameters = copytable(newparameters)
4067            scaleparameters(newparameters,target.parameters)
4068            for name, newvalue in next, newparameters do
4069                oldparameters[name] = newvalue
4070            end
4071        end
4072    end
4073
4074    function mathtweaks.bigslots(target,original,parameters)
4075        local list = parameters.list
4076        if list then
4077            target.bigslots = list
4078        end
4079    end
4080
4081end
4082
4083-- do
4084--
4085--     function mathtweaks.scales(target,original,parameters)
4086--         local fontparameters = target.parameters
4087--         if fontparameters then
4088--             fontparameters.scriptxscale       = parameters.scriptxscale
4089--             fontparameters.scriptyscale       = parameters.scriptyscale
4090--             fontparameters.scriptscriptxscale = parameters.scriptscriptxscale
4091--             fontparameters.scriptscriptyscale = parameters.scriptscriptyscale
4092--             target.scriptxscale       = parameters.scriptxscale
4093--             target.scriptyscale       = parameters.scriptyscale
4094--             target.scriptscriptxscale = parameters.scriptscriptxscale
4095--             target.scriptscriptyscale = parameters.scriptscriptyscale
4096--         end
4097--     end
4098--
4099-- end
4100
4101-- do
4102--
4103--     function mathtweaks.diagnose(target,original,parameters)
4104--         local characters = target.characters
4105--         for k, v in sortedhash(characters) do
4106--             local italic = v.italic
4107--             if italic then
4108--                 report_tweak("italics: %C %p",target,original,k,italic)
4109--             end
4110--         end
4111--     end
4112--
4113-- end
4114
4115do
4116
4117    function mathtweaks.setoptions(target,original,parameters)
4118        local setlist   = parameters.set or parameters.list
4119        local resetlist = parameters.reset
4120        if setlist or resetlist then
4121            local properties = target.properties
4122            local codes      = tex.mathcontrolcodes
4123            local oldcontrol = texget("mathfontcontrol")
4124            local newcontrol = oldcontrol
4125            -- todo: reset
4126            if resetlist then
4127                for i=1,#resetlist do
4128                    local v = tonumber(codes[resetlist[i]])
4129                    if v then
4130                        newcontrol = newcontrol & (not v)
4131                    end
4132                end
4133            end
4134            if setlist then
4135                for i=1,#setlist do
4136                    local v = tonumber(codes[setlist[i]])
4137                    if v then
4138                        newcontrol = newcontrol | v
4139                    end
4140                end
4141            end
4142            newcontrol = newcontrol | codes.usefontcontrol
4143            properties.mathcontrol = newcontrol
4144            target.mathcontrol = newcontrol
4145            if trace_tweaking then
4146                report_tweak("forcing math font options 0x%08X instead of 0x%08X",target,original,newcontrol,oldcontrol)
4147            end
4148        end
4149    end
4150
4151end
4152
4153do
4154
4155    function mathtweaks.setovershoots(target,original,parameters)
4156        local list = parameters.list
4157        if list then
4158            local characters = target.characters
4159            local emwidth    = target.parameters.quad
4160            local done       = false
4161            for i=1,#list do
4162                local entry  = list[i]
4163                local target = entry.target
4164                local top    = entry.topovershoot
4165                local quad   = entry.quad
4166                if target and top then
4167                    local range = blocks[target]
4168                    if range then
4169                        if quad then
4170                            quad = emwidth
4171                        end
4172                        for r = range.first, range.last do
4173                            local unicode = mathgaps[r] or r
4174                            local data = characters[unicode]
4175                            if data then
4176                                data.topovershoot = top * (quad or data.width or 0)
4177                                done = registerdone(done,r)
4178                            end
4179                        end
4180                    end
4181                end
4182            end
4183            feedback_tweak("setovershoots",target,original,done)
4184        end
4185    end
4186
4187    -- there is no real need for this but let's play nice with memory anyway
4188
4189    local efindex = 0
4190    local effects = setmetatableindex (function (t,k)
4191        efindex = efindex + 1
4192        local v = "tweakreplacealphabets" .. efindex
4193        local e = fonts.specifiers.presetcontext(v,"",k)
4194     -- print(k,v,e)
4195        t[k] = v
4196        return v
4197    end)
4198
4199    function mathtweaks.replacealphabets(target,original,parameters)
4200        local list = parameters.list
4201        if list then
4202            local features        = target.specification.features.normal
4203            local definedfont     = fonts.definers.internal
4204            local copiedglyph     = fonts.handlers.vf.math.copy_glyph
4205            -- does a deep copy, including parts and so
4206            local getsubstitution = fonts.handlers.otf.getsubstitution
4207            local fontdata        = fonts.hashes.identifiers
4208            --
4209            local fonts      = target.fonts
4210            local size       = target.size
4211            local characters = target.characters
4212            if not fonts then
4213                fonts = { }
4214                target.fonts = fonts
4215            end
4216            if #fonts == 0 then
4217                fonts[1] = { id = 0, size = size } -- self, will be resolved later
4218            end
4219            for i=1,#list do
4220                local entry      = list[i]
4221                local filename   = entry.filename or parameters.filename
4222                local feature    = entry.feature
4223                local thesource  = entry.source
4224                local thetarget  = entry.target or thesource
4225                local theunicode = entry.unicode
4226                local unicodes   = entry.unicodes
4227                local keep       = (entry.keep == true) or (parameters.keep == true)
4228                if (thesource and thetarget) or unicodes then
4229                    local function getrange(t)
4230                        return (type(t) == "table" and t) or (type(t) == "string" and blocks[t])  -- .gaps
4231                    end
4232                    local sourcerange  = thesource   and getrange(thesource)
4233                    local targetrange  = thetarget   and getrange(thetarget)
4234                    local unicoderange = theunicode  and getrange(theunicode)
4235                    local firsttarget  = targetrange and targetrange.first
4236                    local firstsource  = sourcerange and sourcerange.first
4237                    local lastsource   = sourcerange and sourcerange.last or firstsource
4238                    if (firstsource and firsttarget) or unicodes then
4239                        local offset = unicodes and 0 or (firsttarget - firstsource)
4240                        if filename then
4241                            local rscale = entry.rscale or 1 -- todo
4242                            size = size * rscale -- maybe use scale in vf command
4243                            -- load font, todo: set language and script, the effect hack is ugly
4244                            local fullname = filename
4245                            local effect = features.effect
4246                            if effect then
4247                                fullname = fullname .. "*" .. effects["effect={"..effect.."}"]
4248                            end
4249                            local id = definedfont {
4250                                name = fullname,
4251                                size = size,
4252                            }
4253                            if id <= 0 then
4254                                report_math("font %a doesn't exist",fullname)
4255                            else
4256                                local chars  = fontchars[id]
4257                                local dropin = fontdata[id]
4258                                local index  = false
4259                                for i=1,#fonts do
4260                                    local f = fonts[i]
4261                                    if f.id == id and f.size == size then
4262                                        index = i
4263                                        break
4264                                    end
4265                                end
4266                                if not index then
4267                                    index = #fonts + 1
4268                                    fonts[index] = { id = id, size = size }
4269                                end
4270                                -- copy characters
4271                                local function use(sourceunicode,targetunicode)
4272                                    if keep and characters[targetunicode] then
4273                                        -- okay
4274                                    else
4275                                        if feature then
4276                                            sourceunicode = getsubstitution(dropin,sourceunicode,feature,true,"math","dflt") or sourceunicode
4277                                        end
4278                                        characters[targetunicode] = copiedglyph(target,characters,chars,sourceunicode,index)
4279                                    end
4280                                end
4281                                if unicodes then
4282                                    for i=1,#unicodes do
4283                                        local unicode = unicodes[i]
4284                                        if chars[unicode] then
4285                                            use(unicode,unicode)
4286                                        end
4287                                    end
4288                                else
4289                                    for s=firstsource,lastsource do
4290                                        local t = s + offset
4291                                        local sourceunicode = mathgaps[s] or s
4292                                        if chars[sourceunicode] then
4293                                            use(sourceunicode,mathgaps[t] or t)
4294                                        end
4295                                    end
4296                                end
4297                                --
4298                                local inherit = entry.inherit
4299                                if inherit then
4300                                    local mathparameters = target.mathparameters
4301                                    local dropparameters = fontdata[id].mathparameters
4302                                    if dropparameters then
4303                                        for name in sortedhash(inherit) do
4304                                            local value = dropparameters[name]
4305                                            if value then
4306                                                mathparameters[name] = value
4307                                            end
4308                                        end
4309                                    end
4310                                end
4311                            end
4312                        elseif feature then
4313                            local function use(sourceunicode,targetunicode)
4314                                local variant = getsubstitution(original,sourceunicode,feature,true,"math","dflt")
4315                                local data    = characters[variant]
4316                                if data then
4317                                    characters[targetunicode] = copytable(data)
4318                                end
4319                            end
4320                            if unicodes then
4321                                for i=1,#unicodes do
4322                                    local unicode = unicodes[i]
4323                                    use(unicode,unicode)
4324                                end
4325                            else
4326                                for s=firstsource,lastsource do
4327                                    local t = s + offset
4328                                    use(mathgaps[s] or s,mathgaps[t] or t)
4329                                end
4330                            end
4331                        else
4332                            -- this is a hack for stix providing alphabets without proper unicodes
4333                            if unicodes then
4334                                -- not supported
4335                            else
4336                                local firstcode    = unicoderange and unicoderange.first
4337                                local descriptions = original.descriptions
4338                                for s=firstsource,lastsource do
4339                                    local t = s + offset
4340                                    local sourceunicode = mathgaps[s] or s
4341                                    local targetunicode = mathgaps[t] or t
4342                                    if sourceunicode ~= targetunicode then
4343                                        local data = characters[sourceunicode]
4344                                        if data then
4345                                            if firstcode then
4346                                                local d = descriptions[sourceunicode]
4347                                                if d then
4348                                                    d.unicode = firstcode -- wins
4349                                                end
4350                                                data.unicode = firstcode
4351                                                firstcode = firstcode + 1
4352                                            end
4353                                            characters[targetunicode] = copytable(data)
4354                                        end
4355                                    end
4356                                end
4357                            end
4358                        end
4359                    end
4360                end
4361            end
4362        end
4363    end
4364
4365    function mathtweaks.fallbacks(target,original,parameters)
4366        local fallbacks = target.specification.fallbacks
4367        if fallbacks then
4368            local definitions = fonts.collections.definitions[fallbacks]
4369            if definitions then
4370                local list = { }
4371                for i=1,#definitions do
4372                    local definition = definitions[i]
4373                 -- local check  = definition.check
4374                 -- local force  = definition.force
4375                    local first  = definition.start
4376                    local last   = definition.stop
4377                    local offset = definition.offset or first
4378                    list[#list+1] = {
4379                        filename = definition.font,
4380                        rscale   = definition.rscale or 1,
4381                        source   = { first = first,  last = last },
4382                        target   = { first = offset, last = offset + (last - first) },
4383                    }
4384                end
4385                mathtweaks.replacealphabets(target,original,{
4386                    tweak = "replacealphabets",
4387                    list  = list,
4388                } )
4389            end
4390        end
4391    end
4392
4393end
4394
4395local apply_tweaks   = true  directives.register("math.applytweaks", function(v) apply_tweaks = v end)
4396local applied_tweaks = 0
4397
4398local function tweaklist(target,original,tweaks)
4399    if type(tweaks) == "table" then
4400        for i=1,#tweaks do
4401            local tweak  = tweaks[i]
4402            if type(tweak) == "table" then
4403                local action = mathtweaks[tweak.tweak or ""]
4404                if action then
4405                    local feature  = tweak.feature
4406                    local features = target.specification.features.normal
4407                    if feature == nil or features[feature] then
4408                        local version = tweak.version
4409                        if version and version ~= target.tweakversion then
4410                            report_math("skipping tweak %a version %a",tweak.tweak,version)
4411                        elseif original then
4412                            action(target,original,tweak)
4413                        else
4414                            action(target,tweak)
4415                        end
4416                    end
4417                end
4418            end
4419        end
4420    end
4421end
4422
4423function mathtweaks.tweaks(target,original,parameters)
4424    tweaklist(target,original,parameters.list)
4425end
4426
4427local function applytweaks(when,target,original)
4428    if apply_tweaks then
4429        local goodies = original.goodies
4430        if goodies then
4431            local tweaked = target.tweaked or { }
4432            if tweaked[when] then
4433                if trace_defining then
4434                    report_math("tweaking math of %a @ %p (%s: %s)",target.properties.fullname,target.parameters.size,when,"done")
4435                end
4436            else
4437                for i=1,#goodies do
4438                    local goodie = goodies[i]
4439                    local mathematics = goodie.mathematics
4440                    local tweaks = mathematics and mathematics.tweaks
4441                    if type(tweaks) == "table" then
4442                        statistics.starttiming(mathtweaks)
4443                        applied_tweaks = applied_tweaks + 1
4444                        tweaks = tweaks[when]
4445                        if trace_defining then
4446                            report_math("tweaking math of %a @ %p (%s: %s)",target.properties.fullname,target.parameters.size,when,"okay")
4447                        end
4448                        tweaklist(target,original,tweaks)
4449                    end
4450                    statistics.stoptiming(mathtweaks)
4451                end
4452                tweaked[when] = true
4453                target.tweaked = tweaked
4454            end
4455        end
4456    else
4457        report_math("not tweaking math of %a @ %p (%s)",target.properties.fullname,target.parameters.size,when)
4458    end
4459end
4460
4461local function tweakable(target)
4462    local mathparameters = target.mathparameters
4463--     local features       = target.specification.features
4464--     local mathscript     = features and features.normal and features.normal.script == "math"
4465--     return mathparameters and mathscript -- and target,properties.hasmath
4466    return mathparameters
4467end
4468
4469function mathematics.tweakbeforecopyingfont(target,original)
4470    if use_math_goodies and tweakable(target) then
4471        applytweaks("beforecopying",target,original)
4472    end
4473end
4474
4475function mathematics.tweakaftercopyingfont(target,original)
4476    if use_math_goodies and tweakable(target) then
4477        applytweaks("aftercopying",target,original)
4478        target.properties.hasitalics = false
4479    end
4480end
4481
4482statistics.register("math tweaking time",function()
4483    if applied_tweaks > 0 then
4484        return string.format("%s seconds, %s math goodie tables", statistics.elapsedtime(mathtweaks),applied_tweaks)
4485    end
4486end)
4487
4488do
4489
4490    local defaults = {
4491        {
4492            source = "uppercasescript",
4493            target = "uppercasecalligraphic",
4494        },
4495        {
4496            source = "lowercasescript",
4497            target = "lowercasecalligraphic",
4498        },
4499        {
4500            source = "uppercaseboldscript",
4501            target = "uppercaseboldcalligraphic",
4502        },
4503        {
4504            source = "lowercaseboldscript",
4505            target = "lowercaseboldcalligraphic",
4506        },
4507    }
4508
4509    local reported = table.setmetatableindex("table")
4510
4511    function mathematics.checkaftercopyingfont(target,original)
4512        if tweakable(target) then
4513            local chardata   = characters.data
4514            local characters = target.characters
4515            --
4516            for i=1,#defaults do
4517                -- we assume no ssty here yet .. todo
4518                local default = defaults[i]
4519                local block   = blocks[default.target]
4520                local first   = block.first
4521                local last    = block.last
4522                if not characters[mathgaps[first] or last] then
4523                    mathtweaks.replacealphabets(target,original,{
4524                        tweak   = "replacealphabets",
4525                        list    = { default }
4526                    })
4527                end
4528            end
4529            --
4530            local addvariant = mathematics.addvariant
4531            local function register(old,new)
4532                for i, cold in next, old do
4533                    local cnew = new[i]
4534                    addvariant(target,cold,cold,0xFE00)
4535                    addvariant(target,cnew,cnew,0xFE01)
4536                    addvariant(target,cnew,cold,0xFE00)
4537                    addvariant(target,cold,cnew,0xFE01)
4538                end
4539            end
4540            local sr = mathematics.alphabets.sr.tf
4541            local ca = mathematics.alphabets.ca.tf
4542            register(sr.ucletters,ca.ucletters)
4543            register(sr.lcletters,ca.lcletters)
4544            --
4545            if checkitalics then
4546                local italics  = 0
4547                local metadata = original.shared.rawdata.metadata
4548                local fontname = metadata and metadata.fontname or false
4549             -- for k, v in sortedhash(characters) do
4550                for k, v in next, characters do
4551                    local italic = v.italic
4552                    if italic then
4553                        local unicode = v.unicode
4554                        if unicode and not reported[fontname][unicode] then -- there can be variants
4555                            local data        = chardata[unicode]
4556                            local description = data.description or ""
4557                            local category    = data.category or "--"
4558                            report_tweak("italics: %C %p %s %s",target,original,k,italic,category,description)
4559                            reported[fontname][unicode] = true
4560                        end
4561                        italics = italics + 1
4562                    end
4563                end
4564                if italics > 0 then
4565                    report_tweak("still has %i italics",target,original,italics)
4566                    goto NEXTSTEP
4567                end
4568            end
4569          ::NEXTSTEP::
4570            -- more to come
4571        end
4572    end
4573
4574end
4575
4576function mathematics.beforepassingfonttotex(target,original)
4577    if tweakable(target) then
4578        applytweaks("beforepassing",target,original)
4579    end
4580end
4581
4582sequencers.appendaction("mathparameters","system","mathematics.overloadparameters")
4583sequencers.appendaction("mathparameters","system","mathematics.scaleparameters")
4584
4585sequencers.appendaction("beforecopyingcharacters","system","mathematics.tweakbeforecopyingfont")
4586sequencers.appendaction("aftercopyingcharacters", "system","mathematics.tweakaftercopyingfont")
4587sequencers.appendaction("aftercopyingcharacters", "system","mathematics.checkaftercopyingfont")
4588sequencers.appendaction("beforepassingfonttotex", "system","mathematics.beforepassingfonttotex")
4589
4590-- no, it's a feature now (see good-mth):
4591--
4592-- sequencers.appendaction("aftercopyingcharacters","system","mathematics.overloaddimensions")
4593
4594-- we use numbers at the tex end (otherwise we could stick to chars)
4595
4596local e_left       = extensibles.left
4597local e_right      = extensibles.right
4598local e_horizontal = extensibles.horizontal
4599local e_mixed      = extensibles.mixed
4600local e_unknown    = extensibles.unknown
4601
4602local unknown      = { e_unknown, false, false }
4603
4604-- top curly bracket: 23DE
4605
4606local function extensiblecode(font,unicode)
4607    local characters = fontcharacters[font]
4608    local character = characters[unicode]
4609    if not character then
4610        return unknown
4611    end
4612    local first = character.next
4613    local code = unicode
4614    local next = first
4615    while next do
4616        code = next
4617        character = characters[next]
4618        next = character.next
4619    end
4620    local char = chardata[unicode]
4621    if not char then
4622        return unknown
4623    end
4624    if character.parts then
4625        local m = char.mathextensible
4626        local e = m and extensibles[m]
4627        return e and { e, code, character } or unknown
4628    elseif first then
4629        -- assume accent (they seldom stretch .. sizes)
4630        local m = char.mathextensible or char.mathstretch
4631        local e = m and extensibles[m]
4632        return e and { e, code, character } or unknown
4633    else
4634        return unknown
4635    end
4636end
4637
4638setmetatableindex(extensibles,function(extensibles,font)
4639    local codes = { }
4640    setmetatableindex(codes, function(codes,unicode)
4641        local status = extensiblecode(font,unicode)
4642        codes[unicode] = status
4643        return status
4644    end)
4645    extensibles[font] = codes
4646    return codes
4647end)
4648
4649local function extensiblecode(family,unicode)
4650    return extensibles[getfontoffamily(family or 0)][unicode][1]
4651end
4652
4653-- left       : [head] ...
4654-- right      : ... [head]
4655-- horizontal : [head] ... [head]
4656--
4657-- abs(right["start"] - right["end"]) | right.advance | characters[right.glyph].width
4658
4659local function horizontalcode(family,unicode)
4660    local font    = getfontoffamily(family or 0)
4661    local data    = extensibles[font][unicode]
4662    local kind    = data[1]
4663    local loffset = 0
4664    local roffset = 0
4665    if kind == e_left then
4666        local charlist = data[3].parts
4667        if charlist then
4668            local left = charlist[1]
4669            loffset = abs((left["start"] or 0) - (left["end"] or 0))
4670        end
4671    elseif kind == e_right then
4672        local charlist = data[3].parts
4673        if charlist then
4674            local right = charlist[#charlist]
4675            roffset = abs((right["start"] or 0) - (right["end"] or 0))
4676        end
4677     elseif kind == e_horizontal then
4678        local charlist = data[3].parts
4679        if charlist then
4680            local left  = charlist[1]
4681            local right = charlist[#charlist]
4682            loffset = abs((left ["start"] or 0) - (left ["end"] or 0))
4683            roffset = abs((right["start"] or 0) - (right["end"] or 0))
4684        end
4685    end
4686    return kind, loffset, roffset
4687end
4688
4689mathematics.extensiblecode = extensiblecode
4690mathematics.horizontalcode = horizontalcode
4691
4692-- no longer needed:
4693
4694interfaces.implement {
4695    name      = "extensiblecode",
4696    arguments = "2 integers",
4697 -- usage     = "value", -- maybe
4698    public    = true,
4699    actions   = { extensiblecode, context }
4700}
4701
4702interfaces.implement {
4703    name      = "mathhorizontalcode",
4704    arguments = "2 integers",
4705 -- usage     = "value", -- maybe
4706    public    = true,
4707    actions   = function(family,unicode)
4708        local kind, loffset, roffset = horizontalcode(family,unicode)
4709        texsetdimen(d_scratchleftoffset, loffset)
4710        texsetdimen(d_scratchrightoffset,roffset)
4711        context(kind)
4712    end
4713}
4714
4715function mathematics.variantcode(unicode,variant)
4716    local data = fontcharacters[getfontoffamily(texget("fam"))]
4717    local char = data and data[unicode]
4718    if char then
4719        for i=1,variant do
4720            local next = char.next
4721            if next then
4722                unicode = next
4723                char = data[next]
4724            else
4725                break
4726            end
4727        end
4728    end
4729    return unicode
4730end
4731
4732function mathematics.variantcount(unicode)
4733    local data  = fontcharacters[getfontoffamily(texget("fam"))]
4734    local char  = data and data[unicode]
4735    local count = 0
4736    if char then
4737        while true do
4738            local next = char.next
4739            if next then
4740                count = count + 1
4741                char = data[next]
4742            else
4743                break
4744            end
4745        end
4746    end
4747    return count
4748end
4749
4750interfaces.implement {
4751    name      = "mathvariantcode",
4752    public    = true,
4753    arguments = "2 integers",
4754    actions   = { mathematics.variantcode, context },
4755}
4756
4757interfaces.implement {
4758    name      = "mathvariantcount",
4759    public    = true,
4760    arguments = "integer",
4761    actions   = { mathematics.variantcount, context },
4762}
4763