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

sile-typesetter / sile / 9527474203

15 Jun 2024 10:36AM UTC coverage: 63.678% (+13.2%) from 50.521%
9527474203

push

github

web-flow
chore(packages): Improve error message when a font doesn't support math (#2070)

5 of 7 new or added lines in 1 file covered. (71.43%)

119 existing lines in 10 files now uncovered.

10996 of 17268 relevant lines covered (63.68%)

3159.4 hits per line

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

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

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

9
local elements = {}
10✔
10

11
local mathMode = {
10✔
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 = {
10✔
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
1,914✔
50
end
51

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

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

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

64
local mathScriptConversionTable = {
10✔
65
   capital = {
10✔
66
      [scriptType.upright] = function (codepoint)
10✔
67
         return codepoint
8✔
68
      end,
69
      [scriptType.bold] = function (codepoint)
10✔
70
         return codepoint + 0x1D400 - 0x41
12✔
71
      end,
72
      [scriptType.italic] = function (codepoint)
10✔
73
         return codepoint + 0x1D434 - 0x41
14✔
74
      end,
75
      [scriptType.boldItalic] = function (codepoint)
10✔
76
         return codepoint + 0x1D468 - 0x41
1✔
77
      end,
78
      [scriptType.doubleStruck] = function (codepoint)
10✔
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
   },
10✔
89
   small = {
10✔
90
      [scriptType.upright] = function (codepoint)
10✔
91
         return codepoint
322✔
92
      end,
93
      [scriptType.bold] = function (codepoint)
10✔
94
         return codepoint + 0x1D41A - 0x61
6✔
95
      end,
96
      [scriptType.italic] = function (codepoint)
10✔
97
         return codepoint == 0x68 and 0x210E or codepoint + 0x1D44E - 0x61
178✔
98
      end,
99
      [scriptType.boldItalic] = function (codepoint)
10✔
100
         return codepoint + 0x1D482 - 0x61
1✔
101
      end,
102
      [scriptType.doubleStruck] = function (codepoint)
10✔
103
         return codepoint + 0x1D552 - 0x61
12✔
104
      end,
105
   },
10✔
106
}
107

108
local mathCache = {}
10✔
109

110
local function retrieveMathTable (font)
111
   local key = SILE.font._key(font)
3,566✔
112
   if not mathCache[key] then
3,566✔
113
      SU.debug("math", "Loading math font", key)
32✔
114
      local face = SILE.font.cache(font, SILE.shaper.getFace)
32✔
115
      if not face then
32✔
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")
32✔
120
      if fontHasMathTable then
32✔
121
         mathTableParsable, mathTable = pcall(ot.parseMath, rawMathTable)
64✔
122
      end
123
      if not fontHasMathTable or not mathTableParsable then
32✔
NEW
124
         SU.error(([[You must use a math font for math rendering.
×
125

126
  The math table in '%s' could not be %s.
NEW
127
         ]]):format(face.filename, fontHasMathTable and "parsed" or "loaded"))
×
128
      end
129
      local upem = ot.parseHead(hb.get_table(face, "head")).unitsPerEm
64✔
130
      local constants = {}
32✔
131
      for k, v in pairs(mathTable.mathConstants) do
1,824✔
132
         if type(v) == "table" then
1,792✔
133
            v = v.value
1,632✔
134
         end
135
         if k:sub(-9) == "ScaleDown" then
3,584✔
136
            constants[k] = v / 100
64✔
137
         else
138
            constants[k] = v * font.size / upem
1,728✔
139
         end
140
      end
141
      local italicsCorrection = {}
32✔
142
      for k, v in pairs(mathTable.mathItalicsCorrection) do
13,024✔
143
         italicsCorrection[k] = v.value * font.size / upem
12,992✔
144
      end
145
      mathCache[key] = {
32✔
146
         constants = constants,
32✔
147
         italicsCorrection = italicsCorrection,
32✔
148
         mathVariants = mathTable.mathVariants,
32✔
149
         unitsPerEm = upem,
32✔
150
      }
32✔
151
   end
152
   return mathCache[key]
3,566✔
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
66✔
159
      return mathMode.script
31✔
160
   -- D', T' -> S'
161
   elseif mode == mathMode.displayCramped or mode == mathMode.textCramped then
35✔
162
      return mathMode.scriptCramped
29✔
163
   -- S, SS -> SS
164
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
6✔
165
      return mathMode.scriptScript
3✔
166
   -- S', SS' -> SS'
167
   else
168
      return mathMode.scriptScriptCramped
3✔
169
   end
170
end
171
local function getSubscriptMode (mode)
172
   -- D, T, D', T' -> S'
173
   if
174
      mode == mathMode.display
99✔
175
      or mode == mathMode.text
60✔
176
      or mode == mathMode.displayCramped
50✔
177
      or mode == mathMode.textCramped
50✔
178
   then
179
      return mathMode.scriptCramped
80✔
180
   -- S, SS, S', SS' -> SS'
181
   else
182
      return mathMode.scriptScriptCramped
19✔
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
33✔
190
      return mathMode.text
10✔
191
   -- D' -> T'
192
   elseif mode == mathMode.displayCramped then
23✔
193
      return mathMode.textCramped
×
194
   -- T -> S
195
   elseif mode == mathMode.text then
23✔
196
      return mathMode.script
×
197
   -- T' -> S'
198
   elseif mode == mathMode.textCramped then
23✔
199
      return mathMode.scriptCramped
13✔
200
   -- S, SS -> SS
201
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
10✔
202
      return mathMode.scriptScript
×
203
   -- S', SS' -> SS'
204
   else
205
      return mathMode.scriptScriptCramped
10✔
206
   end
207
end
208
local function getDenominatorMode (mode)
209
   -- D, D' -> T'
210
   if mode == mathMode.display or mode == mathMode.displayCramped then
33✔
211
      return mathMode.textCramped
10✔
212
   -- T, T' -> S'
213
   elseif mode == mathMode.text or mode == mathMode.textCramped then
23✔
214
      return mathMode.scriptCramped
13✔
215
   -- S, SS, S', SS' -> SS'
216
   else
217
      return mathMode.scriptScriptCramped
10✔
218
   end
219
end
220

221
local function getRightMostGlyphId (node)
222
   while node and node:is_a(elements.stackbox) and node.direction == "H" do
274✔
223
      node = node.children[#node.children]
12✔
224
   end
225
   if node and node:is_a(elements.text) then
250✔
226
      return node.value.glyphString[#node.value.glyphString]
123✔
227
   else
228
      return 0
4✔
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 = { ... }
2,411✔
236
   local m
237
   for i, v in ipairs(arg) do
7,477✔
238
      if i == 1 then
5,066✔
239
         m = v
2,411✔
240
      else
241
         if v.length:tonumber() > m.length:tonumber() then
7,965✔
242
            m = v
611✔
243
         end
244
      end
245
   end
246
   return m
2,411✔
247
end
248

249
local function scaleWidth (length, line)
250
   local number = length.length
1,052✔
251
   if line.ratio and line.ratio < 0 and length.shrink:tonumber() > 0 then
1,052✔
252
      number = number + length.shrink * line.ratio
×
253
   elseif line.ratio and line.ratio > 0 and length.stretch:tonumber() > 0 then
2,104✔
254
      number = number + length.stretch * line.ratio
1,174✔
255
   end
256
   return number
1,052✔
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)
20✔
267
elements.mbox._type = "Mbox"
10✔
268

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

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

295
function elements.mbox.styleChildren (_)
20✔
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 (_, _, _)
20✔
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 (_, _, _, _)
20✔
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 ()
20✔
308
   return retrieveMathTable(self.font)
3,566✔
309
end
310

311
function elements.mbox:getScaleDown ()
20✔
312
   local constants = self:getMathMetrics().constants
4,390✔
313
   local scaleDown
314
   if isScriptMode(self.mode) then
4,390✔
315
      scaleDown = constants.scriptPercentScaleDown
405✔
316
   elseif isScriptScriptMode(self.mode) then
3,580✔
317
      scaleDown = constants.scriptScriptPercentScaleDown
181✔
318
   else
319
      scaleDown = 1
1,609✔
320
   end
321
   return scaleDown
2,195✔
322
end
323

324
-- Determine the mode of its descendants
325
function elements.mbox:styleDescendants ()
20✔
326
   self:styleChildren()
1,761✔
327
   for _, n in ipairs(self.children) do
3,460✔
328
      if n then
1,699✔
329
         n:styleDescendants()
1,699✔
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 ()
20✔
339
   for _, n in ipairs(self.children) do
3,460✔
340
      if n then
1,699✔
341
         n:shapeTree()
1,699✔
342
      end
343
   end
344
   self:shape()
1,761✔
345
end
346

347
-- Output the node and all its descendants
348
function elements.mbox:outputTree (x, y, line)
20✔
349
   self:output(x, y, line)
1,761✔
350
   local debug = SILE.settings:get("math.debug.boxes")
1,761✔
351
   if debug and not (self:is_a(elements.space)) then
1,761✔
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
3,460✔
356
      if n then
1,699✔
357
         n:outputTree(x + n.relX, y + n.relY, line)
5,097✔
358
      end
359
   end
360
end
361

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

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

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

424
function elements.stackbox:__tostring ()
20✔
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)
20✔
434
   elements.mbox._init(self)
361✔
435
   if not (direction == "H" or direction == "V") then
361✔
436
      SU.error("Wrong direction '" .. direction .. "'; should be H or V")
×
437
   end
438
   self.direction = direction
361✔
439
   self.children = children
361✔
440
end
441

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

474
function elements.stackbox:shape ()
20✔
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)
722✔
484
   self.depth = SILE.types.length(0)
722✔
485
   if self.direction == "H" then
361✔
486
      for i, n in ipairs(self.children) do
1,518✔
487
         n.relY = SILE.types.length(0)
2,438✔
488
         self.height = i == 1 and n.height or maxLength(self.height, n.height)
2,145✔
489
         self.depth = i == 1 and n.depth or maxLength(self.depth, n.depth)
2,145✔
490
      end
491
      -- Handle stretchy operators
492
      for _, elt in ipairs(self.children) do
1,518✔
493
         if elt.is_a(elements.text) and elt.kind == "operator" and elt.stretchy then
2,438✔
494
            elt:stretchyReshape(self.depth, self.height)
78✔
495
         end
496
      end
497
      -- Set self.width
498
      self.width = SILE.types.length(0)
598✔
499
      for i, n in ipairs(self.children) do
1,518✔
500
         n.relX = self.width
1,219✔
501
         self.width = i == 1 and n.width or self.width + n.width
2,145✔
502
      end
503
   else -- self.direction == "V"
504
      for i, n in ipairs(self.children) do
124✔
505
         n.relX = SILE.types.length(0)
124✔
506
         self.width = i == 1 and n.width or maxLength(self.width, n.width)
62✔
507
      end
508
      -- Set self.height and self.depth
509
      for i, n in ipairs(self.children) do
124✔
510
         self.depth = i == 1 and n.depth or self.depth + n.depth
62✔
511
      end
512
      for i = 1, #self.children do
124✔
513
         local n = self.children[i]
62✔
514
         if i == 1 then
62✔
515
            self.height = n.height
62✔
516
            self.depth = n.depth
62✔
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)
20✔
527
   local mathX = typesetter.frame.state.cursorX
62✔
528
   local mathY = typesetter.frame.state.cursorY
62✔
529
   self:outputTree(self.relX + mathX, self.relY + mathY, line)
186✔
530
   typesetter.frame:advanceWritingDirection(scaleWidth(self.width, line))
124✔
531
end
532

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

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

538
function elements.subscript:__tostring ()
20✔
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)
20✔
548
   elements.mbox._init(self)
102✔
549
   self.base = base
102✔
550
   self.sub = sub
102✔
551
   self.sup = sup
102✔
552
   if self.base then
102✔
553
      table.insert(self.children, self.base)
102✔
554
   end
555
   if self.sub then
102✔
556
      table.insert(self.children, self.sub)
74✔
557
   end
558
   if self.sup then
102✔
559
      table.insert(self.children, self.sup)
42✔
560
   end
561
   self.atom = self.base.atom
102✔
562
end
563

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

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

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

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

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

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

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

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

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

806
function elements.underOver:calculateItalicsCorrection ()
20✔
807
   local lastGid = getRightMostGlyphId(self.base)
25✔
808
   if lastGid > 0 then
25✔
809
      local mathMetrics = self:getMathMetrics()
25✔
810
      if mathMetrics.italicsCorrection[lastGid] then
25✔
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
25✔
822
end
823

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

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

830
function elements.terminal:_init ()
20✔
831
   elements.mbox._init(self)
1,220✔
832
end
833

834
function elements.terminal.styleChildren (_) end
1,230✔
835

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

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

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

845
function elements.space:__tostring ()
20✔
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
888✔
858
      local direction = 1
296✔
859
      if value:sub(1, 1) == "-" then
592✔
860
         value = value:sub(2, -1)
20✔
861
         direction = -1
10✔
862
      end
863
      if value == "thin" then
296✔
864
         return SILE.types.length("3mu") * direction
195✔
865
      elseif value == "med" then
231✔
866
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
288✔
867
      elseif value == "thick" then
135✔
868
         return SILE.types.length("5mu plus 5mu") * direction
333✔
869
      end
870
   end
871
   return SILE.types.length(value)
616✔
872
end
873

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

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

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

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

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

940
function elements.text:shape ()
20✔
941
   self.font.size = self.font.size * self:getScaleDown()
1,848✔
942
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
924✔
943
   local mathMetrics = self:getMathMetrics()
924✔
944
   local glyphs = SILE.shaper:shapeToken(self.text, self.font)
924✔
945
   -- Use bigger variants for big operators in display style
946
   if isDisplayMode(self.mode) and self.largeop then
1,848✔
947
      -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
948
      glyphs = pl.tablex.deepcopy(glyphs)
38✔
949
      local constructions = mathMetrics.mathVariants.vertGlyphConstructions[glyphs[1].gid]
19✔
950
      if constructions then
19✔
951
         local displayVariants = constructions.mathGlyphVariantRecord
19✔
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
19✔
956
         for _, v in ipairs(displayVariants) do
57✔
957
            if v.advanceMeasurement > m then
38✔
958
               biggest = v
38✔
959
               m = v.advanceMeasurement
38✔
960
            end
961
         end
962
         if biggest then
19✔
963
            glyphs[1].gid = biggest.variantGlyph
19✔
964
            local dimen = hb.get_glyph_dimensions(face, self.font.size, biggest.variantGlyph)
19✔
965
            glyphs[1].width = dimen.width
19✔
966
            glyphs[1].glyphAdvance = dimen.glyphAdvance
19✔
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()
38✔
973
            local y_size = dimen.height + dimen.depth
19✔
974
            glyphs[1].height = y_size / 2 + axisHeight
19✔
975
            glyphs[1].depth = y_size / 2 - axisHeight
19✔
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
19✔
980
            glyphs[1].fontDepth = dimen.depth
19✔
981
         end
982
      end
983
   end
984
   SILE.shaper:preAddNodes(glyphs, self.value)
924✔
985
   self.value.items = glyphs
924✔
986
   self.value.glyphString = {}
924✔
987
   if glyphs and #glyphs > 0 then
924✔
988
      for i = 1, #glyphs do
2,095✔
989
         table.insert(self.value.glyphString, glyphs[i].gid)
1,171✔
990
      end
991
      self.width = SILE.types.length(0)
1,848✔
992
      self.widthForSubscript = SILE.types.length(0)
1,848✔
993
      for i = #glyphs, 1, -1 do
2,095✔
994
         self.width = self.width + glyphs[i].glyphAdvance
2,342✔
995
      end
996
      -- Store width without italic correction somewhere
997
      self.widthForSubscript = self.width
924✔
998
      local itCorr = mathMetrics.italicsCorrection[glyphs[#glyphs].gid]
924✔
999
      if itCorr then
924✔
1000
         self.width = self.width + itCorr * self:getScaleDown()
588✔
1001
      end
1002
      for i = 1, #glyphs do
2,095✔
1003
         self.height = i == 1 and SILE.types.length(glyphs[i].height)
1,171✔
1004
            or SILE.types.length(math.max(self.height:tonumber(), glyphs[i].height))
1,665✔
1005
         self.depth = i == 1 and SILE.types.length(glyphs[i].depth)
1,171✔
1006
            or SILE.types.length(math.max(self.depth:tonumber(), glyphs[i].depth))
1,665✔
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)
20✔
1016
   -- Required depth+height of stretched glyph, in font units
1017
   local mathMetrics = self:getMathMetrics()
78✔
1018
   local upem = mathMetrics.unitsPerEm
78✔
1019
   local sz = self.font.size
78✔
1020
   local requiredAdvance = (depth + height):tonumber() * upem / sz
234✔
1021
   SU.debug("math", "stretch: rA =", requiredAdvance)
78✔
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)
78✔
1030
   local constructions = self:getMathMetrics().mathVariants.vertGlyphConstructions[glyphs[1].gid]
156✔
1031
   if constructions then
78✔
1032
      local variants = constructions.mathGlyphVariantRecord
78✔
1033
      SU.debug("math", "stretch: variants =", variants)
78✔
1034
      local closest
1035
      local closestI
1036
      local m = requiredAdvance - (self.depth + self.height):tonumber() * upem / sz
234✔
1037
      SU.debug("math", "stretch: m =", m)
78✔
1038
      for i, v in ipairs(variants) do
1,092✔
1039
         local diff = math.abs(v.advanceMeasurement - requiredAdvance)
1,014✔
1040
         SU.debug("math", "stretch: diff =", diff)
1,014✔
1041
         if diff < m then
1,014✔
1042
            closest = v
110✔
1043
            closestI = i
110✔
1044
            m = diff
110✔
1045
         end
1046
      end
1047
      SU.debug("math", "stretch: closestI =", closestI)
78✔
1048
      if closest then
78✔
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
62✔
1055
         local face = SILE.font.cache(self.font, SILE.shaper.getFace)
62✔
1056
         local dimen = hb.get_glyph_dimensions(face, self.font.size, closest.variantGlyph)
62✔
1057
         glyphs[1].width = dimen.width
62✔
1058
         glyphs[1].height = dimen.height
62✔
1059
         glyphs[1].depth = dimen.depth
62✔
1060
         glyphs[1].glyphAdvance = dimen.glyphAdvance
62✔
1061
         self.width = SILE.types.length(dimen.glyphAdvance)
124✔
1062
         self.depth = SILE.types.length(dimen.depth)
124✔
1063
         self.height = SILE.types.length(dimen.height)
124✔
1064
         SILE.shaper:preAddNodes(glyphs, self.value)
62✔
1065
         self.value.items = glyphs
62✔
1066
         self.value.glyphString = { glyphs[1].gid }
62✔
1067
      end
1068
   end
1069
end
1070

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1213
function elements.table:_init (children, options)
20✔
1214
   elements.mbox._init(self)
8✔
1215
   self.children = children
8✔
1216
   self.options = options
8✔
1217
   self.nrows = #self.children
8✔
1218
   self.ncols = math.max(pl.utils.unpack(mapList(function (_, row)
32✔
1219
      return #row.children
24✔
1220
   end, self.children)))
16✔
1221
   SU.debug("math", "self.ncols =", self.ncols)
8✔
1222
   self.rowspacing = self.options.rowspacing and SILE.types.length(self.options.rowspacing) or SILE.types.length("7pt")
16✔
1223
   self.columnspacing = self.options.columnspacing and SILE.types.length(self.options.columnspacing)
8✔
1224
      or SILE.types.length("6pt")
16✔
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
32✔
1228
      for j = 1, (self.ncols - #row.children) do
24✔
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
8✔
1235
      local l = {}
4✔
1236
      for w in string.gmatch(options.columnalign, "[^%s]+") do
16✔
1237
         if not (w == "left" or w == "center" or w == "right") then
12✔
1238
            SU.error("Invalid specifier in `columnalign` attribute: " .. w)
×
1239
         end
1240
         table.insert(l, w)
12✔
1241
      end
1242
      -- Pad with last value of l if necessary
1243
      for _ = 1, (self.ncols - #l), 1 do
4✔
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
4✔
1248
         table.remove(l)
×
1249
      end
1250
      self.options.columnalign = l
4✔
1251
   else
1252
      self.options.columnalign = pl.List.range(1, self.ncols):map(function (_)
14✔
1253
         return "center"
12✔
1254
      end)
1255
   end
1256
end
1257

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

1270
function elements.table:shape ()
20✔
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
32✔
1275
      row.height = SILE.types.length(0)
48✔
1276
      row.depth = SILE.types.length(0)
48✔
1277
      for _, cell in ipairs(row.children) do
96✔
1278
         row.height = maxLength(row.height, cell.height)
144✔
1279
         row.depth = maxLength(row.depth, cell.depth)
144✔
1280
      end
1281
   end
1282
   self.vertSize = SILE.types.length(0)
16✔
1283
   for i, row in ipairs(self.children) do
32✔
1284
      self.vertSize = self.vertSize
×
1285
         + row.height
24✔
1286
         + row.depth
24✔
1287
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
56✔
1288
   end
1289
   local rowHeightSoFar = SILE.types.length(0)
8✔
1290
   for i, row in ipairs(self.children) do
32✔
1291
      row.relY = rowHeightSoFar + row.height - self.vertSize
72✔
1292
      rowHeightSoFar = rowHeightSoFar
×
1293
         + row.height
24✔
1294
         + row.depth
24✔
1295
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
32✔
1296
   end
1297
   self.width = SILE.types.length(0)
16✔
1298
   local thisColRelX = SILE.types.length(0)
8✔
1299
   -- For every column...
1300
   for i = 1, self.ncols do
32✔
1301
      -- Determine its width
1302
      local columnWidth = SILE.types.length(0)
24✔
1303
      for j = 1, self.nrows do
96✔
1304
         if self.children[j].children[i].width > columnWidth then
72✔
1305
            columnWidth = self.children[j].children[i].width
32✔
1306
         end
1307
      end
1308
      -- Use it to align the contents of every cell as required.
1309
      for j = 1, self.nrows do
96✔
1310
         local cell = self.children[j].children[i]
72✔
1311
         if self.options.columnalign[i] == "left" then
72✔
1312
            cell.relX = thisColRelX
12✔
1313
         elseif self.options.columnalign[i] == "center" then
60✔
1314
            cell.relX = thisColRelX + (columnWidth - cell.width) / 2
192✔
1315
         elseif self.options.columnalign[i] == "right" then
12✔
1316
            cell.relX = thisColRelX + (columnWidth - cell.width)
36✔
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
56✔
1322
   end
1323
   self.width = thisColRelX
8✔
1324
   -- Center myself vertically around the axis, and update relative Ys of rows accordingly
1325
   local axisHeight = self:getMathMetrics().constants.axisHeight * self:getScaleDown()
24✔
1326
   self.height = self.vertSize / 2 + axisHeight
24✔
1327
   self.depth = self.vertSize / 2 - axisHeight
24✔
1328
   for _, row in ipairs(self.children) do
32✔
1329
      row.relY = row.relY + self.vertSize / 2 - axisHeight
96✔
1330
      -- Also adjust width
1331
      row.width = self.width
24✔
1332
   end
1333
end
1334

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

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

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