• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sile-typesetter / sile / 9409557472

07 Jun 2024 12:09AM UTC coverage: 69.448% (-4.5%) from 73.988%
9409557472

push

github

alerque
fix(build): Distribute vendored compat-5.3.c source file

12025 of 17315 relevant lines covered (69.45%)

6023.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.23
/packages/math/base-elements.lua
1
local nodefactory = require("types.node")
13✔
2
local hb = require("justenoughharfbuzz")
13✔
3
local ot = require("core.opentype-parser")
13✔
4
local syms = require("packages.math.unicode-symbols")
13✔
5

6
local atomType = syms.atomType
13✔
7
local symbolDefaults = syms.symbolDefaults
13✔
8

9
local elements = {}
13✔
10

11
local mathMode = {
13✔
12
   display = 0,
13
   displayCramped = 1,
14
   text = 2,
15
   textCramped = 3,
16
   script = 4,
17
   scriptCramped = 5,
18
   scriptScript = 6,
19
   scriptScriptCramped = 7,
20
}
21

22
local scriptType = {
13✔
23
   upright = 1,
24
   bold = 2, -- also have Greek and digits
25
   italic = 3, -- also have Greek
26
   boldItalic = 4, -- also have Greek
27
   script = 5,
28
   boldScript = 6,
29
   fraktur = 7,
30
   boldFraktur = 8,
31
   doubleStruck = 9, -- also have digits
32
   sansSerif = 10, -- also have digits
33
   sansSerifBold = 11, -- also have Greek and digits
34
   sansSerifItalic = 12,
35
   sansSerifBoldItalic = 13, -- also have Greek
36
   monospace = 14, -- also have digits
37
}
38

39
local mathVariantToScriptType = function (attr)
40
   return attr == "normal" and scriptType.upright
59✔
41
      or attr == "bold" and scriptType.bold
59✔
42
      or attr == "italic" and scriptType.italic
37✔
43
      or attr == "bold-italic" and scriptType.boldItalic
25✔
44
      or attr == "double-struck" and scriptType.doubleStruck
25✔
45
      or SU.error('Invalid value "' .. attr .. '" for option mathvariant')
59✔
46
end
47

48
local function isDisplayMode (mode)
49
   return mode <= 1
2,060✔
50
end
51

52
local function isCrampedMode (mode)
53
   return mode % 2 == 1
66✔
54
end
55

56
local function isScriptMode (mode)
57
   return mode == mathMode.script or mode == mathMode.scriptCramped
2,701✔
58
end
59

60
local function isScriptScriptMode (mode)
61
   return mode == mathMode.scriptScript or mode == mathMode.scriptScriptCramped
2,211✔
62
end
63

64
local mathScriptConversionTable = {
13✔
65
   capital = {
13✔
66
      [scriptType.upright] = function (codepoint)
13✔
67
         return codepoint
8✔
68
      end,
69
      [scriptType.bold] = function (codepoint)
13✔
70
         return codepoint + 0x1D400 - 0x41
12✔
71
      end,
72
      [scriptType.italic] = function (codepoint)
13✔
73
         return codepoint + 0x1D434 - 0x41
15✔
74
      end,
75
      [scriptType.boldItalic] = function (codepoint)
13✔
76
         return codepoint + 0x1D468 - 0x41
1✔
77
      end,
78
      [scriptType.doubleStruck] = function (codepoint)
13✔
79
         return codepoint == 0x43 and 0x2102
19✔
80
            or codepoint == 0x48 and 0x210D
19✔
81
            or codepoint == 0x4E and 0x2115
19✔
82
            or codepoint == 0x50 and 0x2119
19✔
83
            or codepoint == 0x51 and 0x211A
13✔
84
            or codepoint == 0x52 and 0x211D
13✔
85
            or codepoint == 0x5A and 0x2124
7✔
86
            or codepoint + 0x1D538 - 0x41
19✔
87
      end,
88
   },
13✔
89
   small = {
13✔
90
      [scriptType.upright] = function (codepoint)
13✔
91
         return codepoint
322✔
92
      end,
93
      [scriptType.bold] = function (codepoint)
13✔
94
         return codepoint + 0x1D41A - 0x61
6✔
95
      end,
96
      [scriptType.italic] = function (codepoint)
13✔
97
         return codepoint == 0x68 and 0x210E or codepoint + 0x1D44E - 0x61
203✔
98
      end,
99
      [scriptType.boldItalic] = function (codepoint)
13✔
100
         return codepoint + 0x1D482 - 0x61
1✔
101
      end,
102
      [scriptType.doubleStruck] = function (codepoint)
13✔
103
         return codepoint + 0x1D552 - 0x61
12✔
104
      end,
105
   },
13✔
106
}
107

108
local mathCache = {}
13✔
109

110
local function retrieveMathTable (font)
111
   local key = SILE.font._key(font)
3,838✔
112
   if not mathCache[key] then
3,838✔
113
      SU.debug("math", "Loading math font", key)
36✔
114
      local face = SILE.font.cache(font, SILE.shaper.getFace)
36✔
115
      if not face then
36✔
116
         SU.error("Could not find requested font " .. font .. " or any suitable substitutes")
×
117
      end
118
      local mathTable = ot.parseMath(hb.get_table(face, "MATH"))
36✔
119
      local upem = ot.parseHead(hb.get_table(face, "head")).unitsPerEm
72✔
120
      if mathTable == nil then
36✔
121
         SU.error("You must use a math font for math rendering.")
×
122
      end
123
      local constants = {}
36✔
124
      for k, v in pairs(mathTable.mathConstants) do
2,052✔
125
         if type(v) == "table" then
2,016✔
126
            v = v.value
1,836✔
127
         end
128
         if k:sub(-9) == "ScaleDown" then
4,032✔
129
            constants[k] = v / 100
72✔
130
         else
131
            constants[k] = v * font.size / upem
1,944✔
132
         end
133
      end
134
      local italicsCorrection = {}
36✔
135
      for k, v in pairs(mathTable.mathItalicsCorrection) do
14,652✔
136
         italicsCorrection[k] = v.value * font.size / upem
14,616✔
137
      end
138
      mathCache[key] = {
36✔
139
         constants = constants,
36✔
140
         italicsCorrection = italicsCorrection,
36✔
141
         mathVariants = mathTable.mathVariants,
36✔
142
         unitsPerEm = upem,
36✔
143
      }
36✔
144
   end
145
   return mathCache[key]
3,838✔
146
end
147

148
-- Style transition functions for superscript and subscript
149
local function getSuperscriptMode (mode)
150
   -- D, T -> S
151
   if mode == mathMode.display or mode == mathMode.text then
75✔
152
      return mathMode.script
40✔
153
   -- D', T' -> S'
154
   elseif mode == mathMode.displayCramped or mode == mathMode.textCramped then
35✔
155
      return mathMode.scriptCramped
29✔
156
   -- S, SS -> SS
157
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
6✔
158
      return mathMode.scriptScript
3✔
159
   -- S', SS' -> SS'
160
   else
161
      return mathMode.scriptScriptCramped
3✔
162
   end
163
end
164
local function getSubscriptMode (mode)
165
   -- D, T, D', T' -> S'
166
   if
167
      mode == mathMode.display
102✔
168
      or mode == mathMode.text
60✔
169
      or mode == mathMode.displayCramped
50✔
170
      or mode == mathMode.textCramped
50✔
171
   then
172
      return mathMode.scriptCramped
83✔
173
   -- S, SS, S', SS' -> SS'
174
   else
175
      return mathMode.scriptScriptCramped
19✔
176
   end
177
end
178

179
-- Style transition functions for fraction (numerator and denominator)
180
local function getNumeratorMode (mode)
181
   -- D -> T
182
   if mode == mathMode.display then
33✔
183
      return mathMode.text
10✔
184
   -- D' -> T'
185
   elseif mode == mathMode.displayCramped then
23✔
186
      return mathMode.textCramped
×
187
   -- T -> S
188
   elseif mode == mathMode.text then
23✔
189
      return mathMode.script
×
190
   -- T' -> S'
191
   elseif mode == mathMode.textCramped then
23✔
192
      return mathMode.scriptCramped
13✔
193
   -- S, SS -> SS
194
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
10✔
195
      return mathMode.scriptScript
×
196
   -- S', SS' -> SS'
197
   else
198
      return mathMode.scriptScriptCramped
10✔
199
   end
200
end
201
local function getDenominatorMode (mode)
202
   -- D, D' -> T'
203
   if mode == mathMode.display or mode == mathMode.displayCramped then
33✔
204
      return mathMode.textCramped
10✔
205
   -- T, T' -> S'
206
   elseif mode == mathMode.text or mode == mathMode.textCramped then
23✔
207
      return mathMode.scriptCramped
13✔
208
   -- S, SS, S', SS' -> SS'
209
   else
210
      return mathMode.scriptScriptCramped
10✔
211
   end
212
end
213

214
local function getRightMostGlyphId (node)
215
   while node and node:is_a(elements.stackbox) and node.direction == "H" do
298✔
216
      node = node.children[#node.children]
12✔
217
   end
218
   if node and node:is_a(elements.text) then
274✔
219
      return node.value.glyphString[#node.value.glyphString]
135✔
220
   else
221
      return 0
4✔
222
   end
223
end
224

225
-- Compares two SILE.types.length, without considering shrink or stretch values, and
226
-- returns the biggest.
227
local function maxLength (...)
228
   local arg = { ... }
2,605✔
229
   local m
230
   for i, v in ipairs(arg) do
8,083✔
231
      if i == 1 then
5,478✔
232
         m = v
2,605✔
233
      else
234
         if v.length:tonumber() > m.length:tonumber() then
8,619✔
235
            m = v
649✔
236
         end
237
      end
238
   end
239
   return m
2,605✔
240
end
241

242
local function scaleWidth (length, line)
243
   local number = length.length
1,135✔
244
   if line.ratio and line.ratio < 0 and length.shrink:tonumber() > 0 then
1,135✔
245
      number = number + length.shrink * line.ratio
×
246
   elseif line.ratio and line.ratio > 0 and length.stretch:tonumber() > 0 then
2,270✔
247
      number = number + length.stretch * line.ratio
1,272✔
248
   end
249
   return number
1,135✔
250
end
251

252
-- math box, box with a horizontal shift value and could contain zero or more
253
-- mbox'es (or its child classes) the entire math environment itself is
254
-- a top-level mbox.
255
-- Typesetting of mbox evolves four steps:
256
--   1. Determine the mode for each mbox according to their parent.
257
--   2. Shape the mbox hierarchy from leaf to top. Get the shape and relative position.
258
--   3. Convert mbox into _nnode's to put in SILE's typesetting framework
259
elements.mbox = pl.class(nodefactory.hbox)
26✔
260
elements.mbox._type = "Mbox"
13✔
261

262
function elements.mbox:__tostring ()
26✔
263
   return self._type
×
264
end
265

266
function elements.mbox:_init ()
26✔
267
   nodefactory.hbox._init(self)
1,896✔
268
   self.font = {}
1,896✔
269
   self.children = {} -- The child nodes
1,896✔
270
   self.relX = SILE.types.length(0) -- x position relative to its parent box
3,792✔
271
   self.relY = SILE.types.length(0) -- y position relative to its parent box
3,792✔
272
   self.value = {}
1,896✔
273
   self.mode = mathMode.display
1,896✔
274
   self.atom = atomType.ordinary
1,896✔
275
   local font = {
1,896✔
276
      family = SILE.settings:get("math.font.family"),
3,792✔
277
      size = SILE.settings:get("math.font.size"),
3,792✔
278
      style = SILE.settings:get("math.font.style"),
3,792✔
279
      weight = SILE.settings:get("math.font.weight"),
3,792✔
280
   }
281
   local filename = SILE.settings:get("math.font.filename")
1,896✔
282
   if filename and filename ~= "" then
1,896✔
283
      font.filename = filename
×
284
   end
285
   self.font = SILE.font.loadDefaults(font)
3,792✔
286
end
287

288
function elements.mbox.styleChildren (_)
26✔
289
   SU.error("styleChildren is a virtual function that need to be overridden by its child classes")
×
290
end
291

292
function elements.mbox.shape (_, _, _)
26✔
293
   SU.error("shape is a virtual function that need to be overridden by its child classes")
×
294
end
295

296
function elements.mbox.output (_, _, _, _)
26✔
297
   SU.error("output is a virtual function that need to be overridden by its child classes")
×
298
end
299

300
function elements.mbox:getMathMetrics ()
26✔
301
   return retrieveMathTable(self.font)
3,838✔
302
end
303

304
function elements.mbox:getScaleDown ()
26✔
305
   local constants = self:getMathMetrics().constants
4,734✔
306
   local scaleDown
307
   if isScriptMode(self.mode) then
4,734✔
308
      scaleDown = constants.scriptPercentScaleDown
430✔
309
   elseif isScriptScriptMode(self.mode) then
3,874✔
310
      scaleDown = constants.scriptScriptPercentScaleDown
181✔
311
   else
312
      scaleDown = 1
1,756✔
313
   end
314
   return scaleDown
2,367✔
315
end
316

317
-- Determine the mode of its descendants
318
function elements.mbox:styleDescendants ()
26✔
319
   self:styleChildren()
1,908✔
320
   for _, n in ipairs(self.children) do
3,744✔
321
      if n then
1,836✔
322
         n:styleDescendants()
1,836✔
323
      end
324
   end
325
end
326

327
-- shapeTree shapes the mbox and all its descendants in a recursive fashion
328
-- The inner-most leaf nodes determine their shape first, and then propagate to their parents
329
-- During the process, each node will determine its size by (width, height, depth)
330
-- and (relX, relY) which the relative position to its parent
331
function elements.mbox:shapeTree ()
26✔
332
   for _, n in ipairs(self.children) do
3,744✔
333
      if n then
1,836✔
334
         n:shapeTree()
1,836✔
335
      end
336
   end
337
   self:shape()
1,908✔
338
end
339

340
-- Output the node and all its descendants
341
function elements.mbox:outputTree (x, y, line)
26✔
342
   self:output(x, y, line)
1,908✔
343
   local debug = SILE.settings:get("math.debug.boxes")
1,908✔
344
   if debug and not (self:is_a(elements.space)) then
1,908✔
345
      SILE.outputter:setCursor(scaleWidth(x, line), y.length)
×
346
      SILE.outputter:debugHbox({ height = self.height.length, depth = self.depth.length }, scaleWidth(self.width, line))
×
347
   end
348
   for _, n in ipairs(self.children) do
3,744✔
349
      if n then
1,836✔
350
         n:outputTree(x + n.relX, y + n.relY, line)
5,508✔
351
      end
352
   end
353
end
354

355
local spaceKind = {
13✔
356
   thin = "thin",
357
   med = "med",
358
   thick = "thick",
359
}
360

361
-- Indexed by left atom
362
local spacingRules = {
13✔
363
   [atomType.ordinary] = {
13✔
364
      [atomType.bigOperator] = { spaceKind.thin },
13✔
365
      [atomType.binaryOperator] = { spaceKind.med, notScript = true },
13✔
366
      [atomType.relationalOperator] = { spaceKind.thick, notScript = true },
13✔
367
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
368
   },
13✔
369
   [atomType.bigOperator] = {
13✔
370
      [atomType.ordinary] = { spaceKind.thin },
13✔
371
      [atomType.bigOperator] = { spaceKind.thin },
13✔
372
      [atomType.relationalOperator] = { spaceKind.thick, notScript = true },
13✔
373
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
374
   },
13✔
375
   [atomType.binaryOperator] = {
13✔
376
      [atomType.ordinary] = { spaceKind.med, notScript = true },
13✔
377
      [atomType.bigOperator] = { spaceKind.med, notScript = true },
13✔
378
      [atomType.openingSymbol] = { spaceKind.med, notScript = true },
13✔
379
      [atomType.inner] = { spaceKind.med, notScript = true },
13✔
380
   },
13✔
381
   [atomType.relationalOperator] = {
13✔
382
      [atomType.ordinary] = { spaceKind.thick, notScript = true },
13✔
383
      [atomType.bigOperator] = { spaceKind.thick, notScript = true },
13✔
384
      [atomType.openingSymbol] = { spaceKind.thick, notScript = true },
13✔
385
      [atomType.inner] = { spaceKind.thick, notScript = true },
13✔
386
   },
13✔
387
   [atomType.closeSymbol] = {
13✔
388
      [atomType.bigOperator] = { spaceKind.thin },
13✔
389
      [atomType.binaryOperator] = { spaceKind.med, notScript = true },
13✔
390
      [atomType.relationalOperator] = { spaceKind.thick, notScript = true },
13✔
391
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
392
   },
13✔
393
   [atomType.punctuationSymbol] = {
13✔
394
      [atomType.ordinary] = { spaceKind.thin, notScript = true },
13✔
395
      [atomType.bigOperator] = { spaceKind.thin, notScript = true },
13✔
396
      [atomType.relationalOperator] = { spaceKind.thin, notScript = true },
13✔
397
      [atomType.openingSymbol] = { spaceKind.thin, notScript = true },
13✔
398
      [atomType.closeSymbol] = { spaceKind.thin, notScript = true },
13✔
399
      [atomType.punctuationSymbol] = { spaceKind.thin, notScript = true },
13✔
400
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
401
   },
13✔
402
   [atomType.inner] = {
13✔
403
      [atomType.ordinary] = { spaceKind.thin, notScript = true },
13✔
404
      [atomType.bigOperator] = { spaceKind.thin },
13✔
405
      [atomType.binaryOperator] = { spaceKind.med, notScript = true },
13✔
406
      [atomType.relationalOperator] = { spaceKind.thick, notScript = true },
13✔
407
      [atomType.openingSymbol] = { spaceKind.thin, notScript = true },
13✔
408
      [atomType.punctuationSymbol] = { spaceKind.thin, notScript = true },
13✔
409
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
410
   },
13✔
411
}
412

413
-- _stackbox stacks its content one, either horizontally or vertically
414
elements.stackbox = pl.class(elements.mbox)
26✔
415
elements.stackbox._type = "Stackbox"
13✔
416

417
function elements.stackbox:__tostring ()
26✔
418
   local result = self.direction .. "Box("
×
419
   for i, n in ipairs(self.children) do
×
420
      result = result .. (i == 1 and "" or ", ") .. tostring(n)
×
421
   end
422
   result = result .. ")"
×
423
   return result
×
424
end
425

426
function elements.stackbox:_init (direction, children)
26✔
427
   elements.mbox._init(self)
399✔
428
   if not (direction == "H" or direction == "V") then
399✔
429
      SU.error("Wrong direction '" .. direction .. "'; should be H or V")
×
430
   end
431
   self.direction = direction
399✔
432
   self.children = children
399✔
433
end
434

435
function elements.stackbox:styleChildren ()
26✔
436
   for _, n in ipairs(self.children) do
1,530✔
437
      n.mode = self.mode
1,131✔
438
   end
439
   if self.direction == "H" then
399✔
440
      -- Insert spaces according to the atom type, following Knuth's guidelines
441
      -- in the TeXbook
442
      local spaces = {}
327✔
443
      for i = 1, #self.children - 1 do
1,065✔
444
         local v = self.children[i]
738✔
445
         local v2 = self.children[i + 1]
738✔
446
         if spacingRules[v.atom] and spacingRules[v.atom][v2.atom] then
738✔
447
            local rule = spacingRules[v.atom][v2.atom]
359✔
448
            if not (rule.notScript and (isScriptMode(self.mode) or isScriptScriptMode(self.mode))) then
967✔
449
               spaces[i + 1] = rule[1]
261✔
450
            end
451
         end
452
      end
453
      local spaceIdx = {}
327✔
454
      for i, _ in pairs(spaces) do
588✔
455
         table.insert(spaceIdx, i)
261✔
456
      end
457
      table.sort(spaceIdx, function (a, b)
654✔
458
         return a > b
377✔
459
      end)
460
      for _, idx in ipairs(spaceIdx) do
588✔
461
         local hsp = elements.space(spaces[idx], 0, 0)
261✔
462
         table.insert(self.children, idx, hsp)
261✔
463
      end
464
   end
465
end
466

467
function elements.stackbox:shape ()
26✔
468
   -- For a horizontal stackbox (i.e. mrow):
469
   -- 1. set self.height and self.depth to max element height & depth
470
   -- 2. handle stretchy operators
471
   -- 3. set self.width
472
   -- For a vertical stackbox:
473
   -- 1. set self.width to max element width
474
   -- 2. set self.height
475
   -- And finally set children's relative coordinates
476
   self.height = SILE.types.length(0)
798✔
477
   self.depth = SILE.types.length(0)
798✔
478
   if self.direction == "H" then
399✔
479
      for i, n in ipairs(self.children) do
1,647✔
480
         n.relY = SILE.types.length(0)
2,640✔
481
         self.height = i == 1 and n.height or maxLength(self.height, n.height)
2,319✔
482
         self.depth = i == 1 and n.depth or maxLength(self.depth, n.depth)
2,319✔
483
      end
484
      -- Handle stretchy operators
485
      for _, elt in ipairs(self.children) do
1,647✔
486
         if elt.is_a(elements.text) and elt.kind == "operator" and elt.stretchy then
2,640✔
487
            elt:stretchyReshape(self.depth, self.height)
79✔
488
         end
489
      end
490
      -- Set self.width
491
      self.width = SILE.types.length(0)
654✔
492
      for i, n in ipairs(self.children) do
1,647✔
493
         n.relX = self.width
1,320✔
494
         self.width = i == 1 and n.width or self.width + n.width
2,319✔
495
      end
496
   else -- self.direction == "V"
497
      for i, n in ipairs(self.children) do
144✔
498
         n.relX = SILE.types.length(0)
144✔
499
         self.width = i == 1 and n.width or maxLength(self.width, n.width)
72✔
500
      end
501
      -- Set self.height and self.depth
502
      for i, n in ipairs(self.children) do
144✔
503
         self.depth = i == 1 and n.depth or self.depth + n.depth
72✔
504
      end
505
      for i = 1, #self.children do
144✔
506
         local n = self.children[i]
72✔
507
         if i == 1 then
72✔
508
            self.height = n.height
72✔
509
            self.depth = n.depth
72✔
510
         elseif i > 1 then
×
511
            n.relY = self.children[i - 1].relY + self.children[i - 1].depth + n.height
×
512
            self.depth = self.depth + n.height + n.depth
×
513
         end
514
      end
515
   end
516
end
517

518
-- Despite of its name, this function actually output the whole tree of nodes recursively.
519
function elements.stackbox:outputYourself (typesetter, line)
26✔
520
   local mathX = typesetter.frame.state.cursorX
72✔
521
   local mathY = typesetter.frame.state.cursorY
72✔
522
   self:outputTree(self.relX + mathX, self.relY + mathY, line)
216✔
523
   typesetter.frame:advanceWritingDirection(scaleWidth(self.width, line))
144✔
524
end
525

526
function elements.stackbox.output (_, _, _, _) end
412✔
527

528
elements.subscript = pl.class(elements.mbox)
26✔
529
elements.subscript._type = "Subscript"
13✔
530

531
function elements.subscript:__tostring ()
26✔
532
   return (self.sub and "Subscript" or "Superscript")
×
533
      .. "("
×
534
      .. tostring(self.base)
×
535
      .. ", "
×
536
      .. tostring(self.sub or self.super)
×
537
      .. ")"
×
538
end
539

540
function elements.subscript:_init (base, sub, sup)
26✔
541
   elements.mbox._init(self)
114✔
542
   self.base = base
114✔
543
   self.sub = sub
114✔
544
   self.sup = sup
114✔
545
   if self.base then
114✔
546
      table.insert(self.children, self.base)
114✔
547
   end
548
   if self.sub then
114✔
549
      table.insert(self.children, self.sub)
77✔
550
   end
551
   if self.sup then
114✔
552
      table.insert(self.children, self.sup)
51✔
553
   end
554
   self.atom = self.base.atom
114✔
555
end
556

557
function elements.subscript:styleChildren ()
26✔
558
   if self.base then
114✔
559
      self.base.mode = self.mode
114✔
560
   end
561
   if self.sub then
114✔
562
      self.sub.mode = getSubscriptMode(self.mode)
154✔
563
   end
564
   if self.sup then
114✔
565
      self.sup.mode = getSuperscriptMode(self.mode)
102✔
566
   end
567
end
568

569
function elements.subscript:calculateItalicsCorrection ()
26✔
570
   local lastGid = getRightMostGlyphId(self.base)
114✔
571
   if lastGid > 0 then
114✔
572
      local mathMetrics = self:getMathMetrics()
110✔
573
      if mathMetrics.italicsCorrection[lastGid] then
110✔
574
         return mathMetrics.italicsCorrection[lastGid]
67✔
575
      end
576
   end
577
   return 0
47✔
578
end
579

580
function elements.subscript:shape ()
26✔
581
   local mathMetrics = self:getMathMetrics()
129✔
582
   local constants = mathMetrics.constants
129✔
583
   local scaleDown = self:getScaleDown()
129✔
584
   if self.base then
129✔
585
      self.base.relX = SILE.types.length(0)
258✔
586
      self.base.relY = SILE.types.length(0)
258✔
587
      -- Use widthForSubscript of base, if available
588
      self.width = self.base.widthForSubscript or self.base.width
129✔
589
   else
590
      self.width = SILE.types.length(0)
×
591
   end
592
   local itCorr = self:calculateItalicsCorrection() * scaleDown
258✔
593
   local subShift
594
   local supShift
595
   if self.sub then
129✔
596
      if self.isUnderOver or self.base.largeop then
92✔
597
         -- Ad hoc correction on integral limits, following LuaTeX's
598
         -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
599
         subShift = -itCorr
33✔
600
      else
601
         subShift = 0
59✔
602
      end
603
      self.sub.relX = self.width + subShift
184✔
604
      self.sub.relY = SILE.types.length(math.max(
184✔
605
         constants.subscriptShiftDown * scaleDown,
92✔
606
         --self.base.depth + constants.subscriptBaselineDropMin * scaleDown,
607
         (self.sub.height - constants.subscriptTopMax * scaleDown):tonumber()
184✔
608
      ))
92✔
609
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or self.base.largeop then
261✔
610
         self.sub.relY = maxLength(self.sub.relY, self.base.depth + constants.subscriptBaselineDropMin * scaleDown)
99✔
611
      end
612
   end
613
   if self.sup then
129✔
614
      if self.isUnderOver or self.base.largeop then
66✔
615
         -- Ad hoc correction on integral limits, following LuaTeX's
616
         -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
617
         supShift = 0
21✔
618
      else
619
         supShift = itCorr
45✔
620
      end
621
      self.sup.relX = self.width + supShift
132✔
622
      self.sup.relY = SILE.types.length(math.max(
132✔
623
         isCrampedMode(self.mode) and constants.superscriptShiftUpCramped * scaleDown
132✔
624
            or constants.superscriptShiftUp * scaleDown, -- or cramped
66✔
625
         --self.base.height - constants.superscriptBaselineDropMax * scaleDown,
626
         (self.sup.depth + constants.superscriptBottomMin * scaleDown):tonumber()
132✔
627
      )) * -1
132✔
628
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or self.base.largeop then
183✔
629
         self.sup.relY = maxLength(
42✔
630
            (0 - self.sup.relY),
21✔
631
            self.base.height - constants.superscriptBaselineDropMax * scaleDown
21✔
632
         ) * -1
42✔
633
      end
634
   end
635
   if self.sub and self.sup then
129✔
636
      local gap = self.sub.relY - self.sub.height - self.sup.relY - self.sup.depth
87✔
637
      if gap.length:tonumber() < constants.subSuperscriptGapMin * scaleDown then
58✔
638
         -- The following adjustment comes directly from Appendix G of he
639
         -- TeXbook (rule 18e).
640
         self.sub.relY = constants.subSuperscriptGapMin * scaleDown + self.sub.height + self.sup.relY + self.sup.depth
32✔
641
         local psi = constants.superscriptBottomMaxWithSubscript * scaleDown + self.sup.relY + self.sup.depth
16✔
642
         if psi:tonumber() > 0 then
16✔
643
            self.sup.relY = self.sup.relY - psi
16✔
644
            self.sub.relY = self.sub.relY - psi
16✔
645
         end
646
      end
647
   end
648
   self.width = self.width
×
649
      + maxLength(
258✔
650
         self.sub and self.sub.width + subShift or SILE.types.length(0),
221✔
651
         self.sup and self.sup.width + supShift or SILE.types.length(0)
195✔
652
      )
129✔
653
      + constants.spaceAfterScript * scaleDown
258✔
654
   self.height = maxLength(
258✔
655
      self.base and self.base.height or SILE.types.length(0),
129✔
656
      self.sub and (self.sub.height - self.sub.relY) or SILE.types.length(0),
221✔
657
      self.sup and (self.sup.height - self.sup.relY) or SILE.types.length(0)
195✔
658
   )
129✔
659
   self.depth = maxLength(
258✔
660
      self.base and self.base.depth or SILE.types.length(0),
129✔
661
      self.sub and (self.sub.depth + self.sub.relY) or SILE.types.length(0),
221✔
662
      self.sup and (self.sup.depth + self.sup.relY) or SILE.types.length(0)
195✔
663
   )
129✔
664
end
665

666
function elements.subscript.output (_, _, _, _) end
127✔
667

668
elements.underOver = pl.class(elements.subscript)
26✔
669
elements.underOver._type = "UnderOver"
13✔
670

671
function elements.underOver:__tostring ()
26✔
672
   return self._type .. "(" .. tostring(self.base) .. ", " .. tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
×
673
end
674

675
function elements.underOver:_init (base, sub, sup)
26✔
676
   elements.mbox._init(self)
25✔
677
   self.atom = base.atom
25✔
678
   self.base = base
25✔
679
   self.sub = sub
25✔
680
   self.sup = sup
25✔
681
   if self.sup then
25✔
682
      table.insert(self.children, self.sup)
24✔
683
   end
684
   if self.base then
25✔
685
      table.insert(self.children, self.base)
25✔
686
   end
687
   if self.sub then
25✔
688
      table.insert(self.children, self.sub)
25✔
689
   end
690
end
691

692
function elements.underOver:styleChildren ()
26✔
693
   if self.base then
25✔
694
      self.base.mode = self.mode
25✔
695
   end
696
   if self.sub then
25✔
697
      self.sub.mode = getSubscriptMode(self.mode)
50✔
698
   end
699
   if self.sup then
25✔
700
      self.sup.mode = getSuperscriptMode(self.mode)
48✔
701
   end
702
end
703

704
function elements.underOver:shape ()
26✔
705
   if not (self.mode == mathMode.display or self.mode == mathMode.displayCramped) then
25✔
706
      self.isUnderOver = true
15✔
707
      elements.subscript.shape(self)
15✔
708
      return
15✔
709
   end
710
   local constants = self:getMathMetrics().constants
20✔
711
   local scaleDown = self:getScaleDown()
10✔
712
   -- Determine relative Ys
713
   if self.base then
10✔
714
      self.base.relY = SILE.types.length(0)
20✔
715
   end
716
   if self.sub then
10✔
717
      self.sub.relY = self.base.depth
10✔
718
         + SILE.types.length(
20✔
719
            math.max(
20✔
720
               (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
20✔
721
               constants.lowerLimitBaselineDropMin * scaleDown
10✔
722
            )
723
         )
20✔
724
   end
725
   if self.sup then
10✔
726
      self.sup.relY = 0
9✔
727
         - self.base.height
9✔
728
         - SILE.types.length(
18✔
729
            math.max(
18✔
730
               (constants.upperLimitGapMin * scaleDown + self.sup.depth):tonumber(),
18✔
731
               constants.upperLimitBaselineRiseMin * scaleDown
9✔
732
            )
733
         )
18✔
734
   end
735
   -- Determine relative Xs based on widest symbol
736
   local widest, a, b
737
   if self.sub and self.sub.width > self.base.width then
10✔
738
      if self.sup and self.sub.width > self.sup.width then
6✔
739
         widest = self.sub
6✔
740
         a = self.base
6✔
741
         b = self.sup
6✔
742
      elseif self.sup then
×
743
         widest = self.sup
×
744
         a = self.base
×
745
         b = self.sub
×
746
      else
747
         widest = self.sub
×
748
         a = self.base
×
749
         b = nil
×
750
      end
751
   else
752
      if self.sup and self.base.width > self.sup.width then
4✔
753
         widest = self.base
3✔
754
         a = self.sub
3✔
755
         b = self.sup
3✔
756
      elseif self.sup then
1✔
757
         widest = self.sup
×
758
         a = self.base
×
759
         b = self.sub
×
760
      else
761
         widest = self.base
1✔
762
         a = self.sub
1✔
763
         b = nil
1✔
764
      end
765
   end
766
   widest.relX = SILE.types.length(0)
20✔
767
   local c = widest.width / 2
10✔
768
   if a then
10✔
769
      a.relX = c - a.width / 2
30✔
770
   end
771
   if b then
10✔
772
      b.relX = c - b.width / 2
27✔
773
   end
774
   local itCorr = self:calculateItalicsCorrection() * scaleDown
20✔
775
   if self.sup then
10✔
776
      self.sup.relX = self.sup.relX + itCorr / 2
18✔
777
   end
778
   if self.sub then
10✔
779
      self.sub.relX = self.sub.relX - itCorr / 2
20✔
780
   end
781
   -- Determine width and height
782
   self.width = maxLength(
20✔
783
      self.base and self.base.width or SILE.types.length(0),
10✔
784
      self.sub and self.sub.width or SILE.types.length(0),
10✔
785
      self.sup and self.sup.width or SILE.types.length(0)
10✔
786
   )
10✔
787
   if self.sup then
10✔
788
      self.height = 0 - self.sup.relY + self.sup.height
27✔
789
   else
790
      self.height = self.base and self.base.height or 0
1✔
791
   end
792
   if self.sub then
10✔
793
      self.depth = self.sub.relY + self.sub.depth
20✔
794
   else
795
      self.depth = self.base and self.base.depth or 0
×
796
   end
797
end
798

799
function elements.underOver:calculateItalicsCorrection ()
26✔
800
   local lastGid = getRightMostGlyphId(self.base)
25✔
801
   if lastGid > 0 then
25✔
802
      local mathMetrics = self:getMathMetrics()
25✔
803
      if mathMetrics.italicsCorrection[lastGid] then
25✔
804
         local c = mathMetrics.italicsCorrection[lastGid]
×
805
         -- If this is a big operator, and we are in display style, then the
806
         -- base glyph may be bigger than the font size. We need to adjust the
807
         -- italic correction accordingly.
808
         if self.base.atom == atomType.bigOperator and isDisplayMode(self.mode) then
×
809
            c = c * (self.base and self.base.font.size / self.font.size or 1.0)
×
810
         end
811
         return c
×
812
      end
813
   end
814
   return 0
25✔
815
end
816

817
function elements.underOver.output (_, _, _, _) end
38✔
818

819
-- terminal is the base class for leaf node
820
elements.terminal = pl.class(elements.mbox)
26✔
821
elements.terminal._type = "Terminal"
13✔
822

823
function elements.terminal:_init ()
26✔
824
   elements.mbox._init(self)
1,316✔
825
end
826

827
function elements.terminal.styleChildren (_) end
1,329✔
828

829
function elements.terminal.shape (_) end
13✔
830

831
elements.space = pl.class(elements.terminal)
26✔
832
elements.space._type = "Space"
13✔
833

834
function elements.space:_init ()
26✔
835
   elements.terminal._init(self)
×
836
end
837

838
function elements.space:__tostring ()
26✔
839
   return self._type
×
840
      .. "(width="
×
841
      .. tostring(self.width)
×
842
      .. ", height="
×
843
      .. tostring(self.height)
×
844
      .. ", depth="
×
845
      .. tostring(self.depth)
×
846
      .. ")"
×
847
end
848

849
local function getStandardLength (value)
850
   if type(value) == "string" then
957✔
851
      local direction = 1
319✔
852
      if value:sub(1, 1) == "-" then
638✔
853
         value = value:sub(2, -1)
20✔
854
         direction = -1
10✔
855
      end
856
      if value == "thin" then
319✔
857
         return SILE.types.length("3mu") * direction
198✔
858
      elseif value == "med" then
253✔
859
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
294✔
860
      elseif value == "thick" then
155✔
861
         return SILE.types.length("5mu plus 5mu") * direction
393✔
862
      end
863
   end
864
   return SILE.types.length(value)
662✔
865
end
866

867
function elements.space:_init (width, height, depth)
26✔
868
   elements.terminal._init(self)
319✔
869
   self.width = getStandardLength(width)
638✔
870
   self.height = getStandardLength(height)
638✔
871
   self.depth = getStandardLength(depth)
638✔
872
end
873

874
function elements.space:shape ()
26✔
875
   self.width = self.width:absolute() * self:getScaleDown()
1,276✔
876
   self.height = self.height:absolute() * self:getScaleDown()
1,276✔
877
   self.depth = self.depth:absolute() * self:getScaleDown()
1,276✔
878
end
879

880
function elements.space.output (_) end
332✔
881

882
-- text node. For any actual text output
883
elements.text = pl.class(elements.terminal)
26✔
884
elements.text._type = "Text"
13✔
885

886
function elements.text:__tostring ()
26✔
887
   return self._type
×
888
      .. "(atom="
×
889
      .. tostring(self.atom)
×
890
      .. ", kind="
×
891
      .. tostring(self.kind)
×
892
      .. ", script="
×
893
      .. tostring(self.script)
×
894
      .. (self.stretchy and ", stretchy" or "")
×
895
      .. (self.largeop and ", largeop" or "")
×
896
      .. ', text="'
×
897
      .. (self.originalText or self.text)
×
898
      .. '")'
×
899
end
900

901
function elements.text:_init (kind, attributes, script, text)
26✔
902
   elements.terminal._init(self)
997✔
903
   if not (kind == "number" or kind == "identifier" or kind == "operator") then
997✔
904
      SU.error("Unknown text node kind '" .. kind .. "'; should be one of: number, identifier, operator.")
×
905
   end
906
   self.kind = kind
997✔
907
   self.script = script
997✔
908
   self.text = text
997✔
909
   if self.script ~= "upright" then
997✔
910
      local converted = ""
997✔
911
      for _, uchr in luautf8.codes(self.text) do
2,241✔
912
         local dst_char = luautf8.char(uchr)
1,244✔
913
         if uchr >= 0x41 and uchr <= 0x5A then -- Latin capital letter
1,244✔
914
            dst_char = luautf8.char(mathScriptConversionTable.capital[self.script](uchr))
110✔
915
         elseif uchr >= 0x61 and uchr <= 0x7A then -- Latin non-capital letter
1,189✔
916
            dst_char = luautf8.char(mathScriptConversionTable.small[self.script](uchr))
1,088✔
917
         end
918
         converted = converted .. dst_char
1,244✔
919
      end
920
      self.originalText = self.text
997✔
921
      self.text = converted
997✔
922
   end
923
   if self.kind == "operator" then
997✔
924
      if self.text == "-" then
394✔
925
         self.text = "−"
13✔
926
      end
927
   end
928
   for attribute, value in pairs(attributes) do
1,457✔
929
      self[attribute] = value
460✔
930
   end
931
end
932

933
function elements.text:shape ()
26✔
934
   self.font.size = self.font.size * self:getScaleDown()
1,994✔
935
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
997✔
936
   local mathMetrics = self:getMathMetrics()
997✔
937
   local glyphs = SILE.shaper:shapeToken(self.text, self.font)
997✔
938
   -- Use bigger variants for big operators in display style
939
   if isDisplayMode(self.mode) and self.largeop then
1,994✔
940
      -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
941
      glyphs = pl.tablex.deepcopy(glyphs)
38✔
942
      local constructions = mathMetrics.mathVariants.vertGlyphConstructions[glyphs[1].gid]
19✔
943
      if constructions then
19✔
944
         local displayVariants = constructions.mathGlyphVariantRecord
19✔
945
         -- We select the biggest variant. TODO: we should probably select the
946
         -- first variant that is higher than displayOperatorMinHeight.
947
         local biggest
948
         local m = 0
19✔
949
         for _, v in ipairs(displayVariants) do
57✔
950
            if v.advanceMeasurement > m then
38✔
951
               biggest = v
38✔
952
               m = v.advanceMeasurement
38✔
953
            end
954
         end
955
         if biggest then
19✔
956
            glyphs[1].gid = biggest.variantGlyph
19✔
957
            local dimen = hb.get_glyph_dimensions(face, self.font.size, biggest.variantGlyph)
19✔
958
            glyphs[1].width = dimen.width
19✔
959
            glyphs[1].glyphAdvance = dimen.glyphAdvance
19✔
960
            --[[ I am told (https://github.com/alif-type/xits/issues/90) that,
961
        in fact, the relative height and depth of display-style big operators
962
        in the font is not relevant, as these should be centered around the
963
        axis. So the following code does that, while conserving their
964
        vertical size (distance from top to bottom). ]]
965
            local axisHeight = mathMetrics.constants.axisHeight * self:getScaleDown()
38✔
966
            local y_size = dimen.height + dimen.depth
19✔
967
            glyphs[1].height = y_size / 2 + axisHeight
19✔
968
            glyphs[1].depth = y_size / 2 - axisHeight
19✔
969
            -- We still need to store the font's height and depth somewhere,
970
            -- because that's what will be used to draw the glyph, and we will need
971
            -- to artificially compensate for that.
972
            glyphs[1].fontHeight = dimen.height
19✔
973
            glyphs[1].fontDepth = dimen.depth
19✔
974
         end
975
      end
976
   end
977
   SILE.shaper:preAddNodes(glyphs, self.value)
997✔
978
   self.value.items = glyphs
997✔
979
   self.value.glyphString = {}
997✔
980
   if glyphs and #glyphs > 0 then
997✔
981
      for i = 1, #glyphs do
2,241✔
982
         table.insert(self.value.glyphString, glyphs[i].gid)
1,244✔
983
      end
984
      self.width = SILE.types.length(0)
1,994✔
985
      self.widthForSubscript = SILE.types.length(0)
1,994✔
986
      for i = #glyphs, 1, -1 do
2,241✔
987
         self.width = self.width + glyphs[i].glyphAdvance
2,488✔
988
      end
989
      -- Store width without italic correction somewhere
990
      self.widthForSubscript = self.width
997✔
991
      local itCorr = mathMetrics.italicsCorrection[glyphs[#glyphs].gid]
997✔
992
      if itCorr then
997✔
993
         self.width = self.width + itCorr * self:getScaleDown()
639✔
994
      end
995
      for i = 1, #glyphs do
2,241✔
996
         self.height = i == 1 and SILE.types.length(glyphs[i].height)
1,244✔
997
            or SILE.types.length(math.max(self.height:tonumber(), glyphs[i].height))
1,738✔
998
         self.depth = i == 1 and SILE.types.length(glyphs[i].depth)
1,244✔
999
            or SILE.types.length(math.max(self.depth:tonumber(), glyphs[i].depth))
1,738✔
1000
      end
1001
   else
1002
      self.width = SILE.types.length(0)
×
1003
      self.height = SILE.types.length(0)
×
1004
      self.depth = SILE.types.length(0)
×
1005
   end
1006
end
1007

1008
function elements.text:stretchyReshape (depth, height)
26✔
1009
   -- Required depth+height of stretched glyph, in font units
1010
   local mathMetrics = self:getMathMetrics()
79✔
1011
   local upem = mathMetrics.unitsPerEm
79✔
1012
   local sz = self.font.size
79✔
1013
   local requiredAdvance = (depth + height):tonumber() * upem / sz
237✔
1014
   SU.debug("math", "stretch: rA =", requiredAdvance)
79✔
1015
   -- Choose variant of the closest size. The criterion we use is to have
1016
   -- an advance measurement as close as possible as the required one.
1017
   -- The advance measurement is simply the depth+height of the glyph.
1018
   -- Therefore, the selected glyph may be smaller or bigger than
1019
   -- required.  TODO: implement assembly of stretchable glyphs form
1020
   -- their parts for cases when the biggest variant is not big enough.
1021
   -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
1022
   local glyphs = pl.tablex.deepcopy(self.value.items)
79✔
1023
   local constructions = self:getMathMetrics().mathVariants.vertGlyphConstructions[glyphs[1].gid]
158✔
1024
   if constructions then
79✔
1025
      local variants = constructions.mathGlyphVariantRecord
79✔
1026
      SU.debug("math", "stretch: variants =", variants)
79✔
1027
      local closest
1028
      local closestI
1029
      local m = requiredAdvance - (self.depth + self.height):tonumber() * upem / sz
237✔
1030
      SU.debug("math", "stretch: m =", m)
79✔
1031
      for i, v in ipairs(variants) do
1,106✔
1032
         local diff = math.abs(v.advanceMeasurement - requiredAdvance)
1,027✔
1033
         SU.debug("math", "stretch: diff =", diff)
1,027✔
1034
         if diff < m then
1,027✔
1035
            closest = v
117✔
1036
            closestI = i
117✔
1037
            m = diff
117✔
1038
         end
1039
      end
1040
      SU.debug("math", "stretch: closestI =", closestI)
79✔
1041
      if closest then
79✔
1042
         -- Now we have to re-shape the glyph chain. We will assume there
1043
         -- is only one glyph.
1044
         -- TODO: this code is probably wrong when the vertical
1045
         -- variants have a different width than the original, because
1046
         -- the shaping phase is already done. Need to do better.
1047
         glyphs[1].gid = closest.variantGlyph
63✔
1048
         local face = SILE.font.cache(self.font, SILE.shaper.getFace)
63✔
1049
         local dimen = hb.get_glyph_dimensions(face, self.font.size, closest.variantGlyph)
63✔
1050
         glyphs[1].width = dimen.width
63✔
1051
         glyphs[1].height = dimen.height
63✔
1052
         glyphs[1].depth = dimen.depth
63✔
1053
         glyphs[1].glyphAdvance = dimen.glyphAdvance
63✔
1054
         self.width = SILE.types.length(dimen.glyphAdvance)
126✔
1055
         self.depth = SILE.types.length(dimen.depth)
126✔
1056
         self.height = SILE.types.length(dimen.height)
126✔
1057
         SILE.shaper:preAddNodes(glyphs, self.value)
63✔
1058
         self.value.items = glyphs
63✔
1059
         self.value.glyphString = { glyphs[1].gid }
63✔
1060
      end
1061
   end
1062
end
1063

1064
function elements.text:output (x, y, line)
26✔
1065
   if not self.value.glyphString then
997✔
1066
      return
×
1067
   end
1068
   local compensatedY
1069
   if isDisplayMode(self.mode) and self.atom == atomType.bigOperator and self.value.items[1].fontDepth then
1,994✔
1070
      compensatedY = SILE.types.length(y.length + self.value.items[1].depth - self.value.items[1].fontDepth)
40✔
1071
   else
1072
      compensatedY = y
987✔
1073
   end
1074
   SILE.outputter:setCursor(scaleWidth(x, line), compensatedY.length)
1,994✔
1075
   SILE.outputter:setFont(self.font)
997✔
1076
   -- There should be no stretch or shrink on the width of a text
1077
   -- element.
1078
   local width = self.width.length
997✔
1079
   SILE.outputter:drawHbox(self.value, width)
997✔
1080
end
1081

1082
elements.fraction = pl.class(elements.mbox)
26✔
1083
elements.fraction._type = "Fraction"
13✔
1084

1085
function elements.fraction:__tostring ()
26✔
1086
   return self._type .. "(" .. tostring(self.numerator) .. ", " .. tostring(self.denominator) .. ")"
×
1087
end
1088

1089
function elements.fraction:_init (numerator, denominator)
26✔
1090
   elements.mbox._init(self)
33✔
1091
   self.numerator = numerator
33✔
1092
   self.denominator = denominator
33✔
1093
   table.insert(self.children, numerator)
33✔
1094
   table.insert(self.children, denominator)
33✔
1095
end
1096

1097
function elements.fraction:styleChildren ()
26✔
1098
   self.numerator.mode = getNumeratorMode(self.mode)
66✔
1099
   self.denominator.mode = getDenominatorMode(self.mode)
66✔
1100
end
1101

1102
function elements.fraction:shape ()
26✔
1103
   -- Determine relative abscissas and width
1104
   local widest, other
1105
   if self.denominator.width > self.numerator.width then
33✔
1106
      widest, other = self.denominator, self.numerator
27✔
1107
   else
1108
      widest, other = self.numerator, self.denominator
6✔
1109
   end
1110
   widest.relX = SILE.types.length(0)
66✔
1111
   other.relX = (widest.width - other.width) / 2
99✔
1112
   self.width = widest.width
33✔
1113
   -- Determine relative ordinates and height
1114
   local constants = self:getMathMetrics().constants
66✔
1115
   local scaleDown = self:getScaleDown()
33✔
1116
   self.axisHeight = constants.axisHeight * scaleDown
33✔
1117
   self.ruleThickness = constants.fractionRuleThickness * scaleDown
33✔
1118
   if isDisplayMode(self.mode) then
66✔
1119
      self.numerator.relY = -self.axisHeight
10✔
1120
         - self.ruleThickness / 2
10✔
1121
         - SILE.types.length(
20✔
1122
            math.max(
20✔
1123
               (constants.fractionNumDisplayStyleGapMin * scaleDown + self.numerator.depth):tonumber(),
20✔
1124
               constants.fractionNumeratorDisplayStyleShiftUp * scaleDown - self.axisHeight - self.ruleThickness / 2
10✔
1125
            )
1126
         )
20✔
1127
   else
1128
      self.numerator.relY = -self.axisHeight
23✔
1129
         - self.ruleThickness / 2
23✔
1130
         - SILE.types.length(
46✔
1131
            math.max(
46✔
1132
               (constants.fractionNumeratorGapMin * scaleDown + self.numerator.depth):tonumber(),
46✔
1133
               constants.fractionNumeratorShiftUp * scaleDown - self.axisHeight - self.ruleThickness / 2
23✔
1134
            )
1135
         )
46✔
1136
   end
1137
   if isDisplayMode(self.mode) then
66✔
1138
      self.denominator.relY = -self.axisHeight
10✔
1139
         + self.ruleThickness / 2
10✔
1140
         + SILE.types.length(
20✔
1141
            math.max(
20✔
1142
               (constants.fractionDenomDisplayStyleGapMin * scaleDown + self.denominator.height):tonumber(),
20✔
1143
               constants.fractionDenominatorDisplayStyleShiftDown * scaleDown + self.axisHeight - self.ruleThickness / 2
10✔
1144
            )
1145
         )
20✔
1146
   else
1147
      self.denominator.relY = -self.axisHeight
23✔
1148
         + self.ruleThickness / 2
23✔
1149
         + SILE.types.length(
46✔
1150
            math.max(
46✔
1151
               (constants.fractionDenominatorGapMin * scaleDown + self.denominator.height):tonumber(),
46✔
1152
               constants.fractionDenominatorShiftDown * scaleDown + self.axisHeight - self.ruleThickness / 2
23✔
1153
            )
1154
         )
46✔
1155
   end
1156
   self.height = self.numerator.height - self.numerator.relY
66✔
1157
   self.depth = self.denominator.relY + self.denominator.depth
66✔
1158
end
1159

1160
function elements.fraction:output (x, y, line)
26✔
1161
   SILE.outputter:drawRule(
66✔
1162
      scaleWidth(x, line),
33✔
1163
      y.length - self.axisHeight - self.ruleThickness / 2,
66✔
1164
      scaleWidth(self.width, line),
33✔
1165
      self.ruleThickness
1166
   )
33✔
1167
end
1168

1169
local function newSubscript (spec)
1170
   return elements.subscript(spec.base, spec.sub, spec.sup)
114✔
1171
end
1172

1173
local function newUnderOver (spec)
1174
   return elements.underOver(spec.base, spec.sub, spec.sup)
25✔
1175
end
1176

1177
-- TODO replace with penlight equivalent
1178
local function mapList (f, l)
1179
   local ret = {}
9✔
1180
   for i, x in ipairs(l) do
35✔
1181
      ret[i] = f(i, x)
52✔
1182
   end
1183
   return ret
9✔
1184
end
1185

1186
elements.mtr = pl.class(elements.mbox)
26✔
1187
-- elements.mtr._type = "" -- TODO why not set?
1188

1189
function elements.mtr:_init (children)
26✔
1190
   self.children = children
12✔
1191
end
1192

1193
function elements.mtr:styleChildren ()
26✔
1194
   for _, c in ipairs(self.children) do
48✔
1195
      c.mode = self.mode
36✔
1196
   end
1197
end
1198

1199
function elements.mtr.shape (_) end -- done by parent table
25✔
1200

1201
function elements.mtr.output (_) end
25✔
1202

1203
elements.table = pl.class(elements.mbox)
26✔
1204
elements.table._type = "table" -- TODO why case difference?
13✔
1205

1206
function elements.table:_init (children, options)
26✔
1207
   elements.mbox._init(self)
9✔
1208
   self.children = children
9✔
1209
   self.options = options
9✔
1210
   self.nrows = #self.children
9✔
1211
   self.ncols = math.max(pl.utils.unpack(mapList(function (_, row)
36✔
1212
      return #row.children
26✔
1213
   end, self.children)))
18✔
1214
   SU.debug("math", "self.ncols =", self.ncols)
9✔
1215
   self.rowspacing = self.options.rowspacing and SILE.types.length(self.options.rowspacing) or SILE.types.length("7pt")
18✔
1216
   self.columnspacing = self.options.columnspacing and SILE.types.length(self.options.columnspacing)
9✔
1217
      or SILE.types.length("6pt")
18✔
1218
   -- Pad rows that do not have enough cells by adding cells to the
1219
   -- right.
1220
   for i, row in ipairs(self.children) do
35✔
1221
      for j = 1, (self.ncols - #row.children) do
26✔
1222
         SU.debug("math", "padding i =", i, "j =", j)
×
1223
         table.insert(row.children, elements.stackbox("H", {}))
×
1224
         SU.debug("math", "size", #row.children)
×
1225
      end
1226
   end
1227
   if options.columnalign then
9✔
1228
      local l = {}
5✔
1229
      for w in string.gmatch(options.columnalign, "[^%s]+") do
20✔
1230
         if not (w == "left" or w == "center" or w == "right") then
15✔
1231
            SU.error("Invalid specifier in `columnalign` attribute: " .. w)
×
1232
         end
1233
         table.insert(l, w)
15✔
1234
      end
1235
      -- Pad with last value of l if necessary
1236
      for _ = 1, (self.ncols - #l), 1 do
5✔
1237
         table.insert(l, l[#l])
×
1238
      end
1239
      -- On the contrary, remove excess values in l if necessary
1240
      for _ = 1, (#l - self.ncols), 1 do
5✔
1241
         table.remove(l)
×
1242
      end
1243
      self.options.columnalign = l
5✔
1244
   else
1245
      self.options.columnalign = pl.List.range(1, self.ncols):map(function (_)
14✔
1246
         return "center"
12✔
1247
      end)
1248
   end
1249
end
1250

1251
function elements.table:styleChildren ()
26✔
1252
   if self.mode == mathMode.display and self.options.displaystyle ~= "false" then
9✔
1253
      for _, c in ipairs(self.children) do
19✔
1254
         c.mode = mathMode.display
14✔
1255
      end
1256
   else
1257
      for _, c in ipairs(self.children) do
16✔
1258
         c.mode = mathMode.text
12✔
1259
      end
1260
   end
1261
end
1262

1263
function elements.table:shape ()
26✔
1264
   -- Determine the height (resp. depth) of each row, which is the max
1265
   -- height (resp. depth) among its elements. Then we only need to add it to
1266
   -- the table's height and center every cell vertically.
1267
   for _, row in ipairs(self.children) do
35✔
1268
      row.height = SILE.types.length(0)
52✔
1269
      row.depth = SILE.types.length(0)
52✔
1270
      for _, cell in ipairs(row.children) do
104✔
1271
         row.height = maxLength(row.height, cell.height)
156✔
1272
         row.depth = maxLength(row.depth, cell.depth)
156✔
1273
      end
1274
   end
1275
   self.vertSize = SILE.types.length(0)
18✔
1276
   for i, row in ipairs(self.children) do
35✔
1277
      self.vertSize = self.vertSize
×
1278
         + row.height
26✔
1279
         + row.depth
26✔
1280
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
61✔
1281
   end
1282
   local rowHeightSoFar = SILE.types.length(0)
9✔
1283
   for i, row in ipairs(self.children) do
35✔
1284
      row.relY = rowHeightSoFar + row.height - self.vertSize
78✔
1285
      rowHeightSoFar = rowHeightSoFar
×
1286
         + row.height
26✔
1287
         + row.depth
26✔
1288
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
35✔
1289
   end
1290
   self.width = SILE.types.length(0)
18✔
1291
   local thisColRelX = SILE.types.length(0)
9✔
1292
   -- For every column...
1293
   for i = 1, self.ncols do
36✔
1294
      -- Determine its width
1295
      local columnWidth = SILE.types.length(0)
27✔
1296
      for j = 1, self.nrows do
105✔
1297
         if self.children[j].children[i].width > columnWidth then
78✔
1298
            columnWidth = self.children[j].children[i].width
37✔
1299
         end
1300
      end
1301
      -- Use it to align the contents of every cell as required.
1302
      for j = 1, self.nrows do
105✔
1303
         local cell = self.children[j].children[i]
78✔
1304
         if self.options.columnalign[i] == "left" then
78✔
1305
            cell.relX = thisColRelX
14✔
1306
         elseif self.options.columnalign[i] == "center" then
64✔
1307
            cell.relX = thisColRelX + (columnWidth - cell.width) / 2
200✔
1308
         elseif self.options.columnalign[i] == "right" then
14✔
1309
            cell.relX = thisColRelX + (columnWidth - cell.width)
42✔
1310
         else
1311
            SU.error("invalid columnalign parameter")
×
1312
         end
1313
      end
1314
      thisColRelX = thisColRelX + columnWidth + (i == self.ncols and SILE.types.length(0) or self.columnspacing) -- Spacing
63✔
1315
   end
1316
   self.width = thisColRelX
9✔
1317
   -- Center myself vertically around the axis, and update relative Ys of rows accordingly
1318
   local axisHeight = self:getMathMetrics().constants.axisHeight * self:getScaleDown()
27✔
1319
   self.height = self.vertSize / 2 + axisHeight
27✔
1320
   self.depth = self.vertSize / 2 - axisHeight
27✔
1321
   for _, row in ipairs(self.children) do
35✔
1322
      row.relY = row.relY + self.vertSize / 2 - axisHeight
104✔
1323
      -- Also adjust width
1324
      row.width = self.width
26✔
1325
   end
1326
end
1327

1328
function elements.table.output (_) end
22✔
1329

1330
elements.mathMode = mathMode
13✔
1331
elements.atomType = atomType
13✔
1332
elements.scriptType = scriptType
13✔
1333
elements.mathVariantToScriptType = mathVariantToScriptType
13✔
1334
elements.symbolDefaults = symbolDefaults
13✔
1335
elements.newSubscript = newSubscript
13✔
1336
elements.newUnderOver = newUnderOver
13✔
1337

1338
return elements
13✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc