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

sile-typesetter / sile / 10616097075

29 Aug 2024 01:29PM UTC coverage: 59.298% (-5.5%) from 64.818%
10616097075

push

github

alerque
ci(actions): Switch to Lua actions forks that fix current LuaJIT issues

10338 of 17434 relevant lines covered (59.3%)

3374.78 hits per line

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

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

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

9
local elements = {}
1✔
10

11
local mathMode = {
1✔
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 = {
1✔
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
30✔
41
      or attr == "bold" and scriptType.bold
30✔
42
      or attr == "italic" and scriptType.italic
22✔
43
      or attr == "bold-italic" and scriptType.boldItalic
22✔
44
      or attr == "double-struck" and scriptType.doubleStruck
22✔
45
      or SU.error('Invalid value "' .. attr .. '" for option mathvariant')
30✔
46
end
47

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

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

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

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

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

108
local mathCache = {}
1✔
109

110
local function retrieveMathTable (font)
111
   local key = SILE.font._key(font)
522✔
112
   if not mathCache[key] then
522✔
113
      SU.debug("math", "Loading math font", key)
4✔
114
      local face = SILE.font.cache(font, SILE.shaper.getFace)
4✔
115
      if not face then
4✔
116
         SU.error("Could not find requested font " .. font .. " or any suitable substitutes")
×
117
      end
118
      local fontHasMathTable, rawMathTable, mathTableParsable, mathTable
119
      fontHasMathTable, rawMathTable = pcall(hb.get_table, face, "MATH")
4✔
120
      if fontHasMathTable then
4✔
121
         mathTableParsable, mathTable = pcall(ot.parseMath, rawMathTable)
8✔
122
      end
123
      if not fontHasMathTable or not mathTableParsable then
4✔
124
         SU.error(([[You must use a math font for math rendering.
×
125

126
  The math table in '%s' could not be %s.
127
         ]]):format(face.filename, fontHasMathTable and "parsed" or "loaded"))
×
128
      end
129
      local upem = ot.parseHead(hb.get_table(face, "head")).unitsPerEm
8✔
130
      local constants = {}
4✔
131
      for k, v in pairs(mathTable.mathConstants) do
228✔
132
         if type(v) == "table" then
224✔
133
            v = v.value
204✔
134
         end
135
         if k:sub(-9) == "ScaleDown" then
448✔
136
            constants[k] = v / 100
8✔
137
         else
138
            constants[k] = v * font.size / upem
216✔
139
         end
140
      end
141
      local italicsCorrection = {}
4✔
142
      for k, v in pairs(mathTable.mathItalicsCorrection) do
1,628✔
143
         italicsCorrection[k] = v.value * font.size / upem
1,624✔
144
      end
145
      mathCache[key] = {
4✔
146
         constants = constants,
4✔
147
         italicsCorrection = italicsCorrection,
4✔
148
         mathVariants = mathTable.mathVariants,
4✔
149
         unitsPerEm = upem,
4✔
150
      }
4✔
151
   end
152
   return mathCache[key]
522✔
153
end
154

155
-- Style transition functions for superscript and subscript
156
local function getSuperscriptMode (mode)
157
   -- D, T -> S
158
   if mode == mathMode.display or mode == mathMode.text then
12✔
159
      return mathMode.script
6✔
160
   -- D', T' -> S'
161
   elseif mode == mathMode.displayCramped or mode == mathMode.textCramped then
6✔
162
      return mathMode.scriptCramped
6✔
163
   -- S, SS -> SS
164
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
×
165
      return mathMode.scriptScript
×
166
   -- S', SS' -> SS'
167
   else
168
      return mathMode.scriptScriptCramped
×
169
   end
170
end
171
local function getSubscriptMode (mode)
172
   -- D, T, D', T' -> S'
173
   if
174
      mode == mathMode.display
×
175
      or mode == mathMode.text
×
176
      or mode == mathMode.displayCramped
×
177
      or mode == mathMode.textCramped
×
178
   then
179
      return mathMode.scriptCramped
×
180
   -- S, SS, S', SS' -> SS'
181
   else
182
      return mathMode.scriptScriptCramped
×
183
   end
184
end
185

186
-- Style transition functions for fraction (numerator and denominator)
187
local function getNumeratorMode (mode)
188
   -- D -> T
189
   if mode == mathMode.display then
×
190
      return mathMode.text
×
191
   -- D' -> T'
192
   elseif mode == mathMode.displayCramped then
×
193
      return mathMode.textCramped
×
194
   -- T -> S
195
   elseif mode == mathMode.text then
×
196
      return mathMode.script
×
197
   -- T' -> S'
198
   elseif mode == mathMode.textCramped then
×
199
      return mathMode.scriptCramped
×
200
   -- S, SS -> SS
201
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
×
202
      return mathMode.scriptScript
×
203
   -- S', SS' -> SS'
204
   else
205
      return mathMode.scriptScriptCramped
×
206
   end
207
end
208
local function getDenominatorMode (mode)
209
   -- D, D' -> T'
210
   if mode == mathMode.display or mode == mathMode.displayCramped then
×
211
      return mathMode.textCramped
×
212
   -- T, T' -> S'
213
   elseif mode == mathMode.text or mode == mathMode.textCramped then
×
214
      return mathMode.scriptCramped
×
215
   -- S, SS, S', SS' -> SS'
216
   else
217
      return mathMode.scriptScriptCramped
×
218
   end
219
end
220

221
local function getRightMostGlyphId (node)
222
   while node and node:is_a(elements.stackbox) and node.direction == "H" do
36✔
223
      node = node.children[#node.children]
6✔
224
   end
225
   if node and node:is_a(elements.text) then
24✔
226
      return node.value.glyphString[#node.value.glyphString]
12✔
227
   else
228
      return 0
×
229
   end
230
end
231

232
-- Compares two SILE.types.length, without considering shrink or stretch values, and
233
-- returns the biggest.
234
local function maxLength (...)
235
   local arg = { ... }
348✔
236
   local m
237
   for i, v in ipairs(arg) do
1,068✔
238
      if i == 1 then
720✔
239
         m = v
348✔
240
      else
241
         if v.length:tonumber() > m.length:tonumber() then
1,116✔
242
            m = v
72✔
243
         end
244
      end
245
   end
246
   return m
348✔
247
end
248

249
local function scaleWidth (length, line)
250
   local number = length.length
132✔
251
   if line.ratio and line.ratio < 0 and length.shrink:tonumber() > 0 then
132✔
252
      number = number + length.shrink * line.ratio
×
253
   elseif line.ratio and line.ratio > 0 and length.stretch:tonumber() > 0 then
264✔
254
      number = number + length.stretch * line.ratio
240✔
255
   end
256
   return number
132✔
257
end
258

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

269
function elements.mbox:__tostring ()
2✔
270
   return self._type
×
271
end
272

273
function elements.mbox:_init ()
2✔
274
   nodefactory.hbox._init(self)
204✔
275
   self.font = {}
204✔
276
   self.children = {} -- The child nodes
204✔
277
   self.relX = SILE.types.length(0) -- x position relative to its parent box
408✔
278
   self.relY = SILE.types.length(0) -- y position relative to its parent box
408✔
279
   self.value = {}
204✔
280
   self.mode = mathMode.display
204✔
281
   self.atom = atomType.ordinary
204✔
282
   local font = {
204✔
283
      family = SILE.settings:get("math.font.family"),
408✔
284
      size = SILE.settings:get("math.font.size"),
408✔
285
      style = SILE.settings:get("math.font.style"),
408✔
286
      weight = SILE.settings:get("math.font.weight"),
408✔
287
   }
288
   local filename = SILE.settings:get("math.font.filename")
204✔
289
   if filename and filename ~= "" then
204✔
290
      font.filename = filename
×
291
   end
292
   self.font = SILE.font.loadDefaults(font)
408✔
293
end
294

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

299
function elements.mbox.shape (_, _, _)
2✔
300
   SU.error("shape is a virtual function that need to be overridden by its child classes")
×
301
end
302

303
function elements.mbox.output (_, _, _, _)
2✔
304
   SU.error("output is a virtual function that need to be overridden by its child classes")
×
305
end
306

307
function elements.mbox:getMathMetrics ()
2✔
308
   return retrieveMathTable(self.font)
522✔
309
end
310

311
function elements.mbox:getScaleDown ()
2✔
312
   local constants = self:getMathMetrics().constants
600✔
313
   local scaleDown
314
   if isScriptMode(self.mode) then
600✔
315
      scaleDown = constants.scriptPercentScaleDown
18✔
316
   elseif isScriptScriptMode(self.mode) then
564✔
317
      scaleDown = constants.scriptScriptPercentScaleDown
×
318
   else
319
      scaleDown = 1
282✔
320
   end
321
   return scaleDown
300✔
322
end
323

324
-- Determine the mode of its descendants
325
function elements.mbox:styleDescendants ()
2✔
326
   self:styleChildren()
204✔
327
   for _, n in ipairs(self.children) do
402✔
328
      if n then
198✔
329
         n:styleDescendants()
198✔
330
      end
331
   end
332
end
333

334
-- shapeTree shapes the mbox and all its descendants in a recursive fashion
335
-- The inner-most leaf nodes determine their shape first, and then propagate to their parents
336
-- During the process, each node will determine its size by (width, height, depth)
337
-- and (relX, relY) which the relative position to its parent
338
function elements.mbox:shapeTree ()
2✔
339
   for _, n in ipairs(self.children) do
402✔
340
      if n then
198✔
341
         n:shapeTree()
198✔
342
      end
343
   end
344
   self:shape()
204✔
345
end
346

347
-- Output the node and all its descendants
348
function elements.mbox:outputTree (x, y, line)
2✔
349
   self:output(x, y, line)
204✔
350
   local debug = SILE.settings:get("math.debug.boxes")
204✔
351
   if debug and not (self:is_a(elements.space)) then
204✔
352
      SILE.outputter:setCursor(scaleWidth(x, line), y.length)
×
353
      SILE.outputter:debugHbox({ height = self.height.length, depth = self.depth.length }, scaleWidth(self.width, line))
×
354
   end
355
   for _, n in ipairs(self.children) do
402✔
356
      if n then
198✔
357
         n:outputTree(x + n.relX, y + n.relY, line)
594✔
358
      end
359
   end
360
end
361

362
local spaceKind = {
1✔
363
   thin = "thin",
364
   med = "med",
365
   thick = "thick",
366
}
367

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

420
-- _stackbox stacks its content one, either horizontally or vertically
421
elements.stackbox = pl.class(elements.mbox)
2✔
422
elements.stackbox._type = "Stackbox"
1✔
423

424
function elements.stackbox:__tostring ()
2✔
425
   local result = self.direction .. "Box("
×
426
   for i, n in ipairs(self.children) do
×
427
      result = result .. (i == 1 and "" or ", ") .. tostring(n)
×
428
   end
429
   result = result .. ")"
×
430
   return result
×
431
end
432

433
function elements.stackbox:_init (direction, children)
2✔
434
   elements.mbox._init(self)
18✔
435
   if not (direction == "H" or direction == "V") then
18✔
436
      SU.error("Wrong direction '" .. direction .. "'; should be H or V")
×
437
   end
438
   self.direction = direction
18✔
439
   self.children = children
18✔
440
end
441

442
function elements.stackbox:styleChildren ()
2✔
443
   for _, n in ipairs(self.children) do
144✔
444
      n.mode = self.mode
126✔
445
   end
446
   if self.direction == "H" then
18✔
447
      -- Insert spaces according to the atom type, following Knuth's guidelines
448
      -- in the TeXbook
449
      local spaces = {}
12✔
450
      for i = 1, #self.children - 1 do
120✔
451
         local v = self.children[i]
108✔
452
         local v2 = self.children[i + 1]
108✔
453
         if spacingRules[v.atom] and spacingRules[v.atom][v2.atom] then
108✔
454
            local rule = spacingRules[v.atom][v2.atom]
48✔
455
            if not (rule.notScript and (isScriptMode(self.mode) or isScriptScriptMode(self.mode))) then
144✔
456
               spaces[i + 1] = rule[1]
48✔
457
            end
458
         end
459
      end
460
      local spaceIdx = {}
12✔
461
      for i, _ in pairs(spaces) do
60✔
462
         table.insert(spaceIdx, i)
48✔
463
      end
464
      table.sort(spaceIdx, function (a, b)
24✔
465
         return a > b
78✔
466
      end)
467
      for _, idx in ipairs(spaceIdx) do
60✔
468
         local hsp = elements.space(spaces[idx], 0, 0)
48✔
469
         table.insert(self.children, idx, hsp)
48✔
470
      end
471
   end
472
end
473

474
function elements.stackbox:shape ()
2✔
475
   -- For a horizontal stackbox (i.e. mrow):
476
   -- 1. set self.height and self.depth to max element height & depth
477
   -- 2. handle stretchy operators
478
   -- 3. set self.width
479
   -- For a vertical stackbox:
480
   -- 1. set self.width to max element width
481
   -- 2. set self.height
482
   -- And finally set children's relative coordinates
483
   self.height = SILE.types.length(0)
36✔
484
   self.depth = SILE.types.length(0)
36✔
485
   if self.direction == "H" then
18✔
486
      for i, n in ipairs(self.children) do
180✔
487
         n.relY = SILE.types.length(0)
336✔
488
         self.height = i == 1 and n.height or maxLength(self.height, n.height)
324✔
489
         self.depth = i == 1 and n.depth or maxLength(self.depth, n.depth)
324✔
490
      end
491
      -- Handle stretchy operators
492
      for _, elt in ipairs(self.children) do
180✔
493
         if elt.is_a(elements.text) and elt.kind == "operator" and elt.stretchy then
336✔
494
            elt:stretchyReshape(self.depth, self.height)
36✔
495
         end
496
      end
497
      -- Set self.width
498
      self.width = SILE.types.length(0)
24✔
499
      for i, n in ipairs(self.children) do
180✔
500
         n.relX = self.width
168✔
501
         self.width = i == 1 and n.width or self.width + n.width
324✔
502
      end
503
   else -- self.direction == "V"
504
      for i, n in ipairs(self.children) do
12✔
505
         n.relX = SILE.types.length(0)
12✔
506
         self.width = i == 1 and n.width or maxLength(self.width, n.width)
6✔
507
      end
508
      -- Set self.height and self.depth
509
      for i, n in ipairs(self.children) do
12✔
510
         self.depth = i == 1 and n.depth or self.depth + n.depth
6✔
511
      end
512
      for i = 1, #self.children do
12✔
513
         local n = self.children[i]
6✔
514
         if i == 1 then
6✔
515
            self.height = n.height
6✔
516
            self.depth = n.depth
6✔
517
         elseif i > 1 then
×
518
            n.relY = self.children[i - 1].relY + self.children[i - 1].depth + n.height
×
519
            self.depth = self.depth + n.height + n.depth
×
520
         end
521
      end
522
   end
523
end
524

525
-- Despite of its name, this function actually output the whole tree of nodes recursively.
526
function elements.stackbox:outputYourself (typesetter, line)
2✔
527
   local mathX = typesetter.frame.state.cursorX
6✔
528
   local mathY = typesetter.frame.state.cursorY
6✔
529
   self:outputTree(self.relX + mathX, self.relY + mathY, line)
18✔
530
   typesetter.frame:advanceWritingDirection(scaleWidth(self.width, line))
12✔
531
end
532

533
function elements.stackbox.output (_, _, _, _) end
19✔
534

535
elements.subscript = pl.class(elements.mbox)
2✔
536
elements.subscript._type = "Subscript"
1✔
537

538
function elements.subscript:__tostring ()
2✔
539
   return (self.sub and "Subscript" or "Superscript")
×
540
      .. "("
×
541
      .. tostring(self.base)
×
542
      .. ", "
×
543
      .. tostring(self.sub or self.super)
×
544
      .. ")"
×
545
end
546

547
function elements.subscript:_init (base, sub, sup)
2✔
548
   elements.mbox._init(self)
12✔
549
   self.base = base
12✔
550
   self.sub = sub
12✔
551
   self.sup = sup
12✔
552
   if self.base then
12✔
553
      table.insert(self.children, self.base)
12✔
554
   end
555
   if self.sub then
12✔
556
      table.insert(self.children, self.sub)
×
557
   end
558
   if self.sup then
12✔
559
      table.insert(self.children, self.sup)
12✔
560
   end
561
   self.atom = self.base.atom
12✔
562
end
563

564
function elements.subscript:styleChildren ()
2✔
565
   if self.base then
12✔
566
      self.base.mode = self.mode
12✔
567
   end
568
   if self.sub then
12✔
569
      self.sub.mode = getSubscriptMode(self.mode)
×
570
   end
571
   if self.sup then
12✔
572
      self.sup.mode = getSuperscriptMode(self.mode)
24✔
573
   end
574
end
575

576
function elements.subscript:calculateItalicsCorrection ()
2✔
577
   local lastGid = getRightMostGlyphId(self.base)
12✔
578
   if lastGid > 0 then
12✔
579
      local mathMetrics = self:getMathMetrics()
12✔
580
      if mathMetrics.italicsCorrection[lastGid] then
12✔
581
         return mathMetrics.italicsCorrection[lastGid]
×
582
      end
583
   end
584
   return 0
12✔
585
end
586

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

673
function elements.subscript.output (_, _, _, _) end
13✔
674

675
elements.underOver = pl.class(elements.subscript)
2✔
676
elements.underOver._type = "UnderOver"
1✔
677

678
function elements.underOver:__tostring ()
2✔
679
   return self._type .. "(" .. tostring(self.base) .. ", " .. tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
×
680
end
681

682
function elements.underOver:_init (base, sub, sup)
2✔
683
   elements.mbox._init(self)
×
684
   self.atom = base.atom
×
685
   self.base = base
×
686
   self.sub = sub
×
687
   self.sup = sup
×
688
   if self.sup then
×
689
      table.insert(self.children, self.sup)
×
690
   end
691
   if self.base then
×
692
      table.insert(self.children, self.base)
×
693
   end
694
   if self.sub then
×
695
      table.insert(self.children, self.sub)
×
696
   end
697
end
698

699
function elements.underOver:styleChildren ()
2✔
700
   if self.base then
×
701
      self.base.mode = self.mode
×
702
   end
703
   if self.sub then
×
704
      self.sub.mode = getSubscriptMode(self.mode)
×
705
   end
706
   if self.sup then
×
707
      self.sup.mode = getSuperscriptMode(self.mode)
×
708
   end
709
end
710

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

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

824
function elements.underOver.output (_, _, _, _) end
1✔
825

826
-- terminal is the base class for leaf node
827
elements.terminal = pl.class(elements.mbox)
2✔
828
elements.terminal._type = "Terminal"
1✔
829

830
function elements.terminal:_init ()
2✔
831
   elements.mbox._init(self)
174✔
832
end
833

834
function elements.terminal.styleChildren (_) end
175✔
835

836
function elements.terminal.shape (_) end
1✔
837

838
elements.space = pl.class(elements.terminal)
2✔
839
elements.space._type = "Space"
1✔
840

841
function elements.space:_init ()
2✔
842
   elements.terminal._init(self)
×
843
end
844

845
function elements.space:__tostring ()
2✔
846
   return self._type
×
847
      .. "(width="
×
848
      .. tostring(self.width)
×
849
      .. ", height="
×
850
      .. tostring(self.height)
×
851
      .. ", depth="
×
852
      .. tostring(self.depth)
×
853
      .. ")"
×
854
end
855

856
local function getStandardLength (value)
857
   if type(value) == "string" then
144✔
858
      local direction = 1
48✔
859
      if value:sub(1, 1) == "-" then
96✔
860
         value = value:sub(2, -1)
×
861
         direction = -1
×
862
      end
863
      if value == "thin" then
48✔
864
         return SILE.types.length("3mu") * direction
36✔
865
      elseif value == "med" then
36✔
866
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
36✔
867
      elseif value == "thick" then
24✔
868
         return SILE.types.length("5mu plus 5mu") * direction
72✔
869
      end
870
   end
871
   return SILE.types.length(value)
96✔
872
end
873

874
function elements.space:_init (width, height, depth)
2✔
875
   elements.terminal._init(self)
48✔
876
   self.width = getStandardLength(width)
96✔
877
   self.height = getStandardLength(height)
96✔
878
   self.depth = getStandardLength(depth)
96✔
879
end
880

881
function elements.space:shape ()
2✔
882
   self.width = self.width:absolute() * self:getScaleDown()
192✔
883
   self.height = self.height:absolute() * self:getScaleDown()
192✔
884
   self.depth = self.depth:absolute() * self:getScaleDown()
192✔
885
end
886

887
function elements.space.output (_) end
49✔
888

889
-- text node. For any actual text output
890
elements.text = pl.class(elements.terminal)
2✔
891
elements.text._type = "Text"
1✔
892

893
function elements.text:__tostring ()
2✔
894
   return self._type
×
895
      .. "(atom="
×
896
      .. tostring(self.atom)
×
897
      .. ", kind="
×
898
      .. tostring(self.kind)
×
899
      .. ", script="
×
900
      .. tostring(self.script)
×
901
      .. (self.stretchy and ", stretchy" or "")
×
902
      .. (self.largeop and ", largeop" or "")
×
903
      .. ', text="'
×
904
      .. (self.originalText or self.text)
×
905
      .. '")'
×
906
end
907

908
function elements.text:_init (kind, attributes, script, text)
2✔
909
   elements.terminal._init(self)
126✔
910
   if not (kind == "number" or kind == "identifier" or kind == "operator") then
126✔
911
      SU.error("Unknown text node kind '" .. kind .. "'; should be one of: number, identifier, operator.")
×
912
   end
913
   self.kind = kind
126✔
914
   self.script = script
126✔
915
   self.text = text
126✔
916
   if self.script ~= "upright" then
126✔
917
      local converted = ""
126✔
918
      for _, uchr in luautf8.codes(self.text) do
264✔
919
         local dst_char = luautf8.char(uchr)
138✔
920
         if uchr >= 0x41 and uchr <= 0x5A then -- Latin capital letter
138✔
921
            dst_char = luautf8.char(mathScriptConversionTable.capital[self.script](uchr))
36✔
922
         elseif uchr >= 0x61 and uchr <= 0x7A then -- Latin non-capital letter
120✔
923
            dst_char = luautf8.char(mathScriptConversionTable.small[self.script](uchr))
84✔
924
         end
925
         converted = converted .. dst_char
138✔
926
      end
927
      self.originalText = self.text
126✔
928
      self.text = converted
126✔
929
   end
930
   if self.kind == "operator" then
126✔
931
      if self.text == "-" then
78✔
932
         self.text = "−"
×
933
      end
934
   end
935
   for attribute, value in pairs(attributes) do
234✔
936
      self[attribute] = value
108✔
937
   end
938
end
939

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

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

1071
function elements.text:output (x, y, line)
2✔
1072
   if not self.value.glyphString then
126✔
1073
      return
×
1074
   end
1075
   local compensatedY
1076
   if isDisplayMode(self.mode) and self.atom == atomType.bigOperator and self.value.items[1].fontDepth then
252✔
1077
      compensatedY = SILE.types.length(y.length + self.value.items[1].depth - self.value.items[1].fontDepth)
×
1078
   else
1079
      compensatedY = y
126✔
1080
   end
1081
   SILE.outputter:setCursor(scaleWidth(x, line), compensatedY.length)
252✔
1082
   SILE.outputter:setFont(self.font)
126✔
1083
   -- There should be no stretch or shrink on the width of a text
1084
   -- element.
1085
   local width = self.width.length
126✔
1086
   SILE.outputter:drawHbox(self.value, width)
126✔
1087
end
1088

1089
elements.fraction = pl.class(elements.mbox)
2✔
1090
elements.fraction._type = "Fraction"
1✔
1091

1092
function elements.fraction:__tostring ()
2✔
1093
   return self._type .. "(" .. tostring(self.numerator) .. ", " .. tostring(self.denominator) .. ")"
×
1094
end
1095

1096
function elements.fraction:_init (numerator, denominator)
2✔
1097
   elements.mbox._init(self)
×
1098
   self.numerator = numerator
×
1099
   self.denominator = denominator
×
1100
   table.insert(self.children, numerator)
×
1101
   table.insert(self.children, denominator)
×
1102
end
1103

1104
function elements.fraction:styleChildren ()
2✔
1105
   self.numerator.mode = getNumeratorMode(self.mode)
×
1106
   self.denominator.mode = getDenominatorMode(self.mode)
×
1107
end
1108

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

1167
function elements.fraction:output (x, y, line)
2✔
1168
   SILE.outputter:drawRule(
×
1169
      scaleWidth(x, line),
×
1170
      y.length - self.axisHeight - self.ruleThickness / 2,
×
1171
      scaleWidth(self.width, line),
×
1172
      self.ruleThickness
1173
   )
1174
end
1175

1176
local function newSubscript (spec)
1177
   return elements.subscript(spec.base, spec.sub, spec.sup)
12✔
1178
end
1179

1180
local function newUnderOver (spec)
1181
   return elements.underOver(spec.base, spec.sub, spec.sup)
×
1182
end
1183

1184
-- TODO replace with penlight equivalent
1185
local function mapList (f, l)
1186
   local ret = {}
×
1187
   for i, x in ipairs(l) do
×
1188
      ret[i] = f(i, x)
×
1189
   end
1190
   return ret
×
1191
end
1192

1193
elements.mtr = pl.class(elements.mbox)
2✔
1194
-- elements.mtr._type = "" -- TODO why not set?
1195

1196
function elements.mtr:_init (children)
2✔
1197
   self.children = children
×
1198
end
1199

1200
function elements.mtr:styleChildren ()
2✔
1201
   for _, c in ipairs(self.children) do
×
1202
      c.mode = self.mode
×
1203
   end
1204
end
1205

1206
function elements.mtr.shape (_) end -- done by parent table
1✔
1207

1208
function elements.mtr.output (_) end
1✔
1209

1210
elements.table = pl.class(elements.mbox)
2✔
1211
elements.table._type = "table" -- TODO why case difference?
1✔
1212

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

1258
function elements.table:styleChildren ()
2✔
1259
   if self.mode == mathMode.display and self.options.displaystyle ~= "false" then
×
1260
      for _, c in ipairs(self.children) do
×
1261
         c.mode = mathMode.display
×
1262
      end
1263
   else
1264
      for _, c in ipairs(self.children) do
×
1265
         c.mode = mathMode.text
×
1266
      end
1267
   end
1268
end
1269

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

1335
function elements.table.output (_) end
1✔
1336

1337
elements.mathMode = mathMode
1✔
1338
elements.atomType = atomType
1✔
1339
elements.scriptType = scriptType
1✔
1340
elements.mathVariantToScriptType = mathVariantToScriptType
1✔
1341
elements.symbolDefaults = symbolDefaults
1✔
1342
elements.newSubscript = newSubscript
1✔
1343
elements.newUnderOver = newUnderOver
1✔
1344

1345
return elements
1✔
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