math-fbk.lmt /size: 25 Kb    last modification: 2021-10-28 13:51
1if not modules then modules = { } end modules ['math-fbk'] = {
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-- This will partly be redone and go via definitions in goodies where we can share
10-- some across fonts.
11
12local next, type = next, type
13
14local trace_fallbacks   = false  trackers.register("math.fallbacks", function(v) trace_fallbacks = v end)
15
16local report_fallbacks  = logs.reporter("math","fallbacks")
17
18local formatters        = string.formatters
19local fastcopy          = table.fastcopy
20local byte              = string.byte
21local sortedhash        = table.sortedhash
22local setmetatableindex = table.setmetatableindex
23
24local fallbacks         = { }
25mathematics.fallbacks   = fallbacks
26
27local helpers           = fonts.helpers
28local prependcommands   = helpers.prependcommands
29local charcommand       = helpers.commands.char
30local leftcommand       = helpers.commands.left
31local rightcommand      = helpers.commands.right
32local upcommand         = helpers.commands.up
33local downcommand       = helpers.commands.down
34local dummycommand      = helpers.commands.dummy
35local popcommand        = helpers.commands.pop
36local pushcommand       = helpers.commands.push
37
38local virtualcharacters = { }
39local virtualforced     = { }
40
41local hashes            = fonts.hashes
42local identifiers       = hashes.identifiers
43local lastmathids       = hashes.lastmathids
44
45-- we need a trick (todo): if we define scriptscript, script and text in
46-- that order we could use their id's .. i.e. we could always add a font
47-- table with those id's .. in fact, we could also add a whole lot more
48-- as it doesn't hurt
49
50function fallbacks.apply(target,original)
51    local mathparameters = target.mathparameters
52    if not mathparameters or not next(mathparameters) then
53        return
54    end
55    -- we also have forcedsize ... at this moment we already passed through
56    -- constructors.scale so we have this set
57    local parameters = target.parameters
58    local properties = target.properties
59    local mathsize   = parameters.mathsize
60    if mathsize < 1 or mathsize > 3 then
61        return
62    end
63    local characters  = target.characters
64    local size        = parameters.size
65    local usedfonts   = target.fonts
66    local compactmath = properties.compactmath
67    if not usedfonts then
68        usedfonts    = { { id = 0 } } -- we need at least one entry (automatically done anyway)
69        target.fonts = usedfonts
70    end
71    -- not used
72    local textid, scriptid, scriptscriptid
73    local textindex, scriptindex, scriptscriptindex
74    local textdata, scriptdata, scriptscriptdata
75    if mathsize == 3 then
76        -- scriptscriptsize
77        textid         = 0
78        scriptid       = 0
79        scriptscriptid = 0
80    elseif mathsize == 2 then
81        -- scriptsize
82        textid         = 0
83        scriptid       = lastmathids[3] or 0
84        scriptscriptid = lastmathids[3] or 0
85    else
86        -- textsize
87        textid         = 0
88        scriptid       = lastmathids[2] or 0
89        scriptscriptid = lastmathids[3] or 0
90    end
91    if textid and textid ~= 0 then
92        textindex = #usedfonts + 1
93        textdata  = target
94        usedfonts[textindex] = { id = textid }
95    else
96        textdata = target
97    end
98    if compactmath then
99        scriptid       = textid
100        scriptscriptid = textid
101    end
102    if scriptid and scriptid ~= 0 then
103        scriptindex = #usedfonts  + 1
104        scriptdata  = identifiers[scriptid]
105        usedfonts[scriptindex] = { id = scriptid }
106    else
107        scriptindex = textindex
108        scriptdata  = textdata
109    end
110    if scriptscriptid and scriptscriptid ~= 0 then
111        scriptscriptindex = #usedfonts  + 1
112        scriptscriptdata  = identifiers[scriptscriptid]
113        usedfonts[scriptscriptindex] = { id = scriptscriptid }
114    else
115        scriptscriptindex = scriptindex
116        scriptscriptdata  = scriptdata
117    end
118    -- report_fallbacks("used textid: %S, used script id: %S, used scriptscript id: %S",textid,scriptid,scriptscriptid)
119    local data = {
120        textdata          = textdata,
121        scriptdata        = scriptdata,
122        scriptscriptdata  = scriptscriptdata,
123        textindex         = textindex,
124        scriptindex       = scriptindex,
125        scriptscriptindex = scriptscriptindex,
126        textid            = textid,
127        scriptid          = scriptid,
128        scriptscriptid    = scriptscriptid,
129        characters        = characters,
130        unicode           = k,
131        target            = target,
132        original          = original,
133        size              = size,
134        mathsize          = mathsize,
135    }
136    target.mathrelation = data
137    --
138    local fullname = trace_fallbacks and target.properties.fullname
139    --
140    for k, v in sortedhash(virtualcharacters) do
141        if not characters[k] or virtualforced[k] then
142            local tv = type(v)
143            local cd = nil
144            if tv == "table" then
145                cd = v
146            elseif tv == "number" then
147                cd = characters[v]
148            elseif tv == "function" then
149                cd = v(data) -- ,k
150            end
151            if cd then
152                characters[k] = cd
153            else
154                -- something else
155            end
156            if trace_fallbacks and characters[k] then
157                report_fallbacks("extending math font %a with %U",fullname,k)
158            end
159        end
160    end
161    data.unicode = nil
162end
163
164utilities.sequencers.appendaction("aftercopyingcharacters","system","mathematics.fallbacks.apply")
165
166function fallbacks.install(unicode,value)
167    virtualcharacters[unicode] = value
168end
169
170-- a few examples:
171
172local function reference(index,char)
173    if index then
174        return { "slot", index, char }
175    else
176        return charcommand[char]
177    end
178end
179
180local function raised(data,replacement,down)
181    local character = data.scriptdata.characters[replacement]
182    if character then
183        local size = data.size
184        return {
185            width    = character.width,
186            height   = character.height,
187            depth    = character.depth,
188            commands = {
189                down and downcommand[size/4] or upcommand[size/2],
190                reference(data.scriptindex,replacement)
191            }
192        }
193    end
194end
195
196-- virtualcharacters[0x207A] = 0x2212
197-- virtualcharacters[0x207B] = 0x002B
198-- virtualcharacters[0x208A] = 0x2212
199-- virtualcharacters[0x208B] = 0x002B
200
201virtualcharacters[0x207A] = function(data) return raised(data,0x002B)      end
202virtualcharacters[0x207B] = function(data) return raised(data,0x2212)      end
203virtualcharacters[0x208A] = function(data) return raised(data,0x002B,true) end
204virtualcharacters[0x208B] = function(data) return raised(data,0x2212,true) end
205
206-- local function repeated(data,char,n,fraction)
207--     local character = data.characters[char]
208--     if character then
209--         local width = character.width
210--         local delta = width - character.italic -- width * fraction
211--         local c = charcommand[char]
212--         local r = rightcommand[right]
213--         local commands = { }
214--         for i=1,n-1 do
215--             width = width + delta
216--             commands[#commands+1] = c
217--             commands[#commands+1] = -delta
218--         end
219--         commands[#commands+1] = c
220--         return {
221--             width    = width,
222--             height   = character.height,
223--             depth    = character.depth,
224--             commands = commands,
225--         }
226--     end
227-- end
228
229-- virtualcharacters[0x222C] = function(data)
230--     return repeated(data,0x222B,2,1/8)
231-- end
232
233-- virtualcharacters[0x222D] = function(data)
234--     return repeated(data,0x222B,3,1/8)
235-- end
236
237local addextra = mathematics.extras.add
238
239addextra(0xFE350) -- MATHEMATICAL DOUBLE ARROW LEFT END
240addextra(0xFE351) -- MATHEMATICAL DOUBLE ARROW MIDDLE PART
241addextra(0xFE352) -- MATHEMATICAL DOUBLE ARROW RIGHT END
242
243local leftarrow  = charcommand[0x2190]
244local relbar     = charcommand[0x2212]
245local rightarrow = charcommand[0x2192]
246
247virtualcharacters[0xFE350] = function(data)
248 -- return combined(data,0x2190,0x2212) -- leftarrow relbar
249    local charone = data.characters[0x2190]
250    local chartwo = data.characters[0x2212]
251    if charone and chartwo then
252        local size = data.size/2
253        return {
254            width    = chartwo.width,
255            height   = size,
256            depth    = size,
257            commands = {
258                pushcommand,
259                downcommand[size/2],
260                leftarrow,
261                popcommand,
262                upcommand[size/2],
263                relbar,
264            }
265        }
266    end
267end
268
269virtualcharacters[0xFE351] = function(data)
270 -- return combined(data,0x2212,0x2212) -- relbar, relbar  (isn't that just equal)
271    local char = data.characters[0x2212]
272    if char then
273        local size = data.size/2
274        return {
275            width    = char.width,
276            height   = size,
277            depth    = size,
278            commands = {
279                pushcommand,
280                downcommand[size/2],
281                relbar,
282                popcommand,
283                upcommand[size/2],
284                relbar,
285            }
286        }
287    end
288end
289
290virtualcharacters[0xFE352] = function(data)
291 -- return combined(data,0x2192,0x2212) -- rightarrow relbar
292    local charone = data.characters[0x2192]
293    local chartwo = data.characters[0x2212]
294    if charone and chartwo then
295        local size = data.size/2
296        return {
297            width    = chartwo.width,
298            height   = size,
299            depth    = size,
300            commands = {
301                pushcommand,
302                downcommand[size/2],
303                relbar,
304                popcommand,
305                rightcommand[chartwo.width - charone.width],
306                upcommand[size/2],
307                rightarrow,
308            }
309        }
310    end
311end
312
313-- we could move the defs from math-act here
314
315local function accent_to_extensible(target,newchr,original,oldchr,height,depth,swap,offset,unicode)
316    local characters = target.characters
317    local olddata = characters[oldchr]
318    -- brrr ... pagella has only next
319    if olddata and not olddata.commands then -- not: and olddata.width > 0
320        local addprivate = fonts.helpers.addprivate
321        if swap then
322            swap   = characters[swap]
323            height = swap.depth or 0
324            depth  = 0
325        else
326            height = height or 0
327            depth  = depth  or 0
328        end
329        local oldheight  = olddata.height or 0
330        local correction = swap and
331            downcommand[oldheight - height]
332         or downcommand[oldheight + (offset or 0)]
333        local newdata = {
334            commands = { correction, charcommand[oldchr] },
335            width    = olddata.width,
336            height   = height,
337            depth    = depth,
338            unicode  = unicode,
339        }
340        local glyphdata = newdata
341        local nextglyph = olddata.next
342        while nextglyph do
343            local oldnextdata = characters[nextglyph]
344            if oldnextdata then
345                local newnextdata = {
346                    commands = { correction, charcommand[nextglyph] },
347                    width    = oldnextdata.width,
348                    height   = height,
349                    depth    = depth,
350                }
351                local newnextglyph = addprivate(target,formatters["M-N-%H"](nextglyph),newnextdata)
352                newdata.next = newnextglyph
353                local nextnextglyph = oldnextdata.next
354                if nextnextglyph == nextglyph then
355                    break
356                else
357                    olddata   = oldnextdata
358                    newdata   = newnextdata
359                    nextglyph = nextnextglyph
360                end
361            else
362                report_fallbacks("error in fallback: no valid next, slot %X",nextglyph)
363                break
364            end
365        end
366        local hv = olddata.horiz_variants
367        if hv then
368            hv = fastcopy(hv)
369            newdata.horiz_variants = hv
370            for i=1,#hv do
371                local hvi = hv[i]
372                local oldglyph = hvi.glyph
373                local olddata = characters[oldglyph]
374                if olddata then
375                    local newdata = {
376                        commands = { correction, charcommand[oldglyph] },
377                        width    = olddata.width,
378                        height   = height,
379                        depth    = depth,
380                    }
381                    hvi.glyph = addprivate(target,formatters["M-H-%H"](oldglyph),newdata)
382                else
383                    report_fallbacks("error in fallback: no valid horiz_variants, slot %X, index %i",oldglyph,i)
384                end
385            end
386        end
387        return glyphdata, true
388    else
389        return olddata, false
390    end
391end
392
393virtualcharacters[0x203E] = function(data)
394    local target = data.target
395    local height = 0
396    local depth  = 0
397 -- local mathparameters = target.mathparameters
398 -- if mathparameters then
399 --     height = mathparameters.OverbarVerticalGap
400 --     depth  = mathparameters.UnderbarVerticalGap
401 -- else
402        height = target.parameters.xheight/4
403        depth  = height
404 -- end
405    return accent_to_extensible(target,0x203E,data.original,0x0305,height,depth,nil,nil,0x203E)
406end
407
408-- virtualcharacters[0xFE33E] = virtualcharacters[0x203E] -- convenient
409-- virtualcharacters[0xFE33F] = virtualcharacters[0x203E] -- convenient
410
411virtualcharacters[0xFE33E] = function(data)
412    local target = data.target
413    local height = 0
414    local depth  = target.parameters.xheight/4
415    return accent_to_extensible(target,0xFE33E,data.original,0x0305,height,depth,nil,nil,0x203E)
416end
417
418virtualcharacters[0xFE33F] = function(data)
419    local target = data.target
420    local height = target.parameters.xheight/8
421    local depth  = height
422    return accent_to_extensible(target,0xFE33F,data.original,0x0305,height,depth,nil,nil,0x203E)
423end
424
425-- spacing (no need for a cache of widths)
426
427local c_zero   = byte('0')
428local c_period = byte('.')
429
430local function spacefraction(data,fraction)
431    local width = fraction * data.target.parameters.space
432    return {
433        width    = width,
434        commands = { rightcommand[width] }
435    }
436end
437
438local function charfraction(data,char)
439    local width = data.target.characters[char].width
440    return {
441        width    = width,
442        commands = { rightcommand[width] }
443    }
444end
445
446local function quadfraction(data,fraction)
447    local width = fraction * data.target.parameters.quad
448    return {
449        width    = width,
450        commands = { rightcommand[width] }
451    }
452end
453
454virtualcharacters[0x00A0] = function(data) return spacefraction(data,1)        end -- nbsp
455virtualcharacters[0x2000] = function(data) return quadfraction (data,1/2)      end -- enquad
456virtualcharacters[0x2001] = function(data) return quadfraction (data,1)        end -- emquad
457virtualcharacters[0x2002] = function(data) return quadfraction (data,1/2)      end -- enspace
458virtualcharacters[0x2003] = function(data) return quadfraction (data,1)        end -- emspace
459virtualcharacters[0x2004] = function(data) return quadfraction (data,1/3)      end -- threeperemspace
460virtualcharacters[0x2005] = function(data) return quadfraction (data,1/4)      end -- fourperemspace
461virtualcharacters[0x2006] = function(data) return quadfraction (data,1/6)      end -- sixperemspace
462virtualcharacters[0x2007] = function(data) return charfraction (data,c_zero)   end -- figurespace
463virtualcharacters[0x2008] = function(data) return charfraction (data,c_period) end -- punctuationspace
464virtualcharacters[0x2009] = function(data) return quadfraction (data,1/8)      end -- breakablethinspace
465virtualcharacters[0x200A] = function(data) return quadfraction (data,1/8)      end -- hairspace
466virtualcharacters[0x200B] = function(data) return quadfraction (data,0)        end -- zerowidthspace
467virtualcharacters[0x202F] = function(data) return quadfraction (data,1/8)      end -- narrownobreakspace
468virtualcharacters[0x205F] = function(data) return spacefraction(data,1/2)      end -- math thinspace
469
470--
471
472local function smashed(data,unicode,swap,private)
473    local target   = data.target
474    local original = data.original
475    local chardata = target.characters[unicode]
476    if chardata and chardata.height > target.parameters.xheight then
477        return accent_to_extensible(target,private,original,unicode,0,0,swap,nil,unicode)
478    else
479        return original.characters[unicode]
480    end
481end
482
483addextra(0xFE3DE) -- EXTENSIBLE OF 0x03DE
484addextra(0xFE3DC) -- EXTENSIBLE OF 0x03DC
485addextra(0xFE3B4) -- EXTENSIBLE OF 0x03B4
486
487virtualcharacters[0xFE3DE] = function(data) return smashed(data,0x23DE,0x23DF,0xFE3DE) end
488virtualcharacters[0xFE3DC] = function(data) return smashed(data,0x23DC,0x23DD,0xFE3DC) end
489virtualcharacters[0xFE3B4] = function(data) return smashed(data,0x23B4,0x23B5,0xFE3B4) end
490
491addextra(0xFE3DF) -- EXTENSIBLE OF 0x03DF
492addextra(0xFE3DD) -- EXTENSIBLE OF 0x03DD
493addextra(0xFE3B5) -- EXTENSIBLE OF 0x03B5
494
495virtualcharacters[0xFE3DF] = function(data) local c = data.target.characters[0x23DF] if c then c.unicode = 0x23DF return c end end
496virtualcharacters[0xFE3DD] = function(data) local c = data.target.characters[0x23DD] if c then c.unicode = 0x23DD return c end end
497virtualcharacters[0xFE3B5] = function(data) local c = data.target.characters[0x23B5] if c then c.unicode = 0x23B5 return c end end
498
499-- todo: add some more .. numbers might change
500
501addextra(0xFE302) -- EXTENSIBLE OF 0x0302
502addextra(0xFE303) -- EXTENSIBLE OF 0x0303
503
504local function smashed(data,unicode,private)
505    local target = data.target
506    local height = target.parameters.xheight / 2
507    local c, done = accent_to_extensible(target,private,data.original,unicode,height,0,nil,-height,unicode)
508    if done then
509        c.top_accent = nil -- or maybe also all the others
510    end
511    return c
512end
513
514virtualcharacters[0xFE302] = function(data) return smashed(data,0x0302,0xFE302) end
515virtualcharacters[0xFE303] = function(data) return smashed(data,0x0303,0xFE303) end
516
517-- another crazy hack .. doesn't work as we define scrscr first .. we now have smaller
518-- primes so we have smaller primes for the moment, big ones will become an option ..
519-- these primes in fonts are a real mess .. kind of a dead end, so don't wonder about
520-- the values below
521
522local function smashed(data,unicode,optional)
523    local oldchar = data.characters[unicode]
524    if oldchar then
525        local height  = 0.85 * data.target.mathparameters.AccentBaseHeight
526        local newchar = table.copy(oldchar)
527        newchar.yoffset = height - oldchar.height
528        newchar.height  = height
529        return newchar
530    elseif not optional then
531        report_fallbacks("missing %U prime in font %a",unicode,data.target.properties.fullname)
532    end
533end
534
535addextra(0xFE932) -- SMASHED PRIME 0x02032
536addextra(0xFE933) -- SMASHED PRIME 0x02033
537addextra(0xFE934) -- SMASHED PRIME 0x02034
538addextra(0xFE957) -- SMASHED PRIME 0x02057
539
540addextra(0xFE935) -- SMASHED BACKWARD PRIME 0x02035
541addextra(0xFE936) -- SMASHED BACKWARD PRIME 0x02036
542addextra(0xFE937) -- SMASHED BACKWARD PRIME 0x02037
543
544virtualcharacters[0xFE932] = function(data) return smashed(data,0x02032) end
545virtualcharacters[0xFE933] = function(data) return smashed(data,0x02033) end
546virtualcharacters[0xFE934] = function(data) return smashed(data,0x02034) end
547virtualcharacters[0xFE957] = function(data) return smashed(data,0x02057) end
548
549virtualcharacters[0xFE935] = function(data) return smashed(data,0x02035,true) end
550virtualcharacters[0xFE936] = function(data) return smashed(data,0x02036,true) end
551virtualcharacters[0xFE937] = function(data) return smashed(data,0x02037,true) end
552
553local hack = nil
554
555function mathematics.getridofprime(target,original)
556--     local mathsize = specification.mathsize
557--     if mathsize == 1 or mathsize == 2 or mathsize == 3) then
558    local mathparameters = original.mathparameters
559    if mathparameters and next(mathparameters) then
560        local changed = original.changed
561        if changed then
562            hack = changed[0x02032]
563            changed[0x02032] = nil
564            changed[0x02033] = nil
565            changed[0x02034] = nil
566            changed[0x02057] = nil
567            changed[0x02035] = nil
568            changed[0x02036] = nil
569            changed[0x02037] = nil
570        end
571    end
572end
573
574function mathematics.setridofprime(target,original)
575    local mathparameters = original.mathparameters
576    if mathparameters and next(mathparameters) and original.changed then
577        target.characters[0xFE931] = target.characters[hack or 0x2032]
578        hack = nil
579    end
580end
581
582utilities.sequencers.appendaction("beforecopyingcharacters","system","mathematics.getridofprime")
583utilities.sequencers.appendaction("aftercopyingcharacters", "system","mathematics.setridofprime")
584
585-- actuarian (beware: xits has an ugly one)
586
587addextra(0xFE940) -- SMALL ANNUITY SYMBOL
588
589local function actuarian(data)
590    local characters = data.target.characters
591    local parameters = data.target.parameters
592    local basechar   = characters[0x0078] -- x (0x0058 X) or 0x1D431
593    local linewidth  = parameters.xheight / 10
594    local basewidth  = basechar.width
595    local baseheight = basechar.height
596    return {
597        -- todo: add alttext
598        -- compromise: lm has large hooks e.g. \actuarial{a}
599        width    = basewidth + 4 * linewidth,
600        height   = basechar.height,
601        depth    = basechar.depth,
602        unicode  = 0x20E7,
603        commands = {
604            rightcommand[2 * linewidth],
605            downcommand[- baseheight - 3 * linewidth],
606            { "rule", linewidth, basewidth + 4 * linewidth },
607            leftcommand[linewidth],
608            downcommand[baseheight + 4 * linewidth],
609            { "rule", baseheight + 5 * linewidth, linewidth },
610        },
611    }
612end
613
614virtualcharacters[0x020E7] = actuarian -- checked
615virtualcharacters[0xFE940] = actuarian -- unchecked
616
617local function equals(data,unicode,snippet,advance,n) -- mathpair needs them
618    local characters = data.target.characters
619    local parameters = data.target.parameters
620    local basechar   = characters[snippet]
621    local advance    = advance * parameters.quad
622    return {
623        unicode  = unicode,
624        width    = n*basechar.width - (n-1)*advance,
625        height   = basechar.height,
626        depth    = basechar.depth,
627        commands = {
628            charcommand[snippet],
629            leftcommand[advance],
630            charcommand[snippet],
631            n > 2 and leftcommand[advance] or nil,
632            n > 2 and charcommand[snippet] or nil,
633        },
634    }
635end
636
637virtualcharacters[0x2A75] = function(data) return equals(data,0x2A75,0x003D, 1/5,2) end -- ==
638virtualcharacters[0x2A76] = function(data) return equals(data,0x2A76,0x003D, 1/5,3) end -- ===
639virtualcharacters[0x2980] = function(data) return equals(data,0x2980,0x007C,-1/8,3) end -- |||
640
641-- addextra(0xFE941) -- EXTREMELY IDENTICAL TO
642--
643-- virtualcharacters[0xFE941] = function(data) -- this character is only needed for mathpairs
644--     local characters = data.target.characters
645--     local parameters = data.target.parameters
646--     local basechar   = characters[0x003D]
647--     local width      = basechar.width or 0
648--     local height     = basechar.height or 0
649--     local depth      = basechar.depth or 0
650--     return {
651--         unicode   = 0xFE941,
652--         width     = width,
653--         height    = height,         -- we cheat (no time now)
654--         depth     = depth,          -- we cheat (no time now)
655--         commands  = {
656--             upcommand[height/2], -- sort of works
657--             charcommand[0x003D],
658--             leftcommand[width],
659--             downcommand[height],     -- sort of works
660--             charcommand[0x003D],
661--         },
662--     }
663-- end
664
665-- lucida needs this
666
667virtualcharacters[0x305] = function(data)
668    local target = data.target
669    local height = target.parameters.xheight/8
670    local width  = target.parameters.emwidth/2
671    local depth  = height
672    local used   = 0.8 * width
673    return {
674        width    = width,
675        height   = height,
676        depth    = depth,
677        commands = { { "rule", height, width } },
678        horiz_variants = {
679            {
680              advance   = width,
681              ["end"]   = used,
682              glyph     = 0x305,
683              start     = 0,
684            },
685            {
686              advance   = width,
687              ["end"]   = 0,
688              extender  = 1,
689              glyph     = 0x305,
690              start     = used,
691            },
692        }
693    }
694end
695
696local function threedots(data,shift)
697    local characters = data.target.characters
698    local parameters = data.target.parameters
699    local periodchar = characters[0x002E]
700    local pluschar   = characters[0x002B]
701    local period     = charcommand[0x002E]
702    local periodwd   = periodchar.width  or 0
703    local periodht   = periodchar.height or 0
704    local perioddp   = periodchar.depth or 0
705    local offset     = 0
706    if shift then
707        local plusht = pluschar.height or 0
708        local plusdp = pluschar.depth or 0
709        local axis   = (plusdp + plusht)//2 - plusdp
710        offset   = axis - periodht//2
711        periodht = axis + periodht//2
712    end
713    return {
714        width    = 3*periodwd,
715        height   = periodht,
716        depth    = 0,
717        commands = { upcommand[offset], period, period, period }
718    }
719end
720
721virtualcharacters[0x2026] = function(data) return threedots(data,false) end virtualforced[0x2026] = true
722virtualcharacters[0x22EF] = function(data) return threedots(data, true) end virtualforced[0x22EF] = true
723