math-fbk.lua /size: 25 Kb    last modification: 2023-12-21 09:44
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
9local next, type = next, type
10local floor = math.floor
11
12local trace_fallbacks   = false  trackers.register("math.fallbacks", function(v) trace_fallbacks = v end)
13
14local report_fallbacks  = logs.reporter("math","fallbacks")
15
16local formatters        = string.formatters
17local fastcopy          = table.fastcopy
18local byte              = string.byte
19local sortedhash        = table.sortedhash
20
21local fallbacks         = { }
22mathematics.fallbacks   = fallbacks
23
24local helpers           = fonts.helpers
25local prependcommands   = helpers.prependcommands
26local charcommand       = helpers.commands.char
27local leftcommand       = helpers.commands.left
28local rightcommand      = helpers.commands.right
29local upcommand         = helpers.commands.up
30local downcommand       = helpers.commands.down
31local dummycommand      = helpers.commands.dummy
32local popcommand        = helpers.commands.pop
33local pushcommand       = helpers.commands.push
34
35local virtualcharacters = { }
36local virtualforced     = { }
37
38local hashes            = fonts.hashes
39local identifiers       = hashes.identifiers
40local lastmathids       = hashes.lastmathids
41
42-- we need a trick (todo): if we define scriptscript, script and text in
43-- that order we could use their id's .. i.e. we could always add a font
44-- table with those id's .. in fact, we could also add a whole lot more
45-- as it doesn't hurt
46
47local scripscriptdelayed = { } -- 1.005 : add characters later
48local scriptdelayed      = { } -- 1.005 : add characters later
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,force)
316    local characters = target.characters
317    local olddata = characters[oldchr]
318    -- brrr ... pagella has only next
319    if force or (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,true)
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,true)
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 = 1.25 * data.target.parameters.xheight
526        local height = 0.85 * data.target.mathparameters.AccentBaseHeight
527        local shift  = oldchar.height - height
528        local newchar = {
529            commands = {
530                downcommand[shift],
531                charcommand[unicode],
532            },
533            height = height,
534            width  = oldchar.width,
535        }
536        return newchar
537    elseif not optional then
538        report_fallbacks("missing %U prime in font %a",unicode,data.target.properties.fullname)
539    end
540end
541
542addextra(0xFE932) -- SMASHED PRIME 0x02032
543addextra(0xFE933) -- SMASHED PRIME 0x02033
544addextra(0xFE934) -- SMASHED PRIME 0x02034
545addextra(0xFE957) -- SMASHED PRIME 0x02057
546
547addextra(0xFE935) -- SMASHED BACKWARD PRIME 0x02035
548addextra(0xFE936) -- SMASHED BACKWARD PRIME 0x02036
549addextra(0xFE937) -- SMASHED BACKWARD PRIME 0x02037
550
551virtualcharacters[0xFE932] = function(data) return smashed(data,0x02032) end
552virtualcharacters[0xFE933] = function(data) return smashed(data,0x02033) end
553virtualcharacters[0xFE934] = function(data) return smashed(data,0x02034) end
554virtualcharacters[0xFE957] = function(data) return smashed(data,0x02057) end
555
556virtualcharacters[0xFE935] = function(data) return smashed(data,0x02035,true) end
557virtualcharacters[0xFE936] = function(data) return smashed(data,0x02036,true) end
558virtualcharacters[0xFE937] = function(data) return smashed(data,0x02037,true) end
559
560local hack = nil
561
562function mathematics.getridofprime(target,original)
563--     local mathsize = specification.mathsize
564--     if mathsize == 1 or mathsize == 2 or mathsize == 3) then
565    local mathparameters = original.mathparameters
566    if mathparameters and next(mathparameters) then
567        local changed = original.changed
568        if changed then
569            hack = changed[0x02032]
570            changed[0x02032] = nil
571            changed[0x02033] = nil
572            changed[0x02034] = nil
573            changed[0x02057] = nil
574            changed[0x02035] = nil
575            changed[0x02036] = nil
576            changed[0x02037] = nil
577        end
578    end
579end
580
581function mathematics.setridofprime(target,original)
582    local mathparameters = original.mathparameters
583    if mathparameters and next(mathparameters) and original.changed then
584        target.characters[0xFE931] = target.characters[hack or 0x2032]
585        hack = nil
586    end
587end
588
589utilities.sequencers.appendaction("beforecopyingcharacters","system","mathematics.getridofprime")
590utilities.sequencers.appendaction("aftercopyingcharacters", "system","mathematics.setridofprime")
591
592-- actuarian (beware: xits has an ugly one)
593
594addextra(0xFE940) -- SMALL ANNUITY SYMBOL
595
596local function actuarian(data)
597    local characters = data.target.characters
598    local parameters = data.target.parameters
599    local basechar   = characters[0x0078] -- x (0x0058 X) or 0x1D431
600    local linewidth  = parameters.xheight / 10
601    local basewidth  = basechar.width
602    local baseheight = basechar.height
603    return {
604        -- todo: add alttext
605        -- compromise: lm has large hooks e.g. \actuarial{a}
606        width    = basewidth + 4 * linewidth,
607        height   = basechar.height,
608        depth    = basechar.depth,
609        unicode  = 0x20E7,
610        commands = {
611            rightcommand[2 * linewidth],
612            downcommand[- baseheight - 3 * linewidth],
613            { "rule", linewidth, basewidth + 4 * linewidth },
614            leftcommand[linewidth],
615            downcommand[baseheight + 4 * linewidth],
616            { "rule", baseheight + 5 * linewidth, linewidth },
617        },
618    }
619end
620
621virtualcharacters[0x020E7] = actuarian -- checked
622virtualcharacters[0xFE940] = actuarian -- unchecked
623
624local function equals(data,unicode,snippet,advance,n) -- mathpair needs them
625    local characters = data.target.characters
626    local parameters = data.target.parameters
627    local basechar   = characters[snippet]
628    local advance    = advance * parameters.quad
629    return {
630        unicode  = unicode,
631        width    = n*basechar.width - (n-1)*advance,
632        height   = basechar.height,
633        depth    = basechar.depth,
634        commands = {
635            charcommand[snippet],
636            leftcommand[advance],
637            charcommand[snippet],
638            n > 2 and leftcommand[advance] or nil,
639            n > 2 and charcommand[snippet] or nil,
640        },
641    }
642end
643
644virtualcharacters[0x2A75] = function(data) return equals(data,0x2A75,0x003D, 1/5,2) end -- ==
645virtualcharacters[0x2A76] = function(data) return equals(data,0x2A76,0x003D, 1/5,3) end -- ===
646virtualcharacters[0x2980] = function(data) return equals(data,0x2980,0x007C,-1/8,3) end -- |||
647
648-- addextra(0xFE941) -- EXTREMELY IDENTICAL TO
649--
650-- virtualcharacters[0xFE941] = function(data) -- this character is only needed for mathpairs
651--     local characters = data.target.characters
652--     local parameters = data.target.parameters
653--     local basechar   = characters[0x003D]
654--     local width      = basechar.width or 0
655--     local height     = basechar.height or 0
656--     local depth      = basechar.depth or 0
657--     return {
658--         unicode   = 0xFE941,
659--         width     = width,
660--         height    = height,         -- we cheat (no time now)
661--         depth     = depth,          -- we cheat (no time now)
662--         commands  = {
663--             upcommand[height/2], -- sort of works
664--             charcommand[0x003D],
665--             leftcommand[width],
666--             downcommand[height],     -- sort of works
667--             charcommand[0x003D],
668--         },
669--     }
670-- end
671
672-- lucida needs this
673
674virtualcharacters[0x305] = function(data)
675    local target = data.target
676    local height = target.parameters.xheight/8
677    local width  = target.parameters.emwidth/2
678    local depth  = height
679    local used   = 0.8 * width
680    return {
681        width    = width,
682        height   = height,
683        depth    = depth,
684        commands = { { "rule", height, width } },
685        horiz_variants = {
686            {
687              advance   = width,
688              ["end"]   = used,
689              glyph     = 0x305,
690              start     = 0,
691            },
692            {
693              advance   = width,
694              ["end"]   = 0,
695              extender  = 1,
696              glyph     = 0x305,
697              start     = used,
698            },
699        }
700    }
701end
702
703local function threedots(data,shift)
704    local characters = data.target.characters
705    local parameters = data.target.parameters
706    local periodchar = characters[0x002E]
707    local pluschar   = characters[0x002B]
708    local period     = charcommand[0x002E]
709    local periodwd   = periodchar.width  or 0
710    local periodht   = periodchar.height or 0
711    local perioddp   = periodchar.depth or 0
712    local offset     = 0
713    if shift then
714        local plusht = pluschar.height or 0
715        local plusdp = pluschar.depth or 0
716        local axis   = floor((plusdp + plusht)/2) - plusdp
717        offset   = axis - floor(periodht/2)
718        periodht = axis + floor(periodht/2)
719    end
720    return {
721        width    = 3*periodwd,
722        height   = periodht,
723        depth    = 0,
724        commands = { upcommand[offset], period, period, period }
725    }
726end
727
728virtualcharacters[0x2026] = function(data) return threedots(data,false) end virtualforced[0x2026] = true
729virtualcharacters[0x22EF] = function(data) return threedots(data, true) end virtualforced[0x22EF] = true
730