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

sile-typesetter / sile / 11658356834

04 Nov 2024 05:01AM UTC coverage: 36.974% (-29.4%) from 66.366%
11658356834

push

github

alerque
feat(cli): Catch missing required arguments at parse time

6656 of 18002 relevant lines covered (36.97%)

1490.04 hits per line

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

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

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

11
local elements = {}
×
12

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

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

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

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

40
local mathCache = {}
×
41

42
local function retrieveMathTable (font)
43
   local key = SILE.font._key(font)
×
44
   if not mathCache[key] then
×
45
      SU.debug("math", "Loading math font", key)
×
46
      local face = SILE.font.cache(font, SILE.shaper.getFace)
×
47
      if not face then
×
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")
×
52
      if fontHasMathTable then
×
53
         mathTableParsable, mathTable = pcall(ot.parseMath, rawMathTable)
×
54
      end
55
      if not fontHasMathTable or not mathTableParsable then
×
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
×
63
      local constants = {}
×
64
      for k, v in pairs(mathTable.mathConstants) do
×
65
         if type(v) == "table" then
×
66
            v = v.value
×
67
         end
68
         if k:sub(-9) == "ScaleDown" then
×
69
            constants[k] = v / 100
×
70
         else
71
            constants[k] = v * font.size / upem
×
72
         end
73
      end
74
      local italicsCorrection = {}
×
75
      for k, v in pairs(mathTable.mathItalicsCorrection) do
×
76
         italicsCorrection[k] = v.value * font.size / upem
×
77
      end
78
      mathCache[key] = {
×
79
         constants = constants,
80
         italicsCorrection = italicsCorrection,
81
         mathVariants = mathTable.mathVariants,
82
         unitsPerEm = upem,
83
      }
84
   end
85
   return mathCache[key]
×
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
×
92
      return mathMode.script
×
93
   -- D', T' -> S'
94
   elseif mode == mathMode.displayCramped or mode == mathMode.textCramped then
×
95
      return mathMode.scriptCramped
×
96
   -- S, SS -> SS
97
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
×
98
      return mathMode.scriptScript
×
99
   -- S', SS' -> SS'
100
   else
101
      return mathMode.scriptScriptCramped
×
102
   end
103
end
104
local function getSubscriptMode (mode)
105
   -- D, T, D', T' -> S'
106
   if
107
      mode == mathMode.display
×
108
      or mode == mathMode.text
×
109
      or mode == mathMode.displayCramped
×
110
      or mode == mathMode.textCramped
×
111
   then
112
      return mathMode.scriptCramped
×
113
   -- S, SS, S', SS' -> SS'
114
   else
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
122
   if mode == mathMode.display then
×
123
      return mathMode.text
×
124
   -- D' -> T'
125
   elseif mode == mathMode.displayCramped then
×
126
      return mathMode.textCramped
×
127
   -- T -> S
128
   elseif mode == mathMode.text then
×
129
      return mathMode.script
×
130
   -- T' -> S'
131
   elseif mode == mathMode.textCramped then
×
132
      return mathMode.scriptCramped
×
133
   -- S, SS -> SS
134
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
×
135
      return mathMode.scriptScript
×
136
   -- S', SS' -> SS'
137
   else
138
      return mathMode.scriptScriptCramped
×
139
   end
140
end
141
local function getDenominatorMode (mode)
142
   -- D, D' -> T'
143
   if mode == mathMode.display or mode == mathMode.displayCramped then
×
144
      return mathMode.textCramped
×
145
   -- T, T' -> S'
146
   elseif mode == mathMode.text or mode == mathMode.textCramped then
×
147
      return mathMode.scriptCramped
×
148
   -- S, SS, S', SS' -> SS'
149
   else
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
×
156
      node = node.children[#node.children]
×
157
   end
158
   if node and node:is_a(elements.text) then
×
159
      return node.value.glyphString[#node.value.glyphString]
×
160
   else
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 = { ... }
×
169
   local m
170
   for i, v in ipairs(arg) do
×
171
      if i == 1 then
×
172
         m = v
×
173
      else
174
         if v.length:tonumber() > m.length:tonumber() then
×
175
            m = v
×
176
         end
177
      end
178
   end
179
   return m
×
180
end
181

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

471
function elements.phantom:_init (children)
×
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 (_, _, _)
×
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)
×
490
elements.subscript._type = "Subscript"
×
491

492
function elements.subscript:__tostring ()
×
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)
×
502
   elements.mbox._init(self)
×
503
   self.base = base
×
504
   self.sub = sub
×
505
   self.sup = sup
×
506
   if self.base then
×
507
      table.insert(self.children, self.base)
×
508
   end
509
   if self.sub then
×
510
      table.insert(self.children, self.sub)
×
511
   end
512
   if self.sup then
×
513
      table.insert(self.children, self.sup)
×
514
   end
515
   self.atom = self.base.atom
×
516
end
517

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

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

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

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

637
elements.underOver = pl.class(elements.subscript)
×
638
elements.underOver._type = "UnderOver"
×
639

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

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

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

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

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

720
function elements.underOver:shape ()
×
721
   local isBaseLargeOp = SU.boolean(self.base and self.base.largeop, false)
×
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" property 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 other 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.
732
      self.isUnderOver = true
×
733
      elements.subscript.shape(self)
×
734
      return
×
735
   end
736
   local constants = self:getMathMetrics().constants
×
737
   local scaleDown = self:getScaleDown()
×
738
   -- Determine relative Ys
739
   if self.base then
×
740
      self.base.relY = SILE.types.length(0)
×
741
   end
742
   if self.sub then
×
743
      self:_stretchyReshapeToBase(self.sub)
×
744
      self.sub.relY = self.base.depth
×
745
         + SILE.types.length(
×
746
            math.max(
×
747
               (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
×
748
               constants.lowerLimitBaselineDropMin * scaleDown
×
749
            )
750
         )
751
   end
752
   if self.sup then
×
753
      self:_stretchyReshapeToBase(self.sup)
×
754
      self.sup.relY = 0
×
755
         - self.base.height
×
756
         - SILE.types.length(
×
757
            math.max(
×
758
               (constants.upperLimitGapMin * scaleDown + self.sup.depth):tonumber(),
×
759
               constants.upperLimitBaselineRiseMin * scaleDown
×
760
            )
761
         )
762
   end
763
   -- Determine relative Xs based on widest symbol
764
   local widest, a, b
765
   if self.sub and self.sub.width > self.base.width then
×
766
      if self.sup and self.sub.width > self.sup.width then
×
767
         widest = self.sub
×
768
         a = self.base
×
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
780
      if self.sup and self.base.width > self.sup.width then
×
781
         widest = self.base
×
782
         a = self.sub
×
783
         b = self.sup
×
784
      elseif self.sup then
×
785
         widest = self.sup
×
786
         a = self.base
×
787
         b = self.sub
×
788
      else
789
         widest = self.base
×
790
         a = self.sub
×
791
         b = nil
×
792
      end
793
   end
794
   widest.relX = SILE.types.length(0)
×
795
   local c = widest.width / 2
×
796
   if a then
×
797
      a.relX = c - a.width / 2
×
798
   end
799
   if b then
×
800
      b.relX = c - b.width / 2
×
801
   end
802
   local itCorr = self:calculateItalicsCorrection() * scaleDown
×
803
   if self.sup then
×
804
      self.sup.relX = self.sup.relX + itCorr / 2
×
805
   end
806
   if self.sub then
×
807
      self.sub.relX = self.sub.relX - itCorr / 2
×
808
   end
809
   -- Determine width and height
810
   self.width = maxLength(
×
811
      self.base and self.base.width or SILE.types.length(0),
×
812
      self.sub and self.sub.width or SILE.types.length(0),
×
813
      self.sup and self.sup.width or SILE.types.length(0)
×
814
   )
815
   if self.sup then
×
816
      self.height = 0 - self.sup.relY + self.sup.height
×
817
   else
818
      self.height = self.base and self.base.height or 0
×
819
   end
820
   if self.sub then
×
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 ()
×
828
   local lastGid = getRightMostGlyphId(self.base)
×
829
   if lastGid > 0 then
×
830
      local mathMetrics = self:getMathMetrics()
×
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
842
   return 0
×
843
end
844

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

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

851
function elements.terminal:_init ()
×
852
   elements.mbox._init(self)
×
853
end
854

855
function elements.terminal.styleChildren (_) end
×
856

857
function elements.terminal.shape (_) end
×
858

859
elements.space = pl.class(elements.terminal)
×
860
elements.space._type = "Space"
×
861

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

866
function elements.space:__tostring ()
×
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
×
879
      local direction = 1
×
880
      if value:sub(1, 1) == "-" then
×
881
         value = value:sub(2, -1)
×
882
         direction = -1
×
883
      end
884
      if value == "thin" then
×
885
         return SILE.types.length("3mu") * direction
×
886
      elseif value == "med" then
×
887
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
×
888
      elseif value == "thick" then
×
889
         return SILE.types.length("5mu plus 5mu") * direction
×
890
      end
891
   end
892
   return SILE.types.length(value)
×
893
end
894

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

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

908
function elements.space.output (_) end
×
909

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

914
function elements.text:__tostring ()
×
915
   return self._type
×
916
      .. "(atom="
×
917
      .. tostring(self.atom)
×
918
      .. ", kind="
×
919
      .. tostring(self.kind)
×
920
      .. ", script="
×
921
      .. tostring(self.script)
×
922
      .. (SU.boolean(self.stretchy, false) and ", stretchy" or "")
×
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)
×
930
   elements.terminal._init(self)
×
931
   if not (kind == "number" or kind == "identifier" or kind == "operator" or kind == "string") then
×
932
      SU.error("Unknown text node kind '" .. kind .. "'; should be one of: number, identifier, operator, string")
×
933
   end
934
   self.kind = kind
×
935
   self.script = script
×
936
   self.text = text
×
937
   if self.script ~= "upright" then
×
938
      local converted = convertMathVariantScript(self.text, self.script)
×
939
      self.originalText = self.text
×
940
      self.text = converted
×
941
   end
942
   if self.kind == "operator" then
×
943
      if self.text == "-" then
×
944
         self.text = "−"
×
945
      end
946
   end
947
   for attribute, value in pairs(attributes) do
×
948
      self[attribute] = value
×
949
   end
950
end
951

952
function elements.text:shape ()
×
953
   self.font.size = self.font.size * self:getScaleDown()
×
954
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
×
955
   local mathMetrics = self:getMathMetrics()
×
956
   local glyphs = SILE.shaper:shapeToken(self.text, self.font)
×
957
   -- Use bigger variants for big operators in display style
958
   if isDisplayMode(self.mode) and SU.boolean(self.largeop, false) then
×
959
      -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
960
      glyphs = pl.tablex.deepcopy(glyphs)
×
961
      local constructions = mathMetrics.mathVariants.vertGlyphConstructions[glyphs[1].gid]
×
962
      if constructions then
×
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
967
         local m = 0
×
968
         for _, v in ipairs(displayVariants) do
×
969
            if v.advanceMeasurement > m then
×
970
               biggest = v
×
971
               m = v.advanceMeasurement
×
972
            end
973
         end
974
         if biggest then
×
975
            glyphs[1].gid = biggest.variantGlyph
×
976
            local dimen = hb.get_glyph_dimensions(face, self.font.size, biggest.variantGlyph)
×
977
            glyphs[1].width = dimen.width
×
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). ]]
984
            local axisHeight = mathMetrics.constants.axisHeight * self:getScaleDown()
×
985
            local y_size = dimen.height + dimen.depth
×
986
            glyphs[1].height = y_size / 2 + axisHeight
×
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.
991
            glyphs[1].fontHeight = dimen.height
×
992
            glyphs[1].fontDepth = dimen.depth
×
993
         end
994
      end
995
   end
996
   SILE.shaper:preAddNodes(glyphs, self.value)
×
997
   self.value.items = glyphs
×
998
   self.value.glyphString = {}
×
999
   if glyphs and #glyphs > 0 then
×
1000
      for i = 1, #glyphs do
×
1001
         table.insert(self.value.glyphString, glyphs[i].gid)
×
1002
      end
1003
      self.width = SILE.types.length(0)
×
1004
      self.widthForSubscript = SILE.types.length(0)
×
1005
      for i = #glyphs, 1, -1 do
×
1006
         self.width = self.width + glyphs[i].glyphAdvance
×
1007
      end
1008
      -- Store width without italic correction somewhere
1009
      self.widthForSubscript = self.width
×
1010
      local itCorr = mathMetrics.italicsCorrection[glyphs[#glyphs].gid]
×
1011
      if itCorr then
×
1012
         self.width = self.width + itCorr * self:getScaleDown()
×
1013
      end
1014
      for i = 1, #glyphs do
×
1015
         self.height = i == 1 and SILE.types.length(glyphs[i].height)
×
1016
            or SILE.types.length(math.max(self.height:tonumber(), glyphs[i].height))
×
1017
         self.depth = i == 1 and SILE.types.length(glyphs[i].depth)
×
1018
            or SILE.types.length(math.max(self.depth:tonumber(), glyphs[i].depth))
×
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)
×
1028
   local closest
1029
   local closestI
1030
   local m = requiredAdvance - currentAdvance
×
1031
   for i, variant in ipairs(variants) do
×
1032
      local diff = math.abs(variant.advanceMeasurement - requiredAdvance)
×
1033
      SU.debug("math", "stretch: diff =", diff)
×
1034
      if diff < m then
×
1035
         closest = variant
×
1036
         closestI = i
×
1037
         m = diff
×
1038
      end
1039
   end
1040
   return closest, closestI
×
1041
end
1042

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

1052
function elements.text:_stretchyReshape (target, direction)
×
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()
×
1056
   local upem = mathMetrics.unitsPerEm
×
1057
   local sz = self.font.size
×
1058
   local requiredAdvance = target:tonumber() * upem / sz
×
1059
   SU.debug("math", "stretch: rA =", requiredAdvance)
×
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)
×
1069
   local glyphConstructions = direction and mathMetrics.mathVariants.vertGlyphConstructions
×
1070
      or mathMetrics.mathVariants.horizGlyphConstructions
×
1071
   local constructions = glyphConstructions[glyphs[1].gid]
×
1072
   if constructions then
×
1073
      local variants = constructions.mathGlyphVariantRecord
×
1074
      SU.debug("math", "stretch: variants =", variants)
×
1075
      local currentAdvance = (direction and (self.depth + self.height):tonumber() or self.width:tonumber()) * upem / sz
×
1076
      local closest, closestI = self:findClosestVariant(variants, requiredAdvance, currentAdvance)
×
1077
      SU.debug("math", "stretch: closestI =", closestI)
×
1078
      if closest then
×
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)
×
1085
         self.width, self.depth, self.height =
×
1086
            SILE.types.length(dimen.glyphAdvance), SILE.types.length(dimen.depth), SILE.types.length(dimen.height)
×
1087
         SILE.shaper:preAddNodes(glyphs, self.value)
×
1088
         self.value.items = glyphs
×
1089
         self.value.glyphString = { glyphs[1].gid }
×
1090
         return true
×
1091
      end
1092
   end
1093
   return false
×
1094
end
1095

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

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

1118
function elements.text:output (x, y, line)
×
1119
   if not self.value.glyphString then
×
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
×
1124
      compensatedY = SILE.types.length(y.length + self.value.items[1].depth - self.value.items[1].fontDepth)
×
1125
   else
1126
      compensatedY = y
×
1127
   end
1128
   SILE.outputter:setCursor(scaleWidth(x, line), compensatedY.length)
×
1129
   SILE.outputter:setFont(self.font)
×
1130
   -- There should be no stretch or shrink on the width of a text
1131
   -- element.
1132
   local width = self.width.length
×
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
×
1139
      local xratio = self.horizScalingRatio or 1
×
1140
      local yratio = self.vertScalingRatio or 1
×
1141
      SU.debug("math", "fake glyph stretch: xratio =", xratio, "yratio =", yratio)
×
1142
      SILE.outputter:scaleFn(x, y, xratio, yratio, function ()
×
1143
         SILE.outputter:drawHbox(self.value, width)
×
1144
      end)
1145
   else
1146
      SILE.outputter:drawHbox(self.value, width)
×
1147
   end
1148
end
1149

1150
elements.fraction = pl.class(elements.mbox)
×
1151
elements.fraction._type = "Fraction"
×
1152

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

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

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

1171
function elements.fraction:shape ()
×
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.
1178
   self.padding = SILE.types.length("1px"):absolute()
×
1179

1180
   -- Determine relative abscissas and width
1181
   local widest, other
1182
   if self.denominator.width > self.numerator.width then
×
1183
      widest, other = self.denominator, self.numerator
×
1184
   else
1185
      widest, other = self.numerator, self.denominator
×
1186
   end
1187
   widest.relX = self.padding
×
1188
   other.relX = self.padding + (widest.width - other.width) / 2
×
1189
   self.width = widest.width + 2 * self.padding
×
1190
   -- Determine relative ordinates and height
1191
   local constants = self:getMathMetrics().constants
×
1192
   local scaleDown = self:getScaleDown()
×
1193
   self.axisHeight = constants.axisHeight * scaleDown
×
1194
   self.ruleThickness = self.attributes.linethickness
×
1195
         and SU.cast("measurement", self.attributes.linethickness):tonumber()
×
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
1206
   if isDisplayMode(self.mode) then
×
1207
      numeratorGapMin = constants.fractionNumDisplayStyleGapMin * scaleDown
×
1208
      denominatorGapMin = constants.fractionDenomDisplayStyleGapMin * scaleDown
×
1209
      numeratorShiftUp = constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
×
1210
      denominatorShiftDown = constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
×
1211
   else
1212
      numeratorGapMin = constants.fractionNumeratorGapMin * scaleDown
×
1213
      denominatorGapMin = constants.fractionDenominatorGapMin * scaleDown
×
1214
      numeratorShiftUp = constants.fractionNumeratorShiftUp * scaleDown
×
1215
      denominatorShiftDown = constants.fractionDenominatorShiftDown * scaleDown
×
1216
   end
1217

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

1238
function elements.fraction:output (x, y, line)
×
1239
   if self.ruleThickness > 0 then
×
1240
      SILE.outputter:drawRule(
×
1241
         scaleWidth(x + self.padding, line),
×
1242
         y.length - self.axisHeight - self.ruleThickness / 2,
×
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)
×
1251
end
1252

1253
local function newUnderOver (spec)
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)
1259
   local ret = {}
×
1260
   for i, x in ipairs(l) do
×
1261
      ret[i] = f(i, x)
×
1262
   end
1263
   return ret
×
1264
end
1265

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

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

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

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

1281
function elements.mtr.output (_) end
×
1282

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

1286
function elements.table:_init (children, options)
×
1287
   elements.mbox._init(self)
×
1288
   self.children = children
×
1289
   self.options = options
×
1290
   self.nrows = #self.children
×
1291
   self.ncols = math.max(pl.utils.unpack(mapList(function (_, row)
×
1292
      return #row.children
×
1293
   end, self.children)))
×
1294
   SU.debug("math", "self.ncols =", self.ncols)
×
1295
   local spacing = SILE.settings:get("math.font.size") * 0.6 -- arbitrary ratio of the current math font size
×
1296
   self.rowspacing = self.options.rowspacing and SILE.types.length(self.options.rowspacing) or spacing
×
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.
1300
   for i, row in ipairs(self.children) do
×
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
1307
   if options.columnalign then
×
1308
      local l = {}
×
1309
      for w in string.gmatch(options.columnalign, "[^%s]+") do
×
1310
         if not (w == "left" or w == "center" or w == "right") then
×
1311
            SU.error("Invalid specifier in `columnalign` attribute: " .. w)
×
1312
         end
1313
         table.insert(l, w)
×
1314
      end
1315
      -- Pad with last value of l if necessary
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
1320
      for _ = 1, (#l - self.ncols), 1 do
×
1321
         table.remove(l)
×
1322
      end
1323
      self.options.columnalign = l
×
1324
   else
1325
      self.options.columnalign = pl.List.range(1, self.ncols):map(function (_)
×
1326
         return "center"
×
1327
      end)
1328
   end
1329
end
1330

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

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

1408
function elements.table.output (_) end
×
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)
×
1429
elements.sqrt._type = "Sqrt"
×
1430

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

1435
function elements.sqrt:_init (radicand, degree)
×
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 ()
×
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 ()
×
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 dimensions 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)
×
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)
×
1558
elements.padded._type = "Padded"
×
1559

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

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

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

1575
function elements.padded:shape ()
×
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.
1579
   local width = self.attributes.width and SU.cast("measurement", self.attributes.width)
×
1580
   local height = self.attributes.height and SU.cast("measurement", self.attributes.height)
×
1581
   local depth = self.attributes.depth and SU.cast("measurement", self.attributes.depth)
×
1582
   local lspace = self.attributes.lspace and SU.cast("measurement", self.attributes.lspace)
×
1583
   local voffset = self.attributes.voffset and SU.cast("measurement", self.attributes.voffset)
×
1584
   -- Clamping for width, height, depth, lspace
1585
   width = width and (width:tonumber() > 0 and width or SILE.types.measurement())
×
1586
   height = height and (height:tonumber() > 0 and height or SILE.types.measurement())
×
1587
   depth = depth and (depth:tonumber() > 0 and depth or SILE.types.measurement())
×
1588
   lspace = lspace and (lspace:tonumber() > 0 and lspace or SILE.types.measurement())
×
1589
   -- No clamping for voffset
1590
   voffset = voffset or SILE.types.measurement(0)
×
1591
   -- Compute the dimensions
1592
   self.width = width and SILE.types.length(width) or self.impadded.width
×
1593
   self.height = height and SILE.types.length(height) or self.impadded.height
×
1594
   self.depth = depth and SILE.types.length(depth) or self.impadded.depth
×
1595
   self.impadded.relX = lspace and SILE.types.length(lspace) or SILE.types.length()
×
1596
   self.impadded.relY = voffset and SILE.types.length(voffset):negate() or SILE.types.length()
×
1597
end
1598

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

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

1607
return elements
×
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