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

sile-typesetter / sile / 10136420878

29 Jul 2024 01:15AM UTC coverage: 56.368% (-8.5%) from 64.818%
10136420878

push

github

web-flow
Merge 06a15474f into b738c1c5c

9648 of 17116 relevant lines covered (56.37%)

2096.86 hits per line

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

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

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

9
local elements = {}
6✔
10

11
local mathMode = {
6✔
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 = {
6✔
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
56✔
41
      or attr == "bold" and scriptType.bold
56✔
42
      or attr == "italic" and scriptType.italic
34✔
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')
56✔
46
end
47

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

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

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

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

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

108
local mathCache = {}
6✔
109

110
local function retrieveMathTable (font)
111
   local key = SILE.font._key(font)
2,530✔
112
   if not mathCache[key] then
2,530✔
113
      SU.debug("math", "Loading math font", key)
19✔
114
      local face = SILE.font.cache(font, SILE.shaper.getFace)
19✔
115
      if not face then
19✔
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")
19✔
120
      if fontHasMathTable then
19✔
121
         mathTableParsable, mathTable = pcall(ot.parseMath, rawMathTable)
38✔
122
      end
123
      if not fontHasMathTable or not mathTableParsable then
19✔
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
38✔
130
      local constants = {}
19✔
131
      for k, v in pairs(mathTable.mathConstants) do
1,083✔
132
         if type(v) == "table" then
1,064✔
133
            v = v.value
969✔
134
         end
135
         if k:sub(-9) == "ScaleDown" then
2,128✔
136
            constants[k] = v / 100
38✔
137
         else
138
            constants[k] = v * font.size / upem
1,026✔
139
         end
140
      end
141
      local italicsCorrection = {}
19✔
142
      for k, v in pairs(mathTable.mathItalicsCorrection) do
7,733✔
143
         italicsCorrection[k] = v.value * font.size / upem
7,714✔
144
      end
145
      mathCache[key] = {
19✔
146
         constants = constants,
19✔
147
         italicsCorrection = italicsCorrection,
19✔
148
         mathVariants = mathTable.mathVariants,
19✔
149
         unitsPerEm = upem,
19✔
150
      }
19✔
151
   end
152
   return mathCache[key]
2,530✔
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
64✔
159
      return mathMode.script
29✔
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
76✔
175
      or mode == mathMode.text
47✔
176
      or mode == mathMode.displayCramped
42✔
177
      or mode == mathMode.textCramped
42✔
178
   then
179
      return mathMode.scriptCramped
61✔
180
   -- S, SS, S', SS' -> SS'
181
   else
182
      return mathMode.scriptScriptCramped
15✔
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
32✔
190
      return mathMode.text
9✔
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
32✔
211
      return mathMode.textCramped
9✔
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
220✔
223
      node = node.children[#node.children]
10✔
224
   end
225
   if node and node:is_a(elements.text) then
200✔
226
      return node.value.glyphString[#node.value.glyphString]
98✔
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 = { ... }
1,640✔
236
   local m
237
   for i, v in ipairs(arg) do
5,115✔
238
      if i == 1 then
3,475✔
239
         m = v
1,640✔
240
      else
241
         if v.length:tonumber() > m.length:tonumber() then
5,505✔
242
            m = v
451✔
243
         end
244
      end
245
   end
246
   return m
1,640✔
247
end
248

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

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

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

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

311
function elements.mbox:getScaleDown ()
12✔
312
   local constants = self:getMathMetrics().constants
2,996✔
313
   local scaleDown
314
   if isScriptMode(self.mode) then
2,996✔
315
      scaleDown = constants.scriptPercentScaleDown
274✔
316
   elseif isScriptScriptMode(self.mode) then
2,448✔
317
      scaleDown = constants.scriptScriptPercentScaleDown
93✔
318
   else
319
      scaleDown = 1
1,131✔
320
   end
321
   return scaleDown
1,498✔
322
end
323

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

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

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

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

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

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

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

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

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

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

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

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

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

587
function elements.subscript:shape ()
12✔
588
   local mathMetrics = self:getMathMetrics()
93✔
589
   local constants = mathMetrics.constants
93✔
590
   local scaleDown = self:getScaleDown()
93✔
591
   if self.base then
93✔
592
      self.base.relX = SILE.types.length(0)
186✔
593
      self.base.relY = SILE.types.length(0)
186✔
594
      -- Use widthForSubscript of base, if available
595
      self.width = self.base.widthForSubscript or self.base.width
93✔
596
   else
597
      self.width = SILE.types.length(0)
×
598
   end
599
   local itCorr = self:calculateItalicsCorrection() * scaleDown
186✔
600
   local subShift
601
   local supShift
602
   if self.sub then
93✔
603
      if self.isUnderOver or self.base.largeop then
67✔
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
34✔
609
      end
610
      self.sub.relX = self.width + subShift
134✔
611
      self.sub.relY = SILE.types.length(math.max(
134✔
612
         constants.subscriptShiftDown * scaleDown,
67✔
613
         --self.base.depth + constants.subscriptBaselineDropMin * scaleDown,
614
         (self.sub.height - constants.subscriptTopMax * scaleDown):tonumber()
134✔
615
      ))
67✔
616
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or self.base.largeop then
186✔
617
         self.sub.relY = maxLength(self.sub.relY, self.base.depth + constants.subscriptBaselineDropMin * scaleDown)
99✔
618
      end
619
   end
620
   if self.sup then
93✔
621
      if self.isUnderOver or self.base.largeop then
55✔
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
34✔
627
      end
628
      self.sup.relX = self.width + supShift
110✔
629
      self.sup.relY = SILE.types.length(math.max(
110✔
630
         isCrampedMode(self.mode) and constants.superscriptShiftUpCramped * scaleDown
110✔
631
            or constants.superscriptShiftUp * scaleDown, -- or cramped
55✔
632
         --self.base.height - constants.superscriptBaselineDropMax * scaleDown,
633
         (self.sup.depth + constants.superscriptBottomMin * scaleDown):tonumber()
110✔
634
      )) * -1
110✔
635
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or self.base.largeop then
150✔
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
93✔
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(
186✔
657
         self.sub and self.sub.width + subShift or SILE.types.length(0),
160✔
658
         self.sup and self.sup.width + supShift or SILE.types.length(0)
148✔
659
      )
93✔
660
      + constants.spaceAfterScript * scaleDown
186✔
661
   self.height = maxLength(
186✔
662
      self.base and self.base.height or SILE.types.length(0),
93✔
663
      self.sub and (self.sub.height - self.sub.relY) or SILE.types.length(0),
160✔
664
      self.sup and (self.sup.height - self.sup.relY) or SILE.types.length(0)
148✔
665
   )
93✔
666
   self.depth = maxLength(
186✔
667
      self.base and self.base.depth or SILE.types.length(0),
93✔
668
      self.sub and (self.sub.depth + self.sub.relY) or SILE.types.length(0),
160✔
669
      self.sup and (self.sup.depth + self.sup.relY) or SILE.types.length(0)
148✔
670
   )
93✔
671
end
672

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

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

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

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

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

711
function elements.underOver:shape ()
12✔
712
   if not (self.mode == mathMode.display or self.mode == mathMode.displayCramped) then
24✔
713
      self.isUnderOver = true
15✔
714
      elements.subscript.shape(self)
15✔
715
      return
15✔
716
   end
717
   local constants = self:getMathMetrics().constants
18✔
718
   local scaleDown = self:getScaleDown()
9✔
719
   -- Determine relative Ys
720
   if self.base then
9✔
721
      self.base.relY = SILE.types.length(0)
18✔
722
   end
723
   if self.sub then
9✔
724
      self.sub.relY = self.base.depth
9✔
725
         + SILE.types.length(
18✔
726
            math.max(
18✔
727
               (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
18✔
728
               constants.lowerLimitBaselineDropMin * scaleDown
9✔
729
            )
730
         )
18✔
731
   end
732
   if self.sup then
9✔
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
9✔
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
3✔
760
         widest = self.base
3✔
761
         a = self.sub
3✔
762
         b = self.sup
3✔
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)
18✔
774
   local c = widest.width / 2
9✔
775
   if a then
9✔
776
      a.relX = c - a.width / 2
27✔
777
   end
778
   if b then
9✔
779
      b.relX = c - b.width / 2
27✔
780
   end
781
   local itCorr = self:calculateItalicsCorrection() * scaleDown
18✔
782
   if self.sup then
9✔
783
      self.sup.relX = self.sup.relX + itCorr / 2
18✔
784
   end
785
   if self.sub then
9✔
786
      self.sub.relX = self.sub.relX - itCorr / 2
18✔
787
   end
788
   -- Determine width and height
789
   self.width = maxLength(
18✔
790
      self.base and self.base.width or SILE.types.length(0),
9✔
791
      self.sub and self.sub.width or SILE.types.length(0),
9✔
792
      self.sup and self.sup.width or SILE.types.length(0)
9✔
793
   )
9✔
794
   if self.sup then
9✔
795
      self.height = 0 - self.sup.relY + self.sup.height
27✔
796
   else
797
      self.height = self.base and self.base.height or 0
×
798
   end
799
   if self.sub then
9✔
800
      self.depth = self.sub.relY + self.sub.depth
18✔
801
   else
802
      self.depth = self.base and self.base.depth or 0
×
803
   end
804
end
805

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

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

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

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

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

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

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

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

845
function elements.space:__tostring ()
12✔
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
534✔
858
      local direction = 1
178✔
859
      if value:sub(1, 1) == "-" then
356✔
860
         value = value:sub(2, -1)
×
861
         direction = -1
×
862
      end
863
      if value == "thin" then
178✔
864
         return SILE.types.length("3mu") * direction
126✔
865
      elseif value == "med" then
136✔
866
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
216✔
867
      elseif value == "thick" then
64✔
868
         return SILE.types.length("5mu plus 5mu") * direction
192✔
869
      end
870
   end
871
   return SILE.types.length(value)
356✔
872
end
873

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

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

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

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

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

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

1071
function elements.text:output (x, y, line)
12✔
1072
   if not self.value.glyphString then
668✔
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,336✔
1077
      compensatedY = SILE.types.length(y.length + self.value.items[1].depth - self.value.items[1].fontDepth)
36✔
1078
   else
1079
      compensatedY = y
659✔
1080
   end
1081
   SILE.outputter:setCursor(scaleWidth(x, line), compensatedY.length)
1,336✔
1082
   SILE.outputter:setFont(self.font)
668✔
1083
   -- There should be no stretch or shrink on the width of a text
1084
   -- element.
1085
   local width = self.width.length
668✔
1086
   SILE.outputter:drawHbox(self.value, width)
668✔
1087
end
1088

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

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

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

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

1109
function elements.fraction:shape ()
12✔
1110
   -- Determine relative abscissas and width
1111
   local widest, other
1112
   if self.denominator.width > self.numerator.width then
32✔
1113
      widest, other = self.denominator, self.numerator
27✔
1114
   else
1115
      widest, other = self.numerator, self.denominator
5✔
1116
   end
1117
   widest.relX = SILE.types.length(0)
64✔
1118
   other.relX = (widest.width - other.width) / 2
96✔
1119
   self.width = widest.width
32✔
1120
   -- Determine relative ordinates and height
1121
   local constants = self:getMathMetrics().constants
64✔
1122
   local scaleDown = self:getScaleDown()
32✔
1123
   self.axisHeight = constants.axisHeight * scaleDown
32✔
1124
   self.ruleThickness = constants.fractionRuleThickness * scaleDown
32✔
1125
   if isDisplayMode(self.mode) then
64✔
1126
      self.numerator.relY = -self.axisHeight
9✔
1127
         - self.ruleThickness / 2
9✔
1128
         - SILE.types.length(
18✔
1129
            math.max(
18✔
1130
               (constants.fractionNumDisplayStyleGapMin * scaleDown + self.numerator.depth):tonumber(),
18✔
1131
               constants.fractionNumeratorDisplayStyleShiftUp * scaleDown - self.axisHeight - self.ruleThickness / 2
9✔
1132
            )
1133
         )
18✔
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
64✔
1145
      self.denominator.relY = -self.axisHeight
9✔
1146
         + self.ruleThickness / 2
9✔
1147
         + SILE.types.length(
18✔
1148
            math.max(
18✔
1149
               (constants.fractionDenomDisplayStyleGapMin * scaleDown + self.denominator.height):tonumber(),
18✔
1150
               constants.fractionDenominatorDisplayStyleShiftDown * scaleDown + self.axisHeight - self.ruleThickness / 2
9✔
1151
            )
1152
         )
18✔
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
64✔
1164
   self.depth = self.denominator.relY + self.denominator.depth
64✔
1165
end
1166

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

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

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

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

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

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

1200
function elements.mtr:styleChildren ()
12✔
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
18✔
1207

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

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

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

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

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

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

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

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

© 2026 Coveralls, Inc