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

sile-typesetter / sile / 11627627247

01 Nov 2024 09:59AM UTC coverage: 65.214% (+1.3%) from 63.912%
11627627247

push

github

web-flow
Merge 336463b5e into 5dffc31aa

121 of 190 new or added lines in 5 files covered. (63.68%)

401 existing lines in 22 files now uncovered.

11895 of 18240 relevant lines covered (65.21%)

3769.0 hits per line

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

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

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

11
local elements = {}
13✔
12

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

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

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

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

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

40
local mathCache = {}
13✔
41

42
local function retrieveMathTable (font)
43
   local key = SILE.font._key(font)
3,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
645✔
176
         end
177
      end
178
   end
179
   return m
2,605✔
180
end
181

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

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

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

206
function elements.mbox:_init ()
26✔
207
   nodefactory.hbox._init(self)
1,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 (_)
26✔
229
   SU.error("styleChildren is a virtual function that need to be overridden by its child classes")
×
230
end
231

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

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

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

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

257
-- Determine the mode of its descendants
258
function elements.mbox:styleDescendants ()
26✔
259
   self:styleChildren()
1,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 ()
26✔
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)
26✔
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 = {
13✔
296
   thin = "thin",
297
   med = "med",
298
   thick = "thick",
299
}
300

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

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

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

366
function elements.stackbox:_init (direction, children)
26✔
367
   elements.mbox._init(self)
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 ()
26✔
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 ()
26✔
408
   -- For a horizontal stackbox (i.e. mrow):
409
   -- 1. set self.height and self.depth to max element height & depth
410
   -- 2. handle stretchy operators
411
   -- 3. set self.width
412
   -- For a vertical stackbox:
413
   -- 1. set self.width to max element width
414
   -- 2. set self.height
415
   -- And finally set children's relative coordinates
416
   self.height = SILE.types.length(0)
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)
26✔
460
   local mathX = typesetter.frame.state.cursorX
72✔
461
   local mathY = typesetter.frame.state.cursorY
72✔
462
   self:outputTree(self.relX + mathX, self.relY + mathY, line)
216✔
463
   typesetter.frame:advanceWritingDirection(scaleWidth(self.width, line))
144✔
464
end
465

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

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

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

482
function elements.phantom:output (_, _, _)
26✔
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)
26✔
490
elements.subscript._type = "Subscript"
13✔
491

492
function elements.subscript:__tostring ()
26✔
UNCOV
493
   return (self.sub and "Subscript" or "Superscript")
×
UNCOV
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)
26✔
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 ()
26✔
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 ()
26✔
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 ()
26✔
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 subShift
555
   local supShift
556
   if self.sub then
129✔
557
      if self.isUnderOver or SU.boolean(self.base.largeop, false) then
169✔
558
         -- Ad hoc correction on integral limits, following LuaTeX's
559
         -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
560
         subShift = -itCorr
33✔
561
      else
562
         subShift = 0
59✔
563
      end
564
      self.sub.relX = self.width + subShift
184✔
565
      self.sub.relY = SILE.types.length(math.max(
184✔
566
         constants.subscriptShiftDown * scaleDown,
92✔
567
         --self.base.depth + constants.subscriptBaselineDropMin * scaleDown,
568
         (self.sub.height - constants.subscriptTopMax * scaleDown):tonumber()
184✔
569
      ))
92✔
570
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or SU.boolean(self.base.largeop, false) then
338✔
571
         self.sub.relY = maxLength(self.sub.relY, self.base.depth + constants.subscriptBaselineDropMin * scaleDown)
99✔
572
      end
573
   end
574
   if self.sup then
129✔
575
      if self.isUnderOver or SU.boolean(self.base.largeop, false) then
117✔
576
         -- Ad hoc correction on integral limits, following LuaTeX's
577
         -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
578
         supShift = 0
21✔
579
      else
580
         supShift = itCorr
45✔
581
      end
582
      self.sup.relX = self.width + supShift
132✔
583
      self.sup.relY = SILE.types.length(math.max(
132✔
584
         isCrampedMode(self.mode) and constants.superscriptShiftUpCramped * scaleDown
132✔
585
            or constants.superscriptShiftUp * scaleDown, -- or cramped
66✔
586
         --self.base.height - constants.superscriptBaselineDropMax * scaleDown,
587
         (self.sup.depth + constants.superscriptBottomMin * scaleDown):tonumber()
132✔
588
      )) * -1
132✔
589
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or SU.boolean(self.base.largeop, false) then
234✔
590
         self.sup.relY = maxLength(
42✔
591
            (0 - self.sup.relY),
21✔
592
            self.base.height - constants.superscriptBaselineDropMax * scaleDown
21✔
593
         ) * -1
42✔
594
      end
595
   end
596
   if self.sub and self.sup then
129✔
597
      local gap = self.sub.relY - self.sub.height - self.sup.relY - self.sup.depth
87✔
598
      if gap.length:tonumber() < constants.subSuperscriptGapMin * scaleDown then
58✔
599
         -- The following adjustment comes directly from Appendix G of he
600
         -- TeXbook (rule 18e).
601
         self.sub.relY = constants.subSuperscriptGapMin * scaleDown + self.sub.height + self.sup.relY + self.sup.depth
32✔
602
         local psi = constants.superscriptBottomMaxWithSubscript * scaleDown + self.sup.relY + self.sup.depth
16✔
603
         if psi:tonumber() > 0 then
16✔
604
            self.sup.relY = self.sup.relY - psi
16✔
605
            self.sub.relY = self.sub.relY - psi
16✔
606
         end
607
      end
608
   end
UNCOV
609
   self.width = self.width
×
610
      + maxLength(
258✔
611
         self.sub and self.sub.width + subShift or SILE.types.length(0),
221✔
612
         self.sup and self.sup.width + supShift or SILE.types.length(0)
195✔
613
      )
129✔
614
      + constants.spaceAfterScript * scaleDown
258✔
615
   self.height = maxLength(
258✔
616
      self.base and self.base.height or SILE.types.length(0),
129✔
617
      self.sub and (self.sub.height - self.sub.relY) or SILE.types.length(0),
221✔
618
      self.sup and (self.sup.height - self.sup.relY) or SILE.types.length(0)
195✔
619
   )
129✔
620
   self.depth = maxLength(
258✔
621
      self.base and self.base.depth or SILE.types.length(0),
129✔
622
      self.sub and (self.sub.depth + self.sub.relY) or SILE.types.length(0),
221✔
623
      self.sup and (self.sup.depth + self.sup.relY) or SILE.types.length(0)
195✔
624
   )
129✔
625
end
626

627
function elements.subscript.output (_, _, _, _) end
127✔
628

629
elements.underOver = pl.class(elements.subscript)
26✔
630
elements.underOver._type = "UnderOver"
13✔
631

632
function elements.underOver:__tostring ()
26✔
UNCOV
633
   return self._type .. "(" .. tostring(self.base) .. ", " .. tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
×
634
end
635

636
function elements.underOver:_init (base, sub, sup)
26✔
637
   elements.mbox._init(self)
25✔
638
   self.atom = base.atom
25✔
639
   self.base = base
25✔
640
   self.sub = sub
25✔
641
   self.sup = sup
25✔
642
   if self.sup then
25✔
643
      table.insert(self.children, self.sup)
24✔
644
   end
645
   if self.base then
25✔
646
      table.insert(self.children, self.base)
25✔
647
   end
648
   if self.sub then
25✔
649
      table.insert(self.children, self.sub)
25✔
650
   end
651
end
652

653
function elements.underOver:styleChildren ()
26✔
654
   if self.base then
25✔
655
      self.base.mode = self.mode
25✔
656
   end
657
   if self.sub then
25✔
658
      self.sub.mode = getSubscriptMode(self.mode)
50✔
659
   end
660
   if self.sup then
25✔
661
      self.sup.mode = getSuperscriptMode(self.mode)
48✔
662
   end
663
end
664

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

702
function elements.underOver:shape ()
26✔
703
   if not (self.mode == mathMode.display or self.mode == mathMode.displayCramped) and SU.boolean(self.base.largeop, false) then
40✔
704
      -- FIXME
705
      -- Added the "largeop" condition, but it's kind of a workaround:
706
      -- It should rather be the "moveablelimits" propery in MathML, but we do not have that yet.
707
      -- When the base is a moveable limit, the under/over scripts are not placed under/over the base,
708
      -- but ather to the right of it, when display mode is not used.
709
      -- Notable effects:
710
      --   Mozilla MathML test 19 (on "k times" > overbrace > base)
711
      --   Maxwell's Equations in MathML3 Test Suite "complex1" (on the vectors in fractions)
712
      -- For now, go with the "largeop" property, but this is not correct.
713
      self.isUnderOver = true
15✔
714
      elements.subscript.shape(self)
15✔
715
      return
15✔
716
   end
717
   local constants = self:getMathMetrics().constants
20✔
718
   local scaleDown = self:getScaleDown()
10✔
719
   -- Determine relative Ys
720
   if self.base then
10✔
721
      self.base.relY = SILE.types.length(0)
20✔
722
   end
723
   if self.sub then
10✔
724
      self:_stretchyReshapeToBase(self.sub)
10✔
725
      self.sub.relY = self.base.depth
10✔
726
         + SILE.types.length(
20✔
727
            math.max(
20✔
728
               (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
20✔
729
               constants.lowerLimitBaselineDropMin * scaleDown
10✔
730
            )
731
         )
20✔
732
   end
733
   if self.sup then
10✔
734
      self:_stretchyReshapeToBase(self.sup)
9✔
735
      self.sup.relY = 0
9✔
736
         - self.base.height
9✔
737
         - SILE.types.length(
18✔
738
            math.max(
18✔
739
               (constants.upperLimitGapMin * scaleDown + self.sup.depth):tonumber(),
18✔
740
               constants.upperLimitBaselineRiseMin * scaleDown
9✔
741
            )
742
         )
18✔
743
   end
744
   -- Determine relative Xs based on widest symbol
745
   local widest, a, b
746
   if self.sub and self.sub.width > self.base.width then
10✔
747
      if self.sup and self.sub.width > self.sup.width then
6✔
748
         widest = self.sub
6✔
749
         a = self.base
6✔
750
         b = self.sup
6✔
751
      elseif self.sup then
×
UNCOV
752
         widest = self.sup
×
UNCOV
753
         a = self.base
×
UNCOV
754
         b = self.sub
×
755
      else
UNCOV
756
         widest = self.sub
×
UNCOV
757
         a = self.base
×
UNCOV
758
         b = nil
×
759
      end
760
   else
761
      if self.sup and self.base.width > self.sup.width then
4✔
762
         widest = self.base
3✔
763
         a = self.sub
3✔
764
         b = self.sup
3✔
765
      elseif self.sup then
1✔
UNCOV
766
         widest = self.sup
×
UNCOV
767
         a = self.base
×
UNCOV
768
         b = self.sub
×
769
      else
770
         widest = self.base
1✔
771
         a = self.sub
1✔
772
         b = nil
1✔
773
      end
774
   end
775
   widest.relX = SILE.types.length(0)
20✔
776
   local c = widest.width / 2
10✔
777
   if a then
10✔
778
      a.relX = c - a.width / 2
30✔
779
   end
780
   if b then
10✔
781
      b.relX = c - b.width / 2
27✔
782
   end
783
   local itCorr = self:calculateItalicsCorrection() * scaleDown
20✔
784
   if self.sup then
10✔
785
      self.sup.relX = self.sup.relX + itCorr / 2
18✔
786
   end
787
   if self.sub then
10✔
788
      self.sub.relX = self.sub.relX - itCorr / 2
20✔
789
   end
790
   -- Determine width and height
791
   self.width = maxLength(
20✔
792
      self.base and self.base.width or SILE.types.length(0),
10✔
793
      self.sub and self.sub.width or SILE.types.length(0),
10✔
794
      self.sup and self.sup.width or SILE.types.length(0)
10✔
795
   )
10✔
796
   if self.sup then
10✔
797
      self.height = 0 - self.sup.relY + self.sup.height
27✔
798
   else
799
      self.height = self.base and self.base.height or 0
1✔
800
   end
801
   if self.sub then
10✔
802
      self.depth = self.sub.relY + self.sub.depth
20✔
803
   else
UNCOV
804
      self.depth = self.base and self.base.depth or 0
×
805
   end
806
end
807

808
function elements.underOver:calculateItalicsCorrection ()
26✔
809
   local lastGid = getRightMostGlyphId(self.base)
25✔
810
   if lastGid > 0 then
25✔
811
      local mathMetrics = self:getMathMetrics()
25✔
812
      if mathMetrics.italicsCorrection[lastGid] then
25✔
813
         local c = mathMetrics.italicsCorrection[lastGid]
×
814
         -- If this is a big operator, and we are in display style, then the
815
         -- base glyph may be bigger than the font size. We need to adjust the
816
         -- italic correction accordingly.
UNCOV
817
         if self.base.atom == atomType.bigOperator and isDisplayMode(self.mode) then
×
UNCOV
818
            c = c * (self.base and self.base.font.size / self.font.size or 1.0)
×
819
         end
UNCOV
820
         return c
×
821
      end
822
   end
823
   return 0
25✔
824
end
825

826
function elements.underOver.output (_, _, _, _) end
38✔
827

828
-- terminal is the base class for leaf node
829
elements.terminal = pl.class(elements.mbox)
26✔
830
elements.terminal._type = "Terminal"
13✔
831

832
function elements.terminal:_init ()
26✔
833
   elements.mbox._init(self)
1,316✔
834
end
835

836
function elements.terminal.styleChildren (_) end
1,329✔
837

838
function elements.terminal.shape (_) end
13✔
839

840
elements.space = pl.class(elements.terminal)
26✔
841
elements.space._type = "Space"
13✔
842

843
function elements.space:_init ()
26✔
844
   elements.terminal._init(self)
×
845
end
846

847
function elements.space:__tostring ()
26✔
848
   return self._type
×
UNCOV
849
      .. "(width="
×
UNCOV
850
      .. tostring(self.width)
×
UNCOV
851
      .. ", height="
×
UNCOV
852
      .. tostring(self.height)
×
UNCOV
853
      .. ", depth="
×
UNCOV
854
      .. tostring(self.depth)
×
UNCOV
855
      .. ")"
×
856
end
857

858
local function getStandardLength (value)
859
   if type(value) == "string" then
957✔
860
      local direction = 1
319✔
861
      if value:sub(1, 1) == "-" then
638✔
862
         value = value:sub(2, -1)
20✔
863
         direction = -1
10✔
864
      end
865
      if value == "thin" then
319✔
866
         return SILE.types.length("3mu") * direction
198✔
867
      elseif value == "med" then
253✔
868
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
294✔
869
      elseif value == "thick" then
155✔
870
         return SILE.types.length("5mu plus 5mu") * direction
393✔
871
      end
872
   end
873
   return SILE.types.length(value)
662✔
874
end
875

876
function elements.space:_init (width, height, depth)
26✔
877
   elements.terminal._init(self)
319✔
878
   self.width = getStandardLength(width)
638✔
879
   self.height = getStandardLength(height)
638✔
880
   self.depth = getStandardLength(depth)
638✔
881
end
882

883
function elements.space:shape ()
26✔
884
   self.width = self.width:absolute() * self:getScaleDown()
1,276✔
885
   self.height = self.height:absolute() * self:getScaleDown()
1,276✔
886
   self.depth = self.depth:absolute() * self:getScaleDown()
1,276✔
887
end
888

889
function elements.space.output (_) end
332✔
890

891
-- text node. For any actual text output
892
elements.text = pl.class(elements.terminal)
26✔
893
elements.text._type = "Text"
13✔
894

895
function elements.text:__tostring ()
26✔
896
   return self._type
×
897
      .. "(atom="
×
898
      .. tostring(self.atom)
×
899
      .. ", kind="
×
900
      .. tostring(self.kind)
×
UNCOV
901
      .. ", script="
×
UNCOV
902
      .. tostring(self.script)
×
NEW
903
      .. (SU.boolean(self.stretchy, false) and ", stretchy" or "")
×
NEW
904
      .. (SU.boolean(self.largeop, false) and ", largeop" or "")
×
905
      .. ', text="'
×
906
      .. (self.originalText or self.text)
×
UNCOV
907
      .. '")'
×
908
end
909

910
function elements.text:_init (kind, attributes, script, text)
26✔
911
   elements.terminal._init(self)
997✔
912
   if not (kind == "number" or kind == "identifier" or kind == "operator" or kind == "string") then
997✔
UNCOV
913
      SU.error("Unknown text node kind '" .. kind .. "'; should be one of: number, identifier, operator, string")
×
914
   end
915
   self.kind = kind
997✔
916
   self.script = script
997✔
917
   self.text = text
997✔
918
   if self.script ~= "upright" then
997✔
919
      local converted = convertMathVariantScript(self.text, self.script)
997✔
920
      self.originalText = self.text
997✔
921
      self.text = converted
997✔
922
   end
923
   if self.kind == "operator" then
997✔
924
      if self.text == "-" then
394✔
925
         self.text = "−"
13✔
926
      end
927
   end
928
   for attribute, value in pairs(attributes) do
1,467✔
929
      self[attribute] = value
470✔
930
   end
931
end
932

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

1008
function elements.text.findClosestVariant(_, variants, requiredAdvance, currentAdvance)
26✔
1009
   local closest
1010
   local closestI
1011
   local m = requiredAdvance - currentAdvance
79✔
1012
   for i, variant in ipairs(variants) do
1,106✔
1013
      local diff = math.abs(variant.advanceMeasurement - requiredAdvance)
1,027✔
1014
      SU.debug("math", "stretch: diff =", diff)
1,027✔
1015
      if diff < m then
1,027✔
1016
         closest = variant
93✔
1017
         closestI = i
93✔
1018
         m = diff
93✔
1019
      end
1020
   end
1021
   return closest, closestI
79✔
1022
end
1023

1024
function elements.text:_reshapeGlyph(glyph, closestVariant, sz)
26✔
1025
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
47✔
1026
   local dimen = hb.get_glyph_dimensions(face, sz, closestVariant.variantGlyph)
47✔
1027
   glyph.gid = closestVariant.variantGlyph
47✔
1028
   glyph.width, glyph.height, glyph.depth, glyph.glyphAdvance = dimen.width, dimen.height, dimen.depth, dimen.glyphAdvance
47✔
1029
   return dimen
47✔
1030
end
1031

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

1076
function elements.text:_vertStretchyReshape(depth, height)
26✔
1077
   local hasStretched = self:_stretchyReshape(depth + height, true)
158✔
1078
   if hasStretched then
79✔
1079
      -- HACK: see output routine
1080
      self.vertExpectedSz = height + depth
94✔
1081
      self.vertScalingRatio = (depth + height):tonumber() / (self.height:tonumber() + self.depth:tonumber())
235✔
1082
      self.height = height
47✔
1083
      self.depth = depth
47✔
1084
   end
1085
   return hasStretched
79✔
1086
end
1087

1088
function elements.text:_horizStretchyReshape(width)
26✔
NEW
1089
   local hasStretched = self:_stretchyReshape(width, false)
×
NEW
1090
   if hasStretched then
×
1091
      -- HACK: see output routine
NEW
1092
      self.horizScalingRatio = width:tonumber() / self.width:tonumber()
×
NEW
1093
      self.width = width
×
1094
   end
NEW
1095
   return hasStretched
×
1096
end
1097

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

1130
elements.fraction = pl.class(elements.mbox)
26✔
1131
elements.fraction._type = "Fraction"
13✔
1132

1133
function elements.fraction:__tostring ()
26✔
1134
   return self._type .. "(" .. tostring(self.numerator) .. ", " .. tostring(self.denominator) .. ")"
×
1135
end
1136

1137
function elements.fraction:_init (attributes, numerator, denominator)
26✔
1138
   elements.mbox._init(self)
33✔
1139
   self.numerator = numerator
33✔
1140
   self.denominator = denominator
33✔
1141
   self.attributes = attributes
33✔
1142
   table.insert(self.children, numerator)
33✔
1143
   table.insert(self.children, denominator)
33✔
1144
end
1145

1146
function elements.fraction:styleChildren ()
26✔
1147
   self.numerator.mode = getNumeratorMode(self.mode)
66✔
1148
   self.denominator.mode = getDenominatorMode(self.mode)
66✔
1149
end
1150

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

1160
   -- Determine relative abscissas and width
1161
   local widest, other
1162
   if self.denominator.width > self.numerator.width then
33✔
1163
      widest, other = self.denominator, self.numerator
25✔
1164
   else
1165
      widest, other = self.numerator, self.denominator
8✔
1166
   end
1167
   widest.relX = self.padding
33✔
1168
   other.relX = self.padding + (widest.width - other.width) / 2
132✔
1169
   self.width = widest.width + 2 * self.padding
99✔
1170
   -- Determine relative ordinates and height
1171
   local constants = self:getMathMetrics().constants
66✔
1172
   local scaleDown = self:getScaleDown()
33✔
1173
   self.axisHeight = constants.axisHeight * scaleDown
33✔
1174
   self.ruleThickness = self.attributes.linethickness
33✔
1175
      and SU.cast("measurement", self.attributes.linethickness):tonumber()
33✔
1176
      or constants.fractionRuleThickness * scaleDown
33✔
1177

1178
   -- MathML Core 3.3.2.2 ("Fraction with zero line thickness") uses
1179
   -- stack(DisplayStyle)GapMin, stackTop(DisplayStyle)ShiftUp and stackBottom(DisplayStyle)ShiftDown.
1180
   -- TODO not implemented
1181
   -- The most common use cases for zero line thickness are:
1182
   --  - Binomial coefficients
1183
   --  - Stacked subscript/superscript on big operators such as sums.
1184

1185
   local numeratorGapMin, denominatorGapMin, numeratorShiftUp, denominatorShiftDown
1186
   if isDisplayMode(self.mode) then
66✔
1187
      numeratorGapMin = constants.fractionNumDisplayStyleGapMin * scaleDown
10✔
1188
      denominatorGapMin = constants.fractionDenomDisplayStyleGapMin * scaleDown
10✔
1189
      numeratorShiftUp = constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
10✔
1190
      denominatorShiftDown = constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
10✔
1191
   else
1192
      numeratorGapMin = constants.fractionNumeratorGapMin * scaleDown
23✔
1193
      denominatorGapMin = constants.fractionDenominatorGapMin * scaleDown
23✔
1194
      numeratorShiftUp = constants.fractionNumeratorShiftUp * scaleDown
23✔
1195
      denominatorShiftDown = constants.fractionDenominatorShiftDown * scaleDown
23✔
1196
   end
1197

1198
   self.numerator.relY = -self.axisHeight
33✔
1199
      - self.ruleThickness / 2
33✔
1200
      - SILE.types.length(
66✔
1201
         math.max(
66✔
1202
            (numeratorGapMin + self.numerator.depth):tonumber(),
66✔
1203
            numeratorShiftUp - self.axisHeight - self.ruleThickness / 2
33✔
1204
         )
1205
      )
66✔
1206
   self.denominator.relY = -self.axisHeight
33✔
1207
      + self.ruleThickness / 2
33✔
1208
      + SILE.types.length(
66✔
1209
         math.max(
66✔
1210
            (denominatorGapMin + self.denominator.height):tonumber(),
66✔
1211
            denominatorShiftDown + self.axisHeight - self.ruleThickness / 2
33✔
1212
         )
1213
      )
66✔
1214
   self.height = self.numerator.height - self.numerator.relY
66✔
1215
   self.depth = self.denominator.relY + self.denominator.depth
66✔
1216
end
1217

1218
function elements.fraction:output (x, y, line)
26✔
1219
   if self.ruleThickness > 0 then
33✔
1220
      SILE.outputter:drawRule(
66✔
1221
         scaleWidth(x + self.padding, line),
66✔
1222
         y.length - self.axisHeight - self.ruleThickness / 2,
66✔
1223
         scaleWidth(self.width - 2 * self.padding, line),
99✔
1224
         self.ruleThickness
1225
      )
33✔
1226
   end
1227
end
1228

1229
local function newSubscript (spec)
1230
   return elements.subscript(spec.base, spec.sub, spec.sup)
114✔
1231
end
1232

1233
local function newUnderOver (spec)
1234
   return elements.underOver(spec.base, spec.sub, spec.sup)
25✔
1235
end
1236

1237
-- TODO replace with penlight equivalent
1238
local function mapList (f, l)
1239
   local ret = {}
9✔
1240
   for i, x in ipairs(l) do
35✔
1241
      ret[i] = f(i, x)
52✔
1242
   end
1243
   return ret
9✔
1244
end
1245

1246
elements.mtr = pl.class(elements.mbox)
26✔
1247
-- elements.mtr._type = "" -- TODO why not set?
1248

1249
function elements.mtr:_init (children)
26✔
1250
   self.children = children
12✔
1251
end
1252

1253
function elements.mtr:styleChildren ()
26✔
1254
   for _, c in ipairs(self.children) do
48✔
1255
      c.mode = self.mode
36✔
1256
   end
1257
end
1258

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

1261
function elements.mtr.output (_) end
25✔
1262

1263
elements.table = pl.class(elements.mbox)
26✔
1264
elements.table._type = "table" -- TODO why case difference?
13✔
1265

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

1311
function elements.table:styleChildren ()
26✔
1312
   if self.mode == mathMode.display and self.options.displaystyle ~= "false" then
9✔
1313
      for _, c in ipairs(self.children) do
19✔
1314
         c.mode = mathMode.display
14✔
1315
      end
1316
   else
1317
      for _, c in ipairs(self.children) do
16✔
1318
         c.mode = mathMode.text
12✔
1319
      end
1320
   end
1321
end
1322

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

1388
function elements.table.output (_) end
22✔
1389

1390
local function getRadicandMode (mode)
1391
   -- Not too sure if we should do something special/
UNCOV
1392
   return mode
×
1393
end
1394

1395
local function getDegreeMode (mode)
1396
   -- 2 levels smaller, up to scriptScript evntually.
1397
   -- Not too sure if we should do something else.
1398
   if mode == mathMode.display then
×
UNCOV
1399
      return mathMode.scriptScript
×
UNCOV
1400
   elseif mode == mathMode.displayCramped then
×
UNCOV
1401
      return mathMode.scriptScriptCramped
×
UNCOV
1402
   elseif mode == mathMode.text or mode == mathMode.script or mode == mathMode.scriptScript then
×
UNCOV
1403
      return mathMode.scriptScript
×
1404
   end
UNCOV
1405
   return mathMode.scriptScriptCramped
×
1406
end
1407

1408
elements.sqrt = pl.class(elements.mbox)
26✔
1409
elements.sqrt._type = "Sqrt"
13✔
1410

1411
function elements.sqrt:__tostring ()
26✔
UNCOV
1412
   return self._type .. "(" .. tostring(self.radicand) .. (self.degree and ", " .. tostring(self.degree) or "") .. ")"
×
1413
end
1414

1415
function elements.sqrt:_init (radicand, degree)
26✔
UNCOV
1416
   elements.mbox._init(self)
×
UNCOV
1417
   self.radicand = radicand
×
UNCOV
1418
   if degree then
×
UNCOV
1419
      self.degree = degree
×
UNCOV
1420
      table.insert(self.children, degree)
×
1421
   end
UNCOV
1422
   table.insert(self.children, radicand)
×
UNCOV
1423
   self.relX = SILE.types.length()
×
1424
   self.relY = SILE.types.length()
×
1425
end
1426

1427
function elements.sqrt:styleChildren ()
26✔
UNCOV
1428
   self.radicand.mode = getRadicandMode(self.mode)
×
UNCOV
1429
   if self.degree then
×
UNCOV
1430
      self.degree.mode = getDegreeMode(self.mode)
×
1431
   end
1432
end
1433

1434
function elements.sqrt:shape ()
26✔
UNCOV
1435
   local mathMetrics = self:getMathMetrics()
×
UNCOV
1436
   local scaleDown = self:getScaleDown()
×
UNCOV
1437
   local constants = mathMetrics.constants
×
1438

UNCOV
1439
   self.radicalRuleThickness = constants.radicalRuleThickness * scaleDown
×
UNCOV
1440
   if self.mode == mathMode.display or self.mode == mathMode.displayCramped then
×
UNCOV
1441
      self.radicalVerticalGap = constants.radicalDisplayStyleVerticalGap * scaleDown
×
1442
   else
UNCOV
1443
      self.radicalVerticalGap = constants.radicalVerticalGap * scaleDown
×
1444
   end
1445
   self.extraAscender = constants.radicalExtraAscender * scaleDown
×
1446

1447
   -- HACK: We draw own own radical sign in the output() method.
1448
   -- Derive dimensions for the radical sign (more or less ad hoc).
1449
   -- Note: In TeX, the radical sign extends a lot below the baseline,
1450
   -- and MathML Core also has a lot of layout text about it.
1451
   -- Not only it doesn't look good, but it's not very clear vs. OpenType.
1452
   local radicalGlyph = SILE.shaper:measureChar("√")
×
1453
   local ratio = (self.radicand.height:tonumber() + self.radicand.depth:tonumber())
×
1454
      / (radicalGlyph.height + radicalGlyph.depth)
×
1455
   local vertAdHocOffset = (ratio > 1 and math.log(ratio) or 0) * self.radicalVerticalGap
×
1456
   self.symbolHeight = SILE.types.length(radicalGlyph.height) * scaleDown
×
UNCOV
1457
   self.symbolDepth = (SILE.types.length(radicalGlyph.depth) + vertAdHocOffset) * scaleDown
×
1458
   self.symbolWidth = (SILE.types.length(radicalGlyph.width) + vertAdHocOffset) * scaleDown
×
1459

1460
   -- Adjust the height of the radical sign if the radicand is higher
UNCOV
1461
   self.symbolHeight = self.radicand.height > self.symbolHeight and self.radicand.height or self.symbolHeight
×
1462
   -- Compute the (max-)height of the short leg of the radical sign
UNCOV
1463
   self.symbolShortHeight = self.symbolHeight * constants.radicalDegreeBottomRaisePercent
×
1464

1465
   self.offsetX = SILE.types.length()
×
UNCOV
1466
   if self.degree then
×
1467
      -- Position the degree
UNCOV
1468
      self.degree.relY = -constants.radicalDegreeBottomRaisePercent * self.symbolHeight
×
1469
      -- Adjust the height of the short leg of the radical sign to ensure the degree is not too close
1470
      -- (empirically use radicalExtraAscender)
1471
      self.symbolShortHeight = self.symbolShortHeight - constants.radicalExtraAscender * scaleDown
×
1472
      -- Compute the width adjustment for the degree
1473
      self.offsetX = self.degree.width
×
UNCOV
1474
         + constants.radicalKernBeforeDegree * scaleDown
×
1475
         + constants.radicalKernAfterDegree * scaleDown
×
1476
   end
1477
   -- Position the radicand
UNCOV
1478
   self.radicand.relX = self.symbolWidth + self.offsetX
×
1479
   -- Compute the dimentions of the whole radical
UNCOV
1480
   self.width = self.radicand.width + self.symbolWidth + self.offsetX
×
1481
   self.height = self.symbolHeight + self.radicalVerticalGap + self.extraAscender
×
1482
   self.depth = self.radicand.depth
×
1483
end
1484

1485
local function _r (number)
1486
   -- Lua 5.3+ formats floats as 1.0 and integers as 1
1487
   -- Also some PDF readers do not like double precision.
1488
   return math.floor(number) == number and math.floor(number) or tonumber(string.format("%.5f", number))
×
1489
end
1490

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

1537
elements.padded = pl.class(elements.mbox)
26✔
1538
elements.padded._type = "Padded"
13✔
1539

1540
function elements.padded:__tostring ()
26✔
NEW
1541
   return self._type .. "(" .. tostring(self.impadded) .. ")"
×
1542
end
1543

1544
function elements.padded:_init (attributes, impadded)
26✔
NEW
1545
   elements.mbox._init(self)
×
NEW
1546
   self.impadded = impadded
×
NEW
1547
   self.attributes = attributes or {}
×
NEW
1548
   table.insert(self.children, impadded)
×
1549
end
1550

1551
function elements.padded:styleChildren ()
26✔
NEW
1552
   self.impadded.mode = self.mode
×
1553
end
1554

1555
function elements.padded:shape ()
26✔
1556
   -- TODO MathML allows percentages font-relative units (em, ex) for padding
1557
   -- But our units work with font.size, not math.font.size (possibly adjusted by scaleDown)
1558
   -- so the expectations might not be met.
NEW
1559
   local width = self.attributes.width and SU.cast("measurement", self.attributes.width)
×
NEW
1560
   local height = self.attributes.height and SU.cast("measurement", self.attributes.height)
×
NEW
1561
   local depth = self.attributes.depth and SU.cast("measurement", self.attributes.depth)
×
NEW
1562
   local lspace = self.attributes.lspace and SU.cast("measurement", self.attributes.lspace)
×
NEW
1563
   local voffset = self.attributes.voffset and SU.cast("measurement", self.attributes.voffset)
×
1564
   -- Clamping for width, height, depth, lspace
NEW
1565
   width = width and (width:tonumber() > 0 and width or SILE.types.measurement())
×
NEW
1566
   height = height and (height:tonumber() > 0 and height or SILE.types.measurement())
×
NEW
1567
   depth = depth and (depth:tonumber() > 0 and depth or SILE.types.measurement())
×
NEW
1568
   lspace = lspace and (lspace:tonumber() > 0 and lspace or SILE.types.measurement())
×
1569
   -- No clamping for voffset
NEW
1570
   voffset = voffset or SILE.types.measurement(0)
×
1571
   -- Compute the dimensions
NEW
1572
   self.width = width and SILE.types.length(width) or self.impadded.width
×
NEW
1573
   self.height = height and SILE.types.length(height) or self.impadded.height
×
NEW
1574
   self.depth = depth and SILE.types.length(depth) or self.impadded.depth
×
NEW
1575
   self.impadded.relX = lspace and SILE.types.length(lspace) or SILE.types.length()
×
NEW
1576
   self.impadded.relY = voffset and SILE.types.length(voffset):negate() or SILE.types.length()
×
1577
end
1578

1579
function elements.padded.output (_, _, _, _) end
13✔
1580

1581
elements.mathMode = mathMode
13✔
1582
elements.atomType = atomType
13✔
1583
elements.symbolDefaults = symbolDefaults
13✔
1584
elements.newSubscript = newSubscript
13✔
1585
elements.newUnderOver = newUnderOver
13✔
1586

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

© 2026 Coveralls, Inc