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

sile-typesetter / sile / 11642878293

02 Nov 2024 12:56PM UTC coverage: 69.34% (+3.7%) from 65.595%
11642878293

push

github

alerque
chore(deps): Bump pinned versions of patch level crate updates

12579 of 18141 relevant lines covered (69.34%)

6066.76 hits per line

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

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

8
local atomType = syms.atomType
13✔
9
local symbolDefaults = syms.symbolDefaults
13✔
10

11
local elements = {}
13✔
12

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

24
local function isDisplayMode (mode)
25
   return mode <= 1
2,027✔
26
end
27

28
local function isCrampedMode (mode)
29
   return mode % 2 == 1
66✔
30
end
31

32
local function isScriptMode (mode)
33
   return mode == mathMode.script or mode == mathMode.scriptCramped
2,736✔
34
end
35

36
local function isScriptScriptMode (mode)
37
   return mode == mathMode.scriptScript or mode == mathMode.scriptScriptCramped
2,226✔
38
end
39

40
local mathCache = {}
13✔
41

42
local function retrieveMathTable (font)
43
   local key = SILE.font._key(font)
3,873✔
44
   if not mathCache[key] then
3,873✔
45
      SU.debug("math", "Loading math font", key)
36✔
46
      local face = SILE.font.cache(font, SILE.shaper.getFace)
36✔
47
      if not face then
36✔
48
         SU.error("Could not find requested font " .. font .. " or any suitable substitutes")
×
49
      end
50
      local fontHasMathTable, rawMathTable, mathTableParsable, mathTable
51
      fontHasMathTable, rawMathTable = pcall(hb.get_table, face, "MATH")
36✔
52
      if fontHasMathTable then
36✔
53
         mathTableParsable, mathTable = pcall(ot.parseMath, rawMathTable)
72✔
54
      end
55
      if not fontHasMathTable or not mathTableParsable then
36✔
56
         SU.error(([[
×
57
            You must use a math font for math rendering
58

59
            The math table in '%s' could not be %s.
60
         ]]):format(face.filename, fontHasMathTable and "parsed" or "loaded"))
×
61
      end
62
      local upem = ot.parseHead(hb.get_table(face, "head")).unitsPerEm
72✔
63
      local constants = {}
36✔
64
      for k, v in pairs(mathTable.mathConstants) do
2,052✔
65
         if type(v) == "table" then
2,016✔
66
            v = v.value
1,836✔
67
         end
68
         if k:sub(-9) == "ScaleDown" then
4,032✔
69
            constants[k] = v / 100
72✔
70
         else
71
            constants[k] = v * font.size / upem
1,944✔
72
         end
73
      end
74
      local italicsCorrection = {}
36✔
75
      for k, v in pairs(mathTable.mathItalicsCorrection) do
15,264✔
76
         italicsCorrection[k] = v.value * font.size / upem
15,228✔
77
      end
78
      mathCache[key] = {
36✔
79
         constants = constants,
36✔
80
         italicsCorrection = italicsCorrection,
36✔
81
         mathVariants = mathTable.mathVariants,
36✔
82
         unitsPerEm = upem,
36✔
83
      }
36✔
84
   end
85
   return mathCache[key]
3,873✔
86
end
87

88
-- Style transition functions for superscript and subscript
89
local function getSuperscriptMode (mode)
90
   -- D, T -> S
91
   if mode == mathMode.display or mode == mathMode.text then
75✔
92
      return mathMode.script
40✔
93
   -- D', T' -> S'
94
   elseif mode == mathMode.displayCramped or mode == mathMode.textCramped then
35✔
95
      return mathMode.scriptCramped
29✔
96
   -- S, SS -> SS
97
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
6✔
98
      return mathMode.scriptScript
3✔
99
   -- S', SS' -> SS'
100
   else
101
      return mathMode.scriptScriptCramped
3✔
102
   end
103
end
104
local function getSubscriptMode (mode)
105
   -- D, T, D', T' -> S'
106
   if
107
      mode == mathMode.display
102✔
108
      or mode == mathMode.text
60✔
109
      or mode == mathMode.displayCramped
50✔
110
      or mode == mathMode.textCramped
50✔
111
   then
112
      return mathMode.scriptCramped
83✔
113
   -- S, SS, S', SS' -> SS'
114
   else
115
      return mathMode.scriptScriptCramped
19✔
116
   end
117
end
118

119
-- Style transition functions for fraction (numerator and denominator)
120
local function getNumeratorMode (mode)
121
   -- D -> T
122
   if mode == mathMode.display then
33✔
123
      return mathMode.text
10✔
124
   -- D' -> T'
125
   elseif mode == mathMode.displayCramped then
23✔
126
      return mathMode.textCramped
×
127
   -- T -> S
128
   elseif mode == mathMode.text then
23✔
129
      return mathMode.script
×
130
   -- T' -> S'
131
   elseif mode == mathMode.textCramped then
23✔
132
      return mathMode.scriptCramped
13✔
133
   -- S, SS -> SS
134
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
10✔
135
      return mathMode.scriptScript
×
136
   -- S', SS' -> SS'
137
   else
138
      return mathMode.scriptScriptCramped
10✔
139
   end
140
end
141
local function getDenominatorMode (mode)
142
   -- D, D' -> T'
143
   if mode == mathMode.display or mode == mathMode.displayCramped then
33✔
144
      return mathMode.textCramped
10✔
145
   -- T, T' -> S'
146
   elseif mode == mathMode.text or mode == mathMode.textCramped then
23✔
147
      return mathMode.scriptCramped
13✔
148
   -- S, SS, S', SS' -> SS'
149
   else
150
      return mathMode.scriptScriptCramped
10✔
151
   end
152
end
153

154
local function getRightMostGlyphId (node)
155
   while node and node:is_a(elements.stackbox) and node.direction == "H" do
298✔
156
      node = node.children[#node.children]
12✔
157
   end
158
   if node and node:is_a(elements.text) then
274✔
159
      return node.value.glyphString[#node.value.glyphString]
135✔
160
   else
161
      return 0
4✔
162
   end
163
end
164

165
-- Compares two SILE.types.length, without considering shrink or stretch values, and
166
-- returns the biggest.
167
local function maxLength (...)
168
   local arg = { ... }
2,605✔
169
   local m
170
   for i, v in ipairs(arg) do
8,083✔
171
      if i == 1 then
5,478✔
172
         m = v
2,605✔
173
      else
174
         if v.length:tonumber() > m.length:tonumber() then
8,619✔
175
            m = v
645✔
176
         end
177
      end
178
   end
179
   return m
2,605✔
180
end
181

182
local function scaleWidth (length, line)
183
   local number = length.length
1,135✔
184
   if line.ratio and line.ratio < 0 and length.shrink:tonumber() > 0 then
1,135✔
185
      number = number + length.shrink * line.ratio
×
186
   elseif line.ratio and line.ratio > 0 and length.stretch:tonumber() > 0 then
2,270✔
187
      number = number + length.stretch * line.ratio
1,272✔
188
   end
189
   return number
1,135✔
190
end
191

192
-- math box, box with a horizontal shift value and could contain zero or more
193
-- mbox'es (or its child classes) the entire math environment itself is
194
-- a top-level mbox.
195
-- Typesetting of mbox evolves four steps:
196
--   1. Determine the mode for each mbox according to their parent.
197
--   2. Shape the mbox hierarchy from leaf to top. Get the shape and relative position.
198
--   3. Convert mbox into _nnode's to put in SILE's typesetting framework
199
elements.mbox = pl.class(nodefactory.hbox)
26✔
200
elements.mbox._type = "Mbox"
13✔
201

202
function elements.mbox:__tostring ()
26✔
203
   return self._type
×
204
end
205

206
function elements.mbox:_init ()
26✔
207
   nodefactory.hbox._init(self)
1,896✔
208
   self.font = {}
1,896✔
209
   self.children = {} -- The child nodes
1,896✔
210
   self.relX = SILE.types.length(0) -- x position relative to its parent box
3,792✔
211
   self.relY = SILE.types.length(0) -- y position relative to its parent box
3,792✔
212
   self.value = {}
1,896✔
213
   self.mode = mathMode.display
1,896✔
214
   self.atom = atomType.ordinary
1,896✔
215
   local font = {
1,896✔
216
      family = SILE.settings:get("math.font.family"),
3,792✔
217
      size = SILE.settings:get("math.font.size"),
3,792✔
218
      style = SILE.settings:get("math.font.style"),
3,792✔
219
      weight = SILE.settings:get("math.font.weight"),
3,792✔
220
   }
221
   local filename = SILE.settings:get("math.font.filename")
1,896✔
222
   if filename and filename ~= "" then
1,896✔
223
      font.filename = filename
×
224
   end
225
   self.font = SILE.font.loadDefaults(font)
3,792✔
226
end
227

228
function elements.mbox.styleChildren (_)
26✔
229
   SU.error("styleChildren is a virtual function that need to be overridden by its child classes")
×
230
end
231

232
function elements.mbox.shape (_, _, _)
26✔
233
   SU.error("shape is a virtual function that need to be overridden by its child classes")
×
234
end
235

236
function elements.mbox.output (_, _, _, _)
26✔
237
   SU.error("output is a virtual function that need to be overridden by its child classes")
×
238
end
239

240
function elements.mbox:getMathMetrics ()
26✔
241
   return retrieveMathTable(self.font)
3,873✔
242
end
243

244
function elements.mbox:getScaleDown ()
26✔
245
   local constants = self:getMathMetrics().constants
4,804✔
246
   local scaleDown
247
   if isScriptMode(self.mode) then
4,804✔
248
      scaleDown = constants.scriptPercentScaleDown
450✔
249
   elseif isScriptScriptMode(self.mode) then
3,904✔
250
      scaleDown = constants.scriptScriptPercentScaleDown
181✔
251
   else
252
      scaleDown = 1
1,771✔
253
   end
254
   return scaleDown
2,402✔
255
end
256

257
-- Determine the mode of its descendants
258
function elements.mbox:styleDescendants ()
26✔
259
   self:styleChildren()
1,908✔
260
   for _, n in ipairs(self.children) do
3,744✔
261
      if n then
1,836✔
262
         n:styleDescendants()
1,836✔
263
      end
264
   end
265
end
266

267
-- shapeTree shapes the mbox and all its descendants in a recursive fashion
268
-- The inner-most leaf nodes determine their shape first, and then propagate to their parents
269
-- During the process, each node will determine its size by (width, height, depth)
270
-- and (relX, relY) which the relative position to its parent
271
function elements.mbox:shapeTree ()
26✔
272
   for _, n in ipairs(self.children) do
3,744✔
273
      if n then
1,836✔
274
         n:shapeTree()
1,836✔
275
      end
276
   end
277
   self:shape()
1,908✔
278
end
279

280
-- Output the node and all its descendants
281
function elements.mbox:outputTree (x, y, line)
26✔
282
   self:output(x, y, line)
1,908✔
283
   local debug = SILE.settings:get("math.debug.boxes")
1,908✔
284
   if debug and not (self:is_a(elements.space)) then
1,908✔
285
      SILE.outputter:setCursor(scaleWidth(x, line), y.length)
×
286
      SILE.outputter:debugHbox({ height = self.height.length, depth = self.depth.length }, scaleWidth(self.width, line))
×
287
   end
288
   for _, n in ipairs(self.children) do
3,744✔
289
      if n then
1,836✔
290
         n:outputTree(x + n.relX, y + n.relY, line)
5,508✔
291
      end
292
   end
293
end
294

295
local spaceKind = {
13✔
296
   thin = "thin",
297
   med = "med",
298
   thick = "thick",
299
}
300

301
-- Indexed by left atom
302
local spacingRules = {
13✔
303
   [atomType.ordinary] = {
13✔
304
      [atomType.bigOperator] = { spaceKind.thin },
13✔
305
      [atomType.binaryOperator] = { spaceKind.med, notScript = true },
13✔
306
      [atomType.relationalOperator] = { spaceKind.thick, notScript = true },
13✔
307
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
308
   },
13✔
309
   [atomType.bigOperator] = {
13✔
310
      [atomType.ordinary] = { spaceKind.thin },
13✔
311
      [atomType.bigOperator] = { spaceKind.thin },
13✔
312
      [atomType.relationalOperator] = { spaceKind.thick, notScript = true },
13✔
313
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
314
   },
13✔
315
   [atomType.binaryOperator] = {
13✔
316
      [atomType.ordinary] = { spaceKind.med, notScript = true },
13✔
317
      [atomType.bigOperator] = { spaceKind.med, notScript = true },
13✔
318
      [atomType.openingSymbol] = { spaceKind.med, notScript = true },
13✔
319
      [atomType.inner] = { spaceKind.med, notScript = true },
13✔
320
   },
13✔
321
   [atomType.relationalOperator] = {
13✔
322
      [atomType.ordinary] = { spaceKind.thick, notScript = true },
13✔
323
      [atomType.bigOperator] = { spaceKind.thick, notScript = true },
13✔
324
      [atomType.openingSymbol] = { spaceKind.thick, notScript = true },
13✔
325
      [atomType.inner] = { spaceKind.thick, notScript = true },
13✔
326
   },
13✔
327
   [atomType.closeSymbol] = {
13✔
328
      [atomType.bigOperator] = { spaceKind.thin },
13✔
329
      [atomType.binaryOperator] = { spaceKind.med, notScript = true },
13✔
330
      [atomType.relationalOperator] = { spaceKind.thick, notScript = true },
13✔
331
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
332
   },
13✔
333
   [atomType.punctuationSymbol] = {
13✔
334
      [atomType.ordinary] = { spaceKind.thin, notScript = true },
13✔
335
      [atomType.bigOperator] = { spaceKind.thin, notScript = true },
13✔
336
      [atomType.relationalOperator] = { spaceKind.thin, notScript = true },
13✔
337
      [atomType.openingSymbol] = { spaceKind.thin, notScript = true },
13✔
338
      [atomType.closeSymbol] = { spaceKind.thin, notScript = true },
13✔
339
      [atomType.punctuationSymbol] = { spaceKind.thin, notScript = true },
13✔
340
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
341
   },
13✔
342
   [atomType.inner] = {
13✔
343
      [atomType.ordinary] = { spaceKind.thin, notScript = true },
13✔
344
      [atomType.bigOperator] = { spaceKind.thin },
13✔
345
      [atomType.binaryOperator] = { spaceKind.med, notScript = true },
13✔
346
      [atomType.relationalOperator] = { spaceKind.thick, notScript = true },
13✔
347
      [atomType.openingSymbol] = { spaceKind.thin, notScript = true },
13✔
348
      [atomType.punctuationSymbol] = { spaceKind.thin, notScript = true },
13✔
349
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
350
   },
13✔
351
}
352

353
-- _stackbox stacks its content one, either horizontally or vertically
354
elements.stackbox = pl.class(elements.mbox)
26✔
355
elements.stackbox._type = "Stackbox"
13✔
356

357
function elements.stackbox:__tostring ()
26✔
358
   local result = self.direction .. "Box("
×
359
   for i, n in ipairs(self.children) do
×
360
      result = result .. (i == 1 and "" or ", ") .. tostring(n)
×
361
   end
362
   result = result .. ")"
×
363
   return result
×
364
end
365

366
function elements.stackbox:_init (direction, children)
26✔
367
   elements.mbox._init(self)
399✔
368
   if not (direction == "H" or direction == "V") then
399✔
369
      SU.error("Wrong direction '" .. direction .. "'; should be H or V")
×
370
   end
371
   self.direction = direction
399✔
372
   self.children = children
399✔
373
end
374

375
function elements.stackbox:styleChildren ()
26✔
376
   for _, n in ipairs(self.children) do
1,530✔
377
      n.mode = self.mode
1,131✔
378
   end
379
   if self.direction == "H" then
399✔
380
      -- Insert spaces according to the atom type, following Knuth's guidelines
381
      -- in the TeXbook
382
      local spaces = {}
327✔
383
      for i = 1, #self.children - 1 do
1,065✔
384
         local v = self.children[i]
738✔
385
         local v2 = self.children[i + 1]
738✔
386
         if spacingRules[v.atom] and spacingRules[v.atom][v2.atom] then
738✔
387
            local rule = spacingRules[v.atom][v2.atom]
359✔
388
            if not (rule.notScript and (isScriptMode(self.mode) or isScriptScriptMode(self.mode))) then
967✔
389
               spaces[i + 1] = rule[1]
261✔
390
            end
391
         end
392
      end
393
      local spaceIdx = {}
327✔
394
      for i, _ in pairs(spaces) do
588✔
395
         table.insert(spaceIdx, i)
261✔
396
      end
397
      table.sort(spaceIdx, function (a, b)
654✔
398
         return a > b
377✔
399
      end)
400
      for _, idx in ipairs(spaceIdx) do
588✔
401
         local hsp = elements.space(spaces[idx], 0, 0)
261✔
402
         table.insert(self.children, idx, hsp)
261✔
403
      end
404
   end
405
end
406

407
function elements.stackbox:shape ()
26✔
408
   -- For a horizontal stackbox (i.e. mrow):
409
   -- 1. set self.height and self.depth to max element height & depth
410
   -- 2. handle stretchy operators
411
   -- 3. set self.width
412
   -- For a vertical stackbox:
413
   -- 1. set self.width to max element width
414
   -- 2. set self.height
415
   -- And finally set children's relative coordinates
416
   self.height = SILE.types.length(0)
798✔
417
   self.depth = SILE.types.length(0)
798✔
418
   if self.direction == "H" then
399✔
419
      for i, n in ipairs(self.children) do
1,647✔
420
         n.relY = SILE.types.length(0)
2,640✔
421
         self.height = i == 1 and n.height or maxLength(self.height, n.height)
2,319✔
422
         self.depth = i == 1 and n.depth or maxLength(self.depth, n.depth)
2,319✔
423
      end
424
      -- Handle stretchy operators
425
      for _, elt in ipairs(self.children) do
1,647✔
426
         if elt.is_a(elements.text) and elt.kind == "operator" and elt.stretchy then
2,640✔
427
            elt:stretchyReshape(self.depth, self.height)
79✔
428
         end
429
      end
430
      -- Set self.width
431
      self.width = SILE.types.length(0)
654✔
432
      for i, n in ipairs(self.children) do
1,647✔
433
         n.relX = self.width
1,320✔
434
         self.width = i == 1 and n.width or self.width + n.width
2,319✔
435
      end
436
   else -- self.direction == "V"
437
      for i, n in ipairs(self.children) do
144✔
438
         n.relX = SILE.types.length(0)
144✔
439
         self.width = i == 1 and n.width or maxLength(self.width, n.width)
72✔
440
      end
441
      -- Set self.height and self.depth
442
      for i, n in ipairs(self.children) do
144✔
443
         self.depth = i == 1 and n.depth or self.depth + n.depth
72✔
444
      end
445
      for i = 1, #self.children do
144✔
446
         local n = self.children[i]
72✔
447
         if i == 1 then
72✔
448
            self.height = n.height
72✔
449
            self.depth = n.depth
72✔
450
         elseif i > 1 then
×
451
            n.relY = self.children[i - 1].relY + self.children[i - 1].depth + n.height
×
452
            self.depth = self.depth + n.height + n.depth
×
453
         end
454
      end
455
   end
456
end
457

458
-- Despite of its name, this function actually output the whole tree of nodes recursively.
459
function elements.stackbox:outputYourself (typesetter, line)
26✔
460
   local mathX = typesetter.frame.state.cursorX
72✔
461
   local mathY = typesetter.frame.state.cursorY
72✔
462
   self:outputTree(self.relX + mathX, self.relY + mathY, line)
216✔
463
   typesetter.frame:advanceWritingDirection(scaleWidth(self.width, line))
144✔
464
end
465

466
function elements.stackbox.output (_, _, _, _) end
412✔
467

468
elements.phantom = pl.class(elements.stackbox) -- inherit from stackbox
26✔
469
elements.phantom._type = "Phantom"
13✔
470

471
function elements.phantom:_init (children, special)
26✔
472
   -- MathML core 3.3.7:
473
   -- "Its layout algorithm is the same as the mrow element".
474
   -- Also not the MathML states that <mphantom> is sort of legacy, "implemented
475
   -- for compatibility with full MathML. Authors whose only target is MathML
476
   -- Core are encouraged to use CSS for styling."
477
   -- The thing is that we don't have CSS in SILE, so supporting <mphantom> is
478
   -- a must.
479
   elements.stackbox._init(self, "H", children)
×
480
   self.special = special
×
481
end
482

483
function elements.phantom:shape ()
26✔
484
   elements.stackbox.shape(self)
×
485
   -- From https://latexref.xyz:
486
   -- "The \vphantom variant produces an invisible box with the same vertical size
487
   -- as subformula, the same height and depth, but having zero width.
488
   -- And \hphantom makes a box with the same width as subformula but
489
   -- with zero height and depth."
490
   if self.special == "v" then
×
491
      self.width = SILE.types.length()
×
492
   elseif self.special == "h" then
×
493
      self.height = SILE.types.length()
×
494
      self.depth = SILE.types.length()
×
495
   end
496
end
497

498
function elements.phantom:output (_, _, _)
26✔
499
   -- Note the trick here: when the tree is rendered, the node's output
500
   -- function is invoked, then all its children's output functions.
501
   -- So we just cancel the list of children here, before it's rendered.
502
   self.children = {}
×
503
end
504

505
elements.subscript = pl.class(elements.mbox)
26✔
506
elements.subscript._type = "Subscript"
13✔
507

508
function elements.subscript:__tostring ()
26✔
509
   return (self.sub and "Subscript" or "Superscript")
×
510
      .. "("
×
511
      .. tostring(self.base)
×
512
      .. ", "
×
513
      .. tostring(self.sub or self.super)
×
514
      .. ")"
×
515
end
516

517
function elements.subscript:_init (base, sub, sup)
26✔
518
   elements.mbox._init(self)
114✔
519
   self.base = base
114✔
520
   self.sub = sub
114✔
521
   self.sup = sup
114✔
522
   if self.base then
114✔
523
      table.insert(self.children, self.base)
114✔
524
   end
525
   if self.sub then
114✔
526
      table.insert(self.children, self.sub)
77✔
527
   end
528
   if self.sup then
114✔
529
      table.insert(self.children, self.sup)
51✔
530
   end
531
   self.atom = self.base.atom
114✔
532
end
533

534
function elements.subscript:styleChildren ()
26✔
535
   if self.base then
114✔
536
      self.base.mode = self.mode
114✔
537
   end
538
   if self.sub then
114✔
539
      self.sub.mode = getSubscriptMode(self.mode)
154✔
540
   end
541
   if self.sup then
114✔
542
      self.sup.mode = getSuperscriptMode(self.mode)
102✔
543
   end
544
end
545

546
function elements.subscript:calculateItalicsCorrection ()
26✔
547
   local lastGid = getRightMostGlyphId(self.base)
114✔
548
   if lastGid > 0 then
114✔
549
      local mathMetrics = self:getMathMetrics()
110✔
550
      if mathMetrics.italicsCorrection[lastGid] then
110✔
551
         return mathMetrics.italicsCorrection[lastGid]
83✔
552
      end
553
   end
554
   return 0
31✔
555
end
556

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

643
function elements.subscript.output (_, _, _, _) end
127✔
644

645
elements.underOver = pl.class(elements.subscript)
26✔
646
elements.underOver._type = "UnderOver"
13✔
647

648
function elements.underOver:__tostring ()
26✔
649
   return self._type .. "(" .. tostring(self.base) .. ", " .. tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
×
650
end
651

652
function elements.underOver:_init (base, sub, sup)
26✔
653
   elements.mbox._init(self)
25✔
654
   self.atom = base.atom
25✔
655
   self.base = base
25✔
656
   self.sub = sub
25✔
657
   self.sup = sup
25✔
658
   if self.sup then
25✔
659
      table.insert(self.children, self.sup)
24✔
660
   end
661
   if self.base then
25✔
662
      table.insert(self.children, self.base)
25✔
663
   end
664
   if self.sub then
25✔
665
      table.insert(self.children, self.sub)
25✔
666
   end
667
end
668

669
function elements.underOver:styleChildren ()
26✔
670
   if self.base then
25✔
671
      self.base.mode = self.mode
25✔
672
   end
673
   if self.sub then
25✔
674
      self.sub.mode = getSubscriptMode(self.mode)
50✔
675
   end
676
   if self.sup then
25✔
677
      self.sup.mode = getSuperscriptMode(self.mode)
48✔
678
   end
679
end
680

681
function elements.underOver:shape ()
26✔
682
   if not (self.mode == mathMode.display or self.mode == mathMode.displayCramped) then
25✔
683
      self.isUnderOver = true
15✔
684
      elements.subscript.shape(self)
15✔
685
      return
15✔
686
   end
687
   local constants = self:getMathMetrics().constants
20✔
688
   local scaleDown = self:getScaleDown()
10✔
689
   -- Determine relative Ys
690
   if self.base then
10✔
691
      self.base.relY = SILE.types.length(0)
20✔
692
   end
693
   if self.sub then
10✔
694
      self.sub.relY = self.base.depth
10✔
695
         + SILE.types.length(
20✔
696
            math.max(
20✔
697
               (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
20✔
698
               constants.lowerLimitBaselineDropMin * scaleDown
10✔
699
            )
700
         )
20✔
701
   end
702
   if self.sup then
10✔
703
      self.sup.relY = 0
9✔
704
         - self.base.height
9✔
705
         - SILE.types.length(
18✔
706
            math.max(
18✔
707
               (constants.upperLimitGapMin * scaleDown + self.sup.depth):tonumber(),
18✔
708
               constants.upperLimitBaselineRiseMin * scaleDown
9✔
709
            )
710
         )
18✔
711
   end
712
   -- Determine relative Xs based on widest symbol
713
   local widest, a, b
714
   if self.sub and self.sub.width > self.base.width then
10✔
715
      if self.sup and self.sub.width > self.sup.width then
6✔
716
         widest = self.sub
6✔
717
         a = self.base
6✔
718
         b = self.sup
6✔
719
      elseif self.sup then
×
720
         widest = self.sup
×
721
         a = self.base
×
722
         b = self.sub
×
723
      else
724
         widest = self.sub
×
725
         a = self.base
×
726
         b = nil
×
727
      end
728
   else
729
      if self.sup and self.base.width > self.sup.width then
4✔
730
         widest = self.base
3✔
731
         a = self.sub
3✔
732
         b = self.sup
3✔
733
      elseif self.sup then
1✔
734
         widest = self.sup
×
735
         a = self.base
×
736
         b = self.sub
×
737
      else
738
         widest = self.base
1✔
739
         a = self.sub
1✔
740
         b = nil
1✔
741
      end
742
   end
743
   widest.relX = SILE.types.length(0)
20✔
744
   local c = widest.width / 2
10✔
745
   if a then
10✔
746
      a.relX = c - a.width / 2
30✔
747
   end
748
   if b then
10✔
749
      b.relX = c - b.width / 2
27✔
750
   end
751
   local itCorr = self:calculateItalicsCorrection() * scaleDown
20✔
752
   if self.sup then
10✔
753
      self.sup.relX = self.sup.relX + itCorr / 2
18✔
754
   end
755
   if self.sub then
10✔
756
      self.sub.relX = self.sub.relX - itCorr / 2
20✔
757
   end
758
   -- Determine width and height
759
   self.width = maxLength(
20✔
760
      self.base and self.base.width or SILE.types.length(0),
10✔
761
      self.sub and self.sub.width or SILE.types.length(0),
10✔
762
      self.sup and self.sup.width or SILE.types.length(0)
10✔
763
   )
10✔
764
   if self.sup then
10✔
765
      self.height = 0 - self.sup.relY + self.sup.height
27✔
766
   else
767
      self.height = self.base and self.base.height or 0
1✔
768
   end
769
   if self.sub then
10✔
770
      self.depth = self.sub.relY + self.sub.depth
20✔
771
   else
772
      self.depth = self.base and self.base.depth or 0
×
773
   end
774
end
775

776
function elements.underOver:calculateItalicsCorrection ()
26✔
777
   local lastGid = getRightMostGlyphId(self.base)
25✔
778
   if lastGid > 0 then
25✔
779
      local mathMetrics = self:getMathMetrics()
25✔
780
      if mathMetrics.italicsCorrection[lastGid] then
25✔
781
         local c = mathMetrics.italicsCorrection[lastGid]
×
782
         -- If this is a big operator, and we are in display style, then the
783
         -- base glyph may be bigger than the font size. We need to adjust the
784
         -- italic correction accordingly.
785
         if self.base.atom == atomType.bigOperator and isDisplayMode(self.mode) then
×
786
            c = c * (self.base and self.base.font.size / self.font.size or 1.0)
×
787
         end
788
         return c
×
789
      end
790
   end
791
   return 0
25✔
792
end
793

794
function elements.underOver.output (_, _, _, _) end
38✔
795

796
-- terminal is the base class for leaf node
797
elements.terminal = pl.class(elements.mbox)
26✔
798
elements.terminal._type = "Terminal"
13✔
799

800
function elements.terminal:_init ()
26✔
801
   elements.mbox._init(self)
1,316✔
802
end
803

804
function elements.terminal.styleChildren (_) end
1,329✔
805

806
function elements.terminal.shape (_) end
13✔
807

808
elements.space = pl.class(elements.terminal)
26✔
809
elements.space._type = "Space"
13✔
810

811
function elements.space:_init ()
26✔
812
   elements.terminal._init(self)
×
813
end
814

815
function elements.space:__tostring ()
26✔
816
   return self._type
×
817
      .. "(width="
×
818
      .. tostring(self.width)
×
819
      .. ", height="
×
820
      .. tostring(self.height)
×
821
      .. ", depth="
×
822
      .. tostring(self.depth)
×
823
      .. ")"
×
824
end
825

826
local function getStandardLength (value)
827
   if type(value) == "string" then
957✔
828
      local direction = 1
319✔
829
      if value:sub(1, 1) == "-" then
638✔
830
         value = value:sub(2, -1)
20✔
831
         direction = -1
10✔
832
      end
833
      if value == "thin" then
319✔
834
         return SILE.types.length("3mu") * direction
198✔
835
      elseif value == "med" then
253✔
836
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
294✔
837
      elseif value == "thick" then
155✔
838
         return SILE.types.length("5mu plus 5mu") * direction
393✔
839
      end
840
   end
841
   return SILE.types.length(value)
662✔
842
end
843

844
function elements.space:_init (width, height, depth)
26✔
845
   elements.terminal._init(self)
319✔
846
   self.width = getStandardLength(width)
638✔
847
   self.height = getStandardLength(height)
638✔
848
   self.depth = getStandardLength(depth)
638✔
849
end
850

851
function elements.space:shape ()
26✔
852
   self.width = self.width:absolute() * self:getScaleDown()
1,276✔
853
   self.height = self.height:absolute() * self:getScaleDown()
1,276✔
854
   self.depth = self.depth:absolute() * self:getScaleDown()
1,276✔
855
end
856

857
function elements.space.output (_) end
332✔
858

859
-- text node. For any actual text output
860
elements.text = pl.class(elements.terminal)
26✔
861
elements.text._type = "Text"
13✔
862

863
function elements.text:__tostring ()
26✔
864
   return self._type
×
865
      .. "(atom="
×
866
      .. tostring(self.atom)
×
867
      .. ", kind="
×
868
      .. tostring(self.kind)
×
869
      .. ", script="
×
870
      .. tostring(self.script)
×
871
      .. (self.stretchy and ", stretchy" or "")
×
872
      .. (self.largeop and ", largeop" or "")
×
873
      .. ', text="'
×
874
      .. (self.originalText or self.text)
×
875
      .. '")'
×
876
end
877

878
function elements.text:_init (kind, attributes, script, text)
26✔
879
   elements.terminal._init(self)
997✔
880
   if not (kind == "number" or kind == "identifier" or kind == "operator" or kind == "string") then
997✔
881
      SU.error("Unknown text node kind '" .. kind .. "'; should be one of: number, identifier, operator, string")
×
882
   end
883
   self.kind = kind
997✔
884
   self.script = script
997✔
885
   self.text = text
997✔
886
   if self.script ~= "upright" then
997✔
887
      local converted = convertMathVariantScript(self.text, self.script)
997✔
888
      self.originalText = self.text
997✔
889
      self.text = converted
997✔
890
   end
891
   if self.kind == "operator" then
997✔
892
      if self.text == "-" then
394✔
893
         self.text = "−"
13✔
894
      end
895
   end
896
   for attribute, value in pairs(attributes) do
1,457✔
897
      self[attribute] = value
460✔
898
   end
899
end
900

901
function elements.text:shape ()
26✔
902
   self.font.size = self.font.size * self:getScaleDown()
1,994✔
903
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
997✔
904
   local mathMetrics = self:getMathMetrics()
997✔
905
   local glyphs = SILE.shaper:shapeToken(self.text, self.font)
997✔
906
   -- Use bigger variants for big operators in display style
907
   if isDisplayMode(self.mode) and self.largeop then
1,994✔
908
      -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
909
      glyphs = pl.tablex.deepcopy(glyphs)
38✔
910
      local constructions = mathMetrics.mathVariants.vertGlyphConstructions[glyphs[1].gid]
19✔
911
      if constructions then
19✔
912
         local displayVariants = constructions.mathGlyphVariantRecord
19✔
913
         -- We select the biggest variant. TODO: we should probably select the
914
         -- first variant that is higher than displayOperatorMinHeight.
915
         local biggest
916
         local m = 0
19✔
917
         for _, v in ipairs(displayVariants) do
57✔
918
            if v.advanceMeasurement > m then
38✔
919
               biggest = v
38✔
920
               m = v.advanceMeasurement
38✔
921
            end
922
         end
923
         if biggest then
19✔
924
            glyphs[1].gid = biggest.variantGlyph
19✔
925
            local dimen = hb.get_glyph_dimensions(face, self.font.size, biggest.variantGlyph)
19✔
926
            glyphs[1].width = dimen.width
19✔
927
            glyphs[1].glyphAdvance = dimen.glyphAdvance
19✔
928
            --[[ I am told (https://github.com/alif-type/xits/issues/90) that,
929
        in fact, the relative height and depth of display-style big operators
930
        in the font is not relevant, as these should be centered around the
931
        axis. So the following code does that, while conserving their
932
        vertical size (distance from top to bottom). ]]
933
            local axisHeight = mathMetrics.constants.axisHeight * self:getScaleDown()
38✔
934
            local y_size = dimen.height + dimen.depth
19✔
935
            glyphs[1].height = y_size / 2 + axisHeight
19✔
936
            glyphs[1].depth = y_size / 2 - axisHeight
19✔
937
            -- We still need to store the font's height and depth somewhere,
938
            -- because that's what will be used to draw the glyph, and we will need
939
            -- to artificially compensate for that.
940
            glyphs[1].fontHeight = dimen.height
19✔
941
            glyphs[1].fontDepth = dimen.depth
19✔
942
         end
943
      end
944
   end
945
   SILE.shaper:preAddNodes(glyphs, self.value)
997✔
946
   self.value.items = glyphs
997✔
947
   self.value.glyphString = {}
997✔
948
   if glyphs and #glyphs > 0 then
997✔
949
      for i = 1, #glyphs do
2,241✔
950
         table.insert(self.value.glyphString, glyphs[i].gid)
1,244✔
951
      end
952
      self.width = SILE.types.length(0)
1,994✔
953
      self.widthForSubscript = SILE.types.length(0)
1,994✔
954
      for i = #glyphs, 1, -1 do
2,241✔
955
         self.width = self.width + glyphs[i].glyphAdvance
2,488✔
956
      end
957
      -- Store width without italic correction somewhere
958
      self.widthForSubscript = self.width
997✔
959
      local itCorr = mathMetrics.italicsCorrection[glyphs[#glyphs].gid]
997✔
960
      if itCorr then
997✔
961
         self.width = self.width + itCorr * self:getScaleDown()
744✔
962
      end
963
      for i = 1, #glyphs do
2,241✔
964
         self.height = i == 1 and SILE.types.length(glyphs[i].height)
1,244✔
965
            or SILE.types.length(math.max(self.height:tonumber(), glyphs[i].height))
1,738✔
966
         self.depth = i == 1 and SILE.types.length(glyphs[i].depth)
1,244✔
967
            or SILE.types.length(math.max(self.depth:tonumber(), glyphs[i].depth))
1,738✔
968
      end
969
   else
970
      self.width = SILE.types.length(0)
×
971
      self.height = SILE.types.length(0)
×
972
      self.depth = SILE.types.length(0)
×
973
   end
974
end
975

976
function elements.text:stretchyReshape (depth, height)
26✔
977
   -- Required depth+height of stretched glyph, in font units
978
   local mathMetrics = self:getMathMetrics()
79✔
979
   local upem = mathMetrics.unitsPerEm
79✔
980
   local sz = self.font.size
79✔
981
   local requiredAdvance = (depth + height):tonumber() * upem / sz
237✔
982
   SU.debug("math", "stretch: rA =", requiredAdvance)
79✔
983
   -- Choose variant of the closest size. The criterion we use is to have
984
   -- an advance measurement as close as possible as the required one.
985
   -- The advance measurement is simply the depth+height of the glyph.
986
   -- Therefore, the selected glyph may be smaller or bigger than
987
   -- required.  TODO: implement assembly of stretchable glyphs form
988
   -- their parts for cases when the biggest variant is not big enough.
989
   -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
990
   local glyphs = pl.tablex.deepcopy(self.value.items)
79✔
991
   local constructions = self:getMathMetrics().mathVariants.vertGlyphConstructions[glyphs[1].gid]
158✔
992
   if constructions then
79✔
993
      local variants = constructions.mathGlyphVariantRecord
79✔
994
      SU.debug("math", "stretch: variants =", variants)
79✔
995
      local closest
996
      local closestI
997
      local m = requiredAdvance - (self.depth + self.height):tonumber() * upem / sz
237✔
998
      SU.debug("math", "stretch: m =", m)
79✔
999
      for i, v in ipairs(variants) do
1,106✔
1000
         local diff = math.abs(v.advanceMeasurement - requiredAdvance)
1,027✔
1001
         SU.debug("math", "stretch: diff =", diff)
1,027✔
1002
         if diff < m then
1,027✔
1003
            closest = v
117✔
1004
            closestI = i
117✔
1005
            m = diff
117✔
1006
         end
1007
      end
1008
      SU.debug("math", "stretch: closestI =", closestI)
79✔
1009
      if closest then
79✔
1010
         -- Now we have to re-shape the glyph chain. We will assume there
1011
         -- is only one glyph.
1012
         -- TODO: this code is probably wrong when the vertical
1013
         -- variants have a different width than the original, because
1014
         -- the shaping phase is already done. Need to do better.
1015
         glyphs[1].gid = closest.variantGlyph
63✔
1016
         local face = SILE.font.cache(self.font, SILE.shaper.getFace)
63✔
1017
         local dimen = hb.get_glyph_dimensions(face, self.font.size, closest.variantGlyph)
63✔
1018
         glyphs[1].width = dimen.width
63✔
1019
         glyphs[1].height = dimen.height
63✔
1020
         glyphs[1].depth = dimen.depth
63✔
1021
         glyphs[1].glyphAdvance = dimen.glyphAdvance
63✔
1022
         self.width = SILE.types.length(dimen.glyphAdvance)
126✔
1023
         self.depth = SILE.types.length(dimen.depth)
126✔
1024
         self.height = SILE.types.length(dimen.height)
126✔
1025
         SILE.shaper:preAddNodes(glyphs, self.value)
63✔
1026
         self.value.items = glyphs
63✔
1027
         self.value.glyphString = { glyphs[1].gid }
63✔
1028
      end
1029
   end
1030
end
1031

1032
function elements.text:output (x, y, line)
26✔
1033
   if not self.value.glyphString then
997✔
1034
      return
×
1035
   end
1036
   local compensatedY
1037
   if isDisplayMode(self.mode) and self.atom == atomType.bigOperator and self.value.items[1].fontDepth then
1,994✔
1038
      compensatedY = SILE.types.length(y.length + self.value.items[1].depth - self.value.items[1].fontDepth)
40✔
1039
   else
1040
      compensatedY = y
987✔
1041
   end
1042
   SILE.outputter:setCursor(scaleWidth(x, line), compensatedY.length)
1,994✔
1043
   SILE.outputter:setFont(self.font)
997✔
1044
   -- There should be no stretch or shrink on the width of a text
1045
   -- element.
1046
   local width = self.width.length
997✔
1047
   SILE.outputter:drawHbox(self.value, width)
997✔
1048
end
1049

1050
elements.fraction = pl.class(elements.mbox)
26✔
1051
elements.fraction._type = "Fraction"
13✔
1052

1053
function elements.fraction:__tostring ()
26✔
1054
   return self._type .. "(" .. tostring(self.numerator) .. ", " .. tostring(self.denominator) .. ")"
×
1055
end
1056

1057
function elements.fraction:_init (numerator, denominator)
26✔
1058
   elements.mbox._init(self)
33✔
1059
   self.numerator = numerator
33✔
1060
   self.denominator = denominator
33✔
1061
   table.insert(self.children, numerator)
33✔
1062
   table.insert(self.children, denominator)
33✔
1063
end
1064

1065
function elements.fraction:styleChildren ()
26✔
1066
   self.numerator.mode = getNumeratorMode(self.mode)
66✔
1067
   self.denominator.mode = getDenominatorMode(self.mode)
66✔
1068
end
1069

1070
function elements.fraction:shape ()
26✔
1071
   -- MathML Core 3.3.2: "To avoid visual confusion between the fraction bar
1072
   -- and another adjacent items (e.g. minus sign or another fraction's bar),"
1073
   -- By convention, here we use 1px = 1/96in = 0.75pt.
1074
   -- Note that PlainTeX would likely use \nulldelimiterspace (default 1.2pt)
1075
   -- but it would depend on the surrounding context, and might be far too
1076
   -- much in some cases, so we stick to MathML's suggested padding.
1077
   self.padding = SILE.types.length(0.75)
66✔
1078

1079
   -- Determine relative abscissas and width
1080
   local widest, other
1081
   if self.denominator.width > self.numerator.width then
33✔
1082
      widest, other = self.denominator, self.numerator
25✔
1083
   else
1084
      widest, other = self.numerator, self.denominator
8✔
1085
   end
1086
   widest.relX = self.padding
33✔
1087
   other.relX = self.padding + (widest.width - other.width) / 2
132✔
1088
   self.width = widest.width + 2 * self.padding
99✔
1089
   -- Determine relative ordinates and height
1090
   local constants = self:getMathMetrics().constants
66✔
1091
   local scaleDown = self:getScaleDown()
33✔
1092
   self.axisHeight = constants.axisHeight * scaleDown
33✔
1093
   self.ruleThickness = constants.fractionRuleThickness * scaleDown
33✔
1094

1095
   local numeratorGapMin, denominatorGapMin, numeratorShiftUp, denominatorShiftDown
1096
   if isDisplayMode(self.mode) then
66✔
1097
      numeratorGapMin = constants.fractionNumDisplayStyleGapMin * scaleDown
10✔
1098
      denominatorGapMin = constants.fractionDenomDisplayStyleGapMin * scaleDown
10✔
1099
      numeratorShiftUp = constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
10✔
1100
      denominatorShiftDown = constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
10✔
1101
   else
1102
      numeratorGapMin = constants.fractionNumeratorGapMin * scaleDown
23✔
1103
      denominatorGapMin = constants.fractionDenominatorGapMin * scaleDown
23✔
1104
      numeratorShiftUp = constants.fractionNumeratorShiftUp * scaleDown
23✔
1105
      denominatorShiftDown = constants.fractionDenominatorShiftDown * scaleDown
23✔
1106
   end
1107

1108
   self.numerator.relY = -self.axisHeight
33✔
1109
      - self.ruleThickness / 2
33✔
1110
      - SILE.types.length(
66✔
1111
         math.max(
66✔
1112
            (numeratorGapMin + self.numerator.depth):tonumber(),
66✔
1113
            numeratorShiftUp - self.axisHeight - self.ruleThickness / 2
33✔
1114
         )
1115
      )
66✔
1116
   self.denominator.relY = -self.axisHeight
33✔
1117
      + self.ruleThickness / 2
33✔
1118
      + SILE.types.length(
66✔
1119
         math.max(
66✔
1120
            (denominatorGapMin + self.denominator.height):tonumber(),
66✔
1121
            denominatorShiftDown + self.axisHeight - self.ruleThickness / 2
33✔
1122
         )
1123
      )
66✔
1124
   self.height = self.numerator.height - self.numerator.relY
66✔
1125
   self.depth = self.denominator.relY + self.denominator.depth
66✔
1126
end
1127

1128
function elements.fraction:output (x, y, line)
26✔
1129
   SILE.outputter:drawRule(
66✔
1130
      scaleWidth(x + self.padding, line),
66✔
1131
      y.length - self.axisHeight - self.ruleThickness / 2,
66✔
1132
      scaleWidth(self.width - 2 * self.padding, line),
99✔
1133
      self.ruleThickness
1134
   )
33✔
1135
end
1136

1137
local function newSubscript (spec)
1138
   return elements.subscript(spec.base, spec.sub, spec.sup)
114✔
1139
end
1140

1141
local function newUnderOver (spec)
1142
   return elements.underOver(spec.base, spec.sub, spec.sup)
25✔
1143
end
1144

1145
-- TODO replace with penlight equivalent
1146
local function mapList (f, l)
1147
   local ret = {}
9✔
1148
   for i, x in ipairs(l) do
35✔
1149
      ret[i] = f(i, x)
52✔
1150
   end
1151
   return ret
9✔
1152
end
1153

1154
elements.mtr = pl.class(elements.mbox)
26✔
1155
-- elements.mtr._type = "" -- TODO why not set?
1156

1157
function elements.mtr:_init (children)
26✔
1158
   self.children = children
12✔
1159
end
1160

1161
function elements.mtr:styleChildren ()
26✔
1162
   for _, c in ipairs(self.children) do
48✔
1163
      c.mode = self.mode
36✔
1164
   end
1165
end
1166

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

1169
function elements.mtr.output (_) end
25✔
1170

1171
elements.table = pl.class(elements.mbox)
26✔
1172
elements.table._type = "table" -- TODO why case difference?
13✔
1173

1174
function elements.table:_init (children, options)
26✔
1175
   elements.mbox._init(self)
9✔
1176
   self.children = children
9✔
1177
   self.options = options
9✔
1178
   self.nrows = #self.children
9✔
1179
   self.ncols = math.max(pl.utils.unpack(mapList(function (_, row)
36✔
1180
      return #row.children
26✔
1181
   end, self.children)))
18✔
1182
   SU.debug("math", "self.ncols =", self.ncols)
9✔
1183
   self.rowspacing = self.options.rowspacing and SILE.types.length(self.options.rowspacing) or SILE.types.length("7pt")
18✔
1184
   self.columnspacing = self.options.columnspacing and SILE.types.length(self.options.columnspacing)
9✔
1185
      or SILE.types.length("6pt")
18✔
1186
   -- Pad rows that do not have enough cells by adding cells to the
1187
   -- right.
1188
   for i, row in ipairs(self.children) do
35✔
1189
      for j = 1, (self.ncols - #row.children) do
26✔
1190
         SU.debug("math", "padding i =", i, "j =", j)
×
1191
         table.insert(row.children, elements.stackbox("H", {}))
×
1192
         SU.debug("math", "size", #row.children)
×
1193
      end
1194
   end
1195
   if options.columnalign then
9✔
1196
      local l = {}
5✔
1197
      for w in string.gmatch(options.columnalign, "[^%s]+") do
20✔
1198
         if not (w == "left" or w == "center" or w == "right") then
15✔
1199
            SU.error("Invalid specifier in `columnalign` attribute: " .. w)
×
1200
         end
1201
         table.insert(l, w)
15✔
1202
      end
1203
      -- Pad with last value of l if necessary
1204
      for _ = 1, (self.ncols - #l), 1 do
5✔
1205
         table.insert(l, l[#l])
×
1206
      end
1207
      -- On the contrary, remove excess values in l if necessary
1208
      for _ = 1, (#l - self.ncols), 1 do
5✔
1209
         table.remove(l)
×
1210
      end
1211
      self.options.columnalign = l
5✔
1212
   else
1213
      self.options.columnalign = pl.List.range(1, self.ncols):map(function (_)
14✔
1214
         return "center"
12✔
1215
      end)
1216
   end
1217
end
1218

1219
function elements.table:styleChildren ()
26✔
1220
   if self.mode == mathMode.display and self.options.displaystyle ~= "false" then
9✔
1221
      for _, c in ipairs(self.children) do
19✔
1222
         c.mode = mathMode.display
14✔
1223
      end
1224
   else
1225
      for _, c in ipairs(self.children) do
16✔
1226
         c.mode = mathMode.text
12✔
1227
      end
1228
   end
1229
end
1230

1231
function elements.table:shape ()
26✔
1232
   -- Determine the height (resp. depth) of each row, which is the max
1233
   -- height (resp. depth) among its elements. Then we only need to add it to
1234
   -- the table's height and center every cell vertically.
1235
   for _, row in ipairs(self.children) do
35✔
1236
      row.height = SILE.types.length(0)
52✔
1237
      row.depth = SILE.types.length(0)
52✔
1238
      for _, cell in ipairs(row.children) do
104✔
1239
         row.height = maxLength(row.height, cell.height)
156✔
1240
         row.depth = maxLength(row.depth, cell.depth)
156✔
1241
      end
1242
   end
1243
   self.vertSize = SILE.types.length(0)
18✔
1244
   for i, row in ipairs(self.children) do
35✔
1245
      self.vertSize = self.vertSize
×
1246
         + row.height
26✔
1247
         + row.depth
26✔
1248
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
61✔
1249
   end
1250
   local rowHeightSoFar = SILE.types.length(0)
9✔
1251
   for i, row in ipairs(self.children) do
35✔
1252
      row.relY = rowHeightSoFar + row.height - self.vertSize
78✔
1253
      rowHeightSoFar = rowHeightSoFar
×
1254
         + row.height
26✔
1255
         + row.depth
26✔
1256
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
35✔
1257
   end
1258
   self.width = SILE.types.length(0)
18✔
1259
   local thisColRelX = SILE.types.length(0)
9✔
1260
   -- For every column...
1261
   for i = 1, self.ncols do
36✔
1262
      -- Determine its width
1263
      local columnWidth = SILE.types.length(0)
27✔
1264
      for j = 1, self.nrows do
105✔
1265
         if self.children[j].children[i].width > columnWidth then
78✔
1266
            columnWidth = self.children[j].children[i].width
37✔
1267
         end
1268
      end
1269
      -- Use it to align the contents of every cell as required.
1270
      for j = 1, self.nrows do
105✔
1271
         local cell = self.children[j].children[i]
78✔
1272
         if self.options.columnalign[i] == "left" then
78✔
1273
            cell.relX = thisColRelX
14✔
1274
         elseif self.options.columnalign[i] == "center" then
64✔
1275
            cell.relX = thisColRelX + (columnWidth - cell.width) / 2
200✔
1276
         elseif self.options.columnalign[i] == "right" then
14✔
1277
            cell.relX = thisColRelX + (columnWidth - cell.width)
42✔
1278
         else
1279
            SU.error("invalid columnalign parameter")
×
1280
         end
1281
      end
1282
      thisColRelX = thisColRelX + columnWidth + (i == self.ncols and SILE.types.length(0) or self.columnspacing) -- Spacing
63✔
1283
   end
1284
   self.width = thisColRelX
9✔
1285
   -- Center myself vertically around the axis, and update relative Ys of rows accordingly
1286
   local axisHeight = self:getMathMetrics().constants.axisHeight * self:getScaleDown()
27✔
1287
   self.height = self.vertSize / 2 + axisHeight
27✔
1288
   self.depth = self.vertSize / 2 - axisHeight
27✔
1289
   for _, row in ipairs(self.children) do
35✔
1290
      row.relY = row.relY + self.vertSize / 2 - axisHeight
104✔
1291
      -- Also adjust width
1292
      row.width = self.width
26✔
1293
   end
1294
end
1295

1296
function elements.table.output (_) end
22✔
1297

1298
local function getRadicandMode (mode)
1299
   -- Not too sure if we should do something special/
1300
   return mode
×
1301
end
1302

1303
local function getDegreeMode (mode)
1304
   -- 2 levels smaller, up to scriptScript evntually.
1305
   -- Not too sure if we should do something else.
1306
   if mode == mathMode.display then
×
1307
      return mathMode.scriptScript
×
1308
   elseif mode == mathMode.displayCramped then
×
1309
      return mathMode.scriptScriptCramped
×
1310
   elseif mode == mathMode.text or mode == mathMode.script or mode == mathMode.scriptScript then
×
1311
      return mathMode.scriptScript
×
1312
   end
1313
   return mathMode.scriptScriptCramped
×
1314
end
1315

1316
elements.sqrt = pl.class(elements.mbox)
26✔
1317
elements.sqrt._type = "Sqrt"
13✔
1318

1319
function elements.sqrt:__tostring ()
26✔
1320
   return self._type .. "(" .. tostring(self.radicand) .. (self.degree and ", " .. tostring(self.degree) or "") .. ")"
×
1321
end
1322

1323
function elements.sqrt:_init (radicand, degree)
26✔
1324
   elements.mbox._init(self)
×
1325
   self.radicand = radicand
×
1326
   if degree then
×
1327
      self.degree = degree
×
1328
      table.insert(self.children, degree)
×
1329
   end
1330
   table.insert(self.children, radicand)
×
1331
   self.relX = SILE.types.length()
×
1332
   self.relY = SILE.types.length()
×
1333
end
1334

1335
function elements.sqrt:styleChildren ()
26✔
1336
   self.radicand.mode = getRadicandMode(self.mode)
×
1337
   if self.degree then
×
1338
      self.degree.mode = getDegreeMode(self.mode)
×
1339
   end
1340
end
1341

1342
function elements.sqrt:shape ()
26✔
1343
   local mathMetrics = self:getMathMetrics()
×
1344
   local scaleDown = self:getScaleDown()
×
1345
   local constants = mathMetrics.constants
×
1346

1347
   self.radicalRuleThickness = constants.radicalRuleThickness * scaleDown
×
1348
   if self.mode == mathMode.display or self.mode == mathMode.displayCramped then
×
1349
      self.radicalVerticalGap = constants.radicalDisplayStyleVerticalGap * scaleDown
×
1350
   else
1351
      self.radicalVerticalGap = constants.radicalVerticalGap * scaleDown
×
1352
   end
1353
   self.extraAscender = constants.radicalExtraAscender * scaleDown
×
1354

1355
   -- HACK: We draw own own radical sign in the output() method.
1356
   -- Derive dimensions for the radical sign (more or less ad hoc).
1357
   -- Note: In TeX, the radical sign extends a lot below the baseline,
1358
   -- and MathML Core also has a lot of layout text about it.
1359
   -- Not only it doesn't look good, but it's not very clear vs. OpenType.
1360
   local radicalGlyph = SILE.shaper:measureChar("√")
×
1361
   local ratio = (self.radicand.height:tonumber() + self.radicand.depth:tonumber())
×
1362
      / (radicalGlyph.height + radicalGlyph.depth)
×
1363
   local vertAdHocOffset = (ratio > 1 and math.log(ratio) or 0) * self.radicalVerticalGap
×
1364
   self.symbolHeight = SILE.types.length(radicalGlyph.height) * scaleDown
×
1365
   self.symbolDepth = (SILE.types.length(radicalGlyph.depth) + vertAdHocOffset) * scaleDown
×
1366
   self.symbolWidth = (SILE.types.length(radicalGlyph.width) + vertAdHocOffset) * scaleDown
×
1367

1368
   -- Adjust the height of the radical sign if the radicand is higher
1369
   self.symbolHeight = self.radicand.height > self.symbolHeight and self.radicand.height or self.symbolHeight
×
1370
   -- Compute the (max-)height of the short leg of the radical sign
1371
   self.symbolShortHeight = self.symbolHeight * constants.radicalDegreeBottomRaisePercent
×
1372

1373
   self.offsetX = SILE.types.length()
×
1374
   if self.degree then
×
1375
      -- Position the degree
1376
      self.degree.relY = -constants.radicalDegreeBottomRaisePercent * self.symbolHeight
×
1377
      -- Adjust the height of the short leg of the radical sign to ensure the degree is not too close
1378
      -- (empirically use radicalExtraAscender)
1379
      self.symbolShortHeight = self.symbolShortHeight - constants.radicalExtraAscender * scaleDown
×
1380
      -- Compute the width adjustment for the degree
1381
      self.offsetX = self.degree.width
×
1382
         + constants.radicalKernBeforeDegree * scaleDown
×
1383
         + constants.radicalKernAfterDegree * scaleDown
×
1384
   end
1385
   -- Position the radicand
1386
   self.radicand.relX = self.symbolWidth + self.offsetX
×
1387
   -- Compute the dimentions of the whole radical
1388
   self.width = self.radicand.width + self.symbolWidth + self.offsetX
×
1389
   self.height = self.symbolHeight + self.radicalVerticalGap + self.extraAscender
×
1390
   self.depth = self.radicand.depth
×
1391
end
1392

1393
local function _r (number)
1394
   -- Lua 5.3+ formats floats as 1.0 and integers as 1
1395
   -- Also some PDF readers do not like double precision.
1396
   return math.floor(number) == number and math.floor(number) or tonumber(string.format("%.5f", number))
×
1397
end
1398

1399
function elements.sqrt:output (x, y, line)
26✔
1400
   -- HACK:
1401
   -- OpenType might say we need to assemble the radical sign from parts.
1402
   -- Frankly, it's much easier to just draw it as a graphic :-)
1403
   -- Hence, here we use a PDF graphic operators to draw a nice radical sign.
1404
   -- Some values here are ad hoc, but they look good.
1405
   local h = self.height:tonumber()
×
1406
   local d = self.depth:tonumber()
×
1407
   local s0 = scaleWidth(self.offsetX, line):tonumber()
×
1408
   local sw = scaleWidth(self.symbolWidth, line):tonumber()
×
1409
   local dsh = h - self.symbolShortHeight:tonumber()
×
1410
   local dsd = self.symbolDepth:tonumber()
×
1411
   local symbol = {
×
1412
      _r(self.radicalRuleThickness),
×
1413
      "w", -- line width
1414
      2,
1415
      "j", -- round line joins
1416
      _r(sw + s0),
×
1417
      _r(self.extraAscender),
×
1418
      "m",
1419
      _r(s0 + sw * 0.90),
×
1420
      _r(self.extraAscender),
×
1421
      "l",
1422
      _r(s0 + sw * 0.4),
×
1423
      _r(h + d + dsd),
×
1424
      "l",
1425
      _r(s0 + sw * 0.2),
×
1426
      _r(dsh),
×
1427
      "l",
1428
      s0 + sw * 0.1,
×
1429
      _r(dsh + 0.5),
×
1430
      "l",
1431
      "S",
1432
   }
1433
   local svg = table.concat(symbol, " ")
×
1434
   local xscaled = scaleWidth(x, line)
×
1435
   SILE.outputter:drawSVG(svg, xscaled, y, sw, h, 1)
×
1436
   -- And now we just need to draw the bar over the radicand
1437
   SILE.outputter:drawRule(
×
1438
      s0 + self.symbolWidth + xscaled,
×
1439
      y.length - self.height + self.extraAscender - self.radicalRuleThickness / 2,
×
1440
      scaleWidth(self.radicand.width, line),
×
1441
      self.radicalRuleThickness
1442
   )
1443
end
1444

1445
elements.mathMode = mathMode
13✔
1446
elements.atomType = atomType
13✔
1447
elements.symbolDefaults = symbolDefaults
13✔
1448
elements.newSubscript = newSubscript
13✔
1449
elements.newUnderOver = newUnderOver
13✔
1450

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

© 2026 Coveralls, Inc