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

sile-typesetter / sile / 12179837841

05 Dec 2024 12:34PM UTC coverage: 32.196% (-11.9%) from 44.129%
12179837841

push

github

web-flow
Merge pull request #2187 from Omikhleia/math-accent-redo

feat(math): Minimal support for accents in MathML and TeX-like commands

0 of 119 new or added lines in 5 files covered. (0.0%)

2141 existing lines in 49 files now uncovered.

6350 of 19723 relevant lines covered (32.2%)

4219.1 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
UNCOV
94
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
×
UNCOV
95
      return mathMode.scriptScript
×
96
   -- S', SS' -> SS'
97
   else
UNCOV
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'
UNCOV
122
   elseif mode == mathMode.displayCramped then
×
123
      return mathMode.textCramped
×
124
   -- T -> S
UNCOV
125
   elseif mode == mathMode.text then
×
126
      return mathMode.script
×
127
   -- T' -> S'
UNCOV
128
   elseif mode == mathMode.textCramped then
×
UNCOV
129
      return mathMode.scriptCramped
×
130
   -- S, SS -> SS
UNCOV
131
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
×
132
      return mathMode.scriptScript
×
133
   -- S', SS' -> SS'
134
   else
UNCOV
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'
UNCOV
143
   elseif mode == mathMode.text or mode == mathMode.textCramped then
×
UNCOV
144
      return mathMode.scriptCramped
×
145
   -- S, SS, S', SS' -> SS'
146
   else
UNCOV
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
   }
UNCOV
218
   local filename = SILE.settings:get("math.font.filename")
×
UNCOV
219
   if filename and filename ~= "" then
×
220
      font.filename = filename
×
221
   end
UNCOV
222
   self.font = SILE.font.loadDefaults(font)
×
223
end
224

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

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

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

UNCOV
237
function elements.mbox:getMathMetrics ()
×
UNCOV
238
   return retrieveMathTable(self.font)
×
239
end
240

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
547
function elements.stackbox.output (_, _, _, _) end
×
548

UNCOV
549
elements.phantom = pl.class(elements.stackbox) -- inherit from stackbox
×
UNCOV
550
elements.phantom._type = "Phantom"
×
551

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

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

UNCOV
570
elements.subscript = pl.class(elements.mbox)
×
UNCOV
571
elements.subscript._type = "Subscript"
×
572

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

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

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

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

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

UNCOV
716
function elements.subscript.output (_, _, _, _) end
×
717

UNCOV
718
elements.underOver = pl.class(elements.subscript)
×
UNCOV
719
elements.underOver._type = "UnderOver"
×
720

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

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

735
local function getAccentMode (mode)
736
   -- Size unchanged but leave display mode
737
   -- See MathML Core §3.4.3
NEW
738
   if mode == mathMode.display then
×
NEW
739
      return mathMode.text
×
740
   end
NEW
741
   if mode == mathMode.displayCramped then
×
NEW
742
      return mathMode.textCramped
×
743
   end
NEW
744
   return mode
×
745
end
746

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

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

UNCOV
784
function elements.underOver:styleChildren ()
×
UNCOV
785
   if self.base then
×
UNCOV
786
      self.base.mode = self.mode
×
787
   end
UNCOV
788
   if self.sub then
×
NEW
789
      self.sub.mode = self.attributes.accentunder and getAccentMode(self.mode) or getSubscriptMode(self.mode)
×
790
   end
UNCOV
791
   if self.sup then
×
NEW
792
      self.sup.mode = self.attributes.accent and getAccentMode(self.mode) or getSuperscriptMode(self.mode)
×
793
   end
794
end
795

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

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

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

UNCOV
978
function elements.underOver.output (_, _, _, _) end
×
979

980
-- terminal is the base class for leaf node
UNCOV
981
elements.terminal = pl.class(elements.mbox)
×
UNCOV
982
elements.terminal._type = "Terminal"
×
983

UNCOV
984
function elements.terminal:_init ()
×
UNCOV
985
   elements.mbox._init(self)
×
986
end
987

UNCOV
988
function elements.terminal.styleChildren (_) end
×
989

UNCOV
990
function elements.terminal.shape (_) end
×
991

UNCOV
992
elements.space = pl.class(elements.terminal)
×
UNCOV
993
elements.space._type = "Space"
×
994

UNCOV
995
function elements.space:_init ()
×
996
   elements.terminal._init(self)
×
997
end
998

UNCOV
999
function elements.space:__tostring ()
×
1000
   return self._type
×
1001
      .. "(width="
×
1002
      .. tostring(self.width)
×
1003
      .. ", height="
×
1004
      .. tostring(self.height)
×
1005
      .. ", depth="
×
1006
      .. tostring(self.depth)
×
1007
      .. ")"
×
1008
end
1009

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

UNCOV
1028
function elements.space:_init (width, height, depth)
×
UNCOV
1029
   elements.terminal._init(self)
×
UNCOV
1030
   self.width = getStandardLength(width)
×
UNCOV
1031
   self.height = getStandardLength(height)
×
UNCOV
1032
   self.depth = getStandardLength(depth)
×
1033
end
1034

UNCOV
1035
function elements.space:shape ()
×
UNCOV
1036
   self.width = self.width:absolute() * self:getScaleDown()
×
UNCOV
1037
   self.height = self.height:absolute() * self:getScaleDown()
×
UNCOV
1038
   self.depth = self.depth:absolute() * self:getScaleDown()
×
1039
end
1040

UNCOV
1041
function elements.space.output (_) end
×
1042

1043
-- text node. For any actual text output
UNCOV
1044
elements.text = pl.class(elements.terminal)
×
UNCOV
1045
elements.text._type = "Text"
×
1046

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

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

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

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

UNCOV
1187
function elements.text:_reshapeGlyph (glyph, closestVariant, sz)
×
UNCOV
1188
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
×
UNCOV
1189
   local dimen = hb.get_glyph_dimensions(face, sz, closestVariant.variantGlyph)
×
UNCOV
1190
   glyph.gid = closestVariant.variantGlyph
×
1191
   glyph.width, glyph.height, glyph.depth, glyph.glyphAdvance =
×
UNCOV
1192
      dimen.width, dimen.height, dimen.depth, dimen.glyphAdvance
×
UNCOV
1193
   return dimen
×
1194
end
1195

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

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

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

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

UNCOV
1306
elements.fraction = pl.class(elements.mbox)
×
UNCOV
1307
elements.fraction._type = "Fraction"
×
1308

UNCOV
1309
function elements.fraction:__tostring ()
×
1310
   return self._type .. "(" .. tostring(self.numerator) .. ", " .. tostring(self.denominator) .. ")"
×
1311
end
1312

UNCOV
1313
function elements.fraction:_init (attributes, numerator, denominator)
×
UNCOV
1314
   elements.mbox._init(self)
×
UNCOV
1315
   self.numerator = numerator
×
UNCOV
1316
   self.denominator = denominator
×
UNCOV
1317
   self.attributes = attributes
×
UNCOV
1318
   table.insert(self.children, numerator)
×
UNCOV
1319
   table.insert(self.children, denominator)
×
1320
end
1321

UNCOV
1322
function elements.fraction:styleChildren ()
×
UNCOV
1323
   self.numerator.mode = getNumeratorMode(self.mode)
×
UNCOV
1324
   self.denominator.mode = getDenominatorMode(self.mode)
×
1325
end
1326

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

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

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

1361
   local numeratorGapMin, denominatorGapMin, numeratorShiftUp, denominatorShiftDown
UNCOV
1362
   if isDisplayMode(self.mode) then
×
UNCOV
1363
      numeratorGapMin = constants.fractionNumDisplayStyleGapMin * scaleDown
×
UNCOV
1364
      denominatorGapMin = constants.fractionDenomDisplayStyleGapMin * scaleDown
×
UNCOV
1365
      numeratorShiftUp = constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
×
UNCOV
1366
      denominatorShiftDown = constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
×
1367
   else
UNCOV
1368
      numeratorGapMin = constants.fractionNumeratorGapMin * scaleDown
×
UNCOV
1369
      denominatorGapMin = constants.fractionDenominatorGapMin * scaleDown
×
UNCOV
1370
      numeratorShiftUp = constants.fractionNumeratorShiftUp * scaleDown
×
UNCOV
1371
      denominatorShiftDown = constants.fractionDenominatorShiftDown * scaleDown
×
1372
   end
1373

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

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

1405
local function newSubscript (spec)
UNCOV
1406
   return elements.subscript(spec.base, spec.sub, spec.sup)
×
1407
end
1408

1409
local function newUnderOver (spec)
NEW
1410
   return elements.underOver(spec.attributes, spec.base, spec.sub, spec.sup)
×
1411
end
1412

1413
-- TODO replace with penlight equivalent
1414
local function mapList (f, l)
UNCOV
1415
   local ret = {}
×
UNCOV
1416
   for i, x in ipairs(l) do
×
UNCOV
1417
      ret[i] = f(i, x)
×
1418
   end
UNCOV
1419
   return ret
×
1420
end
1421

UNCOV
1422
elements.mtr = pl.class(elements.mbox)
×
1423
-- elements.mtr._type = "" -- TODO why not set?
1424

UNCOV
1425
function elements.mtr:_init (children)
×
UNCOV
1426
   self.children = children
×
1427
end
1428

UNCOV
1429
function elements.mtr:styleChildren ()
×
UNCOV
1430
   for _, c in ipairs(self.children) do
×
UNCOV
1431
      c.mode = self.mode
×
1432
   end
1433
end
1434

UNCOV
1435
function elements.mtr.shape (_) end -- done by parent table
×
1436

UNCOV
1437
function elements.mtr.output (_) end
×
1438

UNCOV
1439
elements.table = pl.class(elements.mbox)
×
UNCOV
1440
elements.table._type = "table" -- TODO why case difference?
×
1441

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

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

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

UNCOV
1564
function elements.table.output (_) end
×
1565

1566
local function getRadicandMode (mode)
1567
   -- Not too sure if we should do something special/
1568
   return mode
×
1569
end
1570

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

UNCOV
1584
elements.sqrt = pl.class(elements.mbox)
×
UNCOV
1585
elements.sqrt._type = "Sqrt"
×
1586

UNCOV
1587
function elements.sqrt:__tostring ()
×
1588
   return self._type .. "(" .. tostring(self.radicand) .. (self.degree and ", " .. tostring(self.degree) or "") .. ")"
×
1589
end
1590

UNCOV
1591
function elements.sqrt:_init (radicand, degree)
×
1592
   elements.mbox._init(self)
×
1593
   self.radicand = radicand
×
1594
   if degree then
×
1595
      self.degree = degree
×
1596
      table.insert(self.children, degree)
×
1597
   end
1598
   table.insert(self.children, radicand)
×
1599
   self.relX = SILE.types.length()
×
1600
   self.relY = SILE.types.length()
×
1601
end
1602

1603
function elements.sqrt:styleChildren ()
×
1604
   self.radicand.mode = getRadicandMode(self.mode)
×
1605
   if self.degree then
×
1606
      self.degree.mode = getDegreeMode(self.mode)
×
1607
   end
1608
end
1609

1610
function elements.sqrt:shape ()
×
1611
   local mathMetrics = self:getMathMetrics()
×
1612
   local scaleDown = self:getScaleDown()
×
1613
   local constants = mathMetrics.constants
×
1614

1615
   self.radicalRuleThickness = constants.radicalRuleThickness * scaleDown
×
1616
   if self.mode == mathMode.display or self.mode == mathMode.displayCramped then
×
1617
      self.radicalVerticalGap = constants.radicalDisplayStyleVerticalGap * scaleDown
×
1618
   else
1619
      self.radicalVerticalGap = constants.radicalVerticalGap * scaleDown
×
1620
   end
1621
   self.extraAscender = constants.radicalExtraAscender * scaleDown
×
1622

1623
   -- HACK: We draw own own radical sign in the output() method.
1624
   -- Derive dimensions for the radical sign (more or less ad hoc).
1625
   -- Note: In TeX, the radical sign extends a lot below the baseline,
1626
   -- and MathML Core also has a lot of layout text about it.
1627
   -- Not only it doesn't look good, but it's not very clear vs. OpenType.
1628
   local radicalGlyph = SILE.shaper:measureChar("√")
×
1629
   local ratio = (self.radicand.height:tonumber() + self.radicand.depth:tonumber())
×
1630
      / (radicalGlyph.height + radicalGlyph.depth)
×
1631
   local vertAdHocOffset = (ratio > 1 and math.log(ratio) or 0) * self.radicalVerticalGap
×
1632
   self.symbolHeight = SILE.types.length(radicalGlyph.height) * scaleDown
×
1633
   self.symbolDepth = (SILE.types.length(radicalGlyph.depth) + vertAdHocOffset) * scaleDown
×
1634
   self.symbolWidth = (SILE.types.length(radicalGlyph.width) + vertAdHocOffset) * scaleDown
×
1635

1636
   -- Adjust the height of the radical sign if the radicand is higher
1637
   self.symbolHeight = self.radicand.height > self.symbolHeight and self.radicand.height or self.symbolHeight
×
1638
   -- Compute the (max-)height of the short leg of the radical sign
1639
   self.symbolShortHeight = self.symbolHeight * constants.radicalDegreeBottomRaisePercent
×
1640

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

1661
local function _r (number)
1662
   -- Lua 5.3+ formats floats as 1.0 and integers as 1
1663
   -- Also some PDF readers do not like double precision.
1664
   return math.floor(number) == number and math.floor(number) or tonumber(string.format("%.5f", number))
×
1665
end
1666

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

1713
elements.padded = pl.class(elements.mbox)
×
1714
elements.padded._type = "Padded"
×
1715

1716
function elements.padded:__tostring ()
×
1717
   return self._type .. "(" .. tostring(self.impadded) .. ")"
×
1718
end
1719

1720
function elements.padded:_init (attributes, impadded)
×
1721
   elements.mbox._init(self)
×
1722
   self.impadded = impadded
×
1723
   self.attributes = attributes or {}
×
1724
   table.insert(self.children, impadded)
×
1725
end
1726

1727
function elements.padded:styleChildren ()
×
1728
   self.impadded.mode = self.mode
×
1729
end
1730

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

1755
function elements.padded.output (_, _, _, _) end
×
1756

1757
-- Bevelled fractions are not part of MathML Core, and MathML4 does not
1758
-- exactly specify how to compute the layout.
1759
elements.bevelledFraction = pl.class(elements.fraction) -- Inherit from fraction
×
1760
elements.fraction._type = "BevelledFraction"
×
1761

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

1777
   self.ruleThickness = self.attributes.linethickness
×
1778
         and SU.cast("measurement", self.attributes.linethickness):tonumber()
×
1779
      or constants.fractionRuleThickness * scaleDown
×
1780
   self.numerator.relX = SILE.types.length(0)
×
1781
   self.numerator.relY = SILE.types.length(-vSkewUp)
×
1782
   self.denominator.relX = self.numerator.width + hSkew
×
1783
   self.denominator.relY = SILE.types.length(vSkewDown)
×
1784
   self.width = self.numerator.width + self.denominator.width + hSkew
×
1785
   self.height = maxLength(self.numerator.height + vSkewUp, self.denominator.height - vSkewDown)
×
1786
   self.depth = maxLength(self.numerator.depth - vSkewUp, self.denominator.depth + vSkewDown)
×
1787
   self.barWidth = SILE.types.length(hSkew)
×
1788
   self.barX = self.numerator.relX + self.numerator.width
×
1789
end
1790

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

1814
elements.mathMode = mathMode
×
1815
elements.newSubscript = newSubscript
×
1816
elements.newUnderOver = newUnderOver
×
1817

1818
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