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

sile-typesetter / sile / 14510870853

17 Apr 2025 07:55AM UTC coverage: 31.472% (-25.8%) from 57.267%
14510870853

push

github

web-flow
Merge pull request #2267 from Omikhleia/feat-csl-position

0 of 109 new or added lines in 2 files covered. (0.0%)

4871 existing lines in 34 files now uncovered.

6341 of 20148 relevant lines covered (31.47%)

2774.57 hits per line

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

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

UNCOV
8
local elements = {}
×
9

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

21
local function isDisplayMode (mode)
UNCOV
22
   return mode <= 1
×
23
end
24

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

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

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

UNCOV
37
local mathCache = {}
×
38

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

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

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

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

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

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

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

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

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

UNCOV
203
function elements.mbox:_init ()
×
UNCOV
204
   nodefactory.hbox._init(self)
×
UNCOV
205
   self.font = {}
×
UNCOV
206
   self.children = {} -- The child nodes
×
UNCOV
207
   self.relX = SILE.types.length(0) -- x position relative to its parent box
×
UNCOV
208
   self.relY = SILE.types.length(0) -- y position relative to its parent box
×
UNCOV
209
   self.value = {}
×
UNCOV
210
   self.mode = mathMode.display
×
UNCOV
211
   self.atom = atoms.types.ord
×
UNCOV
212
   local font = {
×
213
      family = SILE.settings:get("math.font.family"),
214
      size = SILE.settings:get("math.font.size"),
215
      style = SILE.settings:get("math.font.style"),
216
      weight = SILE.settings:get("math.font.weight"),
217
      -- https://learn.microsoft.com/en-us/typography/opentype/spec/math#opentype-layout-tags-used-with-the-math-table
218
      --   "Script tag to be used for features in math layout.
219
      --   The only language system supported with this tag is the default language system."
220
      -- Thus, needed for the ssty feature in superscript/subscript to work properly.
221
      script = "math",
222
   }
UNCOV
223
   local filename = SILE.settings:get("math.font.filename")
×
UNCOV
224
   if filename and filename ~= "" then
×
225
      font.filename = filename
×
226
   end
UNCOV
227
   self.font = SILE.font.loadDefaults(font)
×
228
end
229

UNCOV
230
function elements.mbox:styleChildren ()
×
231
   SU.error("styleChildren is a virtual function that need to be overridden by its child classes")
×
232
end
233

UNCOV
234
function elements.mbox:shape (_, _)
×
235
   SU.error("shape is a virtual function that need to be overridden by its child classes")
×
236
end
237

UNCOV
238
function elements.mbox:output (_, _, _)
×
239
   SU.error("output is a virtual function that need to be overridden by its child classes")
×
240
end
241

UNCOV
242
function elements.mbox:getMathMetrics ()
×
UNCOV
243
   return retrieveMathTable(self.font)
×
244
end
245

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

259
-- Determine the mode of its descendants
UNCOV
260
function elements.mbox:styleDescendants ()
×
UNCOV
261
   self:styleChildren()
×
UNCOV
262
   for _, n in ipairs(self.children) do
×
UNCOV
263
      if n then
×
UNCOV
264
         n:styleDescendants()
×
265
      end
266
   end
267
end
268

269
-- shapeTree shapes the mbox and all its descendants in a recursive fashion
270
-- The inner-most leaf nodes determine their shape first, and then propagate to their parents
271
-- During the process, each node will determine its size by (width, height, depth)
272
-- and (relX, relY) which the relative position to its parent
UNCOV
273
function elements.mbox:shapeTree ()
×
UNCOV
274
   for _, n in ipairs(self.children) do
×
UNCOV
275
      if n then
×
UNCOV
276
         n:shapeTree()
×
277
      end
278
   end
UNCOV
279
   self:shape()
×
280
end
281

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

UNCOV
297
local spaceKind = {
×
298
   thin = "thin",
299
   med = "med",
300
   thick = "thick",
301
}
302

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

399
-- _stackbox stacks its content one, either horizontally or vertically
UNCOV
400
elements.stackbox = pl.class(elements.mbox)
×
UNCOV
401
elements.stackbox._type = "Stackbox"
×
402

UNCOV
403
function elements.stackbox:__tostring ()
×
404
   local result = self.direction .. "Box("
×
405
   for i, n in ipairs(self.children) do
×
406
      result = result .. (i == 1 and "" or ", ") .. tostring(n)
×
407
   end
408
   result = result .. ")"
×
409
   return result
×
410
end
411

UNCOV
412
function elements.stackbox:_init (direction, children)
×
UNCOV
413
   elements.mbox._init(self)
×
UNCOV
414
   if not (direction == "H" or direction == "V") then
×
415
      SU.error("Wrong direction '" .. direction .. "'; should be H or V")
×
416
   end
UNCOV
417
   self.direction = direction
×
UNCOV
418
   self.children = children
×
419
end
420

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

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

544
-- Despite of its name, this function actually output the whole tree of nodes recursively.
UNCOV
545
function elements.stackbox:outputYourself (typesetter, line)
×
UNCOV
546
   local mathX = typesetter.frame.state.cursorX
×
UNCOV
547
   local mathY = typesetter.frame.state.cursorY
×
UNCOV
548
   self:outputTree(self.relX + mathX, self.relY + mathY, line)
×
UNCOV
549
   typesetter.frame:advanceWritingDirection(scaleWidth(self.width, line))
×
550
end
551

UNCOV
552
function elements.stackbox:output (_, _, _) end
×
553

UNCOV
554
elements.phantom = pl.class(elements.stackbox) -- inherit from stackbox
×
UNCOV
555
elements.phantom._type = "Phantom"
×
556

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

UNCOV
568
function elements.phantom:output (_, _, _)
×
569
   -- Note the trick here: when the tree is rendered, the node's output
570
   -- function is invoked, then all its children's output functions.
571
   -- So we just cancel the list of children here, before it's rendered.
572
   self.children = {}
×
573
end
574

UNCOV
575
elements.subscript = pl.class(elements.mbox)
×
UNCOV
576
elements.subscript._type = "Subscript"
×
577

UNCOV
578
function elements.subscript:__tostring ()
×
579
   return (self.sub and "Subscript" or "Superscript")
×
580
      .. "("
×
581
      .. tostring(self.base)
×
582
      .. ", "
×
583
      .. tostring(self.sub or self.super)
×
584
      .. ")"
×
585
end
586

UNCOV
587
function elements.subscript:_init (base, sub, sup)
×
UNCOV
588
   elements.mbox._init(self)
×
UNCOV
589
   self.base = base
×
UNCOV
590
   self.sub = sub
×
UNCOV
591
   self.sup = sup
×
UNCOV
592
   if self.base then
×
UNCOV
593
      table.insert(self.children, self.base)
×
594
   end
UNCOV
595
   if self.sub then
×
UNCOV
596
      table.insert(self.children, self.sub)
×
597
   end
UNCOV
598
   if self.sup then
×
UNCOV
599
      table.insert(self.children, self.sup)
×
600
   end
UNCOV
601
   self.atom = self.base.atom
×
602
end
603

UNCOV
604
function elements.subscript:styleChildren ()
×
UNCOV
605
   if self.base then
×
UNCOV
606
      self.base.mode = self.mode
×
607
   end
UNCOV
608
   if self.sub then
×
UNCOV
609
      self.sub.mode = getSubscriptMode(self.mode)
×
610
   end
UNCOV
611
   if self.sup then
×
UNCOV
612
      self.sup.mode = getSuperscriptMode(self.mode)
×
613
   end
614
end
615

UNCOV
616
function elements.subscript:calculateItalicsCorrection ()
×
UNCOV
617
   local lastGid = getRightMostGlyphId(self.base)
×
UNCOV
618
   if lastGid > 0 then
×
UNCOV
619
      local mathMetrics = self:getMathMetrics()
×
UNCOV
620
      if mathMetrics.italicsCorrection[lastGid] then
×
UNCOV
621
         return mathMetrics.italicsCorrection[lastGid]
×
622
      end
623
   end
UNCOV
624
   return 0
×
625
end
626

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

UNCOV
721
function elements.subscript:output (_, _, _) end
×
722

UNCOV
723
elements.underOver = pl.class(elements.subscript)
×
UNCOV
724
elements.underOver._type = "UnderOver"
×
725

UNCOV
726
function elements.underOver:__tostring ()
×
727
   return self._type .. "(" .. tostring(self.base) .. ", " .. tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
×
728
end
729

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

740
local function getAccentMode (mode)
741
   -- Size unchanged but leave display mode
742
   -- See MathML Core §3.4.3
743
   if mode == mathMode.display then
×
744
      return mathMode.text
×
745
   end
746
   if mode == mathMode.displayCramped then
×
747
      return mathMode.textCramped
×
748
   end
749
   return mode
×
750
end
751

752
local function unwrapSingleElementMrow (elt)
753
   -- CODE SMELL.
754
   -- For \overset or \underset in LaTeX, MathML would use <mover> or <munder>.
755
   -- It would need to inherit the base's atom type, especially if the later is an operator
756
   -- (binary, relational etc.), which is a fairly common case, e.g.
757
   --   \overset{R}{=} (equality with a R above the equal in some Ramanujan summations),
758
   -- but we can't remove 1-element mrow's in the math typesetter, or have them inherit
759
   -- their base's atom type here above, because it breaks tables for some reasons
760
   -- that I couldn't figure out.
UNCOV
761
   if elt:is_a(elements.stackbox) and elt.direction == "H" and #elt.children == 1 then
×
UNCOV
762
      return unwrapSingleElementMrow(elt.children[1])
×
763
   else
UNCOV
764
      return elt
×
765
   end
766
end
767

UNCOV
768
function elements.underOver:_init (attributes, base, sub, sup)
×
UNCOV
769
   elements.mbox._init(self)
×
UNCOV
770
   base = unwrapSingleElementMrow(base)
×
UNCOV
771
   self.atom = base.atom
×
UNCOV
772
   self.attributes = attributes or {}
×
UNCOV
773
   self.attributes.accent = SU.boolean(self.attributes.accent, false)
×
UNCOV
774
   self.attributes.accentunder = SU.boolean(self.attributes.accentunder, false)
×
UNCOV
775
   self.base = base
×
UNCOV
776
   self.sub = isNotEmpty(sub) and sub or nil
×
UNCOV
777
   self.sup = isNotEmpty(sup) and sup or nil
×
UNCOV
778
   if self.sup then
×
UNCOV
779
      table.insert(self.children, self.sup)
×
780
   end
UNCOV
781
   if self.base then
×
UNCOV
782
      table.insert(self.children, self.base)
×
783
   end
UNCOV
784
   if self.sub then
×
UNCOV
785
      table.insert(self.children, self.sub)
×
786
   end
787
end
788

UNCOV
789
function elements.underOver:styleChildren ()
×
UNCOV
790
   if self.base then
×
UNCOV
791
      self.base.mode = self.mode
×
792
   end
UNCOV
793
   if self.sub then
×
UNCOV
794
      self.sub.mode = self.attributes.accentunder and getAccentMode(self.mode) or getSubscriptMode(self.mode)
×
795
   end
UNCOV
796
   if self.sup then
×
UNCOV
797
      self.sup.mode = self.attributes.accent and getAccentMode(self.mode) or getSuperscriptMode(self.mode)
×
798
   end
799
end
800

UNCOV
801
function elements.underOver:_stretchyReshapeToBase (part)
×
802
   -- FIXME: Big leap of faith here.
803
   -- MathML Core only mentions stretching along the inline axis in 3.4.2.2,
804
   -- i.e. under the section on <mover>, <munder>, <munderover>.
805
   -- So we are "somewhat" good here, but... the algorithm is totally unclear
806
   -- to me and seems to imply a lot of recursion and reshaping.
807
   -- The implementation below is NOT general and only works for the cases
808
   -- I checked:
809
   --   Mozilla MathML tests: braces in f19, f22
810
   --   Personal tests: vectors in d19, d22, d23
811
   --   Joe Javawaski's tests: braces in 8a, 8b
812
   --   MathML3 "complex1" torture test: Maxwell's Equations (vectors in fractions)
UNCOV
813
   if #part.children == 0 then
×
UNCOV
814
      local elt = part
×
UNCOV
815
      if elt:is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
×
816
         elt:_horizStretchyReshape(self.base.width)
×
817
      end
UNCOV
818
   elseif part:is_a(elements.underOver) then
×
819
      -- Big assumption here: only considering one level of stacked under/over.
820
      local hasStretched = false
×
821
      for _, elt in ipairs(part.children) do
×
822
         if elt:is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
×
823
            local stretched = elt:_horizStretchyReshape(self.base.width)
×
824
            if stretched then
×
825
               hasStretched = true
×
826
            end
827
         end
828
      end
829
      if hasStretched then
×
830
         -- We need to re-calculate the shape so positions are re-calculated on each
831
         -- of its own parts.
832
         -- (Added after seeing that Mozilla test f19 was not rendering correctly.)
833
         part:shape()
×
834
      end
835
   end
836
end
837

UNCOV
838
function elements.underOver:shape ()
×
UNCOV
839
   local constants = self:getMathMetrics().constants
×
UNCOV
840
   local scaleDown = self:getScaleDown()
×
UNCOV
841
   local isMovableLimits = SU.boolean(self.base and self.base.movablelimits, false)
×
UNCOV
842
   local itCorr = self:calculateItalicsCorrection() * scaleDown
×
UNCOV
843
   if not (self.mode == mathMode.display or self.mode == mathMode.displayCramped) and isMovableLimits then
×
844
      -- When the base is a movable limit, the under/over scripts are not placed under/over the base,
845
      -- but other to the right of it, when display mode is not used.
846
      -- Notable effects:
847
      --   Mozilla MathML test 19 (on "k times" > overbrace > base)
848
      --   Maxwell's Equations in MathML3 Test Suite "complex1" (on the vectors in fractions)
849
      self.isUnderOver = true
×
850
      elements.subscript.shape(self)
×
851
      return
×
852
   end
853
   -- Determine relative Ys
UNCOV
854
   if self.base then
×
UNCOV
855
      self.base.relY = SILE.types.length(0)
×
856
   end
UNCOV
857
   if self.sub then
×
UNCOV
858
      self:_stretchyReshapeToBase(self.sub)
×
859
      -- TODO These rules are incomplete and even wrong if we were to fully implement MathML Core.
UNCOV
860
      if self.attributes.accentunder then
×
861
         self.sub.relY = self.base.depth
×
862
            + SILE.types.length(
×
863
               (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber()
×
864
               -- We assume that the accent is aligned on the base.
865
            )
866
      else
UNCOV
867
         self.sub.relY = self.base.depth
×
UNCOV
868
            + SILE.types.length(
×
UNCOV
869
               math.max(
×
UNCOV
870
                  (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
×
UNCOV
871
                  constants.lowerLimitBaselineDropMin * scaleDown
×
872
               )
873
            )
874
      end
875
   end
UNCOV
876
   if self.sup then
×
UNCOV
877
      self:_stretchyReshapeToBase(self.sup)
×
878
      -- TODO These rules are incomplete if we were to fully implement MathML Core.
UNCOV
879
      if self.attributes.accent then
×
880
         self.sup.relY = 0 - self.base.height
×
881
         -- MathML Core wants to align on the accentBaseHeight...
882
         local overShift = math.max(0, constants.accentBaseHeight * scaleDown - self.base.height:tonumber())
×
883
         self.sup.relY = self.sup.relY - SILE.types.length(overShift)
×
884
         -- HACK: .... but improperly dimensioned accents can overshoot the base glyph.
885
         -- So we try some guesswork to correct this.
886
         -- Typically some non-combining symbols are in this case...
887
         local heuristics = 0.5 * constants.flattenedAccentBaseHeight + 0.5 * constants.accentBaseHeight
×
888
         if self.sup.height > SILE.types.length(heuristics * scaleDown) then
×
889
            self.sup.relY = self.sup.relY + SILE.types.length(constants.accentBaseHeight * scaleDown)
×
890
         end
891
      else
UNCOV
892
         self.sup.relY = 0
×
UNCOV
893
            - self.base.height
×
UNCOV
894
            - SILE.types.length(
×
UNCOV
895
               math.max(
×
UNCOV
896
                  (constants.upperLimitGapMin * scaleDown + self.sup.depth):tonumber(),
×
UNCOV
897
                  constants.upperLimitBaselineRiseMin * scaleDown
×
898
               )
899
            )
900
      end
901
   end
902
   -- Determine relative Xs based on widest symbol
903
   local widest, a, b
UNCOV
904
   if self.sub and self.sub.width > self.base.width then
×
UNCOV
905
      if self.sup and self.sub.width > self.sup.width then
×
UNCOV
906
         widest = self.sub
×
UNCOV
907
         a = self.base
×
UNCOV
908
         b = self.sup
×
UNCOV
909
      elseif self.sup then
×
910
         widest = self.sup
×
911
         a = self.base
×
912
         b = self.sub
×
913
      else
UNCOV
914
         widest = self.sub
×
UNCOV
915
         a = self.base
×
UNCOV
916
         b = nil
×
917
      end
918
   else
UNCOV
919
      if self.sup and self.base.width > self.sup.width then
×
UNCOV
920
         widest = self.base
×
UNCOV
921
         a = self.sub
×
UNCOV
922
         b = self.sup
×
UNCOV
923
      elseif self.sup then
×
924
         widest = self.sup
×
925
         a = self.base
×
926
         b = self.sub
×
927
      else
UNCOV
928
         widest = self.base
×
UNCOV
929
         a = self.sub
×
UNCOV
930
         b = nil
×
931
      end
932
   end
UNCOV
933
   widest.relX = SILE.types.length(0)
×
UNCOV
934
   local c = widest.width / 2
×
UNCOV
935
   if a then
×
UNCOV
936
      a.relX = c - a.width / 2
×
937
   end
UNCOV
938
   if b then
×
UNCOV
939
      b.relX = c - b.width / 2
×
940
   end
UNCOV
941
   if self.sup then
×
UNCOV
942
      self.sup.relX = self.sup.relX + itCorr / 2
×
943
   end
UNCOV
944
   if self.sub then
×
UNCOV
945
      self.sub.relX = self.sub.relX - itCorr / 2
×
946
   end
947
   -- Determine width and height
UNCOV
948
   self.width = maxLength(
×
UNCOV
949
      self.base and self.base.width or SILE.types.length(0),
×
UNCOV
950
      self.sub and self.sub.width or SILE.types.length(0),
×
UNCOV
951
      self.sup and self.sup.width or SILE.types.length(0)
×
952
   )
UNCOV
953
   if self.sup then
×
UNCOV
954
      self.height = 0 - self.sup.relY + self.sup.height
×
955
   else
UNCOV
956
      self.height = self.base and self.base.height or 0
×
957
   end
UNCOV
958
   if self.sub then
×
UNCOV
959
      self.depth = self.sub.relY + self.sub.depth
×
960
   else
UNCOV
961
      self.depth = self.base and self.base.depth or 0
×
962
   end
963
end
964

UNCOV
965
function elements.underOver:calculateItalicsCorrection ()
×
UNCOV
966
   local lastGid = getRightMostGlyphId(self.base)
×
UNCOV
967
   if lastGid > 0 then
×
UNCOV
968
      local mathMetrics = self:getMathMetrics()
×
UNCOV
969
      if mathMetrics.italicsCorrection[lastGid] then
×
970
         local c = mathMetrics.italicsCorrection[lastGid]
×
971
         -- If this is a big operator, and we are in display style, then the
972
         -- base glyph may be bigger than the font size. We need to adjust the
973
         -- italic correction accordingly.
974
         if SU.boolean(self.base.largeop) and isDisplayMode(self.mode) then
×
975
            c = c * (self.base and self.base.font.size / self.font.size or 1.0)
×
976
         end
977
         return c
×
978
      end
979
   end
UNCOV
980
   return 0
×
981
end
982

UNCOV
983
function elements.underOver:output (_, _, _) end
×
984

985
-- terminal is the base class for leaf node
UNCOV
986
elements.terminal = pl.class(elements.mbox)
×
UNCOV
987
elements.terminal._type = "Terminal"
×
988

UNCOV
989
function elements.terminal:_init ()
×
UNCOV
990
   elements.mbox._init(self)
×
991
end
992

UNCOV
993
function elements.terminal:styleChildren () end
×
994

UNCOV
995
function elements.terminal:shape () end
×
996

UNCOV
997
elements.space = pl.class(elements.terminal)
×
UNCOV
998
elements.space._type = "Space"
×
999

UNCOV
1000
function elements.space:_init ()
×
1001
   elements.terminal._init(self)
×
1002
end
1003

UNCOV
1004
function elements.space:__tostring ()
×
1005
   return self._type
×
1006
      .. "(width="
×
1007
      .. tostring(self.width)
×
1008
      .. ", height="
×
1009
      .. tostring(self.height)
×
1010
      .. ", depth="
×
1011
      .. tostring(self.depth)
×
1012
      .. ")"
×
1013
end
1014

1015
local function getStandardLength (value)
UNCOV
1016
   if type(value) == "string" then
×
UNCOV
1017
      local direction = 1
×
UNCOV
1018
      if value:sub(1, 1) == "-" then
×
UNCOV
1019
         value = value:sub(2, -1)
×
UNCOV
1020
         direction = -1
×
1021
      end
UNCOV
1022
      if value == "thin" then
×
UNCOV
1023
         return SILE.types.length("3mu") * direction
×
UNCOV
1024
      elseif value == "med" then
×
UNCOV
1025
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
×
UNCOV
1026
      elseif value == "thick" then
×
UNCOV
1027
         return SILE.types.length("5mu plus 5mu") * direction
×
1028
      end
1029
   end
UNCOV
1030
   return SILE.types.length(value)
×
1031
end
1032

UNCOV
1033
function elements.space:_init (width, height, depth)
×
UNCOV
1034
   elements.terminal._init(self)
×
UNCOV
1035
   self.width = getStandardLength(width)
×
UNCOV
1036
   self.height = getStandardLength(height)
×
UNCOV
1037
   self.depth = getStandardLength(depth)
×
1038
end
1039

UNCOV
1040
function elements.space:shape ()
×
UNCOV
1041
   self.width = self.width:absolute() * self:getScaleDown()
×
UNCOV
1042
   self.height = self.height:absolute() * self:getScaleDown()
×
UNCOV
1043
   self.depth = self.depth:absolute() * self:getScaleDown()
×
1044
end
1045

UNCOV
1046
function elements.space:output (_, _, _) end
×
1047

1048
-- text node. For any actual text output
UNCOV
1049
elements.text = pl.class(elements.terminal)
×
UNCOV
1050
elements.text._type = "Text"
×
1051

UNCOV
1052
function elements.text:__tostring ()
×
1053
   return self._type
×
1054
      .. "(atom="
×
1055
      .. tostring(self.atom)
×
1056
      .. ", kind="
×
1057
      .. tostring(self.kind)
×
1058
      .. ", script="
×
1059
      .. tostring(self.script)
×
1060
      .. (SU.boolean(self.stretchy, false) and ", stretchy" or "")
×
1061
      .. (SU.boolean(self.largeop, false) and ", largeop" or "")
×
1062
      .. ', text="'
×
1063
      .. (self.originalText or self.text)
×
1064
      .. '")'
×
1065
end
1066

UNCOV
1067
function elements.text:_init (kind, attributes, script, text)
×
UNCOV
1068
   elements.terminal._init(self)
×
UNCOV
1069
   if not (kind == "number" or kind == "identifier" or kind == "operator" or kind == "string") then
×
1070
      SU.error("Unknown text node kind '" .. kind .. "'; should be one of: number, identifier, operator, string")
×
1071
   end
UNCOV
1072
   self.kind = kind
×
UNCOV
1073
   self.script = script
×
UNCOV
1074
   self.text = text
×
UNCOV
1075
   if self.script ~= "upright" then
×
UNCOV
1076
      local converted = convertMathVariantScript(self.text, self.script)
×
UNCOV
1077
      self.originalText = self.text
×
UNCOV
1078
      self.text = converted
×
1079
   end
UNCOV
1080
   if self.kind == "operator" then
×
UNCOV
1081
      if self.text == "-" then
×
UNCOV
1082
         self.text = "−"
×
1083
      end
1084
   end
UNCOV
1085
   for attribute, value in pairs(attributes) do
×
UNCOV
1086
      self[attribute] = value
×
1087
   end
1088
end
1089

UNCOV
1090
function elements.text:shape ()
×
UNCOV
1091
   self.font.size = self.font.size * self:getScaleDown()
×
UNCOV
1092
   if isScriptMode(self.mode) then
×
UNCOV
1093
      local scriptFeature = SILE.settings:get("math.font.script.feature")
×
UNCOV
1094
      if scriptFeature then
×
UNCOV
1095
         self.font.features = ("+%s=1"):format(scriptFeature)
×
1096
      end
UNCOV
1097
   elseif isScriptScriptMode(self.mode) then
×
UNCOV
1098
      local scriptFeature = SILE.settings:get("math.font.script.feature")
×
UNCOV
1099
      if scriptFeature then
×
UNCOV
1100
         self.font.features = ("+%s=2"):format(scriptFeature)
×
1101
      end
1102
   end
UNCOV
1103
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
×
UNCOV
1104
   local mathMetrics = self:getMathMetrics()
×
UNCOV
1105
   local glyphs = SILE.shaper:shapeToken(self.text, self.font)
×
1106
   -- Use bigger variants for big operators in display style
UNCOV
1107
   if isDisplayMode(self.mode) and SU.boolean(self.largeop, false) then
×
1108
      -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
UNCOV
1109
      glyphs = pl.tablex.deepcopy(glyphs)
×
UNCOV
1110
      local constructions = mathMetrics.mathVariants.vertGlyphConstructions[glyphs[1].gid]
×
UNCOV
1111
      if constructions then
×
UNCOV
1112
         local displayVariants = constructions.mathGlyphVariantRecord
×
1113
         -- We select the biggest variant. TODO: we should probably select the
1114
         -- first variant that is higher than displayOperatorMinHeight.
1115
         local biggest
UNCOV
1116
         local m = 0
×
UNCOV
1117
         for _, v in ipairs(displayVariants) do
×
UNCOV
1118
            if v.advanceMeasurement > m then
×
UNCOV
1119
               biggest = v
×
UNCOV
1120
               m = v.advanceMeasurement
×
1121
            end
1122
         end
UNCOV
1123
         if biggest then
×
UNCOV
1124
            glyphs[1].gid = biggest.variantGlyph
×
UNCOV
1125
            local dimen = hb.get_glyph_dimensions(face, self.font.size, biggest.variantGlyph)
×
UNCOV
1126
            glyphs[1].width = dimen.width
×
UNCOV
1127
            glyphs[1].glyphAdvance = dimen.glyphAdvance
×
1128
            --[[ I am told (https://github.com/alif-type/xits/issues/90) that,
1129
        in fact, the relative height and depth of display-style big operators
1130
        in the font is not relevant, as these should be centered around the
1131
        axis. So the following code does that, while conserving their
1132
        vertical size (distance from top to bottom). ]]
UNCOV
1133
            local axisHeight = mathMetrics.constants.axisHeight * self:getScaleDown()
×
UNCOV
1134
            local y_size = dimen.height + dimen.depth
×
UNCOV
1135
            glyphs[1].height = y_size / 2 + axisHeight
×
UNCOV
1136
            glyphs[1].depth = y_size / 2 - axisHeight
×
1137
            -- We still need to store the font's height and depth somewhere,
1138
            -- because that's what will be used to draw the glyph, and we will need
1139
            -- to artificially compensate for that.
UNCOV
1140
            glyphs[1].fontHeight = dimen.height
×
UNCOV
1141
            glyphs[1].fontDepth = dimen.depth
×
1142
         end
1143
      end
1144
   end
UNCOV
1145
   SILE.shaper:preAddNodes(glyphs, self.value)
×
UNCOV
1146
   self.value.items = glyphs
×
UNCOV
1147
   self.value.glyphString = {}
×
UNCOV
1148
   if glyphs and #glyphs > 0 then
×
UNCOV
1149
      for i = 1, #glyphs do
×
UNCOV
1150
         table.insert(self.value.glyphString, glyphs[i].gid)
×
1151
      end
UNCOV
1152
      self.width = SILE.types.length(0)
×
UNCOV
1153
      self.widthForSubscript = SILE.types.length(0)
×
UNCOV
1154
      for i = #glyphs, 1, -1 do
×
UNCOV
1155
         self.width = self.width + glyphs[i].glyphAdvance
×
1156
      end
1157
      -- Store width without italic correction somewhere
UNCOV
1158
      self.widthForSubscript = self.width
×
UNCOV
1159
      local itCorr = mathMetrics.italicsCorrection[glyphs[#glyphs].gid]
×
UNCOV
1160
      if itCorr then
×
UNCOV
1161
         self.width = self.width + itCorr * self:getScaleDown()
×
1162
      end
UNCOV
1163
      for i = 1, #glyphs do
×
UNCOV
1164
         self.height = i == 1 and SILE.types.length(glyphs[i].height)
×
UNCOV
1165
            or SILE.types.length(math.max(self.height:tonumber(), glyphs[i].height))
×
UNCOV
1166
         self.depth = i == 1 and SILE.types.length(glyphs[i].depth)
×
UNCOV
1167
            or SILE.types.length(math.max(self.depth:tonumber(), glyphs[i].depth))
×
1168
      end
1169
   else
1170
      self.width = SILE.types.length(0)
×
1171
      self.height = SILE.types.length(0)
×
1172
      self.depth = SILE.types.length(0)
×
1173
   end
1174
end
1175

UNCOV
1176
function elements.text:findClosestVariant (variants, requiredAdvance, currentAdvance)
×
1177
   local closest
1178
   local closestI
UNCOV
1179
   local m = requiredAdvance - currentAdvance
×
UNCOV
1180
   for i, variant in ipairs(variants) do
×
UNCOV
1181
      local diff = math.abs(variant.advanceMeasurement - requiredAdvance)
×
UNCOV
1182
      SU.debug("math", "stretch: diff =", diff)
×
UNCOV
1183
      if diff < m then
×
UNCOV
1184
         closest = variant
×
UNCOV
1185
         closestI = i
×
UNCOV
1186
         m = diff
×
1187
      end
1188
   end
UNCOV
1189
   return closest, closestI
×
1190
end
1191

UNCOV
1192
function elements.text:_reshapeGlyph (glyph, closestVariant, sz)
×
UNCOV
1193
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
×
UNCOV
1194
   local dimen = hb.get_glyph_dimensions(face, sz, closestVariant.variantGlyph)
×
UNCOV
1195
   glyph.gid = closestVariant.variantGlyph
×
1196
   glyph.width, glyph.height, glyph.depth, glyph.glyphAdvance =
×
UNCOV
1197
      dimen.width, dimen.height, dimen.depth, dimen.glyphAdvance
×
UNCOV
1198
   return dimen
×
1199
end
1200

UNCOV
1201
function elements.text:_stretchyReshape (target, direction)
×
1202
   -- direction is the required direction of stretching: true for vertical, false for horizontal
1203
   -- target is the required dimension of the stretched glyph, in font units
UNCOV
1204
   local mathMetrics = self:getMathMetrics()
×
UNCOV
1205
   local upem = mathMetrics.unitsPerEm
×
UNCOV
1206
   local sz = self.font.size
×
UNCOV
1207
   local requiredAdvance = target:tonumber() * upem / sz
×
UNCOV
1208
   SU.debug("math", "stretch: rA =", requiredAdvance)
×
1209
   -- Choose variant of the closest size. The criterion we use is to have
1210
   -- an advance measurement as close as possible as the required one.
1211
   -- The advance measurement is simply the dimension of the glyph.
1212
   -- Therefore, the selected glyph may be smaller or bigger than
1213
   -- required.
1214
   -- TODO: implement assembly of stretchable glyphs from their parts for cases
1215
   -- when the biggest variant is not big enough.
1216
   -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
UNCOV
1217
   local glyphs = pl.tablex.deepcopy(self.value.items)
×
UNCOV
1218
   local glyphConstructions = direction and mathMetrics.mathVariants.vertGlyphConstructions
×
UNCOV
1219
      or mathMetrics.mathVariants.horizGlyphConstructions
×
UNCOV
1220
   local constructions = glyphConstructions[glyphs[1].gid]
×
UNCOV
1221
   if constructions then
×
UNCOV
1222
      local variants = constructions.mathGlyphVariantRecord
×
UNCOV
1223
      SU.debug("math", "stretch: variants =", variants)
×
UNCOV
1224
      local currentAdvance = (direction and (self.depth + self.height):tonumber() or self.width:tonumber()) * upem / sz
×
UNCOV
1225
      local closest, closestI = self:findClosestVariant(variants, requiredAdvance, currentAdvance)
×
UNCOV
1226
      SU.debug("math", "stretch: closestI =", closestI)
×
UNCOV
1227
      if closest then
×
1228
         -- Now we have to re-shape the glyph chain. We will assume there
1229
         -- is only one glyph.
1230
         -- TODO: this code is probably wrong when the vertical
1231
         -- variants have a different width than the original, because
1232
         -- the shaping phase is already done. Need to do better.
UNCOV
1233
         local dimen = self:_reshapeGlyph(glyphs[1], closest, sz)
×
1234
         self.width, self.depth, self.height =
×
UNCOV
1235
            SILE.types.length(dimen.glyphAdvance), SILE.types.length(dimen.depth), SILE.types.length(dimen.height)
×
UNCOV
1236
         SILE.shaper:preAddNodes(glyphs, self.value)
×
UNCOV
1237
         self.value.items = glyphs
×
UNCOV
1238
         self.value.glyphString = { glyphs[1].gid }
×
UNCOV
1239
         return true
×
1240
      end
1241
   end
UNCOV
1242
   return false
×
1243
end
1244

UNCOV
1245
function elements.text:_vertStretchyReshape (depth, height)
×
UNCOV
1246
   local hasStretched = self:_stretchyReshape(depth + height, true)
×
UNCOV
1247
   if hasStretched then
×
1248
      -- RESCALING HACK: see output routine
1249
      -- We only do it if the scaling logic found constructions on the vertical block axis.
1250
      -- It's a dirty hack until we properly implement assembly of glyphs in the case we couldn't
1251
      -- find a big enough variant.
UNCOV
1252
      self.vertExpectedSz = height + depth
×
UNCOV
1253
      self.vertScalingRatio = (depth + height):tonumber() / (self.height:tonumber() + self.depth:tonumber())
×
UNCOV
1254
      self.height = height
×
UNCOV
1255
      self.depth = depth
×
1256
   end
UNCOV
1257
   return hasStretched
×
1258
end
1259

UNCOV
1260
function elements.text:_horizStretchyReshape (width)
×
1261
   local hasStretched = self:_stretchyReshape(width, false)
×
1262
   if not hasStretched and width:tonumber() < self.width:tonumber() then
×
1263
      -- Never shrink glyphs, it looks ugly
1264
      return false
×
1265
   end
1266
   -- But if stretching couldn't be done, it will be ugly anyway, so we will force
1267
   -- a re-scaling of the glyph.
1268
   -- (So it slightly different from the vertical case, 'cause MathML just has one stretchy
1269
   -- attribute, whether for stretching on the vertical (block) or horizontal (inline) axis,
1270
   -- and we cannot know which axis is meant unless we implement yet another mapping table
1271
   -- as the one in the MathML Core appendices. Frankly, how many non-normative appendices
1272
   -- do we need to implement MathML correctly?)
1273
   -- RESCALING HACK: see output routine
1274
   self.horizScalingRatio = width:tonumber() / self.width:tonumber()
×
1275
   self.width = width
×
1276
   return true
×
1277
end
1278

UNCOV
1279
function elements.text:output (x, y, line)
×
UNCOV
1280
   if not self.value.glyphString then
×
1281
      return
×
1282
   end
1283
   local compensatedY
UNCOV
1284
   if isDisplayMode(self.mode) and SU.boolean(self.largeop, false) and self.value.items[1].fontDepth then
×
UNCOV
1285
      compensatedY = SILE.types.length(y.length + self.value.items[1].depth - self.value.items[1].fontDepth)
×
1286
   else
UNCOV
1287
      compensatedY = y
×
1288
   end
UNCOV
1289
   SILE.outputter:setCursor(scaleWidth(x, line), compensatedY.length)
×
UNCOV
1290
   SILE.outputter:setFont(self.font)
×
1291
   -- There should be no stretch or shrink on the width of a text
1292
   -- element.
UNCOV
1293
   local width = self.width.length
×
1294
   -- HACK: For stretchy operators, MathML Core and OpenType define how to build large glyphs
1295
   -- from an assembly of smaller ones. It's fairly complex and idealistic...
1296
   -- Anyhow, we do not have that yet, so we just stretch the glyph artificially.
1297
   -- There are cases where this will not look very good.
1298
   -- Call that a compromise, so that long vectors or large matrices look "decent" without assembly.
UNCOV
1299
   if SILE.outputter.scaleFn and (self.horizScalingRatio or self.vertScalingRatio) then
×
UNCOV
1300
      local xratio = self.horizScalingRatio or 1
×
UNCOV
1301
      local yratio = self.vertScalingRatio or 1
×
UNCOV
1302
      SU.debug("math", "fake glyph stretch: xratio =", xratio, "yratio =", yratio)
×
UNCOV
1303
      SILE.outputter:scaleFn(x, y, xratio, yratio, function ()
×
UNCOV
1304
         SILE.outputter:drawHbox(self.value, width)
×
1305
      end)
1306
   else
UNCOV
1307
      SILE.outputter:drawHbox(self.value, width)
×
1308
   end
1309
end
1310

UNCOV
1311
elements.fraction = pl.class(elements.mbox)
×
UNCOV
1312
elements.fraction._type = "Fraction"
×
1313

UNCOV
1314
function elements.fraction:__tostring ()
×
1315
   return self._type .. "(" .. tostring(self.numerator) .. ", " .. tostring(self.denominator) .. ")"
×
1316
end
1317

UNCOV
1318
function elements.fraction:_init (attributes, numerator, denominator)
×
UNCOV
1319
   elements.mbox._init(self)
×
UNCOV
1320
   self.numerator = numerator
×
UNCOV
1321
   self.denominator = denominator
×
UNCOV
1322
   self.attributes = attributes
×
UNCOV
1323
   table.insert(self.children, numerator)
×
UNCOV
1324
   table.insert(self.children, denominator)
×
1325
end
1326

UNCOV
1327
function elements.fraction:styleChildren ()
×
UNCOV
1328
   self.numerator.mode = getNumeratorMode(self.mode)
×
UNCOV
1329
   self.denominator.mode = getDenominatorMode(self.mode)
×
1330
end
1331

UNCOV
1332
function elements.fraction:shape ()
×
1333
   -- MathML Core 3.3.2: "To avoid visual confusion between the fraction bar
1334
   -- and another adjacent items (e.g. minus sign or another fraction's bar),
1335
   -- a default 1-pixel space is added around the element."
1336
   -- Note that PlainTeX would likely use \nulldelimiterspace (default 1.2pt)
1337
   -- but it would depend on the surrounding context, and might be far too
1338
   -- much in some cases, so we stick to MathML's suggested padding.
UNCOV
1339
   self.padding = SILE.types.length("1px"):absolute()
×
1340

1341
   -- Determine relative abscissas and width
1342
   local widest, other
UNCOV
1343
   if self.denominator.width > self.numerator.width then
×
UNCOV
1344
      widest, other = self.denominator, self.numerator
×
1345
   else
1346
      widest, other = self.numerator, self.denominator
×
1347
   end
UNCOV
1348
   widest.relX = self.padding
×
UNCOV
1349
   other.relX = self.padding + (widest.width - other.width) / 2
×
UNCOV
1350
   self.width = widest.width + 2 * self.padding
×
1351
   -- Determine relative ordinates and height
UNCOV
1352
   local constants = self:getMathMetrics().constants
×
UNCOV
1353
   local scaleDown = self:getScaleDown()
×
UNCOV
1354
   self.axisHeight = constants.axisHeight * scaleDown
×
UNCOV
1355
   self.ruleThickness = self.attributes.linethickness
×
UNCOV
1356
         and SU.cast("measurement", self.attributes.linethickness):tonumber()
×
UNCOV
1357
      or constants.fractionRuleThickness * scaleDown
×
1358

1359
   -- MathML Core 3.3.2.2 ("Fraction with zero line thickness") uses
1360
   -- stack(DisplayStyle)GapMin, stackTop(DisplayStyle)ShiftUp and stackBottom(DisplayStyle)ShiftDown.
1361
   -- TODO not implemented
1362
   -- The most common use cases for zero line thickness are:
1363
   --  - Binomial coefficients
1364
   --  - Stacked subscript/superscript on big operators such as sums.
1365

1366
   local numeratorGapMin, denominatorGapMin, numeratorShiftUp, denominatorShiftDown
UNCOV
1367
   if isDisplayMode(self.mode) then
×
UNCOV
1368
      numeratorGapMin = constants.fractionNumDisplayStyleGapMin * scaleDown
×
UNCOV
1369
      denominatorGapMin = constants.fractionDenomDisplayStyleGapMin * scaleDown
×
UNCOV
1370
      numeratorShiftUp = constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
×
UNCOV
1371
      denominatorShiftDown = constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
×
1372
   else
1373
      numeratorGapMin = constants.fractionNumeratorGapMin * scaleDown
×
1374
      denominatorGapMin = constants.fractionDenominatorGapMin * scaleDown
×
1375
      numeratorShiftUp = constants.fractionNumeratorShiftUp * scaleDown
×
1376
      denominatorShiftDown = constants.fractionDenominatorShiftDown * scaleDown
×
1377
   end
1378

UNCOV
1379
   self.numerator.relY = -self.axisHeight
×
UNCOV
1380
      - self.ruleThickness / 2
×
UNCOV
1381
      - SILE.types.length(
×
UNCOV
1382
         math.max(
×
UNCOV
1383
            (numeratorGapMin + self.numerator.depth):tonumber(),
×
UNCOV
1384
            numeratorShiftUp - self.axisHeight - self.ruleThickness / 2
×
1385
         )
1386
      )
UNCOV
1387
   self.denominator.relY = -self.axisHeight
×
UNCOV
1388
      + self.ruleThickness / 2
×
UNCOV
1389
      + SILE.types.length(
×
UNCOV
1390
         math.max(
×
UNCOV
1391
            (denominatorGapMin + self.denominator.height):tonumber(),
×
UNCOV
1392
            denominatorShiftDown + self.axisHeight - self.ruleThickness / 2
×
1393
         )
1394
      )
UNCOV
1395
   self.height = self.numerator.height - self.numerator.relY
×
UNCOV
1396
   self.depth = self.denominator.relY + self.denominator.depth
×
1397
end
1398

UNCOV
1399
function elements.fraction:output (x, y, line)
×
UNCOV
1400
   if self.ruleThickness > 0 then
×
UNCOV
1401
      SILE.outputter:drawRule(
×
UNCOV
1402
         scaleWidth(x + self.padding, line),
×
UNCOV
1403
         y.length - self.axisHeight - self.ruleThickness / 2,
×
UNCOV
1404
         scaleWidth(self.width - 2 * self.padding, line),
×
1405
         self.ruleThickness
1406
      )
1407
   end
1408
end
1409

1410
local function newSubscript (spec)
UNCOV
1411
   return elements.subscript(spec.base, spec.sub, spec.sup)
×
1412
end
1413

1414
local function newUnderOver (spec)
UNCOV
1415
   return elements.underOver(spec.attributes, spec.base, spec.sub, spec.sup)
×
1416
end
1417

1418
-- TODO replace with penlight equivalent
1419
local function mapList (f, l)
UNCOV
1420
   local ret = {}
×
UNCOV
1421
   for i, x in ipairs(l) do
×
UNCOV
1422
      ret[i] = f(i, x)
×
1423
   end
UNCOV
1424
   return ret
×
1425
end
1426

UNCOV
1427
elements.mtr = pl.class(elements.mbox)
×
1428
-- elements.mtr._type = "" -- TODO why not set?
1429

UNCOV
1430
function elements.mtr:_init (children)
×
1431
   self.children = children
×
1432
end
1433

UNCOV
1434
function elements.mtr:styleChildren ()
×
1435
   for _, c in ipairs(self.children) do
×
1436
      c.mode = self.mode
×
1437
   end
1438
end
1439

UNCOV
1440
function elements.mtr:shape () end -- done by parent table
×
1441

UNCOV
1442
function elements.mtr:output () end
×
1443

UNCOV
1444
elements.table = pl.class(elements.mbox)
×
UNCOV
1445
elements.table._type = "table" -- TODO why case difference?
×
1446

UNCOV
1447
function elements.table:_init (children, options)
×
UNCOV
1448
   elements.mbox._init(self)
×
UNCOV
1449
   self.children = children
×
UNCOV
1450
   self.options = options
×
UNCOV
1451
   self.nrows = #self.children
×
UNCOV
1452
   self.ncols = math.max(pl.utils.unpack(mapList(function (_, row)
×
UNCOV
1453
      return #row.children
×
UNCOV
1454
   end, self.children)))
×
UNCOV
1455
   SU.debug("math", "self.ncols =", self.ncols)
×
UNCOV
1456
   local spacing = SILE.settings:get("math.font.size") * 0.6 -- arbitrary ratio of the current math font size
×
UNCOV
1457
   self.rowspacing = self.options.rowspacing and SILE.types.length(self.options.rowspacing) or spacing
×
UNCOV
1458
   self.columnspacing = self.options.columnspacing and SILE.types.length(self.options.columnspacing) or spacing
×
1459
   -- Pad rows that do not have enough cells by adding cells to the
1460
   -- right.
UNCOV
1461
   for i, row in ipairs(self.children) do
×
UNCOV
1462
      for j = 1, (self.ncols - #row.children) do
×
1463
         SU.debug("math", "padding i =", i, "j =", j)
×
1464
         table.insert(row.children, elements.stackbox("H", {}))
×
1465
         SU.debug("math", "size", #row.children)
×
1466
      end
1467
   end
UNCOV
1468
   if options.columnalign then
×
UNCOV
1469
      local l = {}
×
UNCOV
1470
      for w in string.gmatch(options.columnalign, "[^%s]+") do
×
UNCOV
1471
         if not (w == "left" or w == "center" or w == "right") then
×
1472
            SU.error("Invalid specifier in `columnalign` attribute: " .. w)
×
1473
         end
UNCOV
1474
         table.insert(l, w)
×
1475
      end
1476
      -- Pad with last value of l if necessary
UNCOV
1477
      for _ = 1, (self.ncols - #l), 1 do
×
1478
         table.insert(l, l[#l])
×
1479
      end
1480
      -- On the contrary, remove excess values in l if necessary
UNCOV
1481
      for _ = 1, (#l - self.ncols), 1 do
×
1482
         table.remove(l)
×
1483
      end
UNCOV
1484
      self.options.columnalign = l
×
1485
   else
UNCOV
1486
      self.options.columnalign = pl.List.range(1, self.ncols):map(function (_)
×
UNCOV
1487
         return "center"
×
1488
      end)
1489
   end
1490
end
1491

UNCOV
1492
function elements.table:styleChildren ()
×
UNCOV
1493
   if self.mode == mathMode.display and self.options.displaystyle ~= "false" then
×
UNCOV
1494
      for _, c in ipairs(self.children) do
×
UNCOV
1495
         c.mode = mathMode.display
×
1496
      end
1497
   else
UNCOV
1498
      for _, c in ipairs(self.children) do
×
UNCOV
1499
         c.mode = mathMode.text
×
1500
      end
1501
   end
1502
end
1503

UNCOV
1504
function elements.table:shape ()
×
1505
   -- Determine the height (resp. depth) of each row, which is the max
1506
   -- height (resp. depth) among its elements. Then we only need to add it to
1507
   -- the table's height and center every cell vertically.
UNCOV
1508
   for _, row in ipairs(self.children) do
×
UNCOV
1509
      row.height = SILE.types.length(0)
×
UNCOV
1510
      row.depth = SILE.types.length(0)
×
UNCOV
1511
      for _, cell in ipairs(row.children) do
×
UNCOV
1512
         row.height = maxLength(row.height, cell.height)
×
UNCOV
1513
         row.depth = maxLength(row.depth, cell.depth)
×
1514
      end
1515
   end
UNCOV
1516
   self.vertSize = SILE.types.length(0)
×
UNCOV
1517
   for i, row in ipairs(self.children) do
×
1518
      self.vertSize = self.vertSize
×
UNCOV
1519
         + row.height
×
UNCOV
1520
         + row.depth
×
UNCOV
1521
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
×
1522
   end
UNCOV
1523
   local rowHeightSoFar = SILE.types.length(0)
×
UNCOV
1524
   for i, row in ipairs(self.children) do
×
UNCOV
1525
      row.relY = rowHeightSoFar + row.height - self.vertSize
×
1526
      rowHeightSoFar = rowHeightSoFar
×
UNCOV
1527
         + row.height
×
UNCOV
1528
         + row.depth
×
UNCOV
1529
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
×
1530
   end
UNCOV
1531
   self.width = SILE.types.length(0)
×
UNCOV
1532
   local thisColRelX = SILE.types.length(0)
×
1533
   -- For every column...
UNCOV
1534
   for i = 1, self.ncols do
×
1535
      -- Determine its width
UNCOV
1536
      local columnWidth = SILE.types.length(0)
×
UNCOV
1537
      for j = 1, self.nrows do
×
UNCOV
1538
         if self.children[j].children[i].width > columnWidth then
×
UNCOV
1539
            columnWidth = self.children[j].children[i].width
×
1540
         end
1541
      end
1542
      -- Use it to align the contents of every cell as required.
UNCOV
1543
      for j = 1, self.nrows do
×
UNCOV
1544
         local cell = self.children[j].children[i]
×
UNCOV
1545
         if self.options.columnalign[i] == "left" then
×
UNCOV
1546
            cell.relX = thisColRelX
×
UNCOV
1547
         elseif self.options.columnalign[i] == "center" then
×
UNCOV
1548
            cell.relX = thisColRelX + (columnWidth - cell.width) / 2
×
UNCOV
1549
         elseif self.options.columnalign[i] == "right" then
×
UNCOV
1550
            cell.relX = thisColRelX + (columnWidth - cell.width)
×
1551
         else
1552
            SU.error("invalid columnalign parameter")
×
1553
         end
1554
      end
UNCOV
1555
      thisColRelX = thisColRelX + columnWidth + (i == self.ncols and SILE.types.length(0) or self.columnspacing) -- Spacing
×
1556
   end
UNCOV
1557
   self.width = thisColRelX
×
1558
   -- Center myself vertically around the axis, and update relative Ys of rows accordingly
UNCOV
1559
   local axisHeight = self:getMathMetrics().constants.axisHeight * self:getScaleDown()
×
UNCOV
1560
   self.height = self.vertSize / 2 + axisHeight
×
UNCOV
1561
   self.depth = self.vertSize / 2 - axisHeight
×
UNCOV
1562
   for _, row in ipairs(self.children) do
×
UNCOV
1563
      row.relY = row.relY + self.vertSize / 2 - axisHeight
×
1564
      -- Also adjust width
UNCOV
1565
      row.width = self.width
×
1566
   end
1567
end
1568

UNCOV
1569
function elements.table:output () end
×
1570

1571
local function getRadicandMode (mode)
1572
   -- Not too sure if we should do something special/
1573
   return mode
×
1574
end
1575

1576
local function getDegreeMode (mode)
1577
   -- 2 levels smaller, up to scriptScript evntually.
1578
   -- Not too sure if we should do something else.
1579
   if mode == mathMode.display then
×
1580
      return mathMode.scriptScript
×
1581
   elseif mode == mathMode.displayCramped then
×
1582
      return mathMode.scriptScriptCramped
×
1583
   elseif mode == mathMode.text or mode == mathMode.script or mode == mathMode.scriptScript then
×
1584
      return mathMode.scriptScript
×
1585
   end
1586
   return mathMode.scriptScriptCramped
×
1587
end
1588

UNCOV
1589
elements.sqrt = pl.class(elements.mbox)
×
UNCOV
1590
elements.sqrt._type = "Sqrt"
×
1591

UNCOV
1592
function elements.sqrt:__tostring ()
×
1593
   return self._type .. "(" .. tostring(self.radicand) .. (self.degree and ", " .. tostring(self.degree) or "") .. ")"
×
1594
end
1595

UNCOV
1596
function elements.sqrt:_init (radicand, degree)
×
1597
   elements.mbox._init(self)
×
1598
   self.radicand = radicand
×
1599
   if degree then
×
1600
      self.degree = degree
×
1601
      table.insert(self.children, degree)
×
1602
   end
1603
   table.insert(self.children, radicand)
×
1604
   self.relX = SILE.types.length()
×
1605
   self.relY = SILE.types.length()
×
1606
end
1607

UNCOV
1608
function elements.sqrt:styleChildren ()
×
1609
   self.radicand.mode = getRadicandMode(self.mode)
×
1610
   if self.degree then
×
1611
      self.degree.mode = getDegreeMode(self.mode)
×
1612
   end
1613
end
1614

UNCOV
1615
function elements.sqrt:shape ()
×
1616
   local mathMetrics = self:getMathMetrics()
×
1617
   local scaleDown = self:getScaleDown()
×
1618
   local constants = mathMetrics.constants
×
1619

1620
   self.radicalRuleThickness = constants.radicalRuleThickness * scaleDown
×
1621
   if self.mode == mathMode.display or self.mode == mathMode.displayCramped then
×
1622
      self.radicalVerticalGap = constants.radicalDisplayStyleVerticalGap * scaleDown
×
1623
   else
1624
      self.radicalVerticalGap = constants.radicalVerticalGap * scaleDown
×
1625
   end
1626
   self.extraAscender = constants.radicalExtraAscender * scaleDown
×
1627

1628
   -- HACK: We draw own own radical sign in the output() method.
1629
   -- Derive dimensions for the radical sign (more or less ad hoc).
1630
   -- Note: In TeX, the radical sign extends a lot below the baseline,
1631
   -- and MathML Core also has a lot of layout text about it.
1632
   -- Not only it doesn't look good, but it's not very clear vs. OpenType.
1633
   local radicalGlyph, found = SILE.shaper:measureChar("√")
×
1634
   if not found then
×
1635
      SU.error("Math font does not contain a square root glyph")
×
1636
   end
1637
   local ratio = (self.radicand.height:tonumber() + self.radicand.depth:tonumber())
×
1638
      / (radicalGlyph.height + radicalGlyph.depth)
×
1639
   local vertAdHocOffset = (ratio > 1 and math.log(ratio) or 0) * self.radicalVerticalGap
×
1640
   self.symbolHeight = SILE.types.length(radicalGlyph.height) * scaleDown
×
1641
   self.symbolDepth = (SILE.types.length(radicalGlyph.depth) + vertAdHocOffset) * scaleDown
×
1642
   self.symbolWidth = (SILE.types.length(radicalGlyph.width) + vertAdHocOffset) * scaleDown
×
1643

1644
   -- Adjust the height of the radical sign if the radicand is higher
1645
   self.symbolHeight = self.radicand.height > self.symbolHeight and self.radicand.height or self.symbolHeight
×
1646
   -- Compute the (max-)height of the short leg of the radical sign
1647
   self.symbolShortHeight = self.symbolHeight * constants.radicalDegreeBottomRaisePercent
×
1648

1649
   self.offsetX = SILE.types.length()
×
1650
   if self.degree then
×
1651
      -- Position the degree
1652
      self.degree.relY = -constants.radicalDegreeBottomRaisePercent * self.symbolHeight
×
1653
      -- Adjust the height of the short leg of the radical sign to ensure the degree is not too close
1654
      -- (empirically use radicalExtraAscender)
1655
      self.symbolShortHeight = self.symbolShortHeight - constants.radicalExtraAscender * scaleDown
×
1656
      -- Compute the width adjustment for the degree
1657
      self.offsetX = self.degree.width
×
1658
         + constants.radicalKernBeforeDegree * scaleDown
×
1659
         + constants.radicalKernAfterDegree * scaleDown
×
1660
   end
1661
   -- Position the radicand
1662
   self.radicand.relX = self.symbolWidth + self.offsetX
×
1663
   -- Compute the dimensions of the whole radical
1664
   self.width = self.radicand.width + self.symbolWidth + self.offsetX
×
1665
   self.height = self.symbolHeight + self.radicalVerticalGap + self.extraAscender
×
1666
   self.depth = self.radicand.depth
×
1667
end
1668

1669
local function _r (number)
1670
   -- Lua 5.3+ formats floats as 1.0 and integers as 1
1671
   -- Also some PDF readers do not like double precision.
1672
   return math.floor(number) == number and math.floor(number) or tonumber(string.format("%.5f", number))
×
1673
end
1674

UNCOV
1675
function elements.sqrt:output (x, y, line)
×
1676
   -- HACK:
1677
   -- OpenType might say we need to assemble the radical sign from parts.
1678
   -- Frankly, it's much easier to just draw it as a graphic :-)
1679
   -- Hence, here we use a PDF graphic operators to draw a nice radical sign.
1680
   -- Some values here are ad hoc, but they look good.
1681
   local h = self.height:tonumber()
×
1682
   local d = self.depth:tonumber()
×
1683
   local s0 = scaleWidth(self.offsetX, line):tonumber()
×
1684
   local sw = scaleWidth(self.symbolWidth, line):tonumber()
×
1685
   local dsh = h - self.symbolShortHeight:tonumber()
×
1686
   local dsd = self.symbolDepth:tonumber()
×
1687
   local symbol = {
×
1688
      _r(self.radicalRuleThickness),
×
1689
      "w", -- line width
1690
      1,
1691
      "j", -- round line joins
1692
      _r(sw + s0),
×
1693
      _r(self.extraAscender),
×
1694
      "m",
1695
      _r(s0 + sw * 0.90),
×
1696
      _r(self.extraAscender),
×
1697
      "l",
1698
      _r(s0 + sw * 0.4),
×
1699
      _r(h + d + dsd),
×
1700
      "l",
1701
      _r(s0 + sw * 0.2),
×
1702
      _r(dsh),
×
1703
      "l",
1704
      s0 + sw * 0.1,
×
1705
      _r(dsh + 0.5),
×
1706
      "l",
1707
      "S",
1708
   }
1709
   local svg = table.concat(symbol, " ")
×
1710
   local xscaled = scaleWidth(x, line)
×
1711
   SILE.outputter:drawSVG(svg, xscaled, y, sw, h, 1)
×
1712
   -- And now we just need to draw the bar over the radicand
1713
   SILE.outputter:drawRule(
×
1714
      s0 + self.symbolWidth + xscaled,
×
1715
      y.length - self.height + self.extraAscender - self.radicalRuleThickness / 2,
×
1716
      scaleWidth(self.radicand.width, line),
×
1717
      self.radicalRuleThickness
1718
   )
1719
end
1720

UNCOV
1721
elements.padded = pl.class(elements.mbox)
×
UNCOV
1722
elements.padded._type = "Padded"
×
1723

UNCOV
1724
function elements.padded:__tostring ()
×
1725
   return self._type .. "(" .. tostring(self.impadded) .. ")"
×
1726
end
1727

UNCOV
1728
function elements.padded:_init (attributes, impadded)
×
1729
   elements.mbox._init(self)
×
1730
   self.impadded = impadded
×
1731
   self.attributes = attributes or {}
×
1732
   table.insert(self.children, impadded)
×
1733
end
1734

UNCOV
1735
function elements.padded:styleChildren ()
×
1736
   self.impadded.mode = self.mode
×
1737
end
1738

UNCOV
1739
function elements.padded:shape ()
×
1740
   -- TODO MathML allows percentages font-relative units (em, ex) for padding
1741
   -- But our units work with font.size, not math.font.size (possibly adjusted by scaleDown)
1742
   -- so the expectations might not be met.
1743
   local width = self.attributes.width and SU.cast("measurement", self.attributes.width)
×
1744
   local height = self.attributes.height and SU.cast("measurement", self.attributes.height)
×
1745
   local depth = self.attributes.depth and SU.cast("measurement", self.attributes.depth)
×
1746
   local lspace = self.attributes.lspace and SU.cast("measurement", self.attributes.lspace)
×
1747
   local voffset = self.attributes.voffset and SU.cast("measurement", self.attributes.voffset)
×
1748
   -- Clamping for width, height, depth, lspace
1749
   width = width and (width:tonumber() > 0 and width or SILE.types.measurement())
×
1750
   height = height and (height:tonumber() > 0 and height or SILE.types.measurement())
×
1751
   depth = depth and (depth:tonumber() > 0 and depth or SILE.types.measurement())
×
1752
   lspace = lspace and (lspace:tonumber() > 0 and lspace or SILE.types.measurement())
×
1753
   -- No clamping for voffset
1754
   voffset = voffset or SILE.types.measurement(0)
×
1755
   -- Compute the dimensions
1756
   self.width = width and SILE.types.length(width) or self.impadded.width
×
1757
   self.height = height and SILE.types.length(height) or self.impadded.height
×
1758
   self.depth = depth and SILE.types.length(depth) or self.impadded.depth
×
1759
   self.impadded.relX = lspace and SILE.types.length(lspace) or SILE.types.length()
×
1760
   self.impadded.relY = voffset and SILE.types.length(voffset):negate() or SILE.types.length()
×
1761
end
1762

UNCOV
1763
function elements.padded:output (_, _, _) end
×
1764

1765
-- Bevelled fractions are not part of MathML Core, and MathML4 does not
1766
-- exactly specify how to compute the layout.
UNCOV
1767
elements.bevelledFraction = pl.class(elements.fraction) -- Inherit from fraction
×
UNCOV
1768
elements.fraction._type = "BevelledFraction"
×
1769

UNCOV
1770
function elements.bevelledFraction:shape ()
×
1771
   local constants = self:getMathMetrics().constants
×
1772
   local scaleDown = self:getScaleDown()
×
1773
   local hSkew = constants.skewedFractionHorizontalGap * scaleDown
×
1774
   -- OpenType has properties which are not totally explicit.
1775
   -- The definition of skewedFractionVerticalGap (and its value in fonts
1776
   -- such as Libertinus Math) seems to imply that it is measured from the
1777
   -- bottom of the numerator to the top of the denominator.
1778
   -- This does not seem to be a nice general layout.
1779
   -- So we will use superscriptShiftUp(Cramped) for the numerator:
1780
   local vSkewUp = isCrampedMode(self.mode) and constants.superscriptShiftUpCramped * scaleDown
×
1781
      or constants.superscriptShiftUp * scaleDown
×
1782
   -- And all good books say that the denominator should not be shifted down:
1783
   local vSkewDown = 0
×
1784

1785
   self.ruleThickness = self.attributes.linethickness
×
1786
         and SU.cast("measurement", self.attributes.linethickness):tonumber()
×
1787
      or constants.fractionRuleThickness * scaleDown
×
1788
   self.numerator.relX = SILE.types.length(0)
×
1789
   self.numerator.relY = SILE.types.length(-vSkewUp)
×
1790
   self.denominator.relX = self.numerator.width + hSkew
×
1791
   self.denominator.relY = SILE.types.length(vSkewDown)
×
1792
   self.width = self.numerator.width + self.denominator.width + hSkew
×
1793
   self.height = maxLength(self.numerator.height + vSkewUp, self.denominator.height - vSkewDown)
×
1794
   self.depth = maxLength(self.numerator.depth - vSkewUp, self.denominator.depth + vSkewDown)
×
1795
   self.barWidth = SILE.types.length(hSkew)
×
1796
   self.barX = self.numerator.relX + self.numerator.width
×
1797
end
1798

UNCOV
1799
function elements.bevelledFraction:output (x, y, line)
×
1800
   local h = self.height:tonumber()
×
1801
   local d = self.depth:tonumber()
×
1802
   local barwidth = scaleWidth(self.barWidth, line):tonumber()
×
1803
   local xscaled = scaleWidth(x + self.barX, line)
×
1804
   local rd = self.ruleThickness / 2
×
1805
   local symbol = {
×
1806
      _r(self.ruleThickness),
×
1807
      "w", -- line width
1808
      1,
1809
      "J", -- round line caps
1810
      _r(0),
×
1811
      _r(d + h - rd),
×
1812
      "m",
1813
      _r(barwidth),
×
1814
      _r(rd),
×
1815
      "l",
1816
      "S",
1817
   }
1818
   local svg = table.concat(symbol, " ")
×
1819
   SILE.outputter:drawSVG(svg, xscaled, y, barwidth, h, 1)
×
1820
end
1821

UNCOV
1822
elements.mathMode = mathMode
×
UNCOV
1823
elements.newSubscript = newSubscript
×
UNCOV
1824
elements.newUnderOver = newUnderOver
×
1825

UNCOV
1826
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

© 2025 Coveralls, Inc