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

sile-typesetter / sile / 11581195726

29 Oct 2024 07:35PM UTC coverage: 65.595% (+31.3%) from 34.309%
11581195726

push

github

web-flow
Merge pull request #2141 from Omikhleia/feat-math-mroot

Support MathML mroot

3 of 58 new or added lines in 3 files covered. (5.17%)

54 existing lines in 7 files now uncovered.

11796 of 17983 relevant lines covered (65.6%)

5597.52 hits per line

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

82.17
/packages/math/base-elements.lua
1
local nodefactory = require("types.node")
13✔
2
local hb = require("justenoughharfbuzz")
13✔
3
local ot = require("core.opentype-parser")
13✔
4
local syms = require("packages.math.unicode-symbols")
13✔
5

6
local atomType = syms.atomType
13✔
7
local symbolDefaults = syms.symbolDefaults
13✔
8

9
local elements = {}
13✔
10

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

22
local scriptType = {
13✔
23
   upright = 1,
24
   bold = 2, -- also have Greek and digits
25
   italic = 3, -- also have Greek
26
   boldItalic = 4, -- also have Greek
27
   script = 5,
28
   boldScript = 6,
29
   fraktur = 7,
30
   boldFraktur = 8,
31
   doubleStruck = 9, -- also have digits
32
   sansSerif = 10, -- also have digits
33
   sansSerifBold = 11, -- also have Greek and digits
34
   sansSerifItalic = 12,
35
   sansSerifBoldItalic = 13, -- also have Greek
36
   monospace = 14, -- also have digits
37
}
38

39
local mathVariantToScriptType = function (attr)
40
   return attr == "normal" and scriptType.upright
59✔
41
      or attr == "bold" and scriptType.bold
59✔
42
      or attr == "italic" and scriptType.italic
37✔
43
      or attr == "bold-italic" and scriptType.boldItalic
25✔
44
      or attr == "double-struck" and scriptType.doubleStruck
25✔
45
      or SU.error('Invalid value "' .. attr .. '" for option mathvariant')
59✔
46
end
47

48
local function isDisplayMode (mode)
49
   return mode <= 1
2,027✔
50
end
51

52
local function isCrampedMode (mode)
53
   return mode % 2 == 1
66✔
54
end
55

56
local function isScriptMode (mode)
57
   return mode == mathMode.script or mode == mathMode.scriptCramped
2,698✔
58
end
59

60
local function isScriptScriptMode (mode)
61
   return mode == mathMode.scriptScript or mode == mathMode.scriptScriptCramped
2,208✔
62
end
63

64
local mathScriptConversionTable = {
13✔
65
   capital = {
13✔
66
      [scriptType.upright] = function (codepoint)
13✔
67
         return codepoint
8✔
68
      end,
69
      [scriptType.bold] = function (codepoint)
13✔
70
         return codepoint + 0x1D400 - 0x41
12✔
71
      end,
72
      [scriptType.italic] = function (codepoint)
13✔
73
         return codepoint + 0x1D434 - 0x41
15✔
74
      end,
75
      [scriptType.boldItalic] = function (codepoint)
13✔
76
         return codepoint + 0x1D468 - 0x41
1✔
77
      end,
78
      [scriptType.doubleStruck] = function (codepoint)
13✔
79
         return codepoint == 0x43 and 0x2102
19✔
80
            or codepoint == 0x48 and 0x210D
19✔
81
            or codepoint == 0x4E and 0x2115
19✔
82
            or codepoint == 0x50 and 0x2119
19✔
83
            or codepoint == 0x51 and 0x211A
13✔
84
            or codepoint == 0x52 and 0x211D
13✔
85
            or codepoint == 0x5A and 0x2124
7✔
86
            or codepoint + 0x1D538 - 0x41
19✔
87
      end,
88
   },
13✔
89
   small = {
13✔
90
      [scriptType.upright] = function (codepoint)
13✔
91
         return codepoint
322✔
92
      end,
93
      [scriptType.bold] = function (codepoint)
13✔
94
         return codepoint + 0x1D41A - 0x61
6✔
95
      end,
96
      [scriptType.italic] = function (codepoint)
13✔
97
         return codepoint == 0x68 and 0x210E or codepoint + 0x1D44E - 0x61
203✔
98
      end,
99
      [scriptType.boldItalic] = function (codepoint)
13✔
100
         return codepoint + 0x1D482 - 0x61
1✔
101
      end,
102
      [scriptType.doubleStruck] = function (codepoint)
13✔
103
         return codepoint + 0x1D552 - 0x61
12✔
104
      end,
105
   },
13✔
106
}
107

108
local mathCache = {}
13✔
109

110
local function retrieveMathTable (font)
111
   local key = SILE.font._key(font)
3,835✔
112
   if not mathCache[key] then
3,835✔
113
      SU.debug("math", "Loading math font", key)
36✔
114
      local face = SILE.font.cache(font, SILE.shaper.getFace)
36✔
115
      if not face then
36✔
116
         SU.error("Could not find requested font " .. font .. " or any suitable substitutes")
×
117
      end
118
      local fontHasMathTable, rawMathTable, mathTableParsable, mathTable
119
      fontHasMathTable, rawMathTable = pcall(hb.get_table, face, "MATH")
36✔
120
      if fontHasMathTable then
36✔
121
         mathTableParsable, mathTable = pcall(ot.parseMath, rawMathTable)
72✔
122
      end
123
      if not fontHasMathTable or not mathTableParsable then
36✔
124
         SU.error(([[
×
125
            You must use a math font for math rendering
126

127
            The math table in '%s' could not be %s.
128
         ]]):format(face.filename, fontHasMathTable and "parsed" or "loaded"))
×
129
      end
130
      local upem = ot.parseHead(hb.get_table(face, "head")).unitsPerEm
72✔
131
      local constants = {}
36✔
132
      for k, v in pairs(mathTable.mathConstants) do
2,052✔
133
         if type(v) == "table" then
2,016✔
134
            v = v.value
1,836✔
135
         end
136
         if k:sub(-9) == "ScaleDown" then
4,032✔
137
            constants[k] = v / 100
72✔
138
         else
139
            constants[k] = v * font.size / upem
1,944✔
140
         end
141
      end
142
      local italicsCorrection = {}
36✔
143
      for k, v in pairs(mathTable.mathItalicsCorrection) do
15,264✔
144
         italicsCorrection[k] = v.value * font.size / upem
15,228✔
145
      end
146
      mathCache[key] = {
36✔
147
         constants = constants,
36✔
148
         italicsCorrection = italicsCorrection,
36✔
149
         mathVariants = mathTable.mathVariants,
36✔
150
         unitsPerEm = upem,
36✔
151
      }
36✔
152
   end
153
   return mathCache[key]
3,835✔
154
end
155

156
-- Style transition functions for superscript and subscript
157
local function getSuperscriptMode (mode)
158
   -- D, T -> S
159
   if mode == mathMode.display or mode == mathMode.text then
75✔
160
      return mathMode.script
40✔
161
   -- D', T' -> S'
162
   elseif mode == mathMode.displayCramped or mode == mathMode.textCramped then
35✔
163
      return mathMode.scriptCramped
29✔
164
   -- S, SS -> SS
165
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
6✔
166
      return mathMode.scriptScript
3✔
167
   -- S', SS' -> SS'
168
   else
169
      return mathMode.scriptScriptCramped
3✔
170
   end
171
end
172
local function getSubscriptMode (mode)
173
   -- D, T, D', T' -> S'
174
   if
175
      mode == mathMode.display
102✔
176
      or mode == mathMode.text
60✔
177
      or mode == mathMode.displayCramped
50✔
178
      or mode == mathMode.textCramped
50✔
179
   then
180
      return mathMode.scriptCramped
83✔
181
   -- S, SS, S', SS' -> SS'
182
   else
183
      return mathMode.scriptScriptCramped
19✔
184
   end
185
end
186

187
-- Style transition functions for fraction (numerator and denominator)
188
local function getNumeratorMode (mode)
189
   -- D -> T
190
   if mode == mathMode.display then
33✔
191
      return mathMode.text
10✔
192
   -- D' -> T'
193
   elseif mode == mathMode.displayCramped then
23✔
194
      return mathMode.textCramped
×
195
   -- T -> S
196
   elseif mode == mathMode.text then
23✔
197
      return mathMode.script
×
198
   -- T' -> S'
199
   elseif mode == mathMode.textCramped then
23✔
200
      return mathMode.scriptCramped
13✔
201
   -- S, SS -> SS
202
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
10✔
203
      return mathMode.scriptScript
×
204
   -- S', SS' -> SS'
205
   else
206
      return mathMode.scriptScriptCramped
10✔
207
   end
208
end
209
local function getDenominatorMode (mode)
210
   -- D, D' -> T'
211
   if mode == mathMode.display or mode == mathMode.displayCramped then
33✔
212
      return mathMode.textCramped
10✔
213
   -- T, T' -> S'
214
   elseif mode == mathMode.text or mode == mathMode.textCramped then
23✔
215
      return mathMode.scriptCramped
13✔
216
   -- S, SS, S', SS' -> SS'
217
   else
218
      return mathMode.scriptScriptCramped
10✔
219
   end
220
end
221

222
local function getRightMostGlyphId (node)
223
   while node and node:is_a(elements.stackbox) and node.direction == "H" do
298✔
224
      node = node.children[#node.children]
12✔
225
   end
226
   if node and node:is_a(elements.text) then
274✔
227
      return node.value.glyphString[#node.value.glyphString]
135✔
228
   else
229
      return 0
4✔
230
   end
231
end
232

233
-- Compares two SILE.types.length, without considering shrink or stretch values, and
234
-- returns the biggest.
235
local function maxLength (...)
236
   local arg = { ... }
2,605✔
237
   local m
238
   for i, v in ipairs(arg) do
8,083✔
239
      if i == 1 then
5,478✔
240
         m = v
2,605✔
241
      else
242
         if v.length:tonumber() > m.length:tonumber() then
8,619✔
243
            m = v
632✔
244
         end
245
      end
246
   end
247
   return m
2,605✔
248
end
249

250
local function scaleWidth (length, line)
251
   local number = length.length
1,135✔
252
   if line.ratio and line.ratio < 0 and length.shrink:tonumber() > 0 then
1,135✔
253
      number = number + length.shrink * line.ratio
×
254
   elseif line.ratio and line.ratio > 0 and length.stretch:tonumber() > 0 then
2,270✔
255
      number = number + length.stretch * line.ratio
1,272✔
256
   end
257
   return number
1,135✔
258
end
259

260
-- math box, box with a horizontal shift value and could contain zero or more
261
-- mbox'es (or its child classes) the entire math environment itself is
262
-- a top-level mbox.
263
-- Typesetting of mbox evolves four steps:
264
--   1. Determine the mode for each mbox according to their parent.
265
--   2. Shape the mbox hierarchy from leaf to top. Get the shape and relative position.
266
--   3. Convert mbox into _nnode's to put in SILE's typesetting framework
267
elements.mbox = pl.class(nodefactory.hbox)
26✔
268
elements.mbox._type = "Mbox"
13✔
269

270
function elements.mbox:__tostring ()
26✔
271
   return self._type
×
272
end
273

274
function elements.mbox:_init ()
26✔
275
   nodefactory.hbox._init(self)
1,896✔
276
   self.font = {}
1,896✔
277
   self.children = {} -- The child nodes
1,896✔
278
   self.relX = SILE.types.length(0) -- x position relative to its parent box
3,792✔
279
   self.relY = SILE.types.length(0) -- y position relative to its parent box
3,792✔
280
   self.value = {}
1,896✔
281
   self.mode = mathMode.display
1,896✔
282
   self.atom = atomType.ordinary
1,896✔
283
   local font = {
1,896✔
284
      family = SILE.settings:get("math.font.family"),
3,792✔
285
      size = SILE.settings:get("math.font.size"),
3,792✔
286
      style = SILE.settings:get("math.font.style"),
3,792✔
287
      weight = SILE.settings:get("math.font.weight"),
3,792✔
288
   }
289
   local filename = SILE.settings:get("math.font.filename")
1,896✔
290
   if filename and filename ~= "" then
1,896✔
291
      font.filename = filename
×
292
   end
293
   self.font = SILE.font.loadDefaults(font)
3,792✔
294
end
295

296
function elements.mbox.styleChildren (_)
26✔
297
   SU.error("styleChildren is a virtual function that need to be overridden by its child classes")
×
298
end
299

300
function elements.mbox.shape (_, _, _)
26✔
301
   SU.error("shape is a virtual function that need to be overridden by its child classes")
×
302
end
303

304
function elements.mbox.output (_, _, _, _)
26✔
305
   SU.error("output is a virtual function that need to be overridden by its child classes")
×
306
end
307

308
function elements.mbox:getMathMetrics ()
26✔
309
   return retrieveMathTable(self.font)
3,835✔
310
end
311

312
function elements.mbox:getScaleDown ()
26✔
313
   local constants = self:getMathMetrics().constants
4,728✔
314
   local scaleDown
315
   if isScriptMode(self.mode) then
4,728✔
316
      scaleDown = constants.scriptPercentScaleDown
430✔
317
   elseif isScriptScriptMode(self.mode) then
3,868✔
318
      scaleDown = constants.scriptScriptPercentScaleDown
181✔
319
   else
320
      scaleDown = 1
1,753✔
321
   end
322
   return scaleDown
2,364✔
323
end
324

325
-- Determine the mode of its descendants
326
function elements.mbox:styleDescendants ()
26✔
327
   self:styleChildren()
1,908✔
328
   for _, n in ipairs(self.children) do
3,744✔
329
      if n then
1,836✔
330
         n:styleDescendants()
1,836✔
331
      end
332
   end
333
end
334

335
-- shapeTree shapes the mbox and all its descendants in a recursive fashion
336
-- The inner-most leaf nodes determine their shape first, and then propagate to their parents
337
-- During the process, each node will determine its size by (width, height, depth)
338
-- and (relX, relY) which the relative position to its parent
339
function elements.mbox:shapeTree ()
26✔
340
   for _, n in ipairs(self.children) do
3,744✔
341
      if n then
1,836✔
342
         n:shapeTree()
1,836✔
343
      end
344
   end
345
   self:shape()
1,908✔
346
end
347

348
-- Output the node and all its descendants
349
function elements.mbox:outputTree (x, y, line)
26✔
350
   self:output(x, y, line)
1,908✔
351
   local debug = SILE.settings:get("math.debug.boxes")
1,908✔
352
   if debug and not (self:is_a(elements.space)) then
1,908✔
353
      SILE.outputter:setCursor(scaleWidth(x, line), y.length)
×
354
      SILE.outputter:debugHbox({ height = self.height.length, depth = self.depth.length }, scaleWidth(self.width, line))
×
355
   end
356
   for _, n in ipairs(self.children) do
3,744✔
357
      if n then
1,836✔
358
         n:outputTree(x + n.relX, y + n.relY, line)
5,508✔
359
      end
360
   end
361
end
362

363
local spaceKind = {
13✔
364
   thin = "thin",
365
   med = "med",
366
   thick = "thick",
367
}
368

369
-- Indexed by left atom
370
local spacingRules = {
13✔
371
   [atomType.ordinary] = {
13✔
372
      [atomType.bigOperator] = { spaceKind.thin },
13✔
373
      [atomType.binaryOperator] = { spaceKind.med, notScript = true },
13✔
374
      [atomType.relationalOperator] = { spaceKind.thick, notScript = true },
13✔
375
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
376
   },
13✔
377
   [atomType.bigOperator] = {
13✔
378
      [atomType.ordinary] = { spaceKind.thin },
13✔
379
      [atomType.bigOperator] = { spaceKind.thin },
13✔
380
      [atomType.relationalOperator] = { spaceKind.thick, notScript = true },
13✔
381
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
382
   },
13✔
383
   [atomType.binaryOperator] = {
13✔
384
      [atomType.ordinary] = { spaceKind.med, notScript = true },
13✔
385
      [atomType.bigOperator] = { spaceKind.med, notScript = true },
13✔
386
      [atomType.openingSymbol] = { spaceKind.med, notScript = true },
13✔
387
      [atomType.inner] = { spaceKind.med, notScript = true },
13✔
388
   },
13✔
389
   [atomType.relationalOperator] = {
13✔
390
      [atomType.ordinary] = { spaceKind.thick, notScript = true },
13✔
391
      [atomType.bigOperator] = { spaceKind.thick, notScript = true },
13✔
392
      [atomType.openingSymbol] = { spaceKind.thick, notScript = true },
13✔
393
      [atomType.inner] = { spaceKind.thick, notScript = true },
13✔
394
   },
13✔
395
   [atomType.closeSymbol] = {
13✔
396
      [atomType.bigOperator] = { spaceKind.thin },
13✔
397
      [atomType.binaryOperator] = { spaceKind.med, notScript = true },
13✔
398
      [atomType.relationalOperator] = { spaceKind.thick, notScript = true },
13✔
399
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
400
   },
13✔
401
   [atomType.punctuationSymbol] = {
13✔
402
      [atomType.ordinary] = { spaceKind.thin, notScript = true },
13✔
403
      [atomType.bigOperator] = { spaceKind.thin, notScript = true },
13✔
404
      [atomType.relationalOperator] = { spaceKind.thin, notScript = true },
13✔
405
      [atomType.openingSymbol] = { spaceKind.thin, notScript = true },
13✔
406
      [atomType.closeSymbol] = { spaceKind.thin, notScript = true },
13✔
407
      [atomType.punctuationSymbol] = { spaceKind.thin, notScript = true },
13✔
408
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
409
   },
13✔
410
   [atomType.inner] = {
13✔
411
      [atomType.ordinary] = { spaceKind.thin, notScript = true },
13✔
412
      [atomType.bigOperator] = { spaceKind.thin },
13✔
413
      [atomType.binaryOperator] = { spaceKind.med, notScript = true },
13✔
414
      [atomType.relationalOperator] = { spaceKind.thick, notScript = true },
13✔
415
      [atomType.openingSymbol] = { spaceKind.thin, notScript = true },
13✔
416
      [atomType.punctuationSymbol] = { spaceKind.thin, notScript = true },
13✔
417
      [atomType.inner] = { spaceKind.thin, notScript = true },
13✔
418
   },
13✔
419
}
420

421
-- _stackbox stacks its content one, either horizontally or vertically
422
elements.stackbox = pl.class(elements.mbox)
26✔
423
elements.stackbox._type = "Stackbox"
13✔
424

425
function elements.stackbox:__tostring ()
26✔
426
   local result = self.direction .. "Box("
×
427
   for i, n in ipairs(self.children) do
×
428
      result = result .. (i == 1 and "" or ", ") .. tostring(n)
×
429
   end
430
   result = result .. ")"
×
431
   return result
×
432
end
433

434
function elements.stackbox:_init (direction, children)
26✔
435
   elements.mbox._init(self)
399✔
436
   if not (direction == "H" or direction == "V") then
399✔
437
      SU.error("Wrong direction '" .. direction .. "'; should be H or V")
×
438
   end
439
   self.direction = direction
399✔
440
   self.children = children
399✔
441
end
442

443
function elements.stackbox:styleChildren ()
26✔
444
   for _, n in ipairs(self.children) do
1,530✔
445
      n.mode = self.mode
1,131✔
446
   end
447
   if self.direction == "H" then
399✔
448
      -- Insert spaces according to the atom type, following Knuth's guidelines
449
      -- in the TeXbook
450
      local spaces = {}
327✔
451
      for i = 1, #self.children - 1 do
1,065✔
452
         local v = self.children[i]
738✔
453
         local v2 = self.children[i + 1]
738✔
454
         if spacingRules[v.atom] and spacingRules[v.atom][v2.atom] then
738✔
455
            local rule = spacingRules[v.atom][v2.atom]
359✔
456
            if not (rule.notScript and (isScriptMode(self.mode) or isScriptScriptMode(self.mode))) then
967✔
457
               spaces[i + 1] = rule[1]
261✔
458
            end
459
         end
460
      end
461
      local spaceIdx = {}
327✔
462
      for i, _ in pairs(spaces) do
588✔
463
         table.insert(spaceIdx, i)
261✔
464
      end
465
      table.sort(spaceIdx, function (a, b)
654✔
466
         return a > b
377✔
467
      end)
468
      for _, idx in ipairs(spaceIdx) do
588✔
469
         local hsp = elements.space(spaces[idx], 0, 0)
261✔
470
         table.insert(self.children, idx, hsp)
261✔
471
      end
472
   end
473
end
474

475
function elements.stackbox:shape ()
26✔
476
   -- For a horizontal stackbox (i.e. mrow):
477
   -- 1. set self.height and self.depth to max element height & depth
478
   -- 2. handle stretchy operators
479
   -- 3. set self.width
480
   -- For a vertical stackbox:
481
   -- 1. set self.width to max element width
482
   -- 2. set self.height
483
   -- And finally set children's relative coordinates
484
   self.height = SILE.types.length(0)
798✔
485
   self.depth = SILE.types.length(0)
798✔
486
   if self.direction == "H" then
399✔
487
      for i, n in ipairs(self.children) do
1,647✔
488
         n.relY = SILE.types.length(0)
2,640✔
489
         self.height = i == 1 and n.height or maxLength(self.height, n.height)
2,319✔
490
         self.depth = i == 1 and n.depth or maxLength(self.depth, n.depth)
2,319✔
491
      end
492
      -- Handle stretchy operators
493
      for _, elt in ipairs(self.children) do
1,647✔
494
         if elt.is_a(elements.text) and elt.kind == "operator" and elt.stretchy then
2,640✔
495
            elt:stretchyReshape(self.depth, self.height)
79✔
496
         end
497
      end
498
      -- Set self.width
499
      self.width = SILE.types.length(0)
654✔
500
      for i, n in ipairs(self.children) do
1,647✔
501
         n.relX = self.width
1,320✔
502
         self.width = i == 1 and n.width or self.width + n.width
2,319✔
503
      end
504
   else -- self.direction == "V"
505
      for i, n in ipairs(self.children) do
144✔
506
         n.relX = SILE.types.length(0)
144✔
507
         self.width = i == 1 and n.width or maxLength(self.width, n.width)
72✔
508
      end
509
      -- Set self.height and self.depth
510
      for i, n in ipairs(self.children) do
144✔
511
         self.depth = i == 1 and n.depth or self.depth + n.depth
72✔
512
      end
513
      for i = 1, #self.children do
144✔
514
         local n = self.children[i]
72✔
515
         if i == 1 then
72✔
516
            self.height = n.height
72✔
517
            self.depth = n.depth
72✔
518
         elseif i > 1 then
×
519
            n.relY = self.children[i - 1].relY + self.children[i - 1].depth + n.height
×
520
            self.depth = self.depth + n.height + n.depth
×
521
         end
522
      end
523
   end
524
end
525

526
-- Despite of its name, this function actually output the whole tree of nodes recursively.
527
function elements.stackbox:outputYourself (typesetter, line)
26✔
528
   local mathX = typesetter.frame.state.cursorX
72✔
529
   local mathY = typesetter.frame.state.cursorY
72✔
530
   self:outputTree(self.relX + mathX, self.relY + mathY, line)
216✔
531
   typesetter.frame:advanceWritingDirection(scaleWidth(self.width, line))
144✔
532
end
533

534
function elements.stackbox.output (_, _, _, _) end
412✔
535

536
elements.phantom = pl.class(elements.stackbox) -- inherit from stackbox
26✔
537
elements.phantom._type = "Phantom"
13✔
538

539
function elements.phantom:_init (children, special)
26✔
540
   -- MathML core 3.3.7:
541
   -- "Its layout algorithm is the same as the mrow element".
542
   -- Also not the MathML states that <mphantom> is sort of legacy, "implemented
543
   -- for compatibility with full MathML. Authors whose only target is MathML
544
   -- Core are encouraged to use CSS for styling."
545
   -- The thing is that we don't have CSS in SILE, so supporting <mphantom> is
546
   -- a must.
UNCOV
547
   elements.stackbox._init(self, "H", children)
×
548
   self.special = special
×
549
end
550

551
function elements.phantom:shape ()
26✔
552
   elements.stackbox.shape(self)
×
553
   -- From https://latexref.xyz:
554
   -- "The \vphantom variant produces an invisible box with the same vertical size
555
   -- as subformula, the same height and depth, but having zero width.
556
   -- And \hphantom makes a box with the same width as subformula but
557
   -- with zero height and depth."
UNCOV
558
   if self.special == "v" then
×
559
      self.width = SILE.types.length()
×
560
   elseif self.special == "h" then
×
UNCOV
561
      self.height = SILE.types.length()
×
562
      self.depth = SILE.types.length()
×
563
   end
564
end
565

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

573
elements.subscript = pl.class(elements.mbox)
26✔
574
elements.subscript._type = "Subscript"
13✔
575

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

585
function elements.subscript:_init (base, sub, sup)
26✔
586
   elements.mbox._init(self)
114✔
587
   self.base = base
114✔
588
   self.sub = sub
114✔
589
   self.sup = sup
114✔
590
   if self.base then
114✔
591
      table.insert(self.children, self.base)
114✔
592
   end
593
   if self.sub then
114✔
594
      table.insert(self.children, self.sub)
77✔
595
   end
596
   if self.sup then
114✔
597
      table.insert(self.children, self.sup)
51✔
598
   end
599
   self.atom = self.base.atom
114✔
600
end
601

602
function elements.subscript:styleChildren ()
26✔
603
   if self.base then
114✔
604
      self.base.mode = self.mode
114✔
605
   end
606
   if self.sub then
114✔
607
      self.sub.mode = getSubscriptMode(self.mode)
154✔
608
   end
609
   if self.sup then
114✔
610
      self.sup.mode = getSuperscriptMode(self.mode)
102✔
611
   end
612
end
613

614
function elements.subscript:calculateItalicsCorrection ()
26✔
615
   local lastGid = getRightMostGlyphId(self.base)
114✔
616
   if lastGid > 0 then
114✔
617
      local mathMetrics = self:getMathMetrics()
110✔
618
      if mathMetrics.italicsCorrection[lastGid] then
110✔
619
         return mathMetrics.italicsCorrection[lastGid]
64✔
620
      end
621
   end
622
   return 0
50✔
623
end
624

625
function elements.subscript:shape ()
26✔
626
   local mathMetrics = self:getMathMetrics()
129✔
627
   local constants = mathMetrics.constants
129✔
628
   local scaleDown = self:getScaleDown()
129✔
629
   if self.base then
129✔
630
      self.base.relX = SILE.types.length(0)
258✔
631
      self.base.relY = SILE.types.length(0)
258✔
632
      -- Use widthForSubscript of base, if available
633
      self.width = self.base.widthForSubscript or self.base.width
129✔
634
   else
635
      self.width = SILE.types.length(0)
×
636
   end
637
   local itCorr = self:calculateItalicsCorrection() * scaleDown
258✔
638
   local subShift
639
   local supShift
640
   if self.sub then
129✔
641
      if self.isUnderOver or self.base.largeop then
92✔
642
         -- Ad hoc correction on integral limits, following LuaTeX's
643
         -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
644
         subShift = -itCorr
33✔
645
      else
646
         subShift = 0
59✔
647
      end
648
      self.sub.relX = self.width + subShift
184✔
649
      self.sub.relY = SILE.types.length(math.max(
184✔
650
         constants.subscriptShiftDown * scaleDown,
92✔
651
         --self.base.depth + constants.subscriptBaselineDropMin * scaleDown,
652
         (self.sub.height - constants.subscriptTopMax * scaleDown):tonumber()
184✔
653
      ))
92✔
654
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or self.base.largeop then
261✔
655
         self.sub.relY = maxLength(self.sub.relY, self.base.depth + constants.subscriptBaselineDropMin * scaleDown)
99✔
656
      end
657
   end
658
   if self.sup then
129✔
659
      if self.isUnderOver or self.base.largeop then
66✔
660
         -- Ad hoc correction on integral limits, following LuaTeX's
661
         -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
662
         supShift = 0
21✔
663
      else
664
         supShift = itCorr
45✔
665
      end
666
      self.sup.relX = self.width + supShift
132✔
667
      self.sup.relY = SILE.types.length(math.max(
132✔
668
         isCrampedMode(self.mode) and constants.superscriptShiftUpCramped * scaleDown
132✔
669
            or constants.superscriptShiftUp * scaleDown, -- or cramped
66✔
670
         --self.base.height - constants.superscriptBaselineDropMax * scaleDown,
671
         (self.sup.depth + constants.superscriptBottomMin * scaleDown):tonumber()
132✔
672
      )) * -1
132✔
673
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or self.base.largeop then
183✔
674
         self.sup.relY = maxLength(
42✔
675
            (0 - self.sup.relY),
21✔
676
            self.base.height - constants.superscriptBaselineDropMax * scaleDown
21✔
677
         ) * -1
42✔
678
      end
679
   end
680
   if self.sub and self.sup then
129✔
681
      local gap = self.sub.relY - self.sub.height - self.sup.relY - self.sup.depth
87✔
682
      if gap.length:tonumber() < constants.subSuperscriptGapMin * scaleDown then
58✔
683
         -- The following adjustment comes directly from Appendix G of he
684
         -- TeXbook (rule 18e).
685
         self.sub.relY = constants.subSuperscriptGapMin * scaleDown + self.sub.height + self.sup.relY + self.sup.depth
32✔
686
         local psi = constants.superscriptBottomMaxWithSubscript * scaleDown + self.sup.relY + self.sup.depth
16✔
687
         if psi:tonumber() > 0 then
16✔
688
            self.sup.relY = self.sup.relY - psi
16✔
689
            self.sub.relY = self.sub.relY - psi
16✔
690
         end
691
      end
692
   end
693
   self.width = self.width
×
694
      + maxLength(
258✔
695
         self.sub and self.sub.width + subShift or SILE.types.length(0),
221✔
696
         self.sup and self.sup.width + supShift or SILE.types.length(0)
195✔
697
      )
129✔
698
      + constants.spaceAfterScript * scaleDown
258✔
699
   self.height = maxLength(
258✔
700
      self.base and self.base.height or SILE.types.length(0),
129✔
701
      self.sub and (self.sub.height - self.sub.relY) or SILE.types.length(0),
221✔
702
      self.sup and (self.sup.height - self.sup.relY) or SILE.types.length(0)
195✔
703
   )
129✔
704
   self.depth = maxLength(
258✔
705
      self.base and self.base.depth or SILE.types.length(0),
129✔
706
      self.sub and (self.sub.depth + self.sub.relY) or SILE.types.length(0),
221✔
707
      self.sup and (self.sup.depth + self.sup.relY) or SILE.types.length(0)
195✔
708
   )
129✔
709
end
710

711
function elements.subscript.output (_, _, _, _) end
127✔
712

713
elements.underOver = pl.class(elements.subscript)
26✔
714
elements.underOver._type = "UnderOver"
13✔
715

716
function elements.underOver:__tostring ()
26✔
UNCOV
717
   return self._type .. "(" .. tostring(self.base) .. ", " .. tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
×
718
end
719

720
function elements.underOver:_init (base, sub, sup)
26✔
721
   elements.mbox._init(self)
25✔
722
   self.atom = base.atom
25✔
723
   self.base = base
25✔
724
   self.sub = sub
25✔
725
   self.sup = sup
25✔
726
   if self.sup then
25✔
727
      table.insert(self.children, self.sup)
24✔
728
   end
729
   if self.base then
25✔
730
      table.insert(self.children, self.base)
25✔
731
   end
732
   if self.sub then
25✔
733
      table.insert(self.children, self.sub)
25✔
734
   end
735
end
736

737
function elements.underOver:styleChildren ()
26✔
738
   if self.base then
25✔
739
      self.base.mode = self.mode
25✔
740
   end
741
   if self.sub then
25✔
742
      self.sub.mode = getSubscriptMode(self.mode)
50✔
743
   end
744
   if self.sup then
25✔
745
      self.sup.mode = getSuperscriptMode(self.mode)
48✔
746
   end
747
end
748

749
function elements.underOver:shape ()
26✔
750
   if not (self.mode == mathMode.display or self.mode == mathMode.displayCramped) then
25✔
751
      self.isUnderOver = true
15✔
752
      elements.subscript.shape(self)
15✔
753
      return
15✔
754
   end
755
   local constants = self:getMathMetrics().constants
20✔
756
   local scaleDown = self:getScaleDown()
10✔
757
   -- Determine relative Ys
758
   if self.base then
10✔
759
      self.base.relY = SILE.types.length(0)
20✔
760
   end
761
   if self.sub then
10✔
762
      self.sub.relY = self.base.depth
10✔
763
         + SILE.types.length(
20✔
764
            math.max(
20✔
765
               (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
20✔
766
               constants.lowerLimitBaselineDropMin * scaleDown
10✔
767
            )
768
         )
20✔
769
   end
770
   if self.sup then
10✔
771
      self.sup.relY = 0
9✔
772
         - self.base.height
9✔
773
         - SILE.types.length(
18✔
774
            math.max(
18✔
775
               (constants.upperLimitGapMin * scaleDown + self.sup.depth):tonumber(),
18✔
776
               constants.upperLimitBaselineRiseMin * scaleDown
9✔
777
            )
778
         )
18✔
779
   end
780
   -- Determine relative Xs based on widest symbol
781
   local widest, a, b
782
   if self.sub and self.sub.width > self.base.width then
10✔
783
      if self.sup and self.sub.width > self.sup.width then
6✔
784
         widest = self.sub
6✔
785
         a = self.base
6✔
786
         b = self.sup
6✔
787
      elseif self.sup then
×
UNCOV
788
         widest = self.sup
×
UNCOV
789
         a = self.base
×
790
         b = self.sub
×
791
      else
792
         widest = self.sub
×
793
         a = self.base
×
UNCOV
794
         b = nil
×
795
      end
796
   else
797
      if self.sup and self.base.width > self.sup.width then
4✔
798
         widest = self.base
3✔
799
         a = self.sub
3✔
800
         b = self.sup
3✔
801
      elseif self.sup then
1✔
UNCOV
802
         widest = self.sup
×
803
         a = self.base
×
UNCOV
804
         b = self.sub
×
805
      else
806
         widest = self.base
1✔
807
         a = self.sub
1✔
808
         b = nil
1✔
809
      end
810
   end
811
   widest.relX = SILE.types.length(0)
20✔
812
   local c = widest.width / 2
10✔
813
   if a then
10✔
814
      a.relX = c - a.width / 2
30✔
815
   end
816
   if b then
10✔
817
      b.relX = c - b.width / 2
27✔
818
   end
819
   local itCorr = self:calculateItalicsCorrection() * scaleDown
20✔
820
   if self.sup then
10✔
821
      self.sup.relX = self.sup.relX + itCorr / 2
18✔
822
   end
823
   if self.sub then
10✔
824
      self.sub.relX = self.sub.relX - itCorr / 2
20✔
825
   end
826
   -- Determine width and height
827
   self.width = maxLength(
20✔
828
      self.base and self.base.width or SILE.types.length(0),
10✔
829
      self.sub and self.sub.width or SILE.types.length(0),
10✔
830
      self.sup and self.sup.width or SILE.types.length(0)
10✔
831
   )
10✔
832
   if self.sup then
10✔
833
      self.height = 0 - self.sup.relY + self.sup.height
27✔
834
   else
835
      self.height = self.base and self.base.height or 0
1✔
836
   end
837
   if self.sub then
10✔
838
      self.depth = self.sub.relY + self.sub.depth
20✔
839
   else
840
      self.depth = self.base and self.base.depth or 0
×
841
   end
842
end
843

844
function elements.underOver:calculateItalicsCorrection ()
26✔
845
   local lastGid = getRightMostGlyphId(self.base)
25✔
846
   if lastGid > 0 then
25✔
847
      local mathMetrics = self:getMathMetrics()
25✔
848
      if mathMetrics.italicsCorrection[lastGid] then
25✔
849
         local c = mathMetrics.italicsCorrection[lastGid]
×
850
         -- If this is a big operator, and we are in display style, then the
851
         -- base glyph may be bigger than the font size. We need to adjust the
852
         -- italic correction accordingly.
853
         if self.base.atom == atomType.bigOperator and isDisplayMode(self.mode) then
×
854
            c = c * (self.base and self.base.font.size / self.font.size or 1.0)
×
855
         end
UNCOV
856
         return c
×
857
      end
858
   end
859
   return 0
25✔
860
end
861

862
function elements.underOver.output (_, _, _, _) end
38✔
863

864
-- terminal is the base class for leaf node
865
elements.terminal = pl.class(elements.mbox)
26✔
866
elements.terminal._type = "Terminal"
13✔
867

868
function elements.terminal:_init ()
26✔
869
   elements.mbox._init(self)
1,316✔
870
end
871

872
function elements.terminal.styleChildren (_) end
1,329✔
873

874
function elements.terminal.shape (_) end
13✔
875

876
elements.space = pl.class(elements.terminal)
26✔
877
elements.space._type = "Space"
13✔
878

879
function elements.space:_init ()
26✔
UNCOV
880
   elements.terminal._init(self)
×
881
end
882

883
function elements.space:__tostring ()
26✔
884
   return self._type
×
885
      .. "(width="
×
UNCOV
886
      .. tostring(self.width)
×
UNCOV
887
      .. ", height="
×
888
      .. tostring(self.height)
×
UNCOV
889
      .. ", depth="
×
UNCOV
890
      .. tostring(self.depth)
×
891
      .. ")"
×
892
end
893

894
local function getStandardLength (value)
895
   if type(value) == "string" then
957✔
896
      local direction = 1
319✔
897
      if value:sub(1, 1) == "-" then
638✔
898
         value = value:sub(2, -1)
20✔
899
         direction = -1
10✔
900
      end
901
      if value == "thin" then
319✔
902
         return SILE.types.length("3mu") * direction
198✔
903
      elseif value == "med" then
253✔
904
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
294✔
905
      elseif value == "thick" then
155✔
906
         return SILE.types.length("5mu plus 5mu") * direction
393✔
907
      end
908
   end
909
   return SILE.types.length(value)
662✔
910
end
911

912
function elements.space:_init (width, height, depth)
26✔
913
   elements.terminal._init(self)
319✔
914
   self.width = getStandardLength(width)
638✔
915
   self.height = getStandardLength(height)
638✔
916
   self.depth = getStandardLength(depth)
638✔
917
end
918

919
function elements.space:shape ()
26✔
920
   self.width = self.width:absolute() * self:getScaleDown()
1,276✔
921
   self.height = self.height:absolute() * self:getScaleDown()
1,276✔
922
   self.depth = self.depth:absolute() * self:getScaleDown()
1,276✔
923
end
924

925
function elements.space.output (_) end
332✔
926

927
-- text node. For any actual text output
928
elements.text = pl.class(elements.terminal)
26✔
929
elements.text._type = "Text"
13✔
930

931
function elements.text:__tostring ()
26✔
932
   return self._type
×
933
      .. "(atom="
×
UNCOV
934
      .. tostring(self.atom)
×
UNCOV
935
      .. ", kind="
×
936
      .. tostring(self.kind)
×
937
      .. ", script="
×
UNCOV
938
      .. tostring(self.script)
×
UNCOV
939
      .. (self.stretchy and ", stretchy" or "")
×
UNCOV
940
      .. (self.largeop and ", largeop" or "")
×
941
      .. ', text="'
×
942
      .. (self.originalText or self.text)
×
943
      .. '")'
×
944
end
945

946
function elements.text:_init (kind, attributes, script, text)
26✔
947
   elements.terminal._init(self)
997✔
948
   if not (kind == "number" or kind == "identifier" or kind == "operator" or kind == "string") then
997✔
949
      SU.error("Unknown text node kind '" .. kind .. "'; should be one of: number, identifier, operator, string")
×
950
   end
951
   self.kind = kind
997✔
952
   self.script = script
997✔
953
   self.text = text
997✔
954
   if self.script ~= "upright" then
997✔
955
      local converted = ""
997✔
956
      for _, uchr in luautf8.codes(self.text) do
2,241✔
957
         local dst_char = luautf8.char(uchr)
1,244✔
958
         if uchr >= 0x41 and uchr <= 0x5A then -- Latin capital letter
1,244✔
959
            dst_char = luautf8.char(mathScriptConversionTable.capital[self.script](uchr))
110✔
960
         elseif uchr >= 0x61 and uchr <= 0x7A then -- Latin non-capital letter
1,189✔
961
            dst_char = luautf8.char(mathScriptConversionTable.small[self.script](uchr))
1,088✔
962
         end
963
         converted = converted .. dst_char
1,244✔
964
      end
965
      self.originalText = self.text
997✔
966
      self.text = converted
997✔
967
   end
968
   if self.kind == "operator" then
997✔
969
      if self.text == "-" then
394✔
970
         self.text = "−"
13✔
971
      end
972
   end
973
   for attribute, value in pairs(attributes) do
1,457✔
974
      self[attribute] = value
460✔
975
   end
976
end
977

978
function elements.text:shape ()
26✔
979
   self.font.size = self.font.size * self:getScaleDown()
1,994✔
980
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
997✔
981
   local mathMetrics = self:getMathMetrics()
997✔
982
   local glyphs = SILE.shaper:shapeToken(self.text, self.font)
997✔
983
   -- Use bigger variants for big operators in display style
984
   if isDisplayMode(self.mode) and self.largeop then
1,994✔
985
      -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
986
      glyphs = pl.tablex.deepcopy(glyphs)
38✔
987
      local constructions = mathMetrics.mathVariants.vertGlyphConstructions[glyphs[1].gid]
19✔
988
      if constructions then
19✔
989
         local displayVariants = constructions.mathGlyphVariantRecord
19✔
990
         -- We select the biggest variant. TODO: we should probably select the
991
         -- first variant that is higher than displayOperatorMinHeight.
992
         local biggest
993
         local m = 0
19✔
994
         for _, v in ipairs(displayVariants) do
57✔
995
            if v.advanceMeasurement > m then
38✔
996
               biggest = v
38✔
997
               m = v.advanceMeasurement
38✔
998
            end
999
         end
1000
         if biggest then
19✔
1001
            glyphs[1].gid = biggest.variantGlyph
19✔
1002
            local dimen = hb.get_glyph_dimensions(face, self.font.size, biggest.variantGlyph)
19✔
1003
            glyphs[1].width = dimen.width
19✔
1004
            glyphs[1].glyphAdvance = dimen.glyphAdvance
19✔
1005
            --[[ I am told (https://github.com/alif-type/xits/issues/90) that,
1006
        in fact, the relative height and depth of display-style big operators
1007
        in the font is not relevant, as these should be centered around the
1008
        axis. So the following code does that, while conserving their
1009
        vertical size (distance from top to bottom). ]]
1010
            local axisHeight = mathMetrics.constants.axisHeight * self:getScaleDown()
38✔
1011
            local y_size = dimen.height + dimen.depth
19✔
1012
            glyphs[1].height = y_size / 2 + axisHeight
19✔
1013
            glyphs[1].depth = y_size / 2 - axisHeight
19✔
1014
            -- We still need to store the font's height and depth somewhere,
1015
            -- because that's what will be used to draw the glyph, and we will need
1016
            -- to artificially compensate for that.
1017
            glyphs[1].fontHeight = dimen.height
19✔
1018
            glyphs[1].fontDepth = dimen.depth
19✔
1019
         end
1020
      end
1021
   end
1022
   SILE.shaper:preAddNodes(glyphs, self.value)
997✔
1023
   self.value.items = glyphs
997✔
1024
   self.value.glyphString = {}
997✔
1025
   if glyphs and #glyphs > 0 then
997✔
1026
      for i = 1, #glyphs do
2,241✔
1027
         table.insert(self.value.glyphString, glyphs[i].gid)
1,244✔
1028
      end
1029
      self.width = SILE.types.length(0)
1,994✔
1030
      self.widthForSubscript = SILE.types.length(0)
1,994✔
1031
      for i = #glyphs, 1, -1 do
2,241✔
1032
         self.width = self.width + glyphs[i].glyphAdvance
2,488✔
1033
      end
1034
      -- Store width without italic correction somewhere
1035
      self.widthForSubscript = self.width
997✔
1036
      local itCorr = mathMetrics.italicsCorrection[glyphs[#glyphs].gid]
997✔
1037
      if itCorr then
997✔
1038
         self.width = self.width + itCorr * self:getScaleDown()
630✔
1039
      end
1040
      for i = 1, #glyphs do
2,241✔
1041
         self.height = i == 1 and SILE.types.length(glyphs[i].height)
1,244✔
1042
            or SILE.types.length(math.max(self.height:tonumber(), glyphs[i].height))
1,738✔
1043
         self.depth = i == 1 and SILE.types.length(glyphs[i].depth)
1,244✔
1044
            or SILE.types.length(math.max(self.depth:tonumber(), glyphs[i].depth))
1,738✔
1045
      end
1046
   else
UNCOV
1047
      self.width = SILE.types.length(0)
×
1048
      self.height = SILE.types.length(0)
×
1049
      self.depth = SILE.types.length(0)
×
1050
   end
1051
end
1052

1053
function elements.text:stretchyReshape (depth, height)
26✔
1054
   -- Required depth+height of stretched glyph, in font units
1055
   local mathMetrics = self:getMathMetrics()
79✔
1056
   local upem = mathMetrics.unitsPerEm
79✔
1057
   local sz = self.font.size
79✔
1058
   local requiredAdvance = (depth + height):tonumber() * upem / sz
237✔
1059
   SU.debug("math", "stretch: rA =", requiredAdvance)
79✔
1060
   -- Choose variant of the closest size. The criterion we use is to have
1061
   -- an advance measurement as close as possible as the required one.
1062
   -- The advance measurement is simply the depth+height of the glyph.
1063
   -- Therefore, the selected glyph may be smaller or bigger than
1064
   -- required.  TODO: implement assembly of stretchable glyphs form
1065
   -- their parts for cases when the biggest variant is not big enough.
1066
   -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
1067
   local glyphs = pl.tablex.deepcopy(self.value.items)
79✔
1068
   local constructions = self:getMathMetrics().mathVariants.vertGlyphConstructions[glyphs[1].gid]
158✔
1069
   if constructions then
79✔
1070
      local variants = constructions.mathGlyphVariantRecord
79✔
1071
      SU.debug("math", "stretch: variants =", variants)
79✔
1072
      local closest
1073
      local closestI
1074
      local m = requiredAdvance - (self.depth + self.height):tonumber() * upem / sz
237✔
1075
      SU.debug("math", "stretch: m =", m)
79✔
1076
      for i, v in ipairs(variants) do
1,106✔
1077
         local diff = math.abs(v.advanceMeasurement - requiredAdvance)
1,027✔
1078
         SU.debug("math", "stretch: diff =", diff)
1,027✔
1079
         if diff < m then
1,027✔
1080
            closest = v
117✔
1081
            closestI = i
117✔
1082
            m = diff
117✔
1083
         end
1084
      end
1085
      SU.debug("math", "stretch: closestI =", closestI)
79✔
1086
      if closest then
79✔
1087
         -- Now we have to re-shape the glyph chain. We will assume there
1088
         -- is only one glyph.
1089
         -- TODO: this code is probably wrong when the vertical
1090
         -- variants have a different width than the original, because
1091
         -- the shaping phase is already done. Need to do better.
1092
         glyphs[1].gid = closest.variantGlyph
63✔
1093
         local face = SILE.font.cache(self.font, SILE.shaper.getFace)
63✔
1094
         local dimen = hb.get_glyph_dimensions(face, self.font.size, closest.variantGlyph)
63✔
1095
         glyphs[1].width = dimen.width
63✔
1096
         glyphs[1].height = dimen.height
63✔
1097
         glyphs[1].depth = dimen.depth
63✔
1098
         glyphs[1].glyphAdvance = dimen.glyphAdvance
63✔
1099
         self.width = SILE.types.length(dimen.glyphAdvance)
126✔
1100
         self.depth = SILE.types.length(dimen.depth)
126✔
1101
         self.height = SILE.types.length(dimen.height)
126✔
1102
         SILE.shaper:preAddNodes(glyphs, self.value)
63✔
1103
         self.value.items = glyphs
63✔
1104
         self.value.glyphString = { glyphs[1].gid }
63✔
1105
      end
1106
   end
1107
end
1108

1109
function elements.text:output (x, y, line)
26✔
1110
   if not self.value.glyphString then
997✔
UNCOV
1111
      return
×
1112
   end
1113
   local compensatedY
1114
   if isDisplayMode(self.mode) and self.atom == atomType.bigOperator and self.value.items[1].fontDepth then
1,994✔
1115
      compensatedY = SILE.types.length(y.length + self.value.items[1].depth - self.value.items[1].fontDepth)
40✔
1116
   else
1117
      compensatedY = y
987✔
1118
   end
1119
   SILE.outputter:setCursor(scaleWidth(x, line), compensatedY.length)
1,994✔
1120
   SILE.outputter:setFont(self.font)
997✔
1121
   -- There should be no stretch or shrink on the width of a text
1122
   -- element.
1123
   local width = self.width.length
997✔
1124
   SILE.outputter:drawHbox(self.value, width)
997✔
1125
end
1126

1127
elements.fraction = pl.class(elements.mbox)
26✔
1128
elements.fraction._type = "Fraction"
13✔
1129

1130
function elements.fraction:__tostring ()
26✔
1131
   return self._type .. "(" .. tostring(self.numerator) .. ", " .. tostring(self.denominator) .. ")"
×
1132
end
1133

1134
function elements.fraction:_init (numerator, denominator)
26✔
1135
   elements.mbox._init(self)
33✔
1136
   self.numerator = numerator
33✔
1137
   self.denominator = denominator
33✔
1138
   table.insert(self.children, numerator)
33✔
1139
   table.insert(self.children, denominator)
33✔
1140
end
1141

1142
function elements.fraction:styleChildren ()
26✔
1143
   self.numerator.mode = getNumeratorMode(self.mode)
66✔
1144
   self.denominator.mode = getDenominatorMode(self.mode)
66✔
1145
end
1146

1147
function elements.fraction:shape ()
26✔
1148
   -- MathML Core 3.3.2: "To avoid visual confusion between the fraction bar
1149
   -- and another adjacent items (e.g. minus sign or another fraction's bar),"
1150
   -- By convention, here we use 1px = 1/96in = 0.75pt.
1151
   -- Note that PlainTeX would likely use \nulldelimiterspace (default 1.2pt)
1152
   -- but it would depend on the surrounding context, and might be far too
1153
   -- much in some cases, so we stick to MathML's suggested padding.
1154
   self.padding = SILE.types.length(0.75)
66✔
1155

1156
   -- Determine relative abscissas and width
1157
   local widest, other
1158
   if self.denominator.width > self.numerator.width then
33✔
1159
      widest, other = self.denominator, self.numerator
27✔
1160
   else
1161
      widest, other = self.numerator, self.denominator
6✔
1162
   end
1163
   widest.relX = self.padding
33✔
1164
   other.relX = self.padding + (widest.width - other.width) / 2
132✔
1165
   self.width = widest.width + 2 * self.padding
99✔
1166
   -- Determine relative ordinates and height
1167
   local constants = self:getMathMetrics().constants
66✔
1168
   local scaleDown = self:getScaleDown()
33✔
1169
   self.axisHeight = constants.axisHeight * scaleDown
33✔
1170
   self.ruleThickness = constants.fractionRuleThickness * scaleDown
33✔
1171

1172
   local numeratorGapMin, denominatorGapMin, numeratorShiftUp, denominatorShiftDown
1173
   if isDisplayMode(self.mode) then
66✔
1174
      numeratorGapMin = constants.fractionNumDisplayStyleGapMin * scaleDown
10✔
1175
      denominatorGapMin = constants.fractionDenomDisplayStyleGapMin * scaleDown
10✔
1176
      numeratorShiftUp = constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
10✔
1177
      denominatorShiftDown = constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
10✔
1178
   else
1179
      numeratorGapMin = constants.fractionNumeratorGapMin * scaleDown
23✔
1180
      denominatorGapMin = constants.fractionDenominatorGapMin * scaleDown
23✔
1181
      numeratorShiftUp = constants.fractionNumeratorShiftUp * scaleDown
23✔
1182
      denominatorShiftDown = constants.fractionDenominatorShiftDown * scaleDown
23✔
1183
   end
1184

1185
   self.numerator.relY = -self.axisHeight
33✔
1186
      - self.ruleThickness / 2
33✔
1187
      - SILE.types.length(
66✔
1188
         math.max(
66✔
1189
            (numeratorGapMin + self.numerator.depth):tonumber(),
66✔
1190
            numeratorShiftUp - self.axisHeight - self.ruleThickness / 2
33✔
1191
         )
1192
      )
66✔
1193
   self.denominator.relY = -self.axisHeight
33✔
1194
      + self.ruleThickness / 2
33✔
1195
      + SILE.types.length(
66✔
1196
         math.max(
66✔
1197
            (denominatorGapMin + self.denominator.height):tonumber(),
66✔
1198
            denominatorShiftDown + self.axisHeight - self.ruleThickness / 2
33✔
1199
         )
1200
      )
66✔
1201
   self.height = self.numerator.height - self.numerator.relY
66✔
1202
   self.depth = self.denominator.relY + self.denominator.depth
66✔
1203
end
1204

1205
function elements.fraction:output (x, y, line)
26✔
1206
   SILE.outputter:drawRule(
66✔
1207
      scaleWidth(x + self.padding, line),
66✔
1208
      y.length - self.axisHeight - self.ruleThickness / 2,
66✔
1209
      scaleWidth(self.width - 2 * self.padding, line),
99✔
1210
      self.ruleThickness
1211
   )
33✔
1212
end
1213

1214
local function newSubscript (spec)
1215
   return elements.subscript(spec.base, spec.sub, spec.sup)
114✔
1216
end
1217

1218
local function newUnderOver (spec)
1219
   return elements.underOver(spec.base, spec.sub, spec.sup)
25✔
1220
end
1221

1222
-- TODO replace with penlight equivalent
1223
local function mapList (f, l)
1224
   local ret = {}
9✔
1225
   for i, x in ipairs(l) do
35✔
1226
      ret[i] = f(i, x)
52✔
1227
   end
1228
   return ret
9✔
1229
end
1230

1231
elements.mtr = pl.class(elements.mbox)
26✔
1232
-- elements.mtr._type = "" -- TODO why not set?
1233

1234
function elements.mtr:_init (children)
26✔
1235
   self.children = children
12✔
1236
end
1237

1238
function elements.mtr:styleChildren ()
26✔
1239
   for _, c in ipairs(self.children) do
48✔
1240
      c.mode = self.mode
36✔
1241
   end
1242
end
1243

1244
function elements.mtr.shape (_) end -- done by parent table
25✔
1245

1246
function elements.mtr.output (_) end
25✔
1247

1248
elements.table = pl.class(elements.mbox)
26✔
1249
elements.table._type = "table" -- TODO why case difference?
13✔
1250

1251
function elements.table:_init (children, options)
26✔
1252
   elements.mbox._init(self)
9✔
1253
   self.children = children
9✔
1254
   self.options = options
9✔
1255
   self.nrows = #self.children
9✔
1256
   self.ncols = math.max(pl.utils.unpack(mapList(function (_, row)
36✔
1257
      return #row.children
26✔
1258
   end, self.children)))
18✔
1259
   SU.debug("math", "self.ncols =", self.ncols)
9✔
1260
   self.rowspacing = self.options.rowspacing and SILE.types.length(self.options.rowspacing) or SILE.types.length("7pt")
18✔
1261
   self.columnspacing = self.options.columnspacing and SILE.types.length(self.options.columnspacing)
9✔
1262
      or SILE.types.length("6pt")
18✔
1263
   -- Pad rows that do not have enough cells by adding cells to the
1264
   -- right.
1265
   for i, row in ipairs(self.children) do
35✔
1266
      for j = 1, (self.ncols - #row.children) do
26✔
UNCOV
1267
         SU.debug("math", "padding i =", i, "j =", j)
×
UNCOV
1268
         table.insert(row.children, elements.stackbox("H", {}))
×
UNCOV
1269
         SU.debug("math", "size", #row.children)
×
1270
      end
1271
   end
1272
   if options.columnalign then
9✔
1273
      local l = {}
5✔
1274
      for w in string.gmatch(options.columnalign, "[^%s]+") do
20✔
1275
         if not (w == "left" or w == "center" or w == "right") then
15✔
1276
            SU.error("Invalid specifier in `columnalign` attribute: " .. w)
×
1277
         end
1278
         table.insert(l, w)
15✔
1279
      end
1280
      -- Pad with last value of l if necessary
1281
      for _ = 1, (self.ncols - #l), 1 do
5✔
UNCOV
1282
         table.insert(l, l[#l])
×
1283
      end
1284
      -- On the contrary, remove excess values in l if necessary
1285
      for _ = 1, (#l - self.ncols), 1 do
5✔
1286
         table.remove(l)
×
1287
      end
1288
      self.options.columnalign = l
5✔
1289
   else
1290
      self.options.columnalign = pl.List.range(1, self.ncols):map(function (_)
14✔
1291
         return "center"
12✔
1292
      end)
1293
   end
1294
end
1295

1296
function elements.table:styleChildren ()
26✔
1297
   if self.mode == mathMode.display and self.options.displaystyle ~= "false" then
9✔
1298
      for _, c in ipairs(self.children) do
19✔
1299
         c.mode = mathMode.display
14✔
1300
      end
1301
   else
1302
      for _, c in ipairs(self.children) do
16✔
1303
         c.mode = mathMode.text
12✔
1304
      end
1305
   end
1306
end
1307

1308
function elements.table:shape ()
26✔
1309
   -- Determine the height (resp. depth) of each row, which is the max
1310
   -- height (resp. depth) among its elements. Then we only need to add it to
1311
   -- the table's height and center every cell vertically.
1312
   for _, row in ipairs(self.children) do
35✔
1313
      row.height = SILE.types.length(0)
52✔
1314
      row.depth = SILE.types.length(0)
52✔
1315
      for _, cell in ipairs(row.children) do
104✔
1316
         row.height = maxLength(row.height, cell.height)
156✔
1317
         row.depth = maxLength(row.depth, cell.depth)
156✔
1318
      end
1319
   end
1320
   self.vertSize = SILE.types.length(0)
18✔
1321
   for i, row in ipairs(self.children) do
35✔
1322
      self.vertSize = self.vertSize
×
1323
         + row.height
26✔
1324
         + row.depth
26✔
1325
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
61✔
1326
   end
1327
   local rowHeightSoFar = SILE.types.length(0)
9✔
1328
   for i, row in ipairs(self.children) do
35✔
1329
      row.relY = rowHeightSoFar + row.height - self.vertSize
78✔
1330
      rowHeightSoFar = rowHeightSoFar
×
1331
         + row.height
26✔
1332
         + row.depth
26✔
1333
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
35✔
1334
   end
1335
   self.width = SILE.types.length(0)
18✔
1336
   local thisColRelX = SILE.types.length(0)
9✔
1337
   -- For every column...
1338
   for i = 1, self.ncols do
36✔
1339
      -- Determine its width
1340
      local columnWidth = SILE.types.length(0)
27✔
1341
      for j = 1, self.nrows do
105✔
1342
         if self.children[j].children[i].width > columnWidth then
78✔
1343
            columnWidth = self.children[j].children[i].width
37✔
1344
         end
1345
      end
1346
      -- Use it to align the contents of every cell as required.
1347
      for j = 1, self.nrows do
105✔
1348
         local cell = self.children[j].children[i]
78✔
1349
         if self.options.columnalign[i] == "left" then
78✔
1350
            cell.relX = thisColRelX
14✔
1351
         elseif self.options.columnalign[i] == "center" then
64✔
1352
            cell.relX = thisColRelX + (columnWidth - cell.width) / 2
200✔
1353
         elseif self.options.columnalign[i] == "right" then
14✔
1354
            cell.relX = thisColRelX + (columnWidth - cell.width)
42✔
1355
         else
UNCOV
1356
            SU.error("invalid columnalign parameter")
×
1357
         end
1358
      end
1359
      thisColRelX = thisColRelX + columnWidth + (i == self.ncols and SILE.types.length(0) or self.columnspacing) -- Spacing
63✔
1360
   end
1361
   self.width = thisColRelX
9✔
1362
   -- Center myself vertically around the axis, and update relative Ys of rows accordingly
1363
   local axisHeight = self:getMathMetrics().constants.axisHeight * self:getScaleDown()
27✔
1364
   self.height = self.vertSize / 2 + axisHeight
27✔
1365
   self.depth = self.vertSize / 2 - axisHeight
27✔
1366
   for _, row in ipairs(self.children) do
35✔
1367
      row.relY = row.relY + self.vertSize / 2 - axisHeight
104✔
1368
      -- Also adjust width
1369
      row.width = self.width
26✔
1370
   end
1371
end
1372

1373
function elements.table.output (_) end
22✔
1374

1375
local function getRadicandMode (mode)
1376
   -- Not too sure if we should do something special/
NEW
1377
   return mode
×
1378
end
1379

1380
local function getDegreeMode (mode)
1381
   -- 2 levels smaller, up to scriptScript evntually.
1382
   -- Not too sure if we should do something else.
NEW
1383
   if mode == mathMode.display then
×
NEW
1384
      return mathMode.scriptScript
×
NEW
1385
   elseif mode == mathMode.displayCramped then
×
NEW
1386
      return mathMode.scriptScriptCramped
×
NEW
1387
   elseif mode == mathMode.text or mode == mathMode.script or mode == mathMode.scriptScript then
×
NEW
1388
      return mathMode.scriptScript
×
1389
   end
NEW
1390
   return mathMode.scriptScriptCramped
×
1391
end
1392

1393
elements.sqrt = pl.class(elements.mbox)
26✔
1394
elements.sqrt._type = "Sqrt"
13✔
1395

1396
function elements.sqrt:__tostring ()
26✔
NEW
1397
   return self._type .. "(" .. tostring(self.radicand) .. (self.degree and ", " .. tostring(self.degree) or "") .. ")"
×
1398
end
1399

1400
function elements.sqrt:_init (radicand, degree)
26✔
UNCOV
1401
   elements.mbox._init(self)
×
1402
   self.radicand = radicand
×
NEW
1403
   if degree then
×
NEW
1404
      self.degree = degree
×
NEW
1405
      table.insert(self.children, degree)
×
1406
   end
UNCOV
1407
   table.insert(self.children, radicand)
×
NEW
1408
   self.relX = SILE.types.length()
×
NEW
1409
   self.relY = SILE.types.length()
×
1410
end
1411

1412
function elements.sqrt:styleChildren ()
26✔
NEW
1413
   self.radicand.mode = getRadicandMode(self.mode)
×
NEW
1414
   if self.degree then
×
NEW
1415
      self.degree.mode = getDegreeMode(self.mode)
×
1416
   end
1417
end
1418

1419
function elements.sqrt:shape ()
26✔
1420
   local mathMetrics = self:getMathMetrics()
×
1421
   local scaleDown = self:getScaleDown()
×
1422
   local constants = mathMetrics.constants
×
1423

UNCOV
1424
   self.radicalRuleThickness = constants.radicalRuleThickness * scaleDown
×
UNCOV
1425
   if self.mode == mathMode.display or self.mode == mathMode.displayCramped then
×
UNCOV
1426
      self.radicalVerticalGap = constants.radicalDisplayStyleVerticalGap * scaleDown
×
1427
   else
1428
      self.radicalVerticalGap = constants.radicalVerticalGap * scaleDown
×
1429
   end
1430
   self.extraAscender = constants.radicalExtraAscender * scaleDown
×
1431

1432
   -- HACK: We draw own own radical sign in the output() method.
1433
   -- Derive dimensions for the radical sign (more or less ad hoc).
1434
   -- Note: In TeX, the radical sign extends a lot below the baseline,
1435
   -- and MathML Core also has a lot of layout text about it.
1436
   -- Not only it doesn't look good, but it's not very clear vs. OpenType.
NEW
1437
   local radicalGlyph = SILE.shaper:measureChar("√")
×
NEW
1438
   local ratio = (self.radicand.height:tonumber() + self.radicand.depth:tonumber())
×
NEW
1439
      / (radicalGlyph.height + radicalGlyph.depth)
×
NEW
1440
   local vertAdHocOffset = (ratio > 1 and math.log(ratio) or 0) * self.radicalVerticalGap
×
NEW
1441
   self.symbolHeight = SILE.types.length(radicalGlyph.height) * scaleDown
×
NEW
1442
   self.symbolDepth = (SILE.types.length(radicalGlyph.depth) + vertAdHocOffset) * scaleDown
×
NEW
1443
   self.symbolWidth = (SILE.types.length(radicalGlyph.width) + vertAdHocOffset) * scaleDown
×
1444

1445
   -- Adjust the height of the radical sign if the radicand is higher
NEW
1446
   self.symbolHeight = self.radicand.height > self.symbolHeight and self.radicand.height or self.symbolHeight
×
1447
   -- Compute the (max-)height of the short leg of the radical sign
NEW
1448
   self.symbolShortHeight = self.symbolHeight * constants.radicalDegreeBottomRaisePercent
×
1449

NEW
1450
   self.offsetX = SILE.types.length()
×
NEW
1451
   if self.degree then
×
1452
      -- Position the degree
NEW
1453
      self.degree.relY = -constants.radicalDegreeBottomRaisePercent * self.symbolHeight
×
1454
      -- Adjust the height of the short leg of the radical sign to ensure the degree is not too close
1455
      -- (empirically use radicalExtraAscender)
NEW
1456
      self.symbolShortHeight = self.symbolShortHeight - constants.radicalExtraAscender * scaleDown
×
1457
      -- Compute the width adjustment for the degree
NEW
1458
      self.offsetX = self.degree.width
×
NEW
1459
         + constants.radicalKernBeforeDegree * scaleDown
×
NEW
1460
         + constants.radicalKernAfterDegree * scaleDown
×
1461
   end
1462
   -- Position the radicand
NEW
1463
   self.radicand.relX = self.symbolWidth + self.offsetX
×
1464
   -- Compute the dimentions of the whole radical
NEW
1465
   self.width = self.radicand.width + self.symbolWidth + self.offsetX
×
NEW
1466
   self.height = self.symbolHeight + self.radicalVerticalGap + self.extraAscender
×
UNCOV
1467
   self.depth = self.radicand.depth
×
1468
end
1469

1470
local function _r (number)
1471
   -- Lua 5.3+ formats floats as 1.0 and integers as 1
1472
   -- Also some PDF readers do not like double precision.
1473
   return math.floor(number) == number and math.floor(number) or tonumber(string.format("%.5f", number))
×
1474
end
1475

1476
function elements.sqrt:output (x, y, line)
26✔
1477
   -- HACK:
1478
   -- OpenType might say we need to assemble the radical sign from parts.
1479
   -- Frankly, it's much easier to just draw it as a graphic :-)
1480
   -- Hence, here we use a PDF graphic operators to draw a nice radical sign.
1481
   -- Some values here are ad hoc, but they look good.
1482
   local h = self.height:tonumber()
×
1483
   local d = self.depth:tonumber()
×
NEW
1484
   local s0 = scaleWidth(self.offsetX, line):tonumber()
×
NEW
1485
   local sw = scaleWidth(self.symbolWidth, line):tonumber()
×
NEW
1486
   local dsh = h - self.symbolShortHeight:tonumber()
×
NEW
1487
   local dsd = self.symbolDepth:tonumber()
×
1488
   local symbol = {
×
1489
      _r(self.radicalRuleThickness),
×
1490
      "w", -- line width
1491
      2,
1492
      "j", -- round line joins
NEW
1493
      _r(sw + s0),
×
UNCOV
1494
      _r(self.extraAscender),
×
1495
      "m",
NEW
1496
      _r(s0 + sw * 0.90),
×
NEW
1497
      _r(self.extraAscender),
×
1498
      "l",
NEW
1499
      _r(s0 + sw * 0.4),
×
NEW
1500
      _r(h + d + dsd),
×
1501
      "l",
NEW
1502
      _r(s0 + sw * 0.2),
×
NEW
1503
      _r(dsh),
×
1504
      "l",
NEW
1505
      s0 + sw * 0.1,
×
NEW
1506
      _r(dsh + 0.5),
×
1507
      "l",
1508
      "S",
1509
   }
UNCOV
1510
   local svg = table.concat(symbol, " ")
×
NEW
1511
   local xscaled = scaleWidth(x, line)
×
NEW
1512
   SILE.outputter:drawSVG(svg, xscaled, y, sw, h, 1)
×
1513
   -- And now we just need to draw the bar over the radicand
UNCOV
1514
   SILE.outputter:drawRule(
×
NEW
1515
      s0 + self.symbolWidth + xscaled,
×
NEW
1516
      y.length - self.height + self.extraAscender - self.radicalRuleThickness / 2,
×
UNCOV
1517
      scaleWidth(self.radicand.width, line),
×
1518
      self.radicalRuleThickness
1519
   )
1520
end
1521

1522
elements.mathMode = mathMode
13✔
1523
elements.atomType = atomType
13✔
1524
elements.scriptType = scriptType
13✔
1525
elements.mathVariantToScriptType = mathVariantToScriptType
13✔
1526
elements.symbolDefaults = symbolDefaults
13✔
1527
elements.newSubscript = newSubscript
13✔
1528
elements.newUnderOver = newUnderOver
13✔
1529

1530
return elements
13✔
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