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

sile-typesetter / sile / 11645827362

02 Nov 2024 09:08PM UTC coverage: 54.19% (-15.2%) from 69.34%
11645827362

push

github

web-flow
Merge pull request #2151 from Omikhleia/math-more-fixes-and-features

Math more fixes and features

103 of 208 new or added lines in 4 files covered. (49.52%)

2652 existing lines in 91 files now uncovered.

9816 of 18114 relevant lines covered (54.19%)

671.83 hits per line

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

48.69
/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 syms = require("packages.math.unicode-symbols")
1✔
5
local mathvariants = require("packages.math.unicode-mathvariants")
1✔
6
local convertMathVariantScript = mathvariants.convertMathVariantScript
1✔
7

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

11
local elements = {}
1✔
12

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

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

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

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

40
local mathCache = {}
1✔
41

42
local function retrieveMathTable (font)
43
   local key = SILE.font._key(font)
486✔
44
   if not mathCache[key] then
486✔
45
      SU.debug("math", "Loading math font", key)
4✔
46
      local face = SILE.font.cache(font, SILE.shaper.getFace)
4✔
47
      if not face then
4✔
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")
4✔
52
      if fontHasMathTable then
4✔
53
         mathTableParsable, mathTable = pcall(ot.parseMath, rawMathTable)
8✔
54
      end
55
      if not fontHasMathTable or not mathTableParsable then
4✔
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
8✔
63
      local constants = {}
4✔
64
      for k, v in pairs(mathTable.mathConstants) do
228✔
65
         if type(v) == "table" then
224✔
66
            v = v.value
204✔
67
         end
68
         if k:sub(-9) == "ScaleDown" then
448✔
69
            constants[k] = v / 100
8✔
70
         else
71
            constants[k] = v * font.size / upem
216✔
72
         end
73
      end
74
      local italicsCorrection = {}
4✔
75
      for k, v in pairs(mathTable.mathItalicsCorrection) do
1,696✔
76
         italicsCorrection[k] = v.value * font.size / upem
1,692✔
77
      end
78
      mathCache[key] = {
4✔
79
         constants = constants,
4✔
80
         italicsCorrection = italicsCorrection,
4✔
81
         mathVariants = mathTable.mathVariants,
4✔
82
         unitsPerEm = upem,
4✔
83
      }
4✔
84
   end
85
   return mathCache[key]
486✔
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
12✔
92
      return mathMode.script
6✔
93
   -- D', T' -> S'
94
   elseif mode == mathMode.displayCramped or mode == mathMode.textCramped then
6✔
95
      return mathMode.scriptCramped
6✔
96
   -- S, SS -> SS
UNCOV
97
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
×
UNCOV
98
      return mathMode.scriptScript
×
99
   -- S', SS' -> SS'
100
   else
UNCOV
101
      return mathMode.scriptScriptCramped
×
102
   end
103
end
104
local function getSubscriptMode (mode)
105
   -- D, T, D', T' -> S'
106
   if
UNCOV
107
      mode == mathMode.display
×
UNCOV
108
      or mode == mathMode.text
×
UNCOV
109
      or mode == mathMode.displayCramped
×
UNCOV
110
      or mode == mathMode.textCramped
×
111
   then
UNCOV
112
      return mathMode.scriptCramped
×
113
   -- S, SS, S', SS' -> SS'
114
   else
UNCOV
115
      return mathMode.scriptScriptCramped
×
116
   end
117
end
118

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

154
local function getRightMostGlyphId (node)
155
   while node and node:is_a(elements.stackbox) and node.direction == "H" do
40✔
156
      node = node.children[#node.children]
8✔
157
   end
158
   if node and node:is_a(elements.text) then
24✔
159
      return node.value.glyphString[#node.value.glyphString]
12✔
160
   else
UNCOV
161
      return 0
×
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 = { ... }
348✔
169
   local m
170
   for i, v in ipairs(arg) do
1,068✔
171
      if i == 1 then
720✔
172
         m = v
348✔
173
      else
174
         if v.length:tonumber() > m.length:tonumber() then
1,116✔
175
            m = v
60✔
176
         end
177
      end
178
   end
179
   return m
348✔
180
end
181

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

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

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

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

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

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

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

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

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

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

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

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

407
function elements.stackbox:shape ()
2✔
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)
48✔
417
   self.depth = SILE.types.length(0)
48✔
418
   if self.direction == "H" then
24✔
419
      for i, n in ipairs(self.children) do
204✔
420
         n.relY = SILE.types.length(0)
360✔
421
         self.height = i == 1 and n.height or maxLength(self.height, n.height)
336✔
422
         self.depth = i == 1 and n.depth or maxLength(self.depth, n.depth)
336✔
423
      end
424
      -- Handle stretchy operators
425
      for _, elt in ipairs(self.children) do
204✔
426
         if elt.is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
438✔
427
            elt:_vertStretchyReshape(self.depth, self.height)
36✔
428
         end
429
      end
430
      -- Set self.width
431
      self.width = SILE.types.length(0)
48✔
432
      for i, n in ipairs(self.children) do
204✔
433
         n.relX = self.width
180✔
434
         self.width = i == 1 and n.width or self.width + n.width
336✔
435
      end
436
   else -- self.direction == "V"
UNCOV
437
      for i, n in ipairs(self.children) do
×
UNCOV
438
         n.relX = SILE.types.length(0)
×
UNCOV
439
         self.width = i == 1 and n.width or maxLength(self.width, n.width)
×
440
      end
441
      -- Set self.height and self.depth
UNCOV
442
      for i, n in ipairs(self.children) do
×
UNCOV
443
         self.depth = i == 1 and n.depth or self.depth + n.depth
×
444
      end
UNCOV
445
      for i = 1, #self.children do
×
UNCOV
446
         local n = self.children[i]
×
UNCOV
447
         if i == 1 then
×
UNCOV
448
            self.height = n.height
×
UNCOV
449
            self.depth = n.depth
×
450
         elseif i > 1 then
×
451
            n.relY = self.children[i - 1].relY + self.children[i - 1].depth + n.height
×
452
            self.depth = self.depth + n.height + n.depth
×
453
         end
454
      end
455
   end
456
end
457

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

845
function elements.underOver.output (_, _, _, _) end
1✔
846

847
-- terminal is the base class for leaf node
848
elements.terminal = pl.class(elements.mbox)
2✔
849
elements.terminal._type = "Terminal"
1✔
850

851
function elements.terminal:_init ()
2✔
852
   elements.mbox._init(self)
174✔
853
end
854

855
function elements.terminal.styleChildren (_) end
175✔
856

857
function elements.terminal.shape (_) end
1✔
858

859
elements.space = pl.class(elements.terminal)
2✔
860
elements.space._type = "Space"
1✔
861

862
function elements.space:_init ()
2✔
863
   elements.terminal._init(self)
×
864
end
865

866
function elements.space:__tostring ()
2✔
867
   return self._type
×
868
      .. "(width="
×
869
      .. tostring(self.width)
×
870
      .. ", height="
×
871
      .. tostring(self.height)
×
872
      .. ", depth="
×
873
      .. tostring(self.depth)
×
874
      .. ")"
×
875
end
876

877
local function getStandardLength (value)
878
   if type(value) == "string" then
144✔
879
      local direction = 1
48✔
880
      if value:sub(1, 1) == "-" then
96✔
UNCOV
881
         value = value:sub(2, -1)
×
UNCOV
882
         direction = -1
×
883
      end
884
      if value == "thin" then
48✔
885
         return SILE.types.length("3mu") * direction
36✔
886
      elseif value == "med" then
36✔
887
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
36✔
888
      elseif value == "thick" then
24✔
889
         return SILE.types.length("5mu plus 5mu") * direction
72✔
890
      end
891
   end
892
   return SILE.types.length(value)
96✔
893
end
894

895
function elements.space:_init (width, height, depth)
2✔
896
   elements.terminal._init(self)
48✔
897
   self.width = getStandardLength(width)
96✔
898
   self.height = getStandardLength(height)
96✔
899
   self.depth = getStandardLength(depth)
96✔
900
end
901

902
function elements.space:shape ()
2✔
903
   self.width = self.width:absolute() * self:getScaleDown()
192✔
904
   self.height = self.height:absolute() * self:getScaleDown()
192✔
905
   self.depth = self.depth:absolute() * self:getScaleDown()
192✔
906
end
907

908
function elements.space.output (_) end
49✔
909

910
-- text node. For any actual text output
911
elements.text = pl.class(elements.terminal)
2✔
912
elements.text._type = "Text"
1✔
913

914
function elements.text:__tostring ()
2✔
915
   return self._type
×
916
      .. "(atom="
×
917
      .. tostring(self.atom)
×
918
      .. ", kind="
×
919
      .. tostring(self.kind)
×
920
      .. ", script="
×
921
      .. tostring(self.script)
×
NEW
922
      .. (SU.boolean(self.stretchy, false) and ", stretchy" or "")
×
NEW
923
      .. (SU.boolean(self.largeop, false) and ", largeop" or "")
×
924
      .. ', text="'
×
925
      .. (self.originalText or self.text)
×
926
      .. '")'
×
927
end
928

929
function elements.text:_init (kind, attributes, script, text)
2✔
930
   elements.terminal._init(self)
126✔
931
   if not (kind == "number" or kind == "identifier" or kind == "operator" or kind == "string") then
126✔
932
      SU.error("Unknown text node kind '" .. kind .. "'; should be one of: number, identifier, operator, string")
×
933
   end
934
   self.kind = kind
126✔
935
   self.script = script
126✔
936
   self.text = text
126✔
937
   if self.script ~= "upright" then
126✔
938
      local converted = convertMathVariantScript(self.text, self.script)
126✔
939
      self.originalText = self.text
126✔
940
      self.text = converted
126✔
941
   end
942
   if self.kind == "operator" then
126✔
943
      if self.text == "-" then
78✔
UNCOV
944
         self.text = "−"
×
945
      end
946
   end
947
   for attribute, value in pairs(attributes) do
244✔
948
      self[attribute] = value
118✔
949
   end
950
end
951

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

1027
function elements.text.findClosestVariant (_, variants, requiredAdvance, currentAdvance)
2✔
1028
   local closest
1029
   local closestI
1030
   local m = requiredAdvance - currentAdvance
36✔
1031
   for i, variant in ipairs(variants) do
504✔
1032
      local diff = math.abs(variant.advanceMeasurement - requiredAdvance)
468✔
1033
      SU.debug("math", "stretch: diff =", diff)
468✔
1034
      if diff < m then
468✔
1035
         closest = variant
44✔
1036
         closestI = i
44✔
1037
         m = diff
44✔
1038
      end
1039
   end
1040
   return closest, closestI
36✔
1041
end
1042

1043
function elements.text:_reshapeGlyph (glyph, closestVariant, sz)
2✔
1044
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
28✔
1045
   local dimen = hb.get_glyph_dimensions(face, sz, closestVariant.variantGlyph)
28✔
1046
   glyph.gid = closestVariant.variantGlyph
28✔
NEW
1047
   glyph.width, glyph.height, glyph.depth, glyph.glyphAdvance =
×
1048
      dimen.width, dimen.height, dimen.depth, dimen.glyphAdvance
28✔
1049
   return dimen
28✔
1050
end
1051

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

1096
function elements.text:_vertStretchyReshape (depth, height)
2✔
1097
   local hasStretched = self:_stretchyReshape(depth + height, true)
72✔
1098
   if hasStretched then
36✔
1099
      -- HACK: see output routine
1100
      self.vertExpectedSz = height + depth
56✔
1101
      self.vertScalingRatio = (depth + height):tonumber() / (self.height:tonumber() + self.depth:tonumber())
140✔
1102
      self.height = height
28✔
1103
      self.depth = depth
28✔
1104
   end
1105
   return hasStretched
36✔
1106
end
1107

1108
function elements.text:_horizStretchyReshape (width)
2✔
NEW
1109
   local hasStretched = self:_stretchyReshape(width, false)
×
NEW
1110
   if hasStretched then
×
1111
      -- HACK: see output routine
NEW
1112
      self.horizScalingRatio = width:tonumber() / self.width:tonumber()
×
NEW
1113
      self.width = width
×
1114
   end
NEW
1115
   return hasStretched
×
1116
end
1117

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

1150
elements.fraction = pl.class(elements.mbox)
2✔
1151
elements.fraction._type = "Fraction"
1✔
1152

1153
function elements.fraction:__tostring ()
2✔
1154
   return self._type .. "(" .. tostring(self.numerator) .. ", " .. tostring(self.denominator) .. ")"
×
1155
end
1156

1157
function elements.fraction:_init (attributes, numerator, denominator)
2✔
UNCOV
1158
   elements.mbox._init(self)
×
UNCOV
1159
   self.numerator = numerator
×
UNCOV
1160
   self.denominator = denominator
×
NEW
1161
   self.attributes = attributes
×
UNCOV
1162
   table.insert(self.children, numerator)
×
UNCOV
1163
   table.insert(self.children, denominator)
×
1164
end
1165

1166
function elements.fraction:styleChildren ()
2✔
UNCOV
1167
   self.numerator.mode = getNumeratorMode(self.mode)
×
UNCOV
1168
   self.denominator.mode = getDenominatorMode(self.mode)
×
1169
end
1170

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

1180
   -- Determine relative abscissas and width
1181
   local widest, other
UNCOV
1182
   if self.denominator.width > self.numerator.width then
×
UNCOV
1183
      widest, other = self.denominator, self.numerator
×
1184
   else
UNCOV
1185
      widest, other = self.numerator, self.denominator
×
1186
   end
UNCOV
1187
   widest.relX = self.padding
×
UNCOV
1188
   other.relX = self.padding + (widest.width - other.width) / 2
×
UNCOV
1189
   self.width = widest.width + 2 * self.padding
×
1190
   -- Determine relative ordinates and height
UNCOV
1191
   local constants = self:getMathMetrics().constants
×
UNCOV
1192
   local scaleDown = self:getScaleDown()
×
UNCOV
1193
   self.axisHeight = constants.axisHeight * scaleDown
×
NEW
1194
   self.ruleThickness = self.attributes.linethickness
×
NEW
1195
         and SU.cast("measurement", self.attributes.linethickness):tonumber()
×
NEW
1196
      or constants.fractionRuleThickness * scaleDown
×
1197

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

1205
   local numeratorGapMin, denominatorGapMin, numeratorShiftUp, denominatorShiftDown
UNCOV
1206
   if isDisplayMode(self.mode) then
×
UNCOV
1207
      numeratorGapMin = constants.fractionNumDisplayStyleGapMin * scaleDown
×
UNCOV
1208
      denominatorGapMin = constants.fractionDenomDisplayStyleGapMin * scaleDown
×
UNCOV
1209
      numeratorShiftUp = constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
×
UNCOV
1210
      denominatorShiftDown = constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
×
1211
   else
UNCOV
1212
      numeratorGapMin = constants.fractionNumeratorGapMin * scaleDown
×
UNCOV
1213
      denominatorGapMin = constants.fractionDenominatorGapMin * scaleDown
×
UNCOV
1214
      numeratorShiftUp = constants.fractionNumeratorShiftUp * scaleDown
×
UNCOV
1215
      denominatorShiftDown = constants.fractionDenominatorShiftDown * scaleDown
×
1216
   end
1217

UNCOV
1218
   self.numerator.relY = -self.axisHeight
×
UNCOV
1219
      - self.ruleThickness / 2
×
UNCOV
1220
      - SILE.types.length(
×
UNCOV
1221
         math.max(
×
UNCOV
1222
            (numeratorGapMin + self.numerator.depth):tonumber(),
×
UNCOV
1223
            numeratorShiftUp - self.axisHeight - self.ruleThickness / 2
×
1224
         )
1225
      )
UNCOV
1226
   self.denominator.relY = -self.axisHeight
×
UNCOV
1227
      + self.ruleThickness / 2
×
UNCOV
1228
      + SILE.types.length(
×
UNCOV
1229
         math.max(
×
UNCOV
1230
            (denominatorGapMin + self.denominator.height):tonumber(),
×
UNCOV
1231
            denominatorShiftDown + self.axisHeight - self.ruleThickness / 2
×
1232
         )
1233
      )
UNCOV
1234
   self.height = self.numerator.height - self.numerator.relY
×
UNCOV
1235
   self.depth = self.denominator.relY + self.denominator.depth
×
1236
end
1237

1238
function elements.fraction:output (x, y, line)
2✔
NEW
1239
   if self.ruleThickness > 0 then
×
NEW
1240
      SILE.outputter:drawRule(
×
NEW
1241
         scaleWidth(x + self.padding, line),
×
NEW
1242
         y.length - self.axisHeight - self.ruleThickness / 2,
×
NEW
1243
         scaleWidth(self.width - 2 * self.padding, line),
×
1244
         self.ruleThickness
1245
      )
1246
   end
1247
end
1248

1249
local function newSubscript (spec)
1250
   return elements.subscript(spec.base, spec.sub, spec.sup)
12✔
1251
end
1252

1253
local function newUnderOver (spec)
UNCOV
1254
   return elements.underOver(spec.base, spec.sub, spec.sup)
×
1255
end
1256

1257
-- TODO replace with penlight equivalent
1258
local function mapList (f, l)
UNCOV
1259
   local ret = {}
×
UNCOV
1260
   for i, x in ipairs(l) do
×
UNCOV
1261
      ret[i] = f(i, x)
×
1262
   end
UNCOV
1263
   return ret
×
1264
end
1265

1266
elements.mtr = pl.class(elements.mbox)
2✔
1267
-- elements.mtr._type = "" -- TODO why not set?
1268

1269
function elements.mtr:_init (children)
2✔
UNCOV
1270
   self.children = children
×
1271
end
1272

1273
function elements.mtr:styleChildren ()
2✔
UNCOV
1274
   for _, c in ipairs(self.children) do
×
UNCOV
1275
      c.mode = self.mode
×
1276
   end
1277
end
1278

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

1281
function elements.mtr.output (_) end
1✔
1282

1283
elements.table = pl.class(elements.mbox)
2✔
1284
elements.table._type = "table" -- TODO why case difference?
1✔
1285

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

1331
function elements.table:styleChildren ()
2✔
UNCOV
1332
   if self.mode == mathMode.display and self.options.displaystyle ~= "false" then
×
UNCOV
1333
      for _, c in ipairs(self.children) do
×
UNCOV
1334
         c.mode = mathMode.display
×
1335
      end
1336
   else
UNCOV
1337
      for _, c in ipairs(self.children) do
×
UNCOV
1338
         c.mode = mathMode.text
×
1339
      end
1340
   end
1341
end
1342

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

1408
function elements.table.output (_) end
1✔
1409

1410
local function getRadicandMode (mode)
1411
   -- Not too sure if we should do something special/
1412
   return mode
×
1413
end
1414

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

1428
elements.sqrt = pl.class(elements.mbox)
2✔
1429
elements.sqrt._type = "Sqrt"
1✔
1430

1431
function elements.sqrt:__tostring ()
2✔
1432
   return self._type .. "(" .. tostring(self.radicand) .. (self.degree and ", " .. tostring(self.degree) or "") .. ")"
×
1433
end
1434

1435
function elements.sqrt:_init (radicand, degree)
2✔
1436
   elements.mbox._init(self)
×
1437
   self.radicand = radicand
×
1438
   if degree then
×
1439
      self.degree = degree
×
1440
      table.insert(self.children, degree)
×
1441
   end
1442
   table.insert(self.children, radicand)
×
1443
   self.relX = SILE.types.length()
×
1444
   self.relY = SILE.types.length()
×
1445
end
1446

1447
function elements.sqrt:styleChildren ()
2✔
1448
   self.radicand.mode = getRadicandMode(self.mode)
×
1449
   if self.degree then
×
1450
      self.degree.mode = getDegreeMode(self.mode)
×
1451
   end
1452
end
1453

1454
function elements.sqrt:shape ()
2✔
1455
   local mathMetrics = self:getMathMetrics()
×
1456
   local scaleDown = self:getScaleDown()
×
1457
   local constants = mathMetrics.constants
×
1458

1459
   self.radicalRuleThickness = constants.radicalRuleThickness * scaleDown
×
1460
   if self.mode == mathMode.display or self.mode == mathMode.displayCramped then
×
1461
      self.radicalVerticalGap = constants.radicalDisplayStyleVerticalGap * scaleDown
×
1462
   else
1463
      self.radicalVerticalGap = constants.radicalVerticalGap * scaleDown
×
1464
   end
1465
   self.extraAscender = constants.radicalExtraAscender * scaleDown
×
1466

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

1480
   -- Adjust the height of the radical sign if the radicand is higher
1481
   self.symbolHeight = self.radicand.height > self.symbolHeight and self.radicand.height or self.symbolHeight
×
1482
   -- Compute the (max-)height of the short leg of the radical sign
1483
   self.symbolShortHeight = self.symbolHeight * constants.radicalDegreeBottomRaisePercent
×
1484

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

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

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

1557
elements.padded = pl.class(elements.mbox)
2✔
1558
elements.padded._type = "Padded"
1✔
1559

1560
function elements.padded:__tostring ()
2✔
NEW
1561
   return self._type .. "(" .. tostring(self.impadded) .. ")"
×
1562
end
1563

1564
function elements.padded:_init (attributes, impadded)
2✔
NEW
1565
   elements.mbox._init(self)
×
NEW
1566
   self.impadded = impadded
×
NEW
1567
   self.attributes = attributes or {}
×
NEW
1568
   table.insert(self.children, impadded)
×
1569
end
1570

1571
function elements.padded:styleChildren ()
2✔
NEW
1572
   self.impadded.mode = self.mode
×
1573
end
1574

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

1599
function elements.padded.output (_, _, _, _) end
1✔
1600

1601
elements.mathMode = mathMode
1✔
1602
elements.atomType = atomType
1✔
1603
elements.symbolDefaults = symbolDefaults
1✔
1604
elements.newSubscript = newSubscript
1✔
1605
elements.newUnderOver = newUnderOver
1✔
1606

1607
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