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

sile-typesetter / sile / 11826759401

13 Nov 2024 10:16PM UTC coverage: 61.013% (-2.4%) from 63.4%
11826759401

push

github

web-flow
Merge pull request #2165 from Omikhleia/feat-bevelled-fractions

feat(math): Support MathML bevelled fractions

7 of 38 new or added lines in 2 files covered. (18.42%)

581 existing lines in 28 files now uncovered.

11155 of 18283 relevant lines covered (61.01%)

2604.98 hits per line

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

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

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

11
local elements = {}
10✔
12

13
local mathMode = {
10✔
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
1,881✔
26
end
27

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

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

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

40
local mathCache = {}
10✔
41

42
local function retrieveMathTable (font)
43
   local key = SILE.font._key(font)
3,514✔
44
   if not mathCache[key] then
3,514✔
45
      SU.debug("math", "Loading math font", key)
32✔
46
      local face = SILE.font.cache(font, SILE.shaper.getFace)
32✔
47
      if not face then
32✔
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")
32✔
52
      if fontHasMathTable then
32✔
53
         mathTableParsable, mathTable = pcall(ot.parseMath, rawMathTable)
64✔
54
      end
55
      if not fontHasMathTable or not mathTableParsable then
32✔
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
64✔
63
      local constants = {}
32✔
64
      for k, v in pairs(mathTable.mathConstants) do
1,824✔
65
         if type(v) == "table" then
1,792✔
66
            v = v.value
1,632✔
67
         end
68
         if k:sub(-9) == "ScaleDown" then
3,584✔
69
            constants[k] = v / 100
64✔
70
         else
71
            constants[k] = v * font.size / upem
1,728✔
72
         end
73
      end
74
      local italicsCorrection = {}
32✔
75
      for k, v in pairs(mathTable.mathItalicsCorrection) do
13,568✔
76
         italicsCorrection[k] = v.value * font.size / upem
13,536✔
77
      end
78
      mathCache[key] = {
32✔
79
         constants = constants,
32✔
80
         italicsCorrection = italicsCorrection,
32✔
81
         mathVariants = mathTable.mathVariants,
32✔
82
         unitsPerEm = upem,
32✔
83
      }
32✔
84
   end
85
   return mathCache[key]
3,514✔
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
66✔
92
      return mathMode.script
31✔
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
99✔
108
      or mode == mathMode.text
60✔
109
      or mode == mathMode.displayCramped
50✔
110
      or mode == mathMode.textCramped
50✔
111
   then
112
      return mathMode.scriptCramped
80✔
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
280✔
156
      node = node.children[#node.children]
15✔
157
   end
158
   if node and node:is_a(elements.text) then
250✔
159
      return node.value.glyphString[#node.value.glyphString]
123✔
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,411✔
169
   local m
170
   for i, v in ipairs(arg) do
7,477✔
171
      if i == 1 then
5,066✔
172
         m = v
2,411✔
173
      else
174
         if v.length:tonumber() > m.length:tonumber() then
7,965✔
175
            m = v
600✔
176
         end
177
      end
178
   end
179
   return m
2,411✔
180
end
181

182
local function scaleWidth (length, line)
183
   local number = length.length
1,052✔
184
   if line.ratio and line.ratio < 0 and length.shrink:tonumber() > 0 then
1,052✔
185
      number = number + length.shrink * line.ratio
×
186
   elseif line.ratio and line.ratio > 0 and length.stretch:tonumber() > 0 then
2,104✔
187
      number = number + length.stretch * line.ratio
1,174✔
188
   end
189
   return number
1,052✔
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)
20✔
200
elements.mbox._type = "Mbox"
10✔
201

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

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

228
function elements.mbox.styleChildren (_)
20✔
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 (_, _, _)
20✔
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 (_, _, _, _)
20✔
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 ()
20✔
241
   return retrieveMathTable(self.font)
3,514✔
242
end
243

244
function elements.mbox:getScaleDown ()
20✔
245
   local constants = self:getMathMetrics().constants
4,442✔
246
   local scaleDown
247
   if isScriptMode(self.mode) then
4,442✔
248
      scaleDown = constants.scriptPercentScaleDown
416✔
249
   elseif isScriptScriptMode(self.mode) then
3,610✔
250
      scaleDown = constants.scriptScriptPercentScaleDown
181✔
251
   else
252
      scaleDown = 1
1,624✔
253
   end
254
   return scaleDown
2,221✔
255
end
256

257
-- Determine the mode of its descendants
258
function elements.mbox:styleDescendants ()
20✔
259
   self:styleChildren()
1,783✔
260
   for _, n in ipairs(self.children) do
3,504✔
261
      if n then
1,721✔
262
         n:styleDescendants()
1,721✔
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 ()
20✔
272
   for _, n in ipairs(self.children) do
3,504✔
273
      if n then
1,721✔
274
         n:shapeTree()
1,721✔
275
      end
276
   end
277
   self:shape()
1,783✔
278
end
279

280
-- Output the node and all its descendants
281
function elements.mbox:outputTree (x, y, line)
20✔
282
   self:output(x, y, line)
1,783✔
283
   local debug = SILE.settings:get("math.debug.boxes")
1,783✔
284
   if debug and not (self:is_a(elements.space)) then
1,783✔
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,504✔
289
      if n then
1,721✔
290
         n:outputTree(x + n.relX, y + n.relY, line)
5,163✔
291
      end
292
   end
293
end
294

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

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

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

357
function elements.stackbox:__tostring ()
20✔
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)
20✔
367
   elements.mbox._init(self)
383✔
368
   if not (direction == "H" or direction == "V") then
383✔
369
      SU.error("Wrong direction '" .. direction .. "'; should be H or V")
×
370
   end
371
   self.direction = direction
383✔
372
   self.children = children
383✔
373
end
374

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

407
function elements.stackbox:shape ()
20✔
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)
766✔
417
   self.depth = SILE.types.length(0)
766✔
418
   if self.direction == "H" then
383✔
419
      for i, n in ipairs(self.children) do
1,686✔
420
         n.relY = SILE.types.length(0)
2,606✔
421
         self.height = i == 1 and n.height or maxLength(self.height, n.height)
2,229✔
422
         self.depth = i == 1 and n.depth or maxLength(self.depth, n.depth)
2,229✔
423
      end
424
      -- Handle stretchy operators
425
      for _, elt in ipairs(self.children) do
1,686✔
426
         if elt.is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
2,911✔
427
            elt:_vertStretchyReshape(self.depth, self.height)
78✔
428
         end
429
      end
430
      -- Set self.width
431
      self.width = SILE.types.length(0)
766✔
432
      for i, n in ipairs(self.children) do
1,686✔
433
         n.relX = self.width
1,303✔
434
         self.width = i == 1 and n.width or self.width + n.width
2,229✔
435
      end
436
   else -- self.direction == "V"
437
      for i, n in ipairs(self.children) do
×
438
         n.relX = SILE.types.length(0)
×
439
         self.width = i == 1 and n.width or maxLength(self.width, n.width)
×
440
      end
441
      -- Set self.height and self.depth
442
      for i, n in ipairs(self.children) do
×
443
         self.depth = i == 1 and n.depth or self.depth + n.depth
×
444
      end
445
      for i = 1, #self.children do
×
446
         local n = self.children[i]
×
447
         if i == 1 then
×
448
            self.height = n.height
×
449
            self.depth = n.depth
×
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)
20✔
460
   local mathX = typesetter.frame.state.cursorX
62✔
461
   local mathY = typesetter.frame.state.cursorY
62✔
462
   self:outputTree(self.relX + mathX, self.relY + mathY, line)
186✔
463
   typesetter.frame:advanceWritingDirection(scaleWidth(self.width, line))
124✔
464
end
465

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

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

471
function elements.phantom:_init (children)
20✔
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
end
481

482
function elements.phantom:output (_, _, _)
20✔
483
   -- Note the trick here: when the tree is rendered, the node's output
484
   -- function is invoked, then all its children's output functions.
485
   -- So we just cancel the list of children here, before it's rendered.
486
   self.children = {}
×
487
end
488

489
elements.subscript = pl.class(elements.mbox)
20✔
490
elements.subscript._type = "Subscript"
10✔
491

492
function elements.subscript:__tostring ()
20✔
493
   return (self.sub and "Subscript" or "Superscript")
×
494
      .. "("
×
495
      .. tostring(self.base)
×
496
      .. ", "
×
497
      .. tostring(self.sub or self.super)
×
498
      .. ")"
×
499
end
500

501
function elements.subscript:_init (base, sub, sup)
20✔
502
   elements.mbox._init(self)
102✔
503
   self.base = base
102✔
504
   self.sub = sub
102✔
505
   self.sup = sup
102✔
506
   if self.base then
102✔
507
      table.insert(self.children, self.base)
102✔
508
   end
509
   if self.sub then
102✔
510
      table.insert(self.children, self.sub)
74✔
511
   end
512
   if self.sup then
102✔
513
      table.insert(self.children, self.sup)
42✔
514
   end
515
   self.atom = self.base.atom
102✔
516
end
517

518
function elements.subscript:styleChildren ()
20✔
519
   if self.base then
102✔
520
      self.base.mode = self.mode
102✔
521
   end
522
   if self.sub then
102✔
523
      self.sub.mode = getSubscriptMode(self.mode)
148✔
524
   end
525
   if self.sup then
102✔
526
      self.sup.mode = getSuperscriptMode(self.mode)
84✔
527
   end
528
end
529

530
function elements.subscript:calculateItalicsCorrection ()
20✔
531
   local lastGid = getRightMostGlyphId(self.base)
102✔
532
   if lastGid > 0 then
102✔
533
      local mathMetrics = self:getMathMetrics()
98✔
534
      if mathMetrics.italicsCorrection[lastGid] then
98✔
535
         return mathMetrics.italicsCorrection[lastGid]
71✔
536
      end
537
   end
538
   return 0
31✔
539
end
540

541
function elements.subscript:shape ()
20✔
542
   local mathMetrics = self:getMathMetrics()
117✔
543
   local constants = mathMetrics.constants
117✔
544
   local scaleDown = self:getScaleDown()
117✔
545
   if self.base then
117✔
546
      self.base.relX = SILE.types.length(0)
234✔
547
      self.base.relY = SILE.types.length(0)
234✔
548
      -- Use widthForSubscript of base, if available
549
      self.width = self.base.widthForSubscript or self.base.width
117✔
550
   else
551
      self.width = SILE.types.length(0)
×
552
   end
553
   local itCorr = self:calculateItalicsCorrection() * scaleDown
234✔
554
   local isBaseSymbol = not self.base or self.base:is_a(elements.terminal)
234✔
555
   local isBaseLargeOp = SU.boolean(self.base and self.base.largeop, false)
117✔
556
   local subShift
557
   local supShift
558
   if self.sub then
117✔
559
      if self.isUnderOver or isBaseLargeOp then
89✔
560
         -- Ad hoc correction on integral limits, following LuaTeX's
561
         -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
562
         subShift = -itCorr
33✔
563
      else
564
         subShift = 0
56✔
565
      end
566
      self.sub.relX = self.width + subShift
178✔
567
      self.sub.relY = SILE.types.length(
178✔
568
         math.max(
178✔
569
            constants.subscriptShiftDown * scaleDown,
89✔
570
            isBaseSymbol and 0 -- TeX (σ19) is more finicky than MathML Core
89✔
571
               or (self.base.depth + constants.subscriptBaselineDropMin * scaleDown):tonumber(),
93✔
572
            (self.sub.height - constants.subscriptTopMax * scaleDown):tonumber()
178✔
573
         )
574
      )
89✔
575
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or isBaseLargeOp then
252✔
576
         self.sub.relY = maxLength(self.sub.relY, self.base.depth + constants.subscriptBaselineDropMin * scaleDown)
99✔
577
      end
578
   end
579
   if self.sup then
117✔
580
      if self.isUnderOver or isBaseLargeOp then
57✔
581
         -- Ad hoc correction on integral limits, following LuaTeX's
582
         -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
583
         supShift = 0
21✔
584
      else
585
         supShift = itCorr
36✔
586
      end
587
      self.sup.relX = self.width + supShift
114✔
588
      self.sup.relY = SILE.types.length(
114✔
589
         math.max(
114✔
590
            isCrampedMode(self.mode) and constants.superscriptShiftUpCramped * scaleDown
114✔
591
               or constants.superscriptShiftUp * scaleDown,
57✔
592
            isBaseSymbol and 0 -- TeX (σ18) is more finicky than MathML Core
57✔
593
               or (self.base.height - constants.superscriptBaselineDropMax * scaleDown):tonumber(),
69✔
594
            (self.sup.depth + constants.superscriptBottomMin * scaleDown):tonumber()
114✔
595
         )
596
      ) * -1
114✔
597
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or isBaseLargeOp then
156✔
598
         self.sup.relY = maxLength(
42✔
599
            (0 - self.sup.relY),
21✔
600
            self.base.height - constants.superscriptBaselineDropMax * scaleDown
21✔
601
         ) * -1
42✔
602
      end
603
   end
604
   if self.sub and self.sup then
117✔
605
      local gap = self.sub.relY - self.sub.height - self.sup.relY - self.sup.depth
87✔
606
      if gap.length:tonumber() < constants.subSuperscriptGapMin * scaleDown then
58✔
607
         -- The following adjustment comes directly from Appendix G of he
608
         -- TeXbook (rule 18e).
609
         self.sub.relY = constants.subSuperscriptGapMin * scaleDown + self.sub.height + self.sup.relY + self.sup.depth
32✔
610
         local psi = constants.superscriptBottomMaxWithSubscript * scaleDown + self.sup.relY + self.sup.depth
16✔
611
         if psi:tonumber() > 0 then
16✔
612
            self.sup.relY = self.sup.relY - psi
16✔
613
            self.sub.relY = self.sub.relY - psi
16✔
614
         end
615
      end
616
   end
617
   self.width = self.width
×
618
      + maxLength(
234✔
619
         self.sub and self.sub.width + subShift or SILE.types.length(0),
206✔
620
         self.sup and self.sup.width + supShift or SILE.types.length(0)
174✔
621
      )
117✔
622
      + constants.spaceAfterScript * scaleDown
234✔
623
   self.height = maxLength(
234✔
624
      self.base and self.base.height or SILE.types.length(0),
117✔
625
      self.sub and (self.sub.height - self.sub.relY) or SILE.types.length(0),
206✔
626
      self.sup and (self.sup.height - self.sup.relY) or SILE.types.length(0)
174✔
627
   )
117✔
628
   self.depth = maxLength(
234✔
629
      self.base and self.base.depth or SILE.types.length(0),
117✔
630
      self.sub and (self.sub.depth + self.sub.relY) or SILE.types.length(0),
206✔
631
      self.sup and (self.sup.depth + self.sup.relY) or SILE.types.length(0)
174✔
632
   )
117✔
633
end
634

635
function elements.subscript.output (_, _, _, _) end
112✔
636

637
elements.underOver = pl.class(elements.subscript)
20✔
638
elements.underOver._type = "UnderOver"
10✔
639

640
function elements.underOver:__tostring ()
20✔
641
   return self._type .. "(" .. tostring(self.base) .. ", " .. tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
×
642
end
643

644
local function isNotEmpty (element)
645
   -- The MathML test suite uses <munderover> with an empty <mrow> as sub/sup.
646
   -- I don't know why they didn't use a <munder> or <mover> instead...
647
   -- But the expectation is to behave as if the empty element was not there,
648
   -- so that height and depth are not affected by the axis height.
649
   -- See notably:
650
   --   MathML3 "complex1" torture test: Maxwell's Equations (vectors in fractions)
651
   return element and (element:is_a(elements.terminal) or #element.children > 0)
99✔
652
end
653

654
function elements.underOver:_init (base, sub, sup)
20✔
655
   elements.mbox._init(self)
25✔
656
   self.atom = base.atom
25✔
657
   self.base = base
25✔
658
   self.sub = isNotEmpty(sub) and sub or nil
50✔
659
   self.sup = isNotEmpty(sup) and sup or nil
50✔
660
   if self.sup then
25✔
661
      table.insert(self.children, self.sup)
24✔
662
   end
663
   if self.base then
25✔
664
      table.insert(self.children, self.base)
25✔
665
   end
666
   if self.sub then
25✔
667
      table.insert(self.children, self.sub)
25✔
668
   end
669
end
670

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

683
function elements.underOver:_stretchyReshapeToBase (part)
20✔
684
   -- FIXME: Big leap of faith here.
685
   -- MathML Core only mentions stretching along the inline axis in 3.4.2.2,
686
   -- i.e. under the section on <mover>, <munder>, <munderover>.
687
   -- So we are "somewhat" good here, but... the algorithm is totally unclear
688
   -- to me and seems to imply a lot of recursion and reshaping.
689
   -- The implementation below is NOT general and only works for the cases
690
   -- I checked:
691
   --   Mozilla MathML tests: braces in f19, f22
692
   --   Personal tests: vectors in d19, d22, d23
693
   --   Joe Javawaski's tests: braces in 8a, 8b
694
   --   MathML3 "complex1" torture test: Maxwell's Equations (vectors in fractions)
695
   if #part.children == 0 then
19✔
696
      local elt = part
10✔
697
      if elt.is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
28✔
698
         elt:_horizStretchyReshape(self.base.width)
×
699
      end
700
   elseif part:is_a(elements.underOver) then
18✔
701
      -- Big assumption here: only considering one level of stacked under/over.
702
      local hasStretched = false
×
703
      for _, elt in ipairs(part.children) do
×
704
         if elt.is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
×
705
            local stretched = elt:_horizStretchyReshape(self.base.width)
×
706
            if stretched then
×
707
               hasStretched = true
×
708
            end
709
         end
710
      end
711
      if hasStretched then
×
712
         -- We need to re-calculate the shape so positions are re-calculated on each
713
         -- of its own parts.
714
         -- (Added after seeing that Mozilla test f19 was not rendering correctly.)
715
         part:shape()
×
716
      end
717
   end
718
end
719

720
function elements.underOver:shape ()
20✔
721
   local isMovableLimits = SU.boolean(self.base and self.base.movablelimits, false)
25✔
722
   if not (self.mode == mathMode.display or self.mode == mathMode.displayCramped) and isMovableLimits then
25✔
723
      -- When the base is a movable limit, the under/over scripts are not placed under/over the base,
724
      -- but other to the right of it, when display mode is not used.
725
      -- Notable effects:
726
      --   Mozilla MathML test 19 (on "k times" > overbrace > base)
727
      --   Maxwell's Equations in MathML3 Test Suite "complex1" (on the vectors in fractions)
728
      self.isUnderOver = true
15✔
729
      elements.subscript.shape(self)
15✔
730
      return
15✔
731
   end
732
   local constants = self:getMathMetrics().constants
20✔
733
   local scaleDown = self:getScaleDown()
10✔
734
   -- Determine relative Ys
735
   if self.base then
10✔
736
      self.base.relY = SILE.types.length(0)
20✔
737
   end
738
   if self.sub then
10✔
739
      self:_stretchyReshapeToBase(self.sub)
10✔
740
      self.sub.relY = self.base.depth
10✔
741
         + SILE.types.length(
20✔
742
            math.max(
20✔
743
               (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
20✔
744
               constants.lowerLimitBaselineDropMin * scaleDown
10✔
745
            )
746
         )
20✔
747
   end
748
   if self.sup then
10✔
749
      self:_stretchyReshapeToBase(self.sup)
9✔
750
      self.sup.relY = 0
9✔
751
         - self.base.height
9✔
752
         - SILE.types.length(
18✔
753
            math.max(
18✔
754
               (constants.upperLimitGapMin * scaleDown + self.sup.depth):tonumber(),
18✔
755
               constants.upperLimitBaselineRiseMin * scaleDown
9✔
756
            )
757
         )
18✔
758
   end
759
   -- Determine relative Xs based on widest symbol
760
   local widest, a, b
761
   if self.sub and self.sub.width > self.base.width then
10✔
762
      if self.sup and self.sub.width > self.sup.width then
6✔
763
         widest = self.sub
6✔
764
         a = self.base
6✔
765
         b = self.sup
6✔
UNCOV
766
      elseif self.sup then
×
UNCOV
767
         widest = self.sup
×
UNCOV
768
         a = self.base
×
UNCOV
769
         b = self.sub
×
770
      else
771
         widest = self.sub
×
772
         a = self.base
×
773
         b = nil
×
774
      end
775
   else
776
      if self.sup and self.base.width > self.sup.width then
4✔
777
         widest = self.base
3✔
778
         a = self.sub
3✔
779
         b = self.sup
3✔
780
      elseif self.sup then
1✔
UNCOV
781
         widest = self.sup
×
UNCOV
782
         a = self.base
×
UNCOV
783
         b = self.sub
×
784
      else
785
         widest = self.base
1✔
786
         a = self.sub
1✔
787
         b = nil
1✔
788
      end
789
   end
790
   widest.relX = SILE.types.length(0)
20✔
791
   local c = widest.width / 2
10✔
792
   if a then
10✔
793
      a.relX = c - a.width / 2
30✔
794
   end
795
   if b then
10✔
796
      b.relX = c - b.width / 2
27✔
797
   end
798
   local itCorr = self:calculateItalicsCorrection() * scaleDown
20✔
799
   if self.sup then
10✔
800
      self.sup.relX = self.sup.relX + itCorr / 2
18✔
801
   end
802
   if self.sub then
10✔
803
      self.sub.relX = self.sub.relX - itCorr / 2
20✔
804
   end
805
   -- Determine width and height
806
   self.width = maxLength(
20✔
807
      self.base and self.base.width or SILE.types.length(0),
10✔
808
      self.sub and self.sub.width or SILE.types.length(0),
10✔
809
      self.sup and self.sup.width or SILE.types.length(0)
10✔
810
   )
10✔
811
   if self.sup then
10✔
812
      self.height = 0 - self.sup.relY + self.sup.height
27✔
813
   else
814
      self.height = self.base and self.base.height or 0
1✔
815
   end
816
   if self.sub then
10✔
817
      self.depth = self.sub.relY + self.sub.depth
20✔
818
   else
UNCOV
819
      self.depth = self.base and self.base.depth or 0
×
820
   end
821
end
822

823
function elements.underOver:calculateItalicsCorrection ()
20✔
824
   local lastGid = getRightMostGlyphId(self.base)
25✔
825
   if lastGid > 0 then
25✔
826
      local mathMetrics = self:getMathMetrics()
25✔
827
      if mathMetrics.italicsCorrection[lastGid] then
25✔
UNCOV
828
         local c = mathMetrics.italicsCorrection[lastGid]
×
829
         -- If this is a big operator, and we are in display style, then the
830
         -- base glyph may be bigger than the font size. We need to adjust the
831
         -- italic correction accordingly.
832
         if self.base.atom == atomType.bigOperator and isDisplayMode(self.mode) then
×
UNCOV
833
            c = c * (self.base and self.base.font.size / self.font.size or 1.0)
×
834
         end
UNCOV
835
         return c
×
836
      end
837
   end
838
   return 0
25✔
839
end
840

841
function elements.underOver.output (_, _, _, _) end
35✔
842

843
-- terminal is the base class for leaf node
844
elements.terminal = pl.class(elements.mbox)
20✔
845
elements.terminal._type = "Terminal"
10✔
846

847
function elements.terminal:_init ()
20✔
848
   elements.mbox._init(self)
1,220✔
849
end
850

851
function elements.terminal.styleChildren (_) end
1,230✔
852

853
function elements.terminal.shape (_) end
10✔
854

855
elements.space = pl.class(elements.terminal)
20✔
856
elements.space._type = "Space"
10✔
857

858
function elements.space:_init ()
20✔
UNCOV
859
   elements.terminal._init(self)
×
860
end
861

862
function elements.space:__tostring ()
20✔
863
   return self._type
×
UNCOV
864
      .. "(width="
×
UNCOV
865
      .. tostring(self.width)
×
UNCOV
866
      .. ", height="
×
867
      .. tostring(self.height)
×
868
      .. ", depth="
×
869
      .. tostring(self.depth)
×
870
      .. ")"
×
871
end
872

873
local function getStandardLength (value)
874
   if type(value) == "string" then
888✔
875
      local direction = 1
296✔
876
      if value:sub(1, 1) == "-" then
592✔
877
         value = value:sub(2, -1)
20✔
878
         direction = -1
10✔
879
      end
880
      if value == "thin" then
296✔
881
         return SILE.types.length("3mu") * direction
195✔
882
      elseif value == "med" then
231✔
883
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
288✔
884
      elseif value == "thick" then
135✔
885
         return SILE.types.length("5mu plus 5mu") * direction
333✔
886
      end
887
   end
888
   return SILE.types.length(value)
616✔
889
end
890

891
function elements.space:_init (width, height, depth)
20✔
892
   elements.terminal._init(self)
296✔
893
   self.width = getStandardLength(width)
592✔
894
   self.height = getStandardLength(height)
592✔
895
   self.depth = getStandardLength(depth)
592✔
896
end
897

898
function elements.space:shape ()
20✔
899
   self.width = self.width:absolute() * self:getScaleDown()
1,184✔
900
   self.height = self.height:absolute() * self:getScaleDown()
1,184✔
901
   self.depth = self.depth:absolute() * self:getScaleDown()
1,184✔
902
end
903

904
function elements.space.output (_) end
306✔
905

906
-- text node. For any actual text output
907
elements.text = pl.class(elements.terminal)
20✔
908
elements.text._type = "Text"
10✔
909

910
function elements.text:__tostring ()
20✔
UNCOV
911
   return self._type
×
UNCOV
912
      .. "(atom="
×
UNCOV
913
      .. tostring(self.atom)
×
UNCOV
914
      .. ", kind="
×
915
      .. tostring(self.kind)
×
916
      .. ", script="
×
917
      .. tostring(self.script)
×
918
      .. (SU.boolean(self.stretchy, false) and ", stretchy" or "")
×
919
      .. (SU.boolean(self.largeop, false) and ", largeop" or "")
×
920
      .. ', text="'
×
921
      .. (self.originalText or self.text)
×
922
      .. '")'
×
923
end
924

925
function elements.text:_init (kind, attributes, script, text)
20✔
926
   elements.terminal._init(self)
924✔
927
   if not (kind == "number" or kind == "identifier" or kind == "operator" or kind == "string") then
924✔
UNCOV
928
      SU.error("Unknown text node kind '" .. kind .. "'; should be one of: number, identifier, operator, string")
×
929
   end
930
   self.kind = kind
924✔
931
   self.script = script
924✔
932
   self.text = text
924✔
933
   if self.script ~= "upright" then
924✔
934
      local converted = convertMathVariantScript(self.text, self.script)
924✔
935
      self.originalText = self.text
924✔
936
      self.text = converted
924✔
937
   end
938
   if self.kind == "operator" then
924✔
939
      if self.text == "-" then
368✔
940
         self.text = "−"
4✔
941
      end
942
   end
943
   for attribute, value in pairs(attributes) do
1,401✔
944
      self[attribute] = value
477✔
945
   end
946
end
947

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

1023
function elements.text.findClosestVariant (_, variants, requiredAdvance, currentAdvance)
20✔
1024
   local closest
1025
   local closestI
1026
   local m = requiredAdvance - currentAdvance
78✔
1027
   for i, variant in ipairs(variants) do
1,092✔
1028
      local diff = math.abs(variant.advanceMeasurement - requiredAdvance)
1,014✔
1029
      SU.debug("math", "stretch: diff =", diff)
1,014✔
1030
      if diff < m then
1,014✔
1031
         closest = variant
94✔
1032
         closestI = i
94✔
1033
         m = diff
94✔
1034
      end
1035
   end
1036
   return closest, closestI
78✔
1037
end
1038

1039
function elements.text:_reshapeGlyph (glyph, closestVariant, sz)
20✔
1040
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
46✔
1041
   local dimen = hb.get_glyph_dimensions(face, sz, closestVariant.variantGlyph)
46✔
1042
   glyph.gid = closestVariant.variantGlyph
46✔
UNCOV
1043
   glyph.width, glyph.height, glyph.depth, glyph.glyphAdvance =
×
1044
      dimen.width, dimen.height, dimen.depth, dimen.glyphAdvance
46✔
1045
   return dimen
46✔
1046
end
1047

1048
function elements.text:_stretchyReshape (target, direction)
20✔
1049
   -- direction is the required direction of stretching: true for vertical, false for horizontal
1050
   -- target is the required dimension of the stretched glyph, in font units
1051
   local mathMetrics = self:getMathMetrics()
78✔
1052
   local upem = mathMetrics.unitsPerEm
78✔
1053
   local sz = self.font.size
78✔
1054
   local requiredAdvance = target:tonumber() * upem / sz
156✔
1055
   SU.debug("math", "stretch: rA =", requiredAdvance)
78✔
1056
   -- Choose variant of the closest size. The criterion we use is to have
1057
   -- an advance measurement as close as possible as the required one.
1058
   -- The advance measurement is simply the dimension of the glyph.
1059
   -- Therefore, the selected glyph may be smaller or bigger than
1060
   -- required.
1061
   -- TODO: implement assembly of stretchable glyphs from their parts for cases
1062
   -- when the biggest variant is not big enough.
1063
   -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
1064
   local glyphs = pl.tablex.deepcopy(self.value.items)
78✔
1065
   local glyphConstructions = direction and mathMetrics.mathVariants.vertGlyphConstructions
78✔
1066
      or mathMetrics.mathVariants.horizGlyphConstructions
78✔
1067
   local constructions = glyphConstructions[glyphs[1].gid]
78✔
1068
   if constructions then
78✔
1069
      local variants = constructions.mathGlyphVariantRecord
78✔
1070
      SU.debug("math", "stretch: variants =", variants)
78✔
1071
      local currentAdvance = (direction and (self.depth + self.height):tonumber() or self.width:tonumber()) * upem / sz
234✔
1072
      local closest, closestI = self:findClosestVariant(variants, requiredAdvance, currentAdvance)
78✔
1073
      SU.debug("math", "stretch: closestI =", closestI)
78✔
1074
      if closest then
78✔
1075
         -- Now we have to re-shape the glyph chain. We will assume there
1076
         -- is only one glyph.
1077
         -- TODO: this code is probably wrong when the vertical
1078
         -- variants have a different width than the original, because
1079
         -- the shaping phase is already done. Need to do better.
1080
         local dimen = self:_reshapeGlyph(glyphs[1], closest, sz)
46✔
UNCOV
1081
         self.width, self.depth, self.height =
×
1082
            SILE.types.length(dimen.glyphAdvance), SILE.types.length(dimen.depth), SILE.types.length(dimen.height)
184✔
1083
         SILE.shaper:preAddNodes(glyphs, self.value)
46✔
1084
         self.value.items = glyphs
46✔
1085
         self.value.glyphString = { glyphs[1].gid }
46✔
1086
         return true
46✔
1087
      end
1088
   end
1089
   return false
32✔
1090
end
1091

1092
function elements.text:_vertStretchyReshape (depth, height)
20✔
1093
   local hasStretched = self:_stretchyReshape(depth + height, true)
156✔
1094
   if hasStretched then
78✔
1095
      -- HACK: see output routine
1096
      self.vertExpectedSz = height + depth
92✔
1097
      self.vertScalingRatio = (depth + height):tonumber() / (self.height:tonumber() + self.depth:tonumber())
230✔
1098
      self.height = height
46✔
1099
      self.depth = depth
46✔
1100
   end
1101
   return hasStretched
78✔
1102
end
1103

1104
function elements.text:_horizStretchyReshape (width)
20✔
UNCOV
1105
   local hasStretched = self:_stretchyReshape(width, false)
×
UNCOV
1106
   if hasStretched then
×
1107
      -- HACK: see output routine
UNCOV
1108
      self.horizScalingRatio = width:tonumber() / self.width:tonumber()
×
1109
      self.width = width
×
1110
   end
UNCOV
1111
   return hasStretched
×
1112
end
1113

1114
function elements.text:output (x, y, line)
20✔
1115
   if not self.value.glyphString then
924✔
UNCOV
1116
      return
×
1117
   end
1118
   local compensatedY
1119
   if isDisplayMode(self.mode) and SU.boolean(self.largeop, false) and self.value.items[1].fontDepth then
2,157✔
1120
      compensatedY = SILE.types.length(y.length + self.value.items[1].depth - self.value.items[1].fontDepth)
76✔
1121
   else
1122
      compensatedY = y
905✔
1123
   end
1124
   SILE.outputter:setCursor(scaleWidth(x, line), compensatedY.length)
1,848✔
1125
   SILE.outputter:setFont(self.font)
924✔
1126
   -- There should be no stretch or shrink on the width of a text
1127
   -- element.
1128
   local width = self.width.length
924✔
1129
   -- HACK: For stretchy operators, MathML Core and OpenType define how to build large glyphs
1130
   -- from an assembly of smaller ones. It's fairly complex and idealistic...
1131
   -- Anyhow, we do not have that yet, so we just stretch the glyph artificially.
1132
   -- There are cases where this will not look very good.
1133
   -- Call that a compromise, so that long vectors or large matrices look "decent" without assembly.
1134
   if SILE.outputter.scaleFn and (self.horizScalingRatio or self.vertScalingRatio) then
924✔
1135
      local xratio = self.horizScalingRatio or 1
46✔
1136
      local yratio = self.vertScalingRatio or 1
46✔
1137
      SU.debug("math", "fake glyph stretch: xratio =", xratio, "yratio =", yratio)
46✔
1138
      SILE.outputter:scaleFn(x, y, xratio, yratio, function ()
92✔
1139
         SILE.outputter:drawHbox(self.value, width)
46✔
1140
      end)
1141
   else
1142
      SILE.outputter:drawHbox(self.value, width)
878✔
1143
   end
1144
end
1145

1146
elements.fraction = pl.class(elements.mbox)
20✔
1147
elements.fraction._type = "Fraction"
10✔
1148

1149
function elements.fraction:__tostring ()
20✔
UNCOV
1150
   return self._type .. "(" .. tostring(self.numerator) .. ", " .. tostring(self.denominator) .. ")"
×
1151
end
1152

1153
function elements.fraction:_init (attributes, numerator, denominator)
20✔
1154
   elements.mbox._init(self)
33✔
1155
   self.numerator = numerator
33✔
1156
   self.denominator = denominator
33✔
1157
   self.attributes = attributes
33✔
1158
   table.insert(self.children, numerator)
33✔
1159
   table.insert(self.children, denominator)
33✔
1160
end
1161

1162
function elements.fraction:styleChildren ()
20✔
1163
   self.numerator.mode = getNumeratorMode(self.mode)
66✔
1164
   self.denominator.mode = getDenominatorMode(self.mode)
66✔
1165
end
1166

1167
function elements.fraction:shape ()
20✔
1168
   -- MathML Core 3.3.2: "To avoid visual confusion between the fraction bar
1169
   -- and another adjacent items (e.g. minus sign or another fraction's bar),
1170
   -- a default 1-pixel space is added around the element."
1171
   -- Note that PlainTeX would likely use \nulldelimiterspace (default 1.2pt)
1172
   -- but it would depend on the surrounding context, and might be far too
1173
   -- much in some cases, so we stick to MathML's suggested padding.
1174
   self.padding = SILE.types.length("1px"):absolute()
99✔
1175

1176
   -- Determine relative abscissas and width
1177
   local widest, other
1178
   if self.denominator.width > self.numerator.width then
33✔
1179
      widest, other = self.denominator, self.numerator
25✔
1180
   else
1181
      widest, other = self.numerator, self.denominator
8✔
1182
   end
1183
   widest.relX = self.padding
33✔
1184
   other.relX = self.padding + (widest.width - other.width) / 2
132✔
1185
   self.width = widest.width + 2 * self.padding
99✔
1186
   -- Determine relative ordinates and height
1187
   local constants = self:getMathMetrics().constants
66✔
1188
   local scaleDown = self:getScaleDown()
33✔
1189
   self.axisHeight = constants.axisHeight * scaleDown
33✔
1190
   self.ruleThickness = self.attributes.linethickness
33✔
1191
         and SU.cast("measurement", self.attributes.linethickness):tonumber()
33✔
1192
      or constants.fractionRuleThickness * scaleDown
33✔
1193

1194
   -- MathML Core 3.3.2.2 ("Fraction with zero line thickness") uses
1195
   -- stack(DisplayStyle)GapMin, stackTop(DisplayStyle)ShiftUp and stackBottom(DisplayStyle)ShiftDown.
1196
   -- TODO not implemented
1197
   -- The most common use cases for zero line thickness are:
1198
   --  - Binomial coefficients
1199
   --  - Stacked subscript/superscript on big operators such as sums.
1200

1201
   local numeratorGapMin, denominatorGapMin, numeratorShiftUp, denominatorShiftDown
1202
   if isDisplayMode(self.mode) then
66✔
1203
      numeratorGapMin = constants.fractionNumDisplayStyleGapMin * scaleDown
10✔
1204
      denominatorGapMin = constants.fractionDenomDisplayStyleGapMin * scaleDown
10✔
1205
      numeratorShiftUp = constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
10✔
1206
      denominatorShiftDown = constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
10✔
1207
   else
1208
      numeratorGapMin = constants.fractionNumeratorGapMin * scaleDown
23✔
1209
      denominatorGapMin = constants.fractionDenominatorGapMin * scaleDown
23✔
1210
      numeratorShiftUp = constants.fractionNumeratorShiftUp * scaleDown
23✔
1211
      denominatorShiftDown = constants.fractionDenominatorShiftDown * scaleDown
23✔
1212
   end
1213

1214
   self.numerator.relY = -self.axisHeight
33✔
1215
      - self.ruleThickness / 2
33✔
1216
      - SILE.types.length(
66✔
1217
         math.max(
66✔
1218
            (numeratorGapMin + self.numerator.depth):tonumber(),
66✔
1219
            numeratorShiftUp - self.axisHeight - self.ruleThickness / 2
33✔
1220
         )
1221
      )
66✔
1222
   self.denominator.relY = -self.axisHeight
33✔
1223
      + self.ruleThickness / 2
33✔
1224
      + SILE.types.length(
66✔
1225
         math.max(
66✔
1226
            (denominatorGapMin + self.denominator.height):tonumber(),
66✔
1227
            denominatorShiftDown + self.axisHeight - self.ruleThickness / 2
33✔
1228
         )
1229
      )
66✔
1230
   self.height = self.numerator.height - self.numerator.relY
66✔
1231
   self.depth = self.denominator.relY + self.denominator.depth
66✔
1232
end
1233

1234
function elements.fraction:output (x, y, line)
20✔
1235
   if self.ruleThickness > 0 then
33✔
1236
      SILE.outputter:drawRule(
66✔
1237
         scaleWidth(x + self.padding, line),
66✔
1238
         y.length - self.axisHeight - self.ruleThickness / 2,
66✔
1239
         scaleWidth(self.width - 2 * self.padding, line),
99✔
1240
         self.ruleThickness
1241
      )
33✔
1242
   end
1243
end
1244

1245
local function newSubscript (spec)
1246
   return elements.subscript(spec.base, spec.sub, spec.sup)
102✔
1247
end
1248

1249
local function newUnderOver (spec)
1250
   return elements.underOver(spec.base, spec.sub, spec.sup)
25✔
1251
end
1252

1253
-- TODO replace with penlight equivalent
1254
local function mapList (f, l)
1255
   local ret = {}
8✔
1256
   for i, x in ipairs(l) do
32✔
1257
      ret[i] = f(i, x)
48✔
1258
   end
1259
   return ret
8✔
1260
end
1261

1262
elements.mtr = pl.class(elements.mbox)
20✔
1263
-- elements.mtr._type = "" -- TODO why not set?
1264

1265
function elements.mtr:_init (children)
20✔
1266
   self.children = children
12✔
1267
end
1268

1269
function elements.mtr:styleChildren ()
20✔
1270
   for _, c in ipairs(self.children) do
48✔
1271
      c.mode = self.mode
36✔
1272
   end
1273
end
1274

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

1277
function elements.mtr.output (_) end
22✔
1278

1279
elements.table = pl.class(elements.mbox)
20✔
1280
elements.table._type = "table" -- TODO why case difference?
10✔
1281

1282
function elements.table:_init (children, options)
20✔
1283
   elements.mbox._init(self)
8✔
1284
   self.children = children
8✔
1285
   self.options = options
8✔
1286
   self.nrows = #self.children
8✔
1287
   self.ncols = math.max(pl.utils.unpack(mapList(function (_, row)
32✔
1288
      return #row.children
24✔
1289
   end, self.children)))
16✔
1290
   SU.debug("math", "self.ncols =", self.ncols)
8✔
1291
   local spacing = SILE.settings:get("math.font.size") * 0.6 -- arbitrary ratio of the current math font size
16✔
1292
   self.rowspacing = self.options.rowspacing and SILE.types.length(self.options.rowspacing) or spacing
8✔
1293
   self.columnspacing = self.options.columnspacing and SILE.types.length(self.options.columnspacing) or spacing
8✔
1294
   -- Pad rows that do not have enough cells by adding cells to the
1295
   -- right.
1296
   for i, row in ipairs(self.children) do
32✔
1297
      for j = 1, (self.ncols - #row.children) do
24✔
UNCOV
1298
         SU.debug("math", "padding i =", i, "j =", j)
×
UNCOV
1299
         table.insert(row.children, elements.stackbox("H", {}))
×
UNCOV
1300
         SU.debug("math", "size", #row.children)
×
1301
      end
1302
   end
1303
   if options.columnalign then
8✔
1304
      local l = {}
4✔
1305
      for w in string.gmatch(options.columnalign, "[^%s]+") do
16✔
1306
         if not (w == "left" or w == "center" or w == "right") then
12✔
UNCOV
1307
            SU.error("Invalid specifier in `columnalign` attribute: " .. w)
×
1308
         end
1309
         table.insert(l, w)
12✔
1310
      end
1311
      -- Pad with last value of l if necessary
1312
      for _ = 1, (self.ncols - #l), 1 do
4✔
UNCOV
1313
         table.insert(l, l[#l])
×
1314
      end
1315
      -- On the contrary, remove excess values in l if necessary
1316
      for _ = 1, (#l - self.ncols), 1 do
4✔
1317
         table.remove(l)
×
1318
      end
1319
      self.options.columnalign = l
4✔
1320
   else
1321
      self.options.columnalign = pl.List.range(1, self.ncols):map(function (_)
14✔
1322
         return "center"
12✔
1323
      end)
1324
   end
1325
end
1326

1327
function elements.table:styleChildren ()
20✔
1328
   if self.mode == mathMode.display and self.options.displaystyle ~= "false" then
8✔
1329
      for _, c in ipairs(self.children) do
16✔
1330
         c.mode = mathMode.display
12✔
1331
      end
1332
   else
1333
      for _, c in ipairs(self.children) do
16✔
1334
         c.mode = mathMode.text
12✔
1335
      end
1336
   end
1337
end
1338

1339
function elements.table:shape ()
20✔
1340
   -- Determine the height (resp. depth) of each row, which is the max
1341
   -- height (resp. depth) among its elements. Then we only need to add it to
1342
   -- the table's height and center every cell vertically.
1343
   for _, row in ipairs(self.children) do
32✔
1344
      row.height = SILE.types.length(0)
48✔
1345
      row.depth = SILE.types.length(0)
48✔
1346
      for _, cell in ipairs(row.children) do
96✔
1347
         row.height = maxLength(row.height, cell.height)
144✔
1348
         row.depth = maxLength(row.depth, cell.depth)
144✔
1349
      end
1350
   end
1351
   self.vertSize = SILE.types.length(0)
16✔
1352
   for i, row in ipairs(self.children) do
32✔
UNCOV
1353
      self.vertSize = self.vertSize
×
1354
         + row.height
24✔
1355
         + row.depth
24✔
1356
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
56✔
1357
   end
1358
   local rowHeightSoFar = SILE.types.length(0)
8✔
1359
   for i, row in ipairs(self.children) do
32✔
1360
      row.relY = rowHeightSoFar + row.height - self.vertSize
72✔
UNCOV
1361
      rowHeightSoFar = rowHeightSoFar
×
1362
         + row.height
24✔
1363
         + row.depth
24✔
1364
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
32✔
1365
   end
1366
   self.width = SILE.types.length(0)
16✔
1367
   local thisColRelX = SILE.types.length(0)
8✔
1368
   -- For every column...
1369
   for i = 1, self.ncols do
32✔
1370
      -- Determine its width
1371
      local columnWidth = SILE.types.length(0)
24✔
1372
      for j = 1, self.nrows do
96✔
1373
         if self.children[j].children[i].width > columnWidth then
72✔
1374
            columnWidth = self.children[j].children[i].width
32✔
1375
         end
1376
      end
1377
      -- Use it to align the contents of every cell as required.
1378
      for j = 1, self.nrows do
96✔
1379
         local cell = self.children[j].children[i]
72✔
1380
         if self.options.columnalign[i] == "left" then
72✔
1381
            cell.relX = thisColRelX
12✔
1382
         elseif self.options.columnalign[i] == "center" then
60✔
1383
            cell.relX = thisColRelX + (columnWidth - cell.width) / 2
192✔
1384
         elseif self.options.columnalign[i] == "right" then
12✔
1385
            cell.relX = thisColRelX + (columnWidth - cell.width)
36✔
1386
         else
UNCOV
1387
            SU.error("invalid columnalign parameter")
×
1388
         end
1389
      end
1390
      thisColRelX = thisColRelX + columnWidth + (i == self.ncols and SILE.types.length(0) or self.columnspacing) -- Spacing
56✔
1391
   end
1392
   self.width = thisColRelX
8✔
1393
   -- Center myself vertically around the axis, and update relative Ys of rows accordingly
1394
   local axisHeight = self:getMathMetrics().constants.axisHeight * self:getScaleDown()
24✔
1395
   self.height = self.vertSize / 2 + axisHeight
24✔
1396
   self.depth = self.vertSize / 2 - axisHeight
24✔
1397
   for _, row in ipairs(self.children) do
32✔
1398
      row.relY = row.relY + self.vertSize / 2 - axisHeight
96✔
1399
      -- Also adjust width
1400
      row.width = self.width
24✔
1401
   end
1402
end
1403

1404
function elements.table.output (_) end
18✔
1405

1406
local function getRadicandMode (mode)
1407
   -- Not too sure if we should do something special/
UNCOV
1408
   return mode
×
1409
end
1410

1411
local function getDegreeMode (mode)
1412
   -- 2 levels smaller, up to scriptScript evntually.
1413
   -- Not too sure if we should do something else.
UNCOV
1414
   if mode == mathMode.display then
×
UNCOV
1415
      return mathMode.scriptScript
×
UNCOV
1416
   elseif mode == mathMode.displayCramped then
×
UNCOV
1417
      return mathMode.scriptScriptCramped
×
1418
   elseif mode == mathMode.text or mode == mathMode.script or mode == mathMode.scriptScript then
×
1419
      return mathMode.scriptScript
×
1420
   end
1421
   return mathMode.scriptScriptCramped
×
1422
end
1423

1424
elements.sqrt = pl.class(elements.mbox)
20✔
1425
elements.sqrt._type = "Sqrt"
10✔
1426

1427
function elements.sqrt:__tostring ()
20✔
UNCOV
1428
   return self._type .. "(" .. tostring(self.radicand) .. (self.degree and ", " .. tostring(self.degree) or "") .. ")"
×
1429
end
1430

1431
function elements.sqrt:_init (radicand, degree)
20✔
1432
   elements.mbox._init(self)
×
UNCOV
1433
   self.radicand = radicand
×
UNCOV
1434
   if degree then
×
UNCOV
1435
      self.degree = degree
×
1436
      table.insert(self.children, degree)
×
1437
   end
1438
   table.insert(self.children, radicand)
×
1439
   self.relX = SILE.types.length()
×
1440
   self.relY = SILE.types.length()
×
1441
end
1442

1443
function elements.sqrt:styleChildren ()
20✔
1444
   self.radicand.mode = getRadicandMode(self.mode)
×
UNCOV
1445
   if self.degree then
×
UNCOV
1446
      self.degree.mode = getDegreeMode(self.mode)
×
1447
   end
1448
end
1449

1450
function elements.sqrt:shape ()
20✔
UNCOV
1451
   local mathMetrics = self:getMathMetrics()
×
UNCOV
1452
   local scaleDown = self:getScaleDown()
×
UNCOV
1453
   local constants = mathMetrics.constants
×
1454

1455
   self.radicalRuleThickness = constants.radicalRuleThickness * scaleDown
×
1456
   if self.mode == mathMode.display or self.mode == mathMode.displayCramped then
×
1457
      self.radicalVerticalGap = constants.radicalDisplayStyleVerticalGap * scaleDown
×
1458
   else
1459
      self.radicalVerticalGap = constants.radicalVerticalGap * scaleDown
×
1460
   end
1461
   self.extraAscender = constants.radicalExtraAscender * scaleDown
×
1462

1463
   -- HACK: We draw own own radical sign in the output() method.
1464
   -- Derive dimensions for the radical sign (more or less ad hoc).
1465
   -- Note: In TeX, the radical sign extends a lot below the baseline,
1466
   -- and MathML Core also has a lot of layout text about it.
1467
   -- Not only it doesn't look good, but it's not very clear vs. OpenType.
UNCOV
1468
   local radicalGlyph = SILE.shaper:measureChar("√")
×
UNCOV
1469
   local ratio = (self.radicand.height:tonumber() + self.radicand.depth:tonumber())
×
UNCOV
1470
      / (radicalGlyph.height + radicalGlyph.depth)
×
UNCOV
1471
   local vertAdHocOffset = (ratio > 1 and math.log(ratio) or 0) * self.radicalVerticalGap
×
1472
   self.symbolHeight = SILE.types.length(radicalGlyph.height) * scaleDown
×
1473
   self.symbolDepth = (SILE.types.length(radicalGlyph.depth) + vertAdHocOffset) * scaleDown
×
1474
   self.symbolWidth = (SILE.types.length(radicalGlyph.width) + vertAdHocOffset) * scaleDown
×
1475

1476
   -- Adjust the height of the radical sign if the radicand is higher
1477
   self.symbolHeight = self.radicand.height > self.symbolHeight and self.radicand.height or self.symbolHeight
×
1478
   -- Compute the (max-)height of the short leg of the radical sign
UNCOV
1479
   self.symbolShortHeight = self.symbolHeight * constants.radicalDegreeBottomRaisePercent
×
1480

1481
   self.offsetX = SILE.types.length()
×
UNCOV
1482
   if self.degree then
×
1483
      -- Position the degree
UNCOV
1484
      self.degree.relY = -constants.radicalDegreeBottomRaisePercent * self.symbolHeight
×
1485
      -- Adjust the height of the short leg of the radical sign to ensure the degree is not too close
1486
      -- (empirically use radicalExtraAscender)
UNCOV
1487
      self.symbolShortHeight = self.symbolShortHeight - constants.radicalExtraAscender * scaleDown
×
1488
      -- Compute the width adjustment for the degree
UNCOV
1489
      self.offsetX = self.degree.width
×
UNCOV
1490
         + constants.radicalKernBeforeDegree * scaleDown
×
1491
         + constants.radicalKernAfterDegree * scaleDown
×
1492
   end
1493
   -- Position the radicand
1494
   self.radicand.relX = self.symbolWidth + self.offsetX
×
1495
   -- Compute the dimensions of the whole radical
UNCOV
1496
   self.width = self.radicand.width + self.symbolWidth + self.offsetX
×
UNCOV
1497
   self.height = self.symbolHeight + self.radicalVerticalGap + self.extraAscender
×
1498
   self.depth = self.radicand.depth
×
1499
end
1500

1501
local function _r (number)
1502
   -- Lua 5.3+ formats floats as 1.0 and integers as 1
1503
   -- Also some PDF readers do not like double precision.
UNCOV
1504
   return math.floor(number) == number and math.floor(number) or tonumber(string.format("%.5f", number))
×
1505
end
1506

1507
function elements.sqrt:output (x, y, line)
20✔
1508
   -- HACK:
1509
   -- OpenType might say we need to assemble the radical sign from parts.
1510
   -- Frankly, it's much easier to just draw it as a graphic :-)
1511
   -- Hence, here we use a PDF graphic operators to draw a nice radical sign.
1512
   -- Some values here are ad hoc, but they look good.
UNCOV
1513
   local h = self.height:tonumber()
×
UNCOV
1514
   local d = self.depth:tonumber()
×
UNCOV
1515
   local s0 = scaleWidth(self.offsetX, line):tonumber()
×
UNCOV
1516
   local sw = scaleWidth(self.symbolWidth, line):tonumber()
×
1517
   local dsh = h - self.symbolShortHeight:tonumber()
×
1518
   local dsd = self.symbolDepth:tonumber()
×
1519
   local symbol = {
×
1520
      _r(self.radicalRuleThickness),
×
1521
      "w", -- line width
1522
      1,
1523
      "j", -- round line joins
1524
      _r(sw + s0),
×
UNCOV
1525
      _r(self.extraAscender),
×
1526
      "m",
UNCOV
1527
      _r(s0 + sw * 0.90),
×
1528
      _r(self.extraAscender),
×
1529
      "l",
UNCOV
1530
      _r(s0 + sw * 0.4),
×
1531
      _r(h + d + dsd),
×
1532
      "l",
UNCOV
1533
      _r(s0 + sw * 0.2),
×
1534
      _r(dsh),
×
1535
      "l",
UNCOV
1536
      s0 + sw * 0.1,
×
1537
      _r(dsh + 0.5),
×
1538
      "l",
1539
      "S",
1540
   }
1541
   local svg = table.concat(symbol, " ")
×
UNCOV
1542
   local xscaled = scaleWidth(x, line)
×
UNCOV
1543
   SILE.outputter:drawSVG(svg, xscaled, y, sw, h, 1)
×
1544
   -- And now we just need to draw the bar over the radicand
1545
   SILE.outputter:drawRule(
×
1546
      s0 + self.symbolWidth + xscaled,
×
1547
      y.length - self.height + self.extraAscender - self.radicalRuleThickness / 2,
×
UNCOV
1548
      scaleWidth(self.radicand.width, line),
×
1549
      self.radicalRuleThickness
1550
   )
1551
end
1552

1553
elements.padded = pl.class(elements.mbox)
20✔
1554
elements.padded._type = "Padded"
10✔
1555

1556
function elements.padded:__tostring ()
20✔
UNCOV
1557
   return self._type .. "(" .. tostring(self.impadded) .. ")"
×
1558
end
1559

1560
function elements.padded:_init (attributes, impadded)
20✔
1561
   elements.mbox._init(self)
×
UNCOV
1562
   self.impadded = impadded
×
UNCOV
1563
   self.attributes = attributes or {}
×
UNCOV
1564
   table.insert(self.children, impadded)
×
1565
end
1566

1567
function elements.padded:styleChildren ()
20✔
1568
   self.impadded.mode = self.mode
×
1569
end
1570

1571
function elements.padded:shape ()
20✔
1572
   -- TODO MathML allows percentages font-relative units (em, ex) for padding
1573
   -- But our units work with font.size, not math.font.size (possibly adjusted by scaleDown)
1574
   -- so the expectations might not be met.
UNCOV
1575
   local width = self.attributes.width and SU.cast("measurement", self.attributes.width)
×
UNCOV
1576
   local height = self.attributes.height and SU.cast("measurement", self.attributes.height)
×
UNCOV
1577
   local depth = self.attributes.depth and SU.cast("measurement", self.attributes.depth)
×
UNCOV
1578
   local lspace = self.attributes.lspace and SU.cast("measurement", self.attributes.lspace)
×
1579
   local voffset = self.attributes.voffset and SU.cast("measurement", self.attributes.voffset)
×
1580
   -- Clamping for width, height, depth, lspace
1581
   width = width and (width:tonumber() > 0 and width or SILE.types.measurement())
×
1582
   height = height and (height:tonumber() > 0 and height or SILE.types.measurement())
×
1583
   depth = depth and (depth:tonumber() > 0 and depth or SILE.types.measurement())
×
UNCOV
1584
   lspace = lspace and (lspace:tonumber() > 0 and lspace or SILE.types.measurement())
×
1585
   -- No clamping for voffset
1586
   voffset = voffset or SILE.types.measurement(0)
×
1587
   -- Compute the dimensions
1588
   self.width = width and SILE.types.length(width) or self.impadded.width
×
UNCOV
1589
   self.height = height and SILE.types.length(height) or self.impadded.height
×
1590
   self.depth = depth and SILE.types.length(depth) or self.impadded.depth
×
UNCOV
1591
   self.impadded.relX = lspace and SILE.types.length(lspace) or SILE.types.length()
×
1592
   self.impadded.relY = voffset and SILE.types.length(voffset):negate() or SILE.types.length()
×
1593
end
1594

1595
function elements.padded.output (_, _, _, _) end
10✔
1596

1597
-- Bevelled fractions are not part of MathML Core, and MathML4 does not
1598
-- exactly specify how to compute the layout.
1599
elements.bevelledFraction = pl.class(elements.fraction) -- Inherit from fraction
20✔
1600
elements.fraction._type = "BevelledFraction"
10✔
1601

1602
function elements.bevelledFraction:shape ()
20✔
NEW
1603
   local constants = self:getMathMetrics().constants
×
NEW
1604
   local scaleDown = self:getScaleDown()
×
NEW
1605
   local hSkew = constants.skewedFractionHorizontalGap * scaleDown
×
1606
   -- OpenType has properties which are not totally explicit.
1607
   -- The definition of skewedFractionVerticalGap (and its value in fonts
1608
   -- such as Libertinus Math) seems to imply that it is measured from the
1609
   -- bottom of the numerator to the top of the denominator.
1610
   -- This does not seem to be a nice general layout.
1611
   -- So we will use superscriptShiftUp(Cramped) for the numerator:
NEW
1612
   local vSkewUp = isCrampedMode(self.mode) and constants.superscriptShiftUpCramped * scaleDown
×
NEW
1613
      or constants.superscriptShiftUp * scaleDown
×
1614
   -- And all good books say that the denominator should not be shifted down:
NEW
1615
   local vSkewDown = 0
×
1616

NEW
1617
   self.ruleThickness = self.attributes.linethickness
×
NEW
1618
         and SU.cast("measurement", self.attributes.linethickness):tonumber()
×
NEW
1619
      or constants.fractionRuleThickness * scaleDown
×
NEW
1620
   self.numerator.relX = SILE.types.length(0)
×
NEW
1621
   self.numerator.relY = SILE.types.length(-vSkewUp)
×
NEW
1622
   self.denominator.relX = self.numerator.width + hSkew
×
NEW
1623
   self.denominator.relY = SILE.types.length(vSkewDown)
×
NEW
1624
   self.width = self.numerator.width + self.denominator.width + hSkew
×
NEW
1625
   self.height = maxLength(self.numerator.height + vSkewUp, self.denominator.height - vSkewDown)
×
NEW
1626
   self.depth = maxLength(self.numerator.depth - vSkewUp, self.denominator.depth + vSkewDown)
×
NEW
1627
   self.barWidth = SILE.types.length(hSkew)
×
NEW
1628
   self.barX = self.numerator.relX + self.numerator.width
×
1629
end
1630

1631
function elements.bevelledFraction:output (x, y, line)
20✔
NEW
1632
   local h = self.height:tonumber()
×
NEW
1633
   local d = self.depth:tonumber()
×
NEW
1634
   local barwidth = scaleWidth(self.barWidth, line):tonumber()
×
NEW
1635
   local xscaled = scaleWidth(x + self.barX, line)
×
NEW
1636
   local rd = self.ruleThickness / 2
×
NEW
1637
   local symbol = {
×
NEW
1638
      _r(self.ruleThickness),
×
1639
      "w", -- line width
1640
      1,
1641
      "J", -- round line caps
NEW
1642
      _r(0),
×
NEW
1643
      _r(d + h - rd),
×
1644
      "m",
NEW
1645
      _r(barwidth),
×
NEW
1646
      _r(rd),
×
1647
      "l",
1648
      "S",
1649
   }
NEW
1650
   local svg = table.concat(symbol, " ")
×
NEW
1651
   SILE.outputter:drawSVG(svg, xscaled, y, barwidth, h, 1)
×
1652
end
1653

1654
elements.mathMode = mathMode
10✔
1655
elements.atomType = atomType
10✔
1656
elements.symbolDefaults = symbolDefaults
10✔
1657
elements.newSubscript = newSubscript
10✔
1658
elements.newUnderOver = newUnderOver
10✔
1659

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

© 2026 Coveralls, Inc