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

sile-typesetter / sile / 11637450455

01 Nov 2024 10:51PM UTC coverage: 65.274% (+7.6%) from 57.636%
11637450455

push

github

web-flow
Merge 9d9e4ff40 into 5dffc31aa

132 of 201 new or added lines in 5 files covered. (65.67%)

207 existing lines in 13 files now uncovered.

11917 of 18257 relevant lines covered (65.27%)

4285.37 hits per line

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

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

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

11
local elements = {}
12✔
12

13
local mathMode = {
12✔
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 = {}
12✔
41

42
local function retrieveMathTable (font)
43
   local key = SILE.font._key(font)
3,794✔
44
   if not mathCache[key] then
3,794✔
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,794✔
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
304✔
156
      node = node.children[#node.children]
15✔
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
646✔
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)
24✔
200
elements.mbox._type = "Mbox"
12✔
201

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

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

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

244
function elements.mbox:getScaleDown ()
24✔
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 ()
24✔
259
   self:styleChildren()
1,930✔
260
   for _, n in ipairs(self.children) do
3,788✔
261
      if n then
1,858✔
262
         n:styleDescendants()
1,858✔
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 ()
24✔
272
   for _, n in ipairs(self.children) do
3,788✔
273
      if n then
1,858✔
274
         n:shapeTree()
1,858✔
275
      end
276
   end
277
   self:shape()
1,930✔
278
end
279

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

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

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

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

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

375
function elements.stackbox:styleChildren ()
24✔
376
   for _, n in ipairs(self.children) do
1,574✔
377
      n.mode = self.mode
1,153✔
378
   end
379
   if self.direction == "H" then
421✔
380
      -- Insert spaces according to the atom type, following Knuth's guidelines
381
      -- in the TeXbook
382
      local spaces = {}
349✔
383
      for i = 1, #self.children - 1 do
1,087✔
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 = {}
349✔
394
      for i, _ in pairs(spaces) do
610✔
395
         table.insert(spaceIdx, i)
261✔
396
      end
397
      table.sort(spaceIdx, function (a, b)
698✔
398
         return a > b
358✔
399
      end)
400
      for _, idx in ipairs(spaceIdx) do
610✔
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 ()
24✔
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)
842✔
417
   self.depth = SILE.types.length(0)
842✔
418
   if self.direction == "H" then
421✔
419
      for i, n in ipairs(self.children) do
1,691✔
420
         n.relY = SILE.types.length(0)
2,684✔
421
         self.height = i == 1 and n.height or maxLength(self.height, n.height)
2,341✔
422
         self.depth = i == 1 and n.depth or maxLength(self.depth, n.depth)
2,341✔
423
      end
424
      -- Handle stretchy operators
425
      for _, elt in ipairs(self.children) do
1,691✔
426
         if elt.is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
3,015✔
427
            elt:_vertStretchyReshape(self.depth, self.height)
79✔
428
         end
429
      end
430
      -- Set self.width
431
      self.width = SILE.types.length(0)
698✔
432
      for i, n in ipairs(self.children) do
1,691✔
433
         n.relX = self.width
1,342✔
434
         self.width = i == 1 and n.width or self.width + n.width
2,341✔
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)
24✔
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
433✔
467

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

471
function elements.phantom:_init (children)
24✔
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 (_, _, _)
24✔
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.
UNCOV
486
   self.children = {}
×
487
end
488

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

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

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

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

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

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

631
function elements.subscript.output (_, _, _, _) end
126✔
632

633
elements.underOver = pl.class(elements.subscript)
24✔
634
elements.underOver._type = "UnderOver"
12✔
635

636
function elements.underOver:__tostring ()
24✔
637
   return self._type .. "(" .. tostring(self.base) .. ", " .. tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
×
638
end
639

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

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

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

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

716
function elements.underOver:shape ()
24✔
717
   local isBaseLargeOp = SU.boolean(self.base and self.base.largeop, false)
25✔
718
   if not (self.mode == mathMode.display or self.mode == mathMode.displayCramped) and isBaseLargeOp then
25✔
719
      -- FIXME
720
      -- Added the "largeop" condition, but it's kind of a workaround:
721
      -- It should rather be the "moveablelimits" propery in MathML, but we do not have that yet.
722
      -- When the base is a moveable limit, the under/over scripts are not placed under/over the base,
723
      -- but ather to the right of it, when display mode is not used.
724
      -- Notable effects:
725
      --   Mozilla MathML test 19 (on "k times" > overbrace > base)
726
      --   Maxwell's Equations in MathML3 Test Suite "complex1" (on the vectors in fractions)
727
      -- For now, go with the "largeop" property, but this is not correct.
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
×
UNCOV
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✔
781
         widest = self.sup
×
782
         a = self.base
×
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
819
      self.depth = self.base and self.base.depth or 0
×
820
   end
821
end
822

823
function elements.underOver:calculateItalicsCorrection ()
24✔
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✔
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
×
833
            c = c * (self.base and self.base.font.size / self.font.size or 1.0)
×
834
         end
835
         return c
×
836
      end
837
   end
838
   return 0
25✔
839
end
840

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

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

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

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

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

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

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

862
function elements.space:__tostring ()
24✔
UNCOV
863
   return self._type
×
UNCOV
864
      .. "(width="
×
UNCOV
865
      .. tostring(self.width)
×
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
957✔
875
      local direction = 1
319✔
876
      if value:sub(1, 1) == "-" then
638✔
877
         value = value:sub(2, -1)
20✔
878
         direction = -1
10✔
879
      end
880
      if value == "thin" then
319✔
881
         return SILE.types.length("3mu") * direction
198✔
882
      elseif value == "med" then
253✔
883
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
294✔
884
      elseif value == "thick" then
155✔
885
         return SILE.types.length("5mu plus 5mu") * direction
393✔
886
      end
887
   end
888
   return SILE.types.length(value)
662✔
889
end
890

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

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

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

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

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

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

948
function elements.text:shape ()
24✔
949
   self.font.size = self.font.size * self:getScaleDown()
1,994✔
950
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
997✔
951
   local mathMetrics = self:getMathMetrics()
997✔
952
   local glyphs = SILE.shaper:shapeToken(self.text, self.font)
997✔
953
   -- Use bigger variants for big operators in display style
954
   if isDisplayMode(self.mode) and SU.boolean(self.largeop, false) then
2,353✔
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)
997✔
993
   self.value.items = glyphs
997✔
994
   self.value.glyphString = {}
997✔
995
   if glyphs and #glyphs > 0 then
997✔
996
      for i = 1, #glyphs do
2,241✔
997
         table.insert(self.value.glyphString, glyphs[i].gid)
1,244✔
998
      end
999
      self.width = SILE.types.length(0)
1,994✔
1000
      self.widthForSubscript = SILE.types.length(0)
1,994✔
1001
      for i = #glyphs, 1, -1 do
2,241✔
1002
         self.width = self.width + glyphs[i].glyphAdvance
2,488✔
1003
      end
1004
      -- Store width without italic correction somewhere
1005
      self.widthForSubscript = self.width
997✔
1006
      local itCorr = mathMetrics.italicsCorrection[glyphs[#glyphs].gid]
997✔
1007
      if itCorr then
997✔
1008
         self.width = self.width + itCorr * self:getScaleDown()
744✔
1009
      end
1010
      for i = 1, #glyphs do
2,241✔
1011
         self.height = i == 1 and SILE.types.length(glyphs[i].height)
1,244✔
1012
            or SILE.types.length(math.max(self.height:tonumber(), glyphs[i].height))
1,738✔
1013
         self.depth = i == 1 and SILE.types.length(glyphs[i].depth)
1,244✔
1014
            or SILE.types.length(math.max(self.depth:tonumber(), glyphs[i].depth))
1,738✔
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)
24✔
1024
   local closest
1025
   local closestI
1026
   local m = requiredAdvance - currentAdvance
79✔
1027
   for i, variant in ipairs(variants) do
1,106✔
1028
      local diff = math.abs(variant.advanceMeasurement - requiredAdvance)
1,027✔
1029
      SU.debug("math", "stretch: diff =", diff)
1,027✔
1030
      if diff < m then
1,027✔
1031
         closest = variant
101✔
1032
         closestI = i
101✔
1033
         m = diff
101✔
1034
      end
1035
   end
1036
   return closest, closestI
79✔
1037
end
1038

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

1047
function elements.text:_stretchyReshape(target, direction)
24✔
1048
   -- direction is the required direction of stretching: true for vertical, false for horizontal
1049
   -- target is the required dimension of the stretched glyph, in font units
1050
   local mathMetrics = self:getMathMetrics()
79✔
1051
   local upem = mathMetrics.unitsPerEm
79✔
1052
   local sz = self.font.size
79✔
1053
   local requiredAdvance = target:tonumber() * upem / sz
158✔
1054
   SU.debug("math", "stretch: rA =", requiredAdvance)
79✔
1055
   -- Choose variant of the closest size. The criterion we use is to have
1056
   -- an advance measurement as close as possible as the required one.
1057
   -- The advance measurement is simply the dimension of the glyph.
1058
   -- Therefore, the selected glyph may be smaller or bigger than
1059
   -- required.
1060
   -- TODO: implement assembly of stretchable glyphs from their parts for cases
1061
   -- when the biggest variant is not big enough.
1062
   -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
1063
   local glyphs = pl.tablex.deepcopy(self.value.items)
79✔
NEW
1064
   local glyphConstructions = direction
×
1065
      and mathMetrics.mathVariants.vertGlyphConstructions
79✔
1066
      or mathMetrics.mathVariants.horizGlyphConstructions
79✔
1067
   local constructions = glyphConstructions[glyphs[1].gid]
79✔
1068
   if constructions then
79✔
1069
      local variants = constructions.mathGlyphVariantRecord
79✔
1070
      SU.debug("math", "stretch: variants =", variants)
79✔
1071
      local currentAdvance = (direction and (self.depth + self.height):tonumber() or self.width:tonumber()) * upem / sz
237✔
1072
      local closest, closestI = self:findClosestVariant(variants, requiredAdvance, currentAdvance)
79✔
1073
      SU.debug("math", "stretch: closestI =", closestI)
79✔
1074
      if closest then
79✔
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)
47✔
1081
         self.width, self.depth, self.height = SILE.types.length(dimen.glyphAdvance), SILE.types.length(dimen.depth), SILE.types.length(dimen.height)
188✔
1082
         SILE.shaper:preAddNodes(glyphs, self.value)
47✔
1083
         self.value.items = glyphs
47✔
1084
         self.value.glyphString = { glyphs[1].gid }
47✔
1085
         return true
47✔
1086
      end
1087
   end
1088
   return false
32✔
1089
end
1090

1091
function elements.text:_vertStretchyReshape(depth, height)
24✔
1092
   local hasStretched = self:_stretchyReshape(depth + height, true)
158✔
1093
   if hasStretched then
79✔
1094
      -- HACK: see output routine
1095
      self.vertExpectedSz = height + depth
94✔
1096
      self.vertScalingRatio = (depth + height):tonumber() / (self.height:tonumber() + self.depth:tonumber())
235✔
1097
      self.height = height
47✔
1098
      self.depth = depth
47✔
1099
   end
1100
   return hasStretched
79✔
1101
end
1102

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

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

1145
elements.fraction = pl.class(elements.mbox)
24✔
1146
elements.fraction._type = "Fraction"
12✔
1147

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1276
function elements.mtr.output (_) end
24✔
1277

1278
elements.table = pl.class(elements.mbox)
24✔
1279
elements.table._type = "table" -- TODO why case difference?
12✔
1280

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

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

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

1403
function elements.table.output (_) end
21✔
1404

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

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

1423
elements.sqrt = pl.class(elements.mbox)
24✔
1424
elements.sqrt._type = "Sqrt"
12✔
1425

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

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

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

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

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

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

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

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

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

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

1552
elements.padded = pl.class(elements.mbox)
24✔
1553
elements.padded._type = "Padded"
12✔
1554

1555
function elements.padded:__tostring ()
24✔
NEW
1556
   return self._type .. "(" .. tostring(self.impadded) .. ")"
×
1557
end
1558

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

1566
function elements.padded:styleChildren ()
24✔
NEW
1567
   self.impadded.mode = self.mode
×
1568
end
1569

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

1594
function elements.padded.output (_, _, _, _) end
12✔
1595

1596
elements.mathMode = mathMode
12✔
1597
elements.atomType = atomType
12✔
1598
elements.symbolDefaults = symbolDefaults
12✔
1599
elements.newSubscript = newSubscript
12✔
1600
elements.newUnderOver = newUnderOver
12✔
1601

1602
return elements
12✔
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