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

sile-typesetter / sile / 12005578175

25 Nov 2024 07:53AM UTC coverage: 57.011% (-7.3%) from 64.353%
12005578175

push

github

alerque
chore(tooling): Extend and annotate spell check exceptions

11267 of 19763 relevant lines covered (57.01%)

755.25 hits per line

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

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

8
local elements = {}
1✔
9

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

21
local function isDisplayMode (mode)
22
   return mode <= 1
252✔
23
end
24

25
local function isCrampedMode (mode)
26
   return mode % 2 == 1
12✔
27
end
28

29
local function isScriptMode (mode)
30
   return mode == mathMode.script or mode == mathMode.scriptCramped
474✔
31
end
32

33
local function isScriptScriptMode (mode)
34
   return mode == mathMode.scriptScript or mode == mathMode.scriptScriptCramped
444✔
35
end
36

37
local mathCache = {}
1✔
38

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

56
            The math table in '%s' could not be %s.
57
         ]]):format(face.filename, fontHasMathTable and "parsed" or "loaded"))
×
58
      end
59
      local upem = ot.parseHead(hb.get_table(face, "head")).unitsPerEm
8✔
60
      local constants = {}
4✔
61
      for k, v in pairs(mathTable.mathConstants) do
228✔
62
         if type(v) == "table" then
224✔
63
            v = v.value
204✔
64
         end
65
         if k:sub(-9) == "ScaleDown" then
448✔
66
            constants[k] = v / 100
8✔
67
         else
68
            constants[k] = v * font.size / upem
216✔
69
         end
70
      end
71
      local italicsCorrection = {}
4✔
72
      for k, v in pairs(mathTable.mathItalicsCorrection) do
1,696✔
73
         italicsCorrection[k] = v.value * font.size / upem
1,692✔
74
      end
75
      mathCache[key] = {
4✔
76
         constants = constants,
4✔
77
         italicsCorrection = italicsCorrection,
4✔
78
         mathVariants = mathTable.mathVariants,
4✔
79
         unitsPerEm = upem,
4✔
80
      }
4✔
81
   end
82
   return mathCache[key]
486✔
83
end
84

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

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

151
local function getRightMostGlyphId (node)
152
   while node and node:is_a(elements.stackbox) and node.direction == "H" do
40✔
153
      node = node.children[#node.children]
8✔
154
   end
155
   if node and node:is_a(elements.text) then
24✔
156
      return node.value.glyphString[#node.value.glyphString]
12✔
157
   else
158
      return 0
×
159
   end
160
end
161

162
-- Compares two SILE.types.length, without considering shrink or stretch values, and
163
-- returns the biggest.
164
local function maxLength (...)
165
   local arg = { ... }
348✔
166
   local m
167
   for i, v in ipairs(arg) do
1,068✔
168
      if i == 1 then
720✔
169
         m = v
348✔
170
      else
171
         if v.length:tonumber() > m.length:tonumber() then
1,116✔
172
            m = v
60✔
173
         end
174
      end
175
   end
176
   return m
348✔
177
end
178

179
local function scaleWidth (length, line)
180
   local number = length.length
132✔
181
   if line.ratio and line.ratio < 0 and length.shrink:tonumber() > 0 then
132✔
182
      number = number + length.shrink * line.ratio
×
183
   elseif line.ratio and line.ratio > 0 and length.stretch:tonumber() > 0 then
264✔
184
      number = number + length.stretch * line.ratio
240✔
185
   end
186
   return number
132✔
187
end
188

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

199
function elements.mbox:__tostring ()
2✔
200
   return self._type
×
201
end
202

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

225
function elements.mbox.styleChildren (_)
2✔
226
   SU.error("styleChildren is a virtual function that need to be overridden by its child classes")
×
227
end
228

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

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

237
function elements.mbox:getMathMetrics ()
2✔
238
   return retrieveMathTable(self.font)
486✔
239
end
240

241
function elements.mbox:getScaleDown ()
2✔
242
   local constants = self:getMathMetrics().constants
600✔
243
   local scaleDown
244
   if isScriptMode(self.mode) then
600✔
245
      scaleDown = constants.scriptPercentScaleDown
18✔
246
   elseif isScriptScriptMode(self.mode) then
564✔
247
      scaleDown = constants.scriptScriptPercentScaleDown
×
248
   else
249
      scaleDown = 1
282✔
250
   end
251
   return scaleDown
300✔
252
end
253

254
-- Determine the mode of its descendants
255
function elements.mbox:styleDescendants ()
2✔
256
   self:styleChildren()
218✔
257
   for _, n in ipairs(self.children) do
430✔
258
      if n then
212✔
259
         n:styleDescendants()
212✔
260
      end
261
   end
262
end
263

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

277
-- Output the node and all its descendants
278
function elements.mbox:outputTree (x, y, line)
2✔
279
   self:output(x, y, line)
218✔
280
   local debug = SILE.settings:get("math.debug.boxes")
218✔
281
   if debug and not (self:is_a(elements.space)) then
218✔
282
      SILE.outputter:setCursor(scaleWidth(x, line), y.length)
×
283
      SILE.outputter:debugHbox({ height = self.height.length, depth = self.depth.length }, scaleWidth(self.width, line))
×
284
   end
285
   for _, n in ipairs(self.children) do
430✔
286
      if n then
212✔
287
         n:outputTree(x + n.relX, y + n.relY, line)
636✔
288
      end
289
   end
290
end
291

292
local spaceKind = {
1✔
293
   thin = "thin",
294
   med = "med",
295
   thick = "thick",
296
}
297

298
-- Spacing table indexed by left atom, as in TeXbook p. 170.
299
-- Notes
300
--  - the "notScript" key is used to prevent spaces in script and scriptscript modes
301
--    (= parenthesized non-zero value in The TeXbook's table).
302
--  - Cases commented are as expected, just listed for clarity and completeness.
303
--    (= no space i.e. 0 in in The TeXbook's table)
304
--  - Cases marked as impossible are not expected to happen (= stars in the TeXbook):
305
--    "... such cases never arise, because binary atoms must be preceded and followed
306
--    by atoms compatible with the nature of binary operations."
307
--    This must be understood with the context explained onp. 133:
308
--     "... binary operations are treated as ordinary symbols if they don’t occur
309
--     between two quantities that they can operate on." (a rule which notably helps
310
--     addressing binary atoms used as unary operators.)
311
local spacingRules = {
1✔
312
   [atoms.types.ord] = {
1✔
313
      -- [atoms.types.ord] = nil
314
      [atoms.types.op] = { spaceKind.thin },
1✔
315
      [atoms.types.bin] = { spaceKind.med, notScript = true },
1✔
316
      [atoms.types.rel] = { spaceKind.thick, notScript = true },
1✔
317
      -- [atoms.types.open] = nil
318
      -- [atoms.types.close] = nil
319
      -- [atoms.types.punct] = nil
320
      [atoms.types.inner] = { spaceKind.thin, notScript = true },
1✔
321
   },
1✔
322
   [atoms.types.op] = {
1✔
323
      [atoms.types.ord] = { spaceKind.thin },
1✔
324
      [atoms.types.op] = { spaceKind.thin },
1✔
325
      [atoms.types.bin] = { impossible = true },
1✔
326
      [atoms.types.rel] = { spaceKind.thick, notScript = true },
1✔
327
      -- [atoms.types.open] = nil
328
      -- [atoms.types.close] = nil
329
      -- [atoms.types.punct] = nil
330
      [atoms.types.inner] = { spaceKind.thin, notScript = true },
1✔
331
   },
1✔
332
   [atoms.types.bin] = {
1✔
333
      [atoms.types.ord] = { spaceKind.med, notScript = true },
1✔
334
      [atoms.types.op] = { spaceKind.med, notScript = true },
1✔
335
      [atoms.types.bin] = { impossible = true },
1✔
336
      [atoms.types.rel] = { impossible = true },
1✔
337
      [atoms.types.open] = { spaceKind.med, notScript = true },
1✔
338
      [atoms.types.close] = { impossible = true },
1✔
339
      [atoms.types.punct] = { impossible = true },
1✔
340
      [atoms.types.inner] = { spaceKind.med, notScript = true },
1✔
341
   },
1✔
342
   [atoms.types.rel] = {
1✔
343
      [atoms.types.ord] = { spaceKind.thick, notScript = true },
1✔
344
      [atoms.types.op] = { spaceKind.thick, notScript = true },
1✔
345
      [atoms.types.bin] = { impossible = true },
1✔
346
      -- [atoms.types.rel] = nil
347
      [atoms.types.open] = { spaceKind.thick, notScript = true },
1✔
348
      -- [atoms.types.close] = nil
349
      -- [atoms.types.punct] = nil
350
      [atoms.types.inner] = { spaceKind.thick, notScript = true },
1✔
351
   },
1✔
352
   [atoms.types.open] = {
1✔
353
      -- [atoms.types.ord] = nil
354
      -- [atoms.types.op] = nil
355
      [atoms.types.bin] = { impossible = true },
1✔
356
      -- [atoms.types.rel] = nil
357
      -- [atoms.types.open] = nil
358
      -- [atoms.types.close] = nil
359
      -- [atoms.types.punct] = nil
360
      -- [atoms.types.inner] = nil
361
   },
1✔
362
   [atoms.types.close] = {
1✔
363
      -- [atoms.types.ord] = nil
364
      [atoms.types.op] = { spaceKind.thin },
1✔
365
      [atoms.types.bin] = { spaceKind.med, notScript = true },
1✔
366
      [atoms.types.rel] = { spaceKind.thick, notScript = true },
1✔
367
      -- [atoms.types.open] = nil
368
      -- [atoms.types.close] = nil
369
      -- [atoms.types.punct] = nil
370
      [atoms.types.inner] = { spaceKind.thin, notScript = true },
1✔
371
   },
1✔
372
   [atoms.types.punct] = {
1✔
373
      [atoms.types.ord] = { spaceKind.thin, notScript = true },
1✔
374
      [atoms.types.op] = { spaceKind.thin, notScript = true },
1✔
375
      [atoms.types.bin] = { impossible = true },
1✔
376
      [atoms.types.rel] = { spaceKind.thin, notScript = true },
1✔
377
      [atoms.types.open] = { spaceKind.thin, notScript = true },
1✔
378
      [atoms.types.close] = { spaceKind.thin, notScript = true },
1✔
379
      [atoms.types.punct] = { spaceKind.thin, notScript = true },
1✔
380
      [atoms.types.inner] = { spaceKind.thin, notScript = true },
1✔
381
   },
1✔
382
   [atoms.types.inner] = {
1✔
383
      [atoms.types.ord] = { spaceKind.thin, notScript = true },
1✔
384
      [atoms.types.op] = { spaceKind.thin },
1✔
385
      [atoms.types.bin] = { spaceKind.med, notScript = true },
1✔
386
      [atoms.types.rel] = { spaceKind.thick, notScript = true },
1✔
387
      [atoms.types.open] = { spaceKind.thin, notScript = true },
1✔
388
      [atoms.types.punct] = { spaceKind.thin, notScript = true },
1✔
389
      -- [atoms.types.close] = nil
390
      [atoms.types.inner] = { spaceKind.thin, notScript = true },
1✔
391
   },
1✔
392
}
393

394
-- _stackbox stacks its content one, either horizontally or vertically
395
elements.stackbox = pl.class(elements.mbox)
2✔
396
elements.stackbox._type = "Stackbox"
1✔
397

398
function elements.stackbox:__tostring ()
2✔
399
   local result = self.direction .. "Box("
×
400
   for i, n in ipairs(self.children) do
×
401
      result = result .. (i == 1 and "" or ", ") .. tostring(n)
×
402
   end
403
   result = result .. ")"
×
404
   return result
×
405
end
406

407
function elements.stackbox:_init (direction, children)
2✔
408
   elements.mbox._init(self)
32✔
409
   if not (direction == "H" or direction == "V") then
32✔
410
      SU.error("Wrong direction '" .. direction .. "'; should be H or V")
×
411
   end
412
   self.direction = direction
32✔
413
   self.children = children
32✔
414
end
415

416
function elements.stackbox:styleChildren ()
2✔
417
   for _, n in ipairs(self.children) do
172✔
418
      n.mode = self.mode
140✔
419
   end
420
   if self.direction == "H" then
32✔
421
      -- Insert spaces according to the atom type, following Knuth's guidelines
422
      -- in The TeXbook, p. 170 (amended with p. 133 for binary operators)
423
      -- FIXME: This implementation is not using the atom form and the MathML logic (lspace/rspace).
424
      -- (This is notably unsatisfactory for <mphantom> elements)
425
      local spaces = {}
32✔
426
      if #self.children >= 1 then
32✔
427
         -- An interpretation of the TeXbook p. 133 for binary operator exceptions:
428
         -- A binary operator at the beginning of the expression is treated as an ordinary atom
429
         -- (so as to be considered as a unary operator, without more context).
430
         local v = self.children[1]
32✔
431
         if v.atom == atoms.types.bin then
32✔
432
            v.atom = atoms.types.ord
×
433
         end
434
      end
435
      for i = 1, #self.children - 1 do
140✔
436
         local v = self.children[i]
108✔
437
         local v2 = self.children[i + 1]
108✔
438
         -- Handle re-wrapped paired open/close symbols
439
         v = v.is_paired and v.children[#v.children] or v
108✔
440
         v2 = v2.is_paired and v2.children[1] or v2
108✔
441
         if spacingRules[v.atom] and spacingRules[v.atom][v2.atom] then
108✔
442
            local rule = spacingRules[v.atom][v2.atom]
48✔
443
            if rule.impossible then
48✔
444
               -- Another interpretation of the TeXbook p. 133 for binary operator exceptions:
445
               if v2.atom == atoms.types.bin then
×
446
                  -- If a binary atom follows an atom that is not compatible with it, make it an ordinary.
447
                  -- (so as to be conidered as a unary operator).
448
                  -- Typical case: "a = -b" (ord rel bin ord), "a + -b" (ord bin bin ord)
449
                  v2.atom = atoms.types.ord
×
450
               else
451
                  -- If a binary atom precedes an atom that is not compatible with it, make it an ordinary.
452
                  -- Quite unusual case (bin, rel/close/punct) unlikely to happen in practice.
453
                  -- (Not seen in 80+ test formulas)
454
                  -- We might address it a bit late here, the preceding atom has already based its spacing
455
                  -- on the binary atom... but this might not be a big deal.
456
                  -- (i.e. rather than add an extra look-ahead just for this case).
457
                  -- Artificial example: "a + = b" (ord bin rel ord)
458
                  v.atom = atoms.types.ord
×
459
               end
460
               rule = spacingRules[v.atom][v2.atom]
×
461
               if rule and rule.impossible then
×
462
                  -- Should not occur if we did our table based on the TeXbook correctly?
463
                  -- We can still handle it by ignoring the rule: no spacing sounds logical.
464
                  -- But let's have a warning so it might be investigated further.
465
                  SU.warn("Impossible spacing rule for (" .. v.atom .. ", " .. v2.atom .. "), please report this issue")
×
466
                  rule = nil
×
467
               end
468
            end
469
            if rule and not (rule.notScript and (isScriptMode(self.mode) or isScriptScriptMode(self.mode))) then
144✔
470
               spaces[i + 1] = rule[1]
48✔
471
            end
472
         end
473
      end
474
      local spaceIdx = {}
32✔
475
      for i, _ in pairs(spaces) do
80✔
476
         table.insert(spaceIdx, i)
48✔
477
      end
478
      table.sort(spaceIdx, function (a, b)
64✔
479
         return a > b
60✔
480
      end)
481
      for _, idx in ipairs(spaceIdx) do
80✔
482
         local hsp = elements.space(spaces[idx], 0, 0)
48✔
483
         table.insert(self.children, idx, hsp)
48✔
484
      end
485
   end
486
end
487

488
function elements.stackbox:shape ()
2✔
489
   -- For a horizontal stackbox (i.e. mrow):
490
   -- 1. set self.height and self.depth to max element height & depth
491
   -- 2. handle stretchy operators
492
   -- 3. set self.width
493
   -- For a vertical stackbox:
494
   -- 1. set self.width to max element width
495
   -- 2. set self.height
496
   -- And finally set children's relative coordinates
497
   self.height = SILE.types.length(0)
64✔
498
   self.depth = SILE.types.length(0)
64✔
499
   if self.direction == "H" then
32✔
500
      for i, n in ipairs(self.children) do
220✔
501
         n.relY = SILE.types.length(0)
376✔
502
         self.height = i == 1 and n.height or maxLength(self.height, n.height)
344✔
503
         self.depth = i == 1 and n.depth or maxLength(self.depth, n.depth)
344✔
504
      end
505
      -- Handle stretchy operators
506
      for _, elt in ipairs(self.children) do
220✔
507
         if elt:is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
454✔
508
            elt:_vertStretchyReshape(self.depth, self.height)
36✔
509
         end
510
      end
511
      -- Set self.width
512
      self.width = SILE.types.length(0)
64✔
513
      for i, n in ipairs(self.children) do
220✔
514
         n.relX = self.width
188✔
515
         self.width = i == 1 and n.width or self.width + n.width
344✔
516
      end
517
   else -- self.direction == "V"
518
      for i, n in ipairs(self.children) do
×
519
         n.relX = SILE.types.length(0)
×
520
         self.width = i == 1 and n.width or maxLength(self.width, n.width)
×
521
      end
522
      -- Set self.height and self.depth
523
      for i, n in ipairs(self.children) do
×
524
         self.depth = i == 1 and n.depth or self.depth + n.depth
×
525
      end
526
      for i = 1, #self.children do
×
527
         local n = self.children[i]
×
528
         if i == 1 then
×
529
            self.height = n.height
×
530
            self.depth = n.depth
×
531
         elseif i > 1 then
×
532
            n.relY = self.children[i - 1].relY + self.children[i - 1].depth + n.height
×
533
            self.depth = self.depth + n.height + n.depth
×
534
         end
535
      end
536
   end
537
end
538

539
-- Despite of its name, this function actually output the whole tree of nodes recursively.
540
function elements.stackbox:outputYourself (typesetter, line)
2✔
541
   local mathX = typesetter.frame.state.cursorX
6✔
542
   local mathY = typesetter.frame.state.cursorY
6✔
543
   self:outputTree(self.relX + mathX, self.relY + mathY, line)
18✔
544
   typesetter.frame:advanceWritingDirection(scaleWidth(self.width, line))
12✔
545
end
546

547
function elements.stackbox.output (_, _, _, _) end
33✔
548

549
elements.phantom = pl.class(elements.stackbox) -- inherit from stackbox
2✔
550
elements.phantom._type = "Phantom"
1✔
551

552
function elements.phantom:_init (children)
2✔
553
   -- MathML core 3.3.7:
554
   -- "Its layout algorithm is the same as the mrow element".
555
   -- Also not the MathML states that <mphantom> is sort of legacy, "implemented
556
   -- for compatibility with full MathML. Authors whose only target is MathML
557
   -- Core are encouraged to use CSS for styling."
558
   -- The thing is that we don't have CSS in SILE, so supporting <mphantom> is
559
   -- a must.
560
   elements.stackbox._init(self, "H", children)
×
561
end
562

563
function elements.phantom:output (_, _, _)
2✔
564
   -- Note the trick here: when the tree is rendered, the node's output
565
   -- function is invoked, then all its children's output functions.
566
   -- So we just cancel the list of children here, before it's rendered.
567
   self.children = {}
×
568
end
569

570
elements.subscript = pl.class(elements.mbox)
2✔
571
elements.subscript._type = "Subscript"
1✔
572

573
function elements.subscript:__tostring ()
2✔
574
   return (self.sub and "Subscript" or "Superscript")
×
575
      .. "("
×
576
      .. tostring(self.base)
×
577
      .. ", "
×
578
      .. tostring(self.sub or self.super)
×
579
      .. ")"
×
580
end
581

582
function elements.subscript:_init (base, sub, sup)
2✔
583
   elements.mbox._init(self)
12✔
584
   self.base = base
12✔
585
   self.sub = sub
12✔
586
   self.sup = sup
12✔
587
   if self.base then
12✔
588
      table.insert(self.children, self.base)
12✔
589
   end
590
   if self.sub then
12✔
591
      table.insert(self.children, self.sub)
×
592
   end
593
   if self.sup then
12✔
594
      table.insert(self.children, self.sup)
12✔
595
   end
596
   self.atom = self.base.atom
12✔
597
end
598

599
function elements.subscript:styleChildren ()
2✔
600
   if self.base then
12✔
601
      self.base.mode = self.mode
12✔
602
   end
603
   if self.sub then
12✔
604
      self.sub.mode = getSubscriptMode(self.mode)
×
605
   end
606
   if self.sup then
12✔
607
      self.sup.mode = getSuperscriptMode(self.mode)
24✔
608
   end
609
end
610

611
function elements.subscript:calculateItalicsCorrection ()
2✔
612
   local lastGid = getRightMostGlyphId(self.base)
12✔
613
   if lastGid > 0 then
12✔
614
      local mathMetrics = self:getMathMetrics()
12✔
615
      if mathMetrics.italicsCorrection[lastGid] then
12✔
616
         return mathMetrics.italicsCorrection[lastGid]
×
617
      end
618
   end
619
   return 0
12✔
620
end
621

622
function elements.subscript:shape ()
2✔
623
   local mathMetrics = self:getMathMetrics()
12✔
624
   local constants = mathMetrics.constants
12✔
625
   local scaleDown = self:getScaleDown()
12✔
626
   if self.base then
12✔
627
      self.base.relX = SILE.types.length(0)
24✔
628
      self.base.relY = SILE.types.length(0)
24✔
629
      -- Use widthForSubscript of base, if available
630
      self.width = self.base.widthForSubscript or self.base.width
12✔
631
   else
632
      self.width = SILE.types.length(0)
×
633
   end
634
   local itCorr = self:calculateItalicsCorrection() * scaleDown
24✔
635
   local isBaseSymbol = not self.base or self.base:is_a(elements.terminal)
24✔
636
   local isBaseLargeOp = SU.boolean(self.base and self.base.largeop, false)
12✔
637
   local subShift
638
   local supShift
639
   if self.sub then
12✔
640
      if self.isUnderOver or isBaseLargeOp then
×
641
         -- Ad hoc correction on integral limits, following LuaTeX's
642
         -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
643
         subShift = -itCorr
×
644
      else
645
         subShift = 0
×
646
      end
647
      self.sub.relX = self.width + subShift
×
648
      self.sub.relY = SILE.types.length(
×
649
         math.max(
×
650
            constants.subscriptShiftDown * scaleDown,
×
651
            isBaseSymbol and 0 -- TeX (σ19) is more finicky than MathML Core
×
652
               or (self.base.depth + constants.subscriptBaselineDropMin * scaleDown):tonumber(),
×
653
            (self.sub.height - constants.subscriptTopMax * scaleDown):tonumber()
×
654
         )
655
      )
656
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or isBaseLargeOp then
×
657
         self.sub.relY = maxLength(self.sub.relY, self.base.depth + constants.subscriptBaselineDropMin * scaleDown)
×
658
      end
659
   end
660
   if self.sup then
12✔
661
      if self.isUnderOver or isBaseLargeOp then
12✔
662
         -- Ad hoc correction on integral limits, following LuaTeX's
663
         -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
664
         supShift = 0
×
665
      else
666
         supShift = itCorr
12✔
667
      end
668
      self.sup.relX = self.width + supShift
24✔
669
      self.sup.relY = SILE.types.length(
24✔
670
         math.max(
24✔
671
            isCrampedMode(self.mode) and constants.superscriptShiftUpCramped * scaleDown
24✔
672
               or constants.superscriptShiftUp * scaleDown,
12✔
673
            isBaseSymbol and 0 -- TeX (σ18) is more finicky than MathML Core
12✔
674
               or (self.base.height - constants.superscriptBaselineDropMax * scaleDown):tonumber(),
18✔
675
            (self.sup.depth + constants.superscriptBottomMin * scaleDown):tonumber()
24✔
676
         )
677
      ) * -1
24✔
678
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or isBaseLargeOp then
36✔
679
         self.sup.relY = maxLength(
×
680
            (0 - self.sup.relY),
×
681
            self.base.height - constants.superscriptBaselineDropMax * scaleDown
×
682
         ) * -1
×
683
      end
684
   end
685
   if self.sub and self.sup then
12✔
686
      local gap = self.sub.relY - self.sub.height - self.sup.relY - self.sup.depth
×
687
      if gap.length:tonumber() < constants.subSuperscriptGapMin * scaleDown then
×
688
         -- The following adjustment comes directly from Appendix G of he
689
         -- TeXbook (rule 18e).
690
         self.sub.relY = constants.subSuperscriptGapMin * scaleDown + self.sub.height + self.sup.relY + self.sup.depth
×
691
         local psi = constants.superscriptBottomMaxWithSubscript * scaleDown + self.sup.relY + self.sup.depth
×
692
         if psi:tonumber() > 0 then
×
693
            self.sup.relY = self.sup.relY - psi
×
694
            self.sub.relY = self.sub.relY - psi
×
695
         end
696
      end
697
   end
698
   self.width = self.width
×
699
      + maxLength(
24✔
700
         self.sub and self.sub.width + subShift or SILE.types.length(0),
12✔
701
         self.sup and self.sup.width + supShift or SILE.types.length(0)
24✔
702
      )
12✔
703
      + constants.spaceAfterScript * scaleDown
24✔
704
   self.height = maxLength(
24✔
705
      self.base and self.base.height or SILE.types.length(0),
12✔
706
      self.sub and (self.sub.height - self.sub.relY) or SILE.types.length(0),
12✔
707
      self.sup and (self.sup.height - self.sup.relY) or SILE.types.length(0)
24✔
708
   )
12✔
709
   self.depth = maxLength(
24✔
710
      self.base and self.base.depth or SILE.types.length(0),
12✔
711
      self.sub and (self.sub.depth + self.sub.relY) or SILE.types.length(0),
12✔
712
      self.sup and (self.sup.depth + self.sup.relY) or SILE.types.length(0)
24✔
713
   )
12✔
714
end
715

716
function elements.subscript.output (_, _, _, _) end
13✔
717

718
elements.underOver = pl.class(elements.subscript)
2✔
719
elements.underOver._type = "UnderOver"
1✔
720

721
function elements.underOver:__tostring ()
2✔
722
   return self._type .. "(" .. tostring(self.base) .. ", " .. tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
×
723
end
724

725
local function isNotEmpty (element)
726
   -- The MathML test suite uses <munderover> with an empty <mrow> as sub/sup.
727
   -- I don't know why they didn't use a <munder> or <mover> instead...
728
   -- But the expectation is to behave as if the empty element was not there,
729
   -- so that height and depth are not affected by the axis height.
730
   -- See notably:
731
   --   MathML3 "complex1" torture test: Maxwell's Equations (vectors in fractions)
732
   return element and (element:is_a(elements.terminal) or #element.children > 0)
×
733
end
734

735
function elements.underOver:_init (base, sub, sup)
2✔
736
   elements.mbox._init(self)
×
737
   self.atom = base.atom
×
738
   self.base = base
×
739
   self.sub = isNotEmpty(sub) and sub or nil
×
740
   self.sup = isNotEmpty(sup) and sup or nil
×
741
   if self.sup then
×
742
      table.insert(self.children, self.sup)
×
743
   end
744
   if self.base then
×
745
      table.insert(self.children, self.base)
×
746
   end
747
   if self.sub then
×
748
      table.insert(self.children, self.sub)
×
749
   end
750
end
751

752
function elements.underOver:styleChildren ()
2✔
753
   if self.base then
×
754
      self.base.mode = self.mode
×
755
   end
756
   if self.sub then
×
757
      self.sub.mode = getSubscriptMode(self.mode)
×
758
   end
759
   if self.sup then
×
760
      self.sup.mode = getSuperscriptMode(self.mode)
×
761
   end
762
end
763

764
function elements.underOver:_stretchyReshapeToBase (part)
2✔
765
   -- FIXME: Big leap of faith here.
766
   -- MathML Core only mentions stretching along the inline axis in 3.4.2.2,
767
   -- i.e. under the section on <mover>, <munder>, <munderover>.
768
   -- So we are "somewhat" good here, but... the algorithm is totally unclear
769
   -- to me and seems to imply a lot of recursion and reshaping.
770
   -- The implementation below is NOT general and only works for the cases
771
   -- I checked:
772
   --   Mozilla MathML tests: braces in f19, f22
773
   --   Personal tests: vectors in d19, d22, d23
774
   --   Joe Javawaski's tests: braces in 8a, 8b
775
   --   MathML3 "complex1" torture test: Maxwell's Equations (vectors in fractions)
776
   if #part.children == 0 then
×
777
      local elt = part
×
778
      if elt:is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
×
779
         elt:_horizStretchyReshape(self.base.width)
×
780
      end
781
   elseif part:is_a(elements.underOver) then
×
782
      -- Big assumption here: only considering one level of stacked under/over.
783
      local hasStretched = false
×
784
      for _, elt in ipairs(part.children) do
×
785
         if elt:is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
×
786
            local stretched = elt:_horizStretchyReshape(self.base.width)
×
787
            if stretched then
×
788
               hasStretched = true
×
789
            end
790
         end
791
      end
792
      if hasStretched then
×
793
         -- We need to re-calculate the shape so positions are re-calculated on each
794
         -- of its own parts.
795
         -- (Added after seeing that Mozilla test f19 was not rendering correctly.)
796
         part:shape()
×
797
      end
798
   end
799
end
800

801
function elements.underOver:shape ()
2✔
802
   local isMovableLimits = SU.boolean(self.base and self.base.movablelimits, false)
×
803
   if not (self.mode == mathMode.display or self.mode == mathMode.displayCramped) and isMovableLimits then
×
804
      -- When the base is a movable limit, the under/over scripts are not placed under/over the base,
805
      -- but other to the right of it, when display mode is not used.
806
      -- Notable effects:
807
      --   Mozilla MathML test 19 (on "k times" > overbrace > base)
808
      --   Maxwell's Equations in MathML3 Test Suite "complex1" (on the vectors in fractions)
809
      self.isUnderOver = true
×
810
      elements.subscript.shape(self)
×
811
      return
×
812
   end
813
   local constants = self:getMathMetrics().constants
×
814
   local scaleDown = self:getScaleDown()
×
815
   -- Determine relative Ys
816
   if self.base then
×
817
      self.base.relY = SILE.types.length(0)
×
818
   end
819
   if self.sub then
×
820
      self:_stretchyReshapeToBase(self.sub)
×
821
      self.sub.relY = self.base.depth
×
822
         + SILE.types.length(
×
823
            math.max(
×
824
               (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
×
825
               constants.lowerLimitBaselineDropMin * scaleDown
×
826
            )
827
         )
828
   end
829
   if self.sup then
×
830
      self:_stretchyReshapeToBase(self.sup)
×
831
      self.sup.relY = 0
×
832
         - self.base.height
×
833
         - SILE.types.length(
×
834
            math.max(
×
835
               (constants.upperLimitGapMin * scaleDown + self.sup.depth):tonumber(),
×
836
               constants.upperLimitBaselineRiseMin * scaleDown
×
837
            )
838
         )
839
   end
840
   -- Determine relative Xs based on widest symbol
841
   local widest, a, b
842
   if self.sub and self.sub.width > self.base.width then
×
843
      if self.sup and self.sub.width > self.sup.width then
×
844
         widest = self.sub
×
845
         a = self.base
×
846
         b = self.sup
×
847
      elseif self.sup then
×
848
         widest = self.sup
×
849
         a = self.base
×
850
         b = self.sub
×
851
      else
852
         widest = self.sub
×
853
         a = self.base
×
854
         b = nil
×
855
      end
856
   else
857
      if self.sup and self.base.width > self.sup.width then
×
858
         widest = self.base
×
859
         a = self.sub
×
860
         b = self.sup
×
861
      elseif self.sup then
×
862
         widest = self.sup
×
863
         a = self.base
×
864
         b = self.sub
×
865
      else
866
         widest = self.base
×
867
         a = self.sub
×
868
         b = nil
×
869
      end
870
   end
871
   widest.relX = SILE.types.length(0)
×
872
   local c = widest.width / 2
×
873
   if a then
×
874
      a.relX = c - a.width / 2
×
875
   end
876
   if b then
×
877
      b.relX = c - b.width / 2
×
878
   end
879
   local itCorr = self:calculateItalicsCorrection() * scaleDown
×
880
   if self.sup then
×
881
      self.sup.relX = self.sup.relX + itCorr / 2
×
882
   end
883
   if self.sub then
×
884
      self.sub.relX = self.sub.relX - itCorr / 2
×
885
   end
886
   -- Determine width and height
887
   self.width = maxLength(
×
888
      self.base and self.base.width or SILE.types.length(0),
×
889
      self.sub and self.sub.width or SILE.types.length(0),
×
890
      self.sup and self.sup.width or SILE.types.length(0)
×
891
   )
892
   if self.sup then
×
893
      self.height = 0 - self.sup.relY + self.sup.height
×
894
   else
895
      self.height = self.base and self.base.height or 0
×
896
   end
897
   if self.sub then
×
898
      self.depth = self.sub.relY + self.sub.depth
×
899
   else
900
      self.depth = self.base and self.base.depth or 0
×
901
   end
902
end
903

904
function elements.underOver:calculateItalicsCorrection ()
2✔
905
   local lastGid = getRightMostGlyphId(self.base)
×
906
   if lastGid > 0 then
×
907
      local mathMetrics = self:getMathMetrics()
×
908
      if mathMetrics.italicsCorrection[lastGid] then
×
909
         local c = mathMetrics.italicsCorrection[lastGid]
×
910
         -- If this is a big operator, and we are in display style, then the
911
         -- base glyph may be bigger than the font size. We need to adjust the
912
         -- italic correction accordingly.
913
         if SU.boolean(self.base.largeop) and isDisplayMode(self.mode) then
×
914
            c = c * (self.base and self.base.font.size / self.font.size or 1.0)
×
915
         end
916
         return c
×
917
      end
918
   end
919
   return 0
×
920
end
921

922
function elements.underOver.output (_, _, _, _) end
1✔
923

924
-- terminal is the base class for leaf node
925
elements.terminal = pl.class(elements.mbox)
2✔
926
elements.terminal._type = "Terminal"
1✔
927

928
function elements.terminal:_init ()
2✔
929
   elements.mbox._init(self)
174✔
930
end
931

932
function elements.terminal.styleChildren (_) end
175✔
933

934
function elements.terminal.shape (_) end
1✔
935

936
elements.space = pl.class(elements.terminal)
2✔
937
elements.space._type = "Space"
1✔
938

939
function elements.space:_init ()
2✔
940
   elements.terminal._init(self)
×
941
end
942

943
function elements.space:__tostring ()
2✔
944
   return self._type
×
945
      .. "(width="
×
946
      .. tostring(self.width)
×
947
      .. ", height="
×
948
      .. tostring(self.height)
×
949
      .. ", depth="
×
950
      .. tostring(self.depth)
×
951
      .. ")"
×
952
end
953

954
local function getStandardLength (value)
955
   if type(value) == "string" then
144✔
956
      local direction = 1
48✔
957
      if value:sub(1, 1) == "-" then
96✔
958
         value = value:sub(2, -1)
×
959
         direction = -1
×
960
      end
961
      if value == "thin" then
48✔
962
         return SILE.types.length("3mu") * direction
36✔
963
      elseif value == "med" then
36✔
964
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
36✔
965
      elseif value == "thick" then
24✔
966
         return SILE.types.length("5mu plus 5mu") * direction
72✔
967
      end
968
   end
969
   return SILE.types.length(value)
96✔
970
end
971

972
function elements.space:_init (width, height, depth)
2✔
973
   elements.terminal._init(self)
48✔
974
   self.width = getStandardLength(width)
96✔
975
   self.height = getStandardLength(height)
96✔
976
   self.depth = getStandardLength(depth)
96✔
977
end
978

979
function elements.space:shape ()
2✔
980
   self.width = self.width:absolute() * self:getScaleDown()
192✔
981
   self.height = self.height:absolute() * self:getScaleDown()
192✔
982
   self.depth = self.depth:absolute() * self:getScaleDown()
192✔
983
end
984

985
function elements.space.output (_) end
49✔
986

987
-- text node. For any actual text output
988
elements.text = pl.class(elements.terminal)
2✔
989
elements.text._type = "Text"
1✔
990

991
function elements.text:__tostring ()
2✔
992
   return self._type
×
993
      .. "(atom="
×
994
      .. tostring(self.atom)
×
995
      .. ", kind="
×
996
      .. tostring(self.kind)
×
997
      .. ", script="
×
998
      .. tostring(self.script)
×
999
      .. (SU.boolean(self.stretchy, false) and ", stretchy" or "")
×
1000
      .. (SU.boolean(self.largeop, false) and ", largeop" or "")
×
1001
      .. ', text="'
×
1002
      .. (self.originalText or self.text)
×
1003
      .. '")'
×
1004
end
1005

1006
function elements.text:_init (kind, attributes, script, text)
2✔
1007
   elements.terminal._init(self)
126✔
1008
   if not (kind == "number" or kind == "identifier" or kind == "operator" or kind == "string") then
126✔
1009
      SU.error("Unknown text node kind '" .. kind .. "'; should be one of: number, identifier, operator, string")
×
1010
   end
1011
   self.kind = kind
126✔
1012
   self.script = script
126✔
1013
   self.text = text
126✔
1014
   if self.script ~= "upright" then
126✔
1015
      local converted = convertMathVariantScript(self.text, self.script)
126✔
1016
      self.originalText = self.text
126✔
1017
      self.text = converted
126✔
1018
   end
1019
   if self.kind == "operator" then
126✔
1020
      if self.text == "-" then
78✔
1021
         self.text = "−"
×
1022
      end
1023
   end
1024
   for attribute, value in pairs(attributes) do
634✔
1025
      self[attribute] = value
508✔
1026
   end
1027
end
1028

1029
function elements.text:shape ()
2✔
1030
   self.font.size = self.font.size * self:getScaleDown()
252✔
1031
   if isScriptMode(self.mode) then
252✔
1032
      local scriptFeature = SILE.settings:get("math.font.script.feature")
12✔
1033
      if scriptFeature then
12✔
1034
         self.font.features = ("+%s=1"):format(scriptFeature)
12✔
1035
      end
1036
   elseif isScriptScriptMode(self.mode) then
228✔
1037
      local scriptFeature = SILE.settings:get("math.font.script.feature")
×
1038
      if scriptFeature then
×
1039
         self.font.features = ("+%s=2"):format(scriptFeature)
×
1040
      end
1041
   end
1042
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
126✔
1043
   local mathMetrics = self:getMathMetrics()
126✔
1044
   local glyphs = SILE.shaper:shapeToken(self.text, self.font)
126✔
1045
   -- Use bigger variants for big operators in display style
1046
   if isDisplayMode(self.mode) and SU.boolean(self.largeop, false) then
309✔
1047
      -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
1048
      glyphs = pl.tablex.deepcopy(glyphs)
×
1049
      local constructions = mathMetrics.mathVariants.vertGlyphConstructions[glyphs[1].gid]
×
1050
      if constructions then
×
1051
         local displayVariants = constructions.mathGlyphVariantRecord
×
1052
         -- We select the biggest variant. TODO: we should probably select the
1053
         -- first variant that is higher than displayOperatorMinHeight.
1054
         local biggest
1055
         local m = 0
×
1056
         for _, v in ipairs(displayVariants) do
×
1057
            if v.advanceMeasurement > m then
×
1058
               biggest = v
×
1059
               m = v.advanceMeasurement
×
1060
            end
1061
         end
1062
         if biggest then
×
1063
            glyphs[1].gid = biggest.variantGlyph
×
1064
            local dimen = hb.get_glyph_dimensions(face, self.font.size, biggest.variantGlyph)
×
1065
            glyphs[1].width = dimen.width
×
1066
            glyphs[1].glyphAdvance = dimen.glyphAdvance
×
1067
            --[[ I am told (https://github.com/alif-type/xits/issues/90) that,
1068
        in fact, the relative height and depth of display-style big operators
1069
        in the font is not relevant, as these should be centered around the
1070
        axis. So the following code does that, while conserving their
1071
        vertical size (distance from top to bottom). ]]
1072
            local axisHeight = mathMetrics.constants.axisHeight * self:getScaleDown()
×
1073
            local y_size = dimen.height + dimen.depth
×
1074
            glyphs[1].height = y_size / 2 + axisHeight
×
1075
            glyphs[1].depth = y_size / 2 - axisHeight
×
1076
            -- We still need to store the font's height and depth somewhere,
1077
            -- because that's what will be used to draw the glyph, and we will need
1078
            -- to artificially compensate for that.
1079
            glyphs[1].fontHeight = dimen.height
×
1080
            glyphs[1].fontDepth = dimen.depth
×
1081
         end
1082
      end
1083
   end
1084
   SILE.shaper:preAddNodes(glyphs, self.value)
126✔
1085
   self.value.items = glyphs
126✔
1086
   self.value.glyphString = {}
126✔
1087
   if glyphs and #glyphs > 0 then
126✔
1088
      for i = 1, #glyphs do
264✔
1089
         table.insert(self.value.glyphString, glyphs[i].gid)
138✔
1090
      end
1091
      self.width = SILE.types.length(0)
252✔
1092
      self.widthForSubscript = SILE.types.length(0)
252✔
1093
      for i = #glyphs, 1, -1 do
264✔
1094
         self.width = self.width + glyphs[i].glyphAdvance
276✔
1095
      end
1096
      -- Store width without italic correction somewhere
1097
      self.widthForSubscript = self.width
126✔
1098
      local itCorr = mathMetrics.italicsCorrection[glyphs[#glyphs].gid]
126✔
1099
      if itCorr then
126✔
1100
         self.width = self.width + itCorr * self:getScaleDown()
54✔
1101
      end
1102
      for i = 1, #glyphs do
264✔
1103
         self.height = i == 1 and SILE.types.length(glyphs[i].height)
138✔
1104
            or SILE.types.length(math.max(self.height:tonumber(), glyphs[i].height))
162✔
1105
         self.depth = i == 1 and SILE.types.length(glyphs[i].depth)
138✔
1106
            or SILE.types.length(math.max(self.depth:tonumber(), glyphs[i].depth))
162✔
1107
      end
1108
   else
1109
      self.width = SILE.types.length(0)
×
1110
      self.height = SILE.types.length(0)
×
1111
      self.depth = SILE.types.length(0)
×
1112
   end
1113
end
1114

1115
function elements.text.findClosestVariant (_, variants, requiredAdvance, currentAdvance)
2✔
1116
   local closest
1117
   local closestI
1118
   local m = requiredAdvance - currentAdvance
36✔
1119
   for i, variant in ipairs(variants) do
504✔
1120
      local diff = math.abs(variant.advanceMeasurement - requiredAdvance)
468✔
1121
      SU.debug("math", "stretch: diff =", diff)
468✔
1122
      if diff < m then
468✔
1123
         closest = variant
12✔
1124
         closestI = i
12✔
1125
         m = diff
12✔
1126
      end
1127
   end
1128
   return closest, closestI
36✔
1129
end
1130

1131
function elements.text:_reshapeGlyph (glyph, closestVariant, sz)
2✔
1132
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
12✔
1133
   local dimen = hb.get_glyph_dimensions(face, sz, closestVariant.variantGlyph)
12✔
1134
   glyph.gid = closestVariant.variantGlyph
12✔
1135
   glyph.width, glyph.height, glyph.depth, glyph.glyphAdvance =
×
1136
      dimen.width, dimen.height, dimen.depth, dimen.glyphAdvance
12✔
1137
   return dimen
12✔
1138
end
1139

1140
function elements.text:_stretchyReshape (target, direction)
2✔
1141
   -- direction is the required direction of stretching: true for vertical, false for horizontal
1142
   -- target is the required dimension of the stretched glyph, in font units
1143
   local mathMetrics = self:getMathMetrics()
36✔
1144
   local upem = mathMetrics.unitsPerEm
36✔
1145
   local sz = self.font.size
36✔
1146
   local requiredAdvance = target:tonumber() * upem / sz
72✔
1147
   SU.debug("math", "stretch: rA =", requiredAdvance)
36✔
1148
   -- Choose variant of the closest size. The criterion we use is to have
1149
   -- an advance measurement as close as possible as the required one.
1150
   -- The advance measurement is simply the dimension of the glyph.
1151
   -- Therefore, the selected glyph may be smaller or bigger than
1152
   -- required.
1153
   -- TODO: implement assembly of stretchable glyphs from their parts for cases
1154
   -- when the biggest variant is not big enough.
1155
   -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
1156
   local glyphs = pl.tablex.deepcopy(self.value.items)
36✔
1157
   local glyphConstructions = direction and mathMetrics.mathVariants.vertGlyphConstructions
36✔
1158
      or mathMetrics.mathVariants.horizGlyphConstructions
36✔
1159
   local constructions = glyphConstructions[glyphs[1].gid]
36✔
1160
   if constructions then
36✔
1161
      local variants = constructions.mathGlyphVariantRecord
36✔
1162
      SU.debug("math", "stretch: variants =", variants)
36✔
1163
      local currentAdvance = (direction and (self.depth + self.height):tonumber() or self.width:tonumber()) * upem / sz
108✔
1164
      local closest, closestI = self:findClosestVariant(variants, requiredAdvance, currentAdvance)
36✔
1165
      SU.debug("math", "stretch: closestI =", closestI)
36✔
1166
      if closest then
36✔
1167
         -- Now we have to re-shape the glyph chain. We will assume there
1168
         -- is only one glyph.
1169
         -- TODO: this code is probably wrong when the vertical
1170
         -- variants have a different width than the original, because
1171
         -- the shaping phase is already done. Need to do better.
1172
         local dimen = self:_reshapeGlyph(glyphs[1], closest, sz)
12✔
1173
         self.width, self.depth, self.height =
×
1174
            SILE.types.length(dimen.glyphAdvance), SILE.types.length(dimen.depth), SILE.types.length(dimen.height)
48✔
1175
         SILE.shaper:preAddNodes(glyphs, self.value)
12✔
1176
         self.value.items = glyphs
12✔
1177
         self.value.glyphString = { glyphs[1].gid }
12✔
1178
         return true
12✔
1179
      end
1180
   end
1181
   return false
24✔
1182
end
1183

1184
function elements.text:_vertStretchyReshape (depth, height)
2✔
1185
   local hasStretched = self:_stretchyReshape(depth + height, true)
72✔
1186
   if hasStretched then
36✔
1187
      -- HACK: see output routine
1188
      self.vertExpectedSz = height + depth
24✔
1189
      self.vertScalingRatio = (depth + height):tonumber() / (self.height:tonumber() + self.depth:tonumber())
60✔
1190
      self.height = height
12✔
1191
      self.depth = depth
12✔
1192
   end
1193
   return hasStretched
36✔
1194
end
1195

1196
function elements.text:_horizStretchyReshape (width)
2✔
1197
   local hasStretched = self:_stretchyReshape(width, false)
×
1198
   if hasStretched then
×
1199
      -- HACK: see output routine
1200
      self.horizScalingRatio = width:tonumber() / self.width:tonumber()
×
1201
      self.width = width
×
1202
   end
1203
   return hasStretched
×
1204
end
1205

1206
function elements.text:output (x, y, line)
2✔
1207
   if not self.value.glyphString then
126✔
1208
      return
×
1209
   end
1210
   local compensatedY
1211
   if isDisplayMode(self.mode) and SU.boolean(self.largeop, false) and self.value.items[1].fontDepth then
309✔
1212
      compensatedY = SILE.types.length(y.length + self.value.items[1].depth - self.value.items[1].fontDepth)
×
1213
   else
1214
      compensatedY = y
126✔
1215
   end
1216
   SILE.outputter:setCursor(scaleWidth(x, line), compensatedY.length)
252✔
1217
   SILE.outputter:setFont(self.font)
126✔
1218
   -- There should be no stretch or shrink on the width of a text
1219
   -- element.
1220
   local width = self.width.length
126✔
1221
   -- HACK: For stretchy operators, MathML Core and OpenType define how to build large glyphs
1222
   -- from an assembly of smaller ones. It's fairly complex and idealistic...
1223
   -- Anyhow, we do not have that yet, so we just stretch the glyph artificially.
1224
   -- There are cases where this will not look very good.
1225
   -- Call that a compromise, so that long vectors or large matrices look "decent" without assembly.
1226
   if SILE.outputter.scaleFn and (self.horizScalingRatio or self.vertScalingRatio) then
126✔
1227
      local xratio = self.horizScalingRatio or 1
12✔
1228
      local yratio = self.vertScalingRatio or 1
12✔
1229
      SU.debug("math", "fake glyph stretch: xratio =", xratio, "yratio =", yratio)
12✔
1230
      SILE.outputter:scaleFn(x, y, xratio, yratio, function ()
24✔
1231
         SILE.outputter:drawHbox(self.value, width)
12✔
1232
      end)
1233
   else
1234
      SILE.outputter:drawHbox(self.value, width)
114✔
1235
   end
1236
end
1237

1238
elements.fraction = pl.class(elements.mbox)
2✔
1239
elements.fraction._type = "Fraction"
1✔
1240

1241
function elements.fraction:__tostring ()
2✔
1242
   return self._type .. "(" .. tostring(self.numerator) .. ", " .. tostring(self.denominator) .. ")"
×
1243
end
1244

1245
function elements.fraction:_init (attributes, numerator, denominator)
2✔
1246
   elements.mbox._init(self)
×
1247
   self.numerator = numerator
×
1248
   self.denominator = denominator
×
1249
   self.attributes = attributes
×
1250
   table.insert(self.children, numerator)
×
1251
   table.insert(self.children, denominator)
×
1252
end
1253

1254
function elements.fraction:styleChildren ()
2✔
1255
   self.numerator.mode = getNumeratorMode(self.mode)
×
1256
   self.denominator.mode = getDenominatorMode(self.mode)
×
1257
end
1258

1259
function elements.fraction:shape ()
2✔
1260
   -- MathML Core 3.3.2: "To avoid visual confusion between the fraction bar
1261
   -- and another adjacent items (e.g. minus sign or another fraction's bar),
1262
   -- a default 1-pixel space is added around the element."
1263
   -- Note that PlainTeX would likely use \nulldelimiterspace (default 1.2pt)
1264
   -- but it would depend on the surrounding context, and might be far too
1265
   -- much in some cases, so we stick to MathML's suggested padding.
1266
   self.padding = SILE.types.length("1px"):absolute()
×
1267

1268
   -- Determine relative abscissas and width
1269
   local widest, other
1270
   if self.denominator.width > self.numerator.width then
×
1271
      widest, other = self.denominator, self.numerator
×
1272
   else
1273
      widest, other = self.numerator, self.denominator
×
1274
   end
1275
   widest.relX = self.padding
×
1276
   other.relX = self.padding + (widest.width - other.width) / 2
×
1277
   self.width = widest.width + 2 * self.padding
×
1278
   -- Determine relative ordinates and height
1279
   local constants = self:getMathMetrics().constants
×
1280
   local scaleDown = self:getScaleDown()
×
1281
   self.axisHeight = constants.axisHeight * scaleDown
×
1282
   self.ruleThickness = self.attributes.linethickness
×
1283
         and SU.cast("measurement", self.attributes.linethickness):tonumber()
×
1284
      or constants.fractionRuleThickness * scaleDown
×
1285

1286
   -- MathML Core 3.3.2.2 ("Fraction with zero line thickness") uses
1287
   -- stack(DisplayStyle)GapMin, stackTop(DisplayStyle)ShiftUp and stackBottom(DisplayStyle)ShiftDown.
1288
   -- TODO not implemented
1289
   -- The most common use cases for zero line thickness are:
1290
   --  - Binomial coefficients
1291
   --  - Stacked subscript/superscript on big operators such as sums.
1292

1293
   local numeratorGapMin, denominatorGapMin, numeratorShiftUp, denominatorShiftDown
1294
   if isDisplayMode(self.mode) then
×
1295
      numeratorGapMin = constants.fractionNumDisplayStyleGapMin * scaleDown
×
1296
      denominatorGapMin = constants.fractionDenomDisplayStyleGapMin * scaleDown
×
1297
      numeratorShiftUp = constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
×
1298
      denominatorShiftDown = constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
×
1299
   else
1300
      numeratorGapMin = constants.fractionNumeratorGapMin * scaleDown
×
1301
      denominatorGapMin = constants.fractionDenominatorGapMin * scaleDown
×
1302
      numeratorShiftUp = constants.fractionNumeratorShiftUp * scaleDown
×
1303
      denominatorShiftDown = constants.fractionDenominatorShiftDown * scaleDown
×
1304
   end
1305

1306
   self.numerator.relY = -self.axisHeight
×
1307
      - self.ruleThickness / 2
×
1308
      - SILE.types.length(
×
1309
         math.max(
×
1310
            (numeratorGapMin + self.numerator.depth):tonumber(),
×
1311
            numeratorShiftUp - self.axisHeight - self.ruleThickness / 2
×
1312
         )
1313
      )
1314
   self.denominator.relY = -self.axisHeight
×
1315
      + self.ruleThickness / 2
×
1316
      + SILE.types.length(
×
1317
         math.max(
×
1318
            (denominatorGapMin + self.denominator.height):tonumber(),
×
1319
            denominatorShiftDown + self.axisHeight - self.ruleThickness / 2
×
1320
         )
1321
      )
1322
   self.height = self.numerator.height - self.numerator.relY
×
1323
   self.depth = self.denominator.relY + self.denominator.depth
×
1324
end
1325

1326
function elements.fraction:output (x, y, line)
2✔
1327
   if self.ruleThickness > 0 then
×
1328
      SILE.outputter:drawRule(
×
1329
         scaleWidth(x + self.padding, line),
×
1330
         y.length - self.axisHeight - self.ruleThickness / 2,
×
1331
         scaleWidth(self.width - 2 * self.padding, line),
×
1332
         self.ruleThickness
1333
      )
1334
   end
1335
end
1336

1337
local function newSubscript (spec)
1338
   return elements.subscript(spec.base, spec.sub, spec.sup)
12✔
1339
end
1340

1341
local function newUnderOver (spec)
1342
   return elements.underOver(spec.base, spec.sub, spec.sup)
×
1343
end
1344

1345
-- TODO replace with penlight equivalent
1346
local function mapList (f, l)
1347
   local ret = {}
×
1348
   for i, x in ipairs(l) do
×
1349
      ret[i] = f(i, x)
×
1350
   end
1351
   return ret
×
1352
end
1353

1354
elements.mtr = pl.class(elements.mbox)
2✔
1355
-- elements.mtr._type = "" -- TODO why not set?
1356

1357
function elements.mtr:_init (children)
2✔
1358
   self.children = children
×
1359
end
1360

1361
function elements.mtr:styleChildren ()
2✔
1362
   for _, c in ipairs(self.children) do
×
1363
      c.mode = self.mode
×
1364
   end
1365
end
1366

1367
function elements.mtr.shape (_) end -- done by parent table
1✔
1368

1369
function elements.mtr.output (_) end
1✔
1370

1371
elements.table = pl.class(elements.mbox)
2✔
1372
elements.table._type = "table" -- TODO why case difference?
1✔
1373

1374
function elements.table:_init (children, options)
2✔
1375
   elements.mbox._init(self)
×
1376
   self.children = children
×
1377
   self.options = options
×
1378
   self.nrows = #self.children
×
1379
   self.ncols = math.max(pl.utils.unpack(mapList(function (_, row)
×
1380
      return #row.children
×
1381
   end, self.children)))
×
1382
   SU.debug("math", "self.ncols =", self.ncols)
×
1383
   local spacing = SILE.settings:get("math.font.size") * 0.6 -- arbitrary ratio of the current math font size
×
1384
   self.rowspacing = self.options.rowspacing and SILE.types.length(self.options.rowspacing) or spacing
×
1385
   self.columnspacing = self.options.columnspacing and SILE.types.length(self.options.columnspacing) or spacing
×
1386
   -- Pad rows that do not have enough cells by adding cells to the
1387
   -- right.
1388
   for i, row in ipairs(self.children) do
×
1389
      for j = 1, (self.ncols - #row.children) do
×
1390
         SU.debug("math", "padding i =", i, "j =", j)
×
1391
         table.insert(row.children, elements.stackbox("H", {}))
×
1392
         SU.debug("math", "size", #row.children)
×
1393
      end
1394
   end
1395
   if options.columnalign then
×
1396
      local l = {}
×
1397
      for w in string.gmatch(options.columnalign, "[^%s]+") do
×
1398
         if not (w == "left" or w == "center" or w == "right") then
×
1399
            SU.error("Invalid specifier in `columnalign` attribute: " .. w)
×
1400
         end
1401
         table.insert(l, w)
×
1402
      end
1403
      -- Pad with last value of l if necessary
1404
      for _ = 1, (self.ncols - #l), 1 do
×
1405
         table.insert(l, l[#l])
×
1406
      end
1407
      -- On the contrary, remove excess values in l if necessary
1408
      for _ = 1, (#l - self.ncols), 1 do
×
1409
         table.remove(l)
×
1410
      end
1411
      self.options.columnalign = l
×
1412
   else
1413
      self.options.columnalign = pl.List.range(1, self.ncols):map(function (_)
×
1414
         return "center"
×
1415
      end)
1416
   end
1417
end
1418

1419
function elements.table:styleChildren ()
2✔
1420
   if self.mode == mathMode.display and self.options.displaystyle ~= "false" then
×
1421
      for _, c in ipairs(self.children) do
×
1422
         c.mode = mathMode.display
×
1423
      end
1424
   else
1425
      for _, c in ipairs(self.children) do
×
1426
         c.mode = mathMode.text
×
1427
      end
1428
   end
1429
end
1430

1431
function elements.table:shape ()
2✔
1432
   -- Determine the height (resp. depth) of each row, which is the max
1433
   -- height (resp. depth) among its elements. Then we only need to add it to
1434
   -- the table's height and center every cell vertically.
1435
   for _, row in ipairs(self.children) do
×
1436
      row.height = SILE.types.length(0)
×
1437
      row.depth = SILE.types.length(0)
×
1438
      for _, cell in ipairs(row.children) do
×
1439
         row.height = maxLength(row.height, cell.height)
×
1440
         row.depth = maxLength(row.depth, cell.depth)
×
1441
      end
1442
   end
1443
   self.vertSize = SILE.types.length(0)
×
1444
   for i, row in ipairs(self.children) do
×
1445
      self.vertSize = self.vertSize
×
1446
         + row.height
×
1447
         + row.depth
×
1448
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
×
1449
   end
1450
   local rowHeightSoFar = SILE.types.length(0)
×
1451
   for i, row in ipairs(self.children) do
×
1452
      row.relY = rowHeightSoFar + row.height - self.vertSize
×
1453
      rowHeightSoFar = rowHeightSoFar
×
1454
         + row.height
×
1455
         + row.depth
×
1456
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
×
1457
   end
1458
   self.width = SILE.types.length(0)
×
1459
   local thisColRelX = SILE.types.length(0)
×
1460
   -- For every column...
1461
   for i = 1, self.ncols do
×
1462
      -- Determine its width
1463
      local columnWidth = SILE.types.length(0)
×
1464
      for j = 1, self.nrows do
×
1465
         if self.children[j].children[i].width > columnWidth then
×
1466
            columnWidth = self.children[j].children[i].width
×
1467
         end
1468
      end
1469
      -- Use it to align the contents of every cell as required.
1470
      for j = 1, self.nrows do
×
1471
         local cell = self.children[j].children[i]
×
1472
         if self.options.columnalign[i] == "left" then
×
1473
            cell.relX = thisColRelX
×
1474
         elseif self.options.columnalign[i] == "center" then
×
1475
            cell.relX = thisColRelX + (columnWidth - cell.width) / 2
×
1476
         elseif self.options.columnalign[i] == "right" then
×
1477
            cell.relX = thisColRelX + (columnWidth - cell.width)
×
1478
         else
1479
            SU.error("invalid columnalign parameter")
×
1480
         end
1481
      end
1482
      thisColRelX = thisColRelX + columnWidth + (i == self.ncols and SILE.types.length(0) or self.columnspacing) -- Spacing
×
1483
   end
1484
   self.width = thisColRelX
×
1485
   -- Center myself vertically around the axis, and update relative Ys of rows accordingly
1486
   local axisHeight = self:getMathMetrics().constants.axisHeight * self:getScaleDown()
×
1487
   self.height = self.vertSize / 2 + axisHeight
×
1488
   self.depth = self.vertSize / 2 - axisHeight
×
1489
   for _, row in ipairs(self.children) do
×
1490
      row.relY = row.relY + self.vertSize / 2 - axisHeight
×
1491
      -- Also adjust width
1492
      row.width = self.width
×
1493
   end
1494
end
1495

1496
function elements.table.output (_) end
1✔
1497

1498
local function getRadicandMode (mode)
1499
   -- Not too sure if we should do something special/
1500
   return mode
×
1501
end
1502

1503
local function getDegreeMode (mode)
1504
   -- 2 levels smaller, up to scriptScript evntually.
1505
   -- Not too sure if we should do something else.
1506
   if mode == mathMode.display then
×
1507
      return mathMode.scriptScript
×
1508
   elseif mode == mathMode.displayCramped then
×
1509
      return mathMode.scriptScriptCramped
×
1510
   elseif mode == mathMode.text or mode == mathMode.script or mode == mathMode.scriptScript then
×
1511
      return mathMode.scriptScript
×
1512
   end
1513
   return mathMode.scriptScriptCramped
×
1514
end
1515

1516
elements.sqrt = pl.class(elements.mbox)
2✔
1517
elements.sqrt._type = "Sqrt"
1✔
1518

1519
function elements.sqrt:__tostring ()
2✔
1520
   return self._type .. "(" .. tostring(self.radicand) .. (self.degree and ", " .. tostring(self.degree) or "") .. ")"
×
1521
end
1522

1523
function elements.sqrt:_init (radicand, degree)
2✔
1524
   elements.mbox._init(self)
×
1525
   self.radicand = radicand
×
1526
   if degree then
×
1527
      self.degree = degree
×
1528
      table.insert(self.children, degree)
×
1529
   end
1530
   table.insert(self.children, radicand)
×
1531
   self.relX = SILE.types.length()
×
1532
   self.relY = SILE.types.length()
×
1533
end
1534

1535
function elements.sqrt:styleChildren ()
2✔
1536
   self.radicand.mode = getRadicandMode(self.mode)
×
1537
   if self.degree then
×
1538
      self.degree.mode = getDegreeMode(self.mode)
×
1539
   end
1540
end
1541

1542
function elements.sqrt:shape ()
2✔
1543
   local mathMetrics = self:getMathMetrics()
×
1544
   local scaleDown = self:getScaleDown()
×
1545
   local constants = mathMetrics.constants
×
1546

1547
   self.radicalRuleThickness = constants.radicalRuleThickness * scaleDown
×
1548
   if self.mode == mathMode.display or self.mode == mathMode.displayCramped then
×
1549
      self.radicalVerticalGap = constants.radicalDisplayStyleVerticalGap * scaleDown
×
1550
   else
1551
      self.radicalVerticalGap = constants.radicalVerticalGap * scaleDown
×
1552
   end
1553
   self.extraAscender = constants.radicalExtraAscender * scaleDown
×
1554

1555
   -- HACK: We draw own own radical sign in the output() method.
1556
   -- Derive dimensions for the radical sign (more or less ad hoc).
1557
   -- Note: In TeX, the radical sign extends a lot below the baseline,
1558
   -- and MathML Core also has a lot of layout text about it.
1559
   -- Not only it doesn't look good, but it's not very clear vs. OpenType.
1560
   local radicalGlyph = SILE.shaper:measureChar("√")
×
1561
   local ratio = (self.radicand.height:tonumber() + self.radicand.depth:tonumber())
×
1562
      / (radicalGlyph.height + radicalGlyph.depth)
×
1563
   local vertAdHocOffset = (ratio > 1 and math.log(ratio) or 0) * self.radicalVerticalGap
×
1564
   self.symbolHeight = SILE.types.length(radicalGlyph.height) * scaleDown
×
1565
   self.symbolDepth = (SILE.types.length(radicalGlyph.depth) + vertAdHocOffset) * scaleDown
×
1566
   self.symbolWidth = (SILE.types.length(radicalGlyph.width) + vertAdHocOffset) * scaleDown
×
1567

1568
   -- Adjust the height of the radical sign if the radicand is higher
1569
   self.symbolHeight = self.radicand.height > self.symbolHeight and self.radicand.height or self.symbolHeight
×
1570
   -- Compute the (max-)height of the short leg of the radical sign
1571
   self.symbolShortHeight = self.symbolHeight * constants.radicalDegreeBottomRaisePercent
×
1572

1573
   self.offsetX = SILE.types.length()
×
1574
   if self.degree then
×
1575
      -- Position the degree
1576
      self.degree.relY = -constants.radicalDegreeBottomRaisePercent * self.symbolHeight
×
1577
      -- Adjust the height of the short leg of the radical sign to ensure the degree is not too close
1578
      -- (empirically use radicalExtraAscender)
1579
      self.symbolShortHeight = self.symbolShortHeight - constants.radicalExtraAscender * scaleDown
×
1580
      -- Compute the width adjustment for the degree
1581
      self.offsetX = self.degree.width
×
1582
         + constants.radicalKernBeforeDegree * scaleDown
×
1583
         + constants.radicalKernAfterDegree * scaleDown
×
1584
   end
1585
   -- Position the radicand
1586
   self.radicand.relX = self.symbolWidth + self.offsetX
×
1587
   -- Compute the dimensions of the whole radical
1588
   self.width = self.radicand.width + self.symbolWidth + self.offsetX
×
1589
   self.height = self.symbolHeight + self.radicalVerticalGap + self.extraAscender
×
1590
   self.depth = self.radicand.depth
×
1591
end
1592

1593
local function _r (number)
1594
   -- Lua 5.3+ formats floats as 1.0 and integers as 1
1595
   -- Also some PDF readers do not like double precision.
1596
   return math.floor(number) == number and math.floor(number) or tonumber(string.format("%.5f", number))
×
1597
end
1598

1599
function elements.sqrt:output (x, y, line)
2✔
1600
   -- HACK:
1601
   -- OpenType might say we need to assemble the radical sign from parts.
1602
   -- Frankly, it's much easier to just draw it as a graphic :-)
1603
   -- Hence, here we use a PDF graphic operators to draw a nice radical sign.
1604
   -- Some values here are ad hoc, but they look good.
1605
   local h = self.height:tonumber()
×
1606
   local d = self.depth:tonumber()
×
1607
   local s0 = scaleWidth(self.offsetX, line):tonumber()
×
1608
   local sw = scaleWidth(self.symbolWidth, line):tonumber()
×
1609
   local dsh = h - self.symbolShortHeight:tonumber()
×
1610
   local dsd = self.symbolDepth:tonumber()
×
1611
   local symbol = {
×
1612
      _r(self.radicalRuleThickness),
×
1613
      "w", -- line width
1614
      1,
1615
      "j", -- round line joins
1616
      _r(sw + s0),
×
1617
      _r(self.extraAscender),
×
1618
      "m",
1619
      _r(s0 + sw * 0.90),
×
1620
      _r(self.extraAscender),
×
1621
      "l",
1622
      _r(s0 + sw * 0.4),
×
1623
      _r(h + d + dsd),
×
1624
      "l",
1625
      _r(s0 + sw * 0.2),
×
1626
      _r(dsh),
×
1627
      "l",
1628
      s0 + sw * 0.1,
×
1629
      _r(dsh + 0.5),
×
1630
      "l",
1631
      "S",
1632
   }
1633
   local svg = table.concat(symbol, " ")
×
1634
   local xscaled = scaleWidth(x, line)
×
1635
   SILE.outputter:drawSVG(svg, xscaled, y, sw, h, 1)
×
1636
   -- And now we just need to draw the bar over the radicand
1637
   SILE.outputter:drawRule(
×
1638
      s0 + self.symbolWidth + xscaled,
×
1639
      y.length - self.height + self.extraAscender - self.radicalRuleThickness / 2,
×
1640
      scaleWidth(self.radicand.width, line),
×
1641
      self.radicalRuleThickness
1642
   )
1643
end
1644

1645
elements.padded = pl.class(elements.mbox)
2✔
1646
elements.padded._type = "Padded"
1✔
1647

1648
function elements.padded:__tostring ()
2✔
1649
   return self._type .. "(" .. tostring(self.impadded) .. ")"
×
1650
end
1651

1652
function elements.padded:_init (attributes, impadded)
2✔
1653
   elements.mbox._init(self)
×
1654
   self.impadded = impadded
×
1655
   self.attributes = attributes or {}
×
1656
   table.insert(self.children, impadded)
×
1657
end
1658

1659
function elements.padded:styleChildren ()
2✔
1660
   self.impadded.mode = self.mode
×
1661
end
1662

1663
function elements.padded:shape ()
2✔
1664
   -- TODO MathML allows percentages font-relative units (em, ex) for padding
1665
   -- But our units work with font.size, not math.font.size (possibly adjusted by scaleDown)
1666
   -- so the expectations might not be met.
1667
   local width = self.attributes.width and SU.cast("measurement", self.attributes.width)
×
1668
   local height = self.attributes.height and SU.cast("measurement", self.attributes.height)
×
1669
   local depth = self.attributes.depth and SU.cast("measurement", self.attributes.depth)
×
1670
   local lspace = self.attributes.lspace and SU.cast("measurement", self.attributes.lspace)
×
1671
   local voffset = self.attributes.voffset and SU.cast("measurement", self.attributes.voffset)
×
1672
   -- Clamping for width, height, depth, lspace
1673
   width = width and (width:tonumber() > 0 and width or SILE.types.measurement())
×
1674
   height = height and (height:tonumber() > 0 and height or SILE.types.measurement())
×
1675
   depth = depth and (depth:tonumber() > 0 and depth or SILE.types.measurement())
×
1676
   lspace = lspace and (lspace:tonumber() > 0 and lspace or SILE.types.measurement())
×
1677
   -- No clamping for voffset
1678
   voffset = voffset or SILE.types.measurement(0)
×
1679
   -- Compute the dimensions
1680
   self.width = width and SILE.types.length(width) or self.impadded.width
×
1681
   self.height = height and SILE.types.length(height) or self.impadded.height
×
1682
   self.depth = depth and SILE.types.length(depth) or self.impadded.depth
×
1683
   self.impadded.relX = lspace and SILE.types.length(lspace) or SILE.types.length()
×
1684
   self.impadded.relY = voffset and SILE.types.length(voffset):negate() or SILE.types.length()
×
1685
end
1686

1687
function elements.padded.output (_, _, _, _) end
1✔
1688

1689
-- Bevelled fractions are not part of MathML Core, and MathML4 does not
1690
-- exactly specify how to compute the layout.
1691
elements.bevelledFraction = pl.class(elements.fraction) -- Inherit from fraction
2✔
1692
elements.fraction._type = "BevelledFraction"
1✔
1693

1694
function elements.bevelledFraction:shape ()
2✔
1695
   local constants = self:getMathMetrics().constants
×
1696
   local scaleDown = self:getScaleDown()
×
1697
   local hSkew = constants.skewedFractionHorizontalGap * scaleDown
×
1698
   -- OpenType has properties which are not totally explicit.
1699
   -- The definition of skewedFractionVerticalGap (and its value in fonts
1700
   -- such as Libertinus Math) seems to imply that it is measured from the
1701
   -- bottom of the numerator to the top of the denominator.
1702
   -- This does not seem to be a nice general layout.
1703
   -- So we will use superscriptShiftUp(Cramped) for the numerator:
1704
   local vSkewUp = isCrampedMode(self.mode) and constants.superscriptShiftUpCramped * scaleDown
×
1705
      or constants.superscriptShiftUp * scaleDown
×
1706
   -- And all good books say that the denominator should not be shifted down:
1707
   local vSkewDown = 0
×
1708

1709
   self.ruleThickness = self.attributes.linethickness
×
1710
         and SU.cast("measurement", self.attributes.linethickness):tonumber()
×
1711
      or constants.fractionRuleThickness * scaleDown
×
1712
   self.numerator.relX = SILE.types.length(0)
×
1713
   self.numerator.relY = SILE.types.length(-vSkewUp)
×
1714
   self.denominator.relX = self.numerator.width + hSkew
×
1715
   self.denominator.relY = SILE.types.length(vSkewDown)
×
1716
   self.width = self.numerator.width + self.denominator.width + hSkew
×
1717
   self.height = maxLength(self.numerator.height + vSkewUp, self.denominator.height - vSkewDown)
×
1718
   self.depth = maxLength(self.numerator.depth - vSkewUp, self.denominator.depth + vSkewDown)
×
1719
   self.barWidth = SILE.types.length(hSkew)
×
1720
   self.barX = self.numerator.relX + self.numerator.width
×
1721
end
1722

1723
function elements.bevelledFraction:output (x, y, line)
2✔
1724
   local h = self.height:tonumber()
×
1725
   local d = self.depth:tonumber()
×
1726
   local barwidth = scaleWidth(self.barWidth, line):tonumber()
×
1727
   local xscaled = scaleWidth(x + self.barX, line)
×
1728
   local rd = self.ruleThickness / 2
×
1729
   local symbol = {
×
1730
      _r(self.ruleThickness),
×
1731
      "w", -- line width
1732
      1,
1733
      "J", -- round line caps
1734
      _r(0),
×
1735
      _r(d + h - rd),
×
1736
      "m",
1737
      _r(barwidth),
×
1738
      _r(rd),
×
1739
      "l",
1740
      "S",
1741
   }
1742
   local svg = table.concat(symbol, " ")
×
1743
   SILE.outputter:drawSVG(svg, xscaled, y, barwidth, h, 1)
×
1744
end
1745

1746
elements.mathMode = mathMode
1✔
1747
elements.newSubscript = newSubscript
1✔
1748
elements.newUnderOver = newUnderOver
1✔
1749

1750
return elements
1✔
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