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

sile-typesetter / sile / 11574339945

29 Oct 2024 12:58PM UTC coverage: 34.309% (-33.9%) from 68.22%
11574339945

push

github

web-flow
Merge pull request #2142 from Omikhleia/fix-math-fraction-padding

fix(math): Fractions must have padding to avoid visual confusion

0 of 26 new or added lines in 1 file covered. (0.0%)

5779 existing lines in 69 files now uncovered.

6014 of 17529 relevant lines covered (34.31%)

4588.59 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 syms = require("packages.math.unicode-symbols")
×
5

UNCOV
6
local atomType = syms.atomType
×
UNCOV
7
local symbolDefaults = syms.symbolDefaults
×
8

UNCOV
9
local elements = {}
×
10

UNCOV
11
local mathMode = {
×
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

UNCOV
22
local scriptType = {
×
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)
UNCOV
40
   return attr == "normal" and scriptType.upright
×
UNCOV
41
      or attr == "bold" and scriptType.bold
×
UNCOV
42
      or attr == "italic" and scriptType.italic
×
UNCOV
43
      or attr == "bold-italic" and scriptType.boldItalic
×
UNCOV
44
      or attr == "double-struck" and scriptType.doubleStruck
×
UNCOV
45
      or SU.error('Invalid value "' .. attr .. '" for option mathvariant')
×
46
end
47

48
local function isDisplayMode (mode)
UNCOV
49
   return mode <= 1
×
50
end
51

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

56
local function isScriptMode (mode)
UNCOV
57
   return mode == mathMode.script or mode == mathMode.scriptCramped
×
58
end
59

60
local function isScriptScriptMode (mode)
UNCOV
61
   return mode == mathMode.scriptScript or mode == mathMode.scriptScriptCramped
×
62
end
63

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

UNCOV
108
local mathCache = {}
×
109

110
local function retrieveMathTable (font)
UNCOV
111
   local key = SILE.font._key(font)
×
UNCOV
112
   if not mathCache[key] then
×
UNCOV
113
      SU.debug("math", "Loading math font", key)
×
UNCOV
114
      local face = SILE.font.cache(font, SILE.shaper.getFace)
×
UNCOV
115
      if not face then
×
116
         SU.error("Could not find requested font " .. font .. " or any suitable substitutes")
×
117
      end
118
      local fontHasMathTable, rawMathTable, mathTableParsable, mathTable
UNCOV
119
      fontHasMathTable, rawMathTable = pcall(hb.get_table, face, "MATH")
×
UNCOV
120
      if fontHasMathTable then
×
UNCOV
121
         mathTableParsable, mathTable = pcall(ot.parseMath, rawMathTable)
×
122
      end
UNCOV
123
      if not fontHasMathTable or not mathTableParsable then
×
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
UNCOV
130
      local upem = ot.parseHead(hb.get_table(face, "head")).unitsPerEm
×
UNCOV
131
      local constants = {}
×
UNCOV
132
      for k, v in pairs(mathTable.mathConstants) do
×
UNCOV
133
         if type(v) == "table" then
×
UNCOV
134
            v = v.value
×
135
         end
UNCOV
136
         if k:sub(-9) == "ScaleDown" then
×
UNCOV
137
            constants[k] = v / 100
×
138
         else
UNCOV
139
            constants[k] = v * font.size / upem
×
140
         end
141
      end
UNCOV
142
      local italicsCorrection = {}
×
UNCOV
143
      for k, v in pairs(mathTable.mathItalicsCorrection) do
×
UNCOV
144
         italicsCorrection[k] = v.value * font.size / upem
×
145
      end
UNCOV
146
      mathCache[key] = {
×
147
         constants = constants,
148
         italicsCorrection = italicsCorrection,
149
         mathVariants = mathTable.mathVariants,
150
         unitsPerEm = upem,
151
      }
152
   end
UNCOV
153
   return mathCache[key]
×
154
end
155

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

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

222
local function getRightMostGlyphId (node)
UNCOV
223
   while node and node:is_a(elements.stackbox) and node.direction == "H" do
×
UNCOV
224
      node = node.children[#node.children]
×
225
   end
UNCOV
226
   if node and node:is_a(elements.text) then
×
UNCOV
227
      return node.value.glyphString[#node.value.glyphString]
×
228
   else
UNCOV
229
      return 0
×
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 (...)
UNCOV
236
   local arg = { ... }
×
237
   local m
UNCOV
238
   for i, v in ipairs(arg) do
×
UNCOV
239
      if i == 1 then
×
UNCOV
240
         m = v
×
241
      else
UNCOV
242
         if v.length:tonumber() > m.length:tonumber() then
×
UNCOV
243
            m = v
×
244
         end
245
      end
246
   end
UNCOV
247
   return m
×
248
end
249

250
local function scaleWidth (length, line)
UNCOV
251
   local number = length.length
×
UNCOV
252
   if line.ratio and line.ratio < 0 and length.shrink:tonumber() > 0 then
×
253
      number = number + length.shrink * line.ratio
×
UNCOV
254
   elseif line.ratio and line.ratio > 0 and length.stretch:tonumber() > 0 then
×
UNCOV
255
      number = number + length.stretch * line.ratio
×
256
   end
UNCOV
257
   return number
×
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
UNCOV
267
elements.mbox = pl.class(nodefactory.hbox)
×
UNCOV
268
elements.mbox._type = "Mbox"
×
269

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

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

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

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

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

UNCOV
308
function elements.mbox:getMathMetrics ()
×
UNCOV
309
   return retrieveMathTable(self.font)
×
310
end
311

UNCOV
312
function elements.mbox:getScaleDown ()
×
UNCOV
313
   local constants = self:getMathMetrics().constants
×
314
   local scaleDown
UNCOV
315
   if isScriptMode(self.mode) then
×
UNCOV
316
      scaleDown = constants.scriptPercentScaleDown
×
UNCOV
317
   elseif isScriptScriptMode(self.mode) then
×
UNCOV
318
      scaleDown = constants.scriptScriptPercentScaleDown
×
319
   else
UNCOV
320
      scaleDown = 1
×
321
   end
UNCOV
322
   return scaleDown
×
323
end
324

325
-- Determine the mode of its descendants
UNCOV
326
function elements.mbox:styleDescendants ()
×
UNCOV
327
   self:styleChildren()
×
UNCOV
328
   for _, n in ipairs(self.children) do
×
UNCOV
329
      if n then
×
UNCOV
330
         n:styleDescendants()
×
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
UNCOV
339
function elements.mbox:shapeTree ()
×
UNCOV
340
   for _, n in ipairs(self.children) do
×
UNCOV
341
      if n then
×
UNCOV
342
         n:shapeTree()
×
343
      end
344
   end
UNCOV
345
   self:shape()
×
346
end
347

348
-- Output the node and all its descendants
UNCOV
349
function elements.mbox:outputTree (x, y, line)
×
UNCOV
350
   self:output(x, y, line)
×
UNCOV
351
   local debug = SILE.settings:get("math.debug.boxes")
×
UNCOV
352
   if debug and not (self:is_a(elements.space)) then
×
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
UNCOV
356
   for _, n in ipairs(self.children) do
×
UNCOV
357
      if n then
×
UNCOV
358
         n:outputTree(x + n.relX, y + n.relY, line)
×
359
      end
360
   end
361
end
362

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

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

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

UNCOV
425
function elements.stackbox:__tostring ()
×
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

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

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

UNCOV
475
function elements.stackbox:shape ()
×
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
UNCOV
484
   self.height = SILE.types.length(0)
×
UNCOV
485
   self.depth = SILE.types.length(0)
×
UNCOV
486
   if self.direction == "H" then
×
UNCOV
487
      for i, n in ipairs(self.children) do
×
UNCOV
488
         n.relY = SILE.types.length(0)
×
UNCOV
489
         self.height = i == 1 and n.height or maxLength(self.height, n.height)
×
UNCOV
490
         self.depth = i == 1 and n.depth or maxLength(self.depth, n.depth)
×
491
      end
492
      -- Handle stretchy operators
UNCOV
493
      for _, elt in ipairs(self.children) do
×
UNCOV
494
         if elt.is_a(elements.text) and elt.kind == "operator" and elt.stretchy then
×
UNCOV
495
            elt:stretchyReshape(self.depth, self.height)
×
496
         end
497
      end
498
      -- Set self.width
UNCOV
499
      self.width = SILE.types.length(0)
×
UNCOV
500
      for i, n in ipairs(self.children) do
×
UNCOV
501
         n.relX = self.width
×
UNCOV
502
         self.width = i == 1 and n.width or self.width + n.width
×
503
      end
504
   else -- self.direction == "V"
UNCOV
505
      for i, n in ipairs(self.children) do
×
UNCOV
506
         n.relX = SILE.types.length(0)
×
UNCOV
507
         self.width = i == 1 and n.width or maxLength(self.width, n.width)
×
508
      end
509
      -- Set self.height and self.depth
UNCOV
510
      for i, n in ipairs(self.children) do
×
UNCOV
511
         self.depth = i == 1 and n.depth or self.depth + n.depth
×
512
      end
UNCOV
513
      for i = 1, #self.children do
×
UNCOV
514
         local n = self.children[i]
×
UNCOV
515
         if i == 1 then
×
UNCOV
516
            self.height = n.height
×
UNCOV
517
            self.depth = n.depth
×
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.
UNCOV
527
function elements.stackbox:outputYourself (typesetter, line)
×
UNCOV
528
   local mathX = typesetter.frame.state.cursorX
×
UNCOV
529
   local mathY = typesetter.frame.state.cursorY
×
UNCOV
530
   self:outputTree(self.relX + mathX, self.relY + mathY, line)
×
UNCOV
531
   typesetter.frame:advanceWritingDirection(scaleWidth(self.width, line))
×
532
end
533

UNCOV
534
function elements.stackbox.output (_, _, _, _) end
×
535

UNCOV
536
elements.subscript = pl.class(elements.mbox)
×
UNCOV
537
elements.subscript._type = "Subscript"
×
538

UNCOV
539
function elements.subscript:__tostring ()
×
540
   return (self.sub and "Subscript" or "Superscript")
×
541
      .. "("
×
542
      .. tostring(self.base)
×
543
      .. ", "
×
544
      .. tostring(self.sub or self.super)
×
545
      .. ")"
×
546
end
547

UNCOV
548
function elements.subscript:_init (base, sub, sup)
×
UNCOV
549
   elements.mbox._init(self)
×
UNCOV
550
   self.base = base
×
UNCOV
551
   self.sub = sub
×
UNCOV
552
   self.sup = sup
×
UNCOV
553
   if self.base then
×
UNCOV
554
      table.insert(self.children, self.base)
×
555
   end
UNCOV
556
   if self.sub then
×
UNCOV
557
      table.insert(self.children, self.sub)
×
558
   end
UNCOV
559
   if self.sup then
×
UNCOV
560
      table.insert(self.children, self.sup)
×
561
   end
UNCOV
562
   self.atom = self.base.atom
×
563
end
564

UNCOV
565
function elements.subscript:styleChildren ()
×
UNCOV
566
   if self.base then
×
UNCOV
567
      self.base.mode = self.mode
×
568
   end
UNCOV
569
   if self.sub then
×
UNCOV
570
      self.sub.mode = getSubscriptMode(self.mode)
×
571
   end
UNCOV
572
   if self.sup then
×
UNCOV
573
      self.sup.mode = getSuperscriptMode(self.mode)
×
574
   end
575
end
576

UNCOV
577
function elements.subscript:calculateItalicsCorrection ()
×
UNCOV
578
   local lastGid = getRightMostGlyphId(self.base)
×
UNCOV
579
   if lastGid > 0 then
×
UNCOV
580
      local mathMetrics = self:getMathMetrics()
×
UNCOV
581
      if mathMetrics.italicsCorrection[lastGid] then
×
UNCOV
582
         return mathMetrics.italicsCorrection[lastGid]
×
583
      end
584
   end
UNCOV
585
   return 0
×
586
end
587

UNCOV
588
function elements.subscript:shape ()
×
UNCOV
589
   local mathMetrics = self:getMathMetrics()
×
UNCOV
590
   local constants = mathMetrics.constants
×
UNCOV
591
   local scaleDown = self:getScaleDown()
×
UNCOV
592
   if self.base then
×
UNCOV
593
      self.base.relX = SILE.types.length(0)
×
UNCOV
594
      self.base.relY = SILE.types.length(0)
×
595
      -- Use widthForSubscript of base, if available
UNCOV
596
      self.width = self.base.widthForSubscript or self.base.width
×
597
   else
598
      self.width = SILE.types.length(0)
×
599
   end
UNCOV
600
   local itCorr = self:calculateItalicsCorrection() * scaleDown
×
601
   local subShift
602
   local supShift
UNCOV
603
   if self.sub then
×
UNCOV
604
      if self.isUnderOver or self.base.largeop then
×
605
         -- Ad hoc correction on integral limits, following LuaTeX's
606
         -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
UNCOV
607
         subShift = -itCorr
×
608
      else
UNCOV
609
         subShift = 0
×
610
      end
UNCOV
611
      self.sub.relX = self.width + subShift
×
UNCOV
612
      self.sub.relY = SILE.types.length(math.max(
×
UNCOV
613
         constants.subscriptShiftDown * scaleDown,
×
614
         --self.base.depth + constants.subscriptBaselineDropMin * scaleDown,
UNCOV
615
         (self.sub.height - constants.subscriptTopMax * scaleDown):tonumber()
×
616
      ))
UNCOV
617
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or self.base.largeop then
×
UNCOV
618
         self.sub.relY = maxLength(self.sub.relY, self.base.depth + constants.subscriptBaselineDropMin * scaleDown)
×
619
      end
620
   end
UNCOV
621
   if self.sup then
×
UNCOV
622
      if self.isUnderOver or self.base.largeop then
×
623
         -- Ad hoc correction on integral limits, following LuaTeX's
624
         -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
UNCOV
625
         supShift = 0
×
626
      else
UNCOV
627
         supShift = itCorr
×
628
      end
UNCOV
629
      self.sup.relX = self.width + supShift
×
UNCOV
630
      self.sup.relY = SILE.types.length(math.max(
×
UNCOV
631
         isCrampedMode(self.mode) and constants.superscriptShiftUpCramped * scaleDown
×
UNCOV
632
            or constants.superscriptShiftUp * scaleDown, -- or cramped
×
633
         --self.base.height - constants.superscriptBaselineDropMax * scaleDown,
UNCOV
634
         (self.sup.depth + constants.superscriptBottomMin * scaleDown):tonumber()
×
UNCOV
635
      )) * -1
×
UNCOV
636
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or self.base.largeop then
×
UNCOV
637
         self.sup.relY = maxLength(
×
UNCOV
638
            (0 - self.sup.relY),
×
UNCOV
639
            self.base.height - constants.superscriptBaselineDropMax * scaleDown
×
UNCOV
640
         ) * -1
×
641
      end
642
   end
UNCOV
643
   if self.sub and self.sup then
×
UNCOV
644
      local gap = self.sub.relY - self.sub.height - self.sup.relY - self.sup.depth
×
UNCOV
645
      if gap.length:tonumber() < constants.subSuperscriptGapMin * scaleDown then
×
646
         -- The following adjustment comes directly from Appendix G of he
647
         -- TeXbook (rule 18e).
UNCOV
648
         self.sub.relY = constants.subSuperscriptGapMin * scaleDown + self.sub.height + self.sup.relY + self.sup.depth
×
UNCOV
649
         local psi = constants.superscriptBottomMaxWithSubscript * scaleDown + self.sup.relY + self.sup.depth
×
UNCOV
650
         if psi:tonumber() > 0 then
×
UNCOV
651
            self.sup.relY = self.sup.relY - psi
×
UNCOV
652
            self.sub.relY = self.sub.relY - psi
×
653
         end
654
      end
655
   end
656
   self.width = self.width
×
UNCOV
657
      + maxLength(
×
UNCOV
658
         self.sub and self.sub.width + subShift or SILE.types.length(0),
×
UNCOV
659
         self.sup and self.sup.width + supShift or SILE.types.length(0)
×
660
      )
UNCOV
661
      + constants.spaceAfterScript * scaleDown
×
UNCOV
662
   self.height = maxLength(
×
UNCOV
663
      self.base and self.base.height or SILE.types.length(0),
×
UNCOV
664
      self.sub and (self.sub.height - self.sub.relY) or SILE.types.length(0),
×
UNCOV
665
      self.sup and (self.sup.height - self.sup.relY) or SILE.types.length(0)
×
666
   )
UNCOV
667
   self.depth = maxLength(
×
UNCOV
668
      self.base and self.base.depth or SILE.types.length(0),
×
UNCOV
669
      self.sub and (self.sub.depth + self.sub.relY) or SILE.types.length(0),
×
UNCOV
670
      self.sup and (self.sup.depth + self.sup.relY) or SILE.types.length(0)
×
671
   )
672
end
673

UNCOV
674
function elements.subscript.output (_, _, _, _) end
×
675

UNCOV
676
elements.underOver = pl.class(elements.subscript)
×
UNCOV
677
elements.underOver._type = "UnderOver"
×
678

UNCOV
679
function elements.underOver:__tostring ()
×
680
   return self._type .. "(" .. tostring(self.base) .. ", " .. tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
×
681
end
682

UNCOV
683
function elements.underOver:_init (base, sub, sup)
×
UNCOV
684
   elements.mbox._init(self)
×
UNCOV
685
   self.atom = base.atom
×
UNCOV
686
   self.base = base
×
UNCOV
687
   self.sub = sub
×
UNCOV
688
   self.sup = sup
×
UNCOV
689
   if self.sup then
×
UNCOV
690
      table.insert(self.children, self.sup)
×
691
   end
UNCOV
692
   if self.base then
×
UNCOV
693
      table.insert(self.children, self.base)
×
694
   end
UNCOV
695
   if self.sub then
×
UNCOV
696
      table.insert(self.children, self.sub)
×
697
   end
698
end
699

UNCOV
700
function elements.underOver:styleChildren ()
×
UNCOV
701
   if self.base then
×
UNCOV
702
      self.base.mode = self.mode
×
703
   end
UNCOV
704
   if self.sub then
×
UNCOV
705
      self.sub.mode = getSubscriptMode(self.mode)
×
706
   end
UNCOV
707
   if self.sup then
×
UNCOV
708
      self.sup.mode = getSuperscriptMode(self.mode)
×
709
   end
710
end
711

UNCOV
712
function elements.underOver:shape ()
×
UNCOV
713
   if not (self.mode == mathMode.display or self.mode == mathMode.displayCramped) then
×
UNCOV
714
      self.isUnderOver = true
×
UNCOV
715
      elements.subscript.shape(self)
×
UNCOV
716
      return
×
717
   end
UNCOV
718
   local constants = self:getMathMetrics().constants
×
UNCOV
719
   local scaleDown = self:getScaleDown()
×
720
   -- Determine relative Ys
UNCOV
721
   if self.base then
×
UNCOV
722
      self.base.relY = SILE.types.length(0)
×
723
   end
UNCOV
724
   if self.sub then
×
UNCOV
725
      self.sub.relY = self.base.depth
×
UNCOV
726
         + SILE.types.length(
×
UNCOV
727
            math.max(
×
UNCOV
728
               (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
×
UNCOV
729
               constants.lowerLimitBaselineDropMin * scaleDown
×
730
            )
731
         )
732
   end
UNCOV
733
   if self.sup then
×
UNCOV
734
      self.sup.relY = 0
×
UNCOV
735
         - self.base.height
×
UNCOV
736
         - SILE.types.length(
×
UNCOV
737
            math.max(
×
UNCOV
738
               (constants.upperLimitGapMin * scaleDown + self.sup.depth):tonumber(),
×
UNCOV
739
               constants.upperLimitBaselineRiseMin * scaleDown
×
740
            )
741
         )
742
   end
743
   -- Determine relative Xs based on widest symbol
744
   local widest, a, b
UNCOV
745
   if self.sub and self.sub.width > self.base.width then
×
UNCOV
746
      if self.sup and self.sub.width > self.sup.width then
×
UNCOV
747
         widest = self.sub
×
UNCOV
748
         a = self.base
×
UNCOV
749
         b = self.sup
×
750
      elseif self.sup then
×
751
         widest = self.sup
×
752
         a = self.base
×
753
         b = self.sub
×
754
      else
755
         widest = self.sub
×
756
         a = self.base
×
757
         b = nil
×
758
      end
759
   else
UNCOV
760
      if self.sup and self.base.width > self.sup.width then
×
UNCOV
761
         widest = self.base
×
UNCOV
762
         a = self.sub
×
UNCOV
763
         b = self.sup
×
UNCOV
764
      elseif self.sup then
×
765
         widest = self.sup
×
766
         a = self.base
×
767
         b = self.sub
×
768
      else
UNCOV
769
         widest = self.base
×
UNCOV
770
         a = self.sub
×
UNCOV
771
         b = nil
×
772
      end
773
   end
UNCOV
774
   widest.relX = SILE.types.length(0)
×
UNCOV
775
   local c = widest.width / 2
×
UNCOV
776
   if a then
×
UNCOV
777
      a.relX = c - a.width / 2
×
778
   end
UNCOV
779
   if b then
×
UNCOV
780
      b.relX = c - b.width / 2
×
781
   end
UNCOV
782
   local itCorr = self:calculateItalicsCorrection() * scaleDown
×
UNCOV
783
   if self.sup then
×
UNCOV
784
      self.sup.relX = self.sup.relX + itCorr / 2
×
785
   end
UNCOV
786
   if self.sub then
×
UNCOV
787
      self.sub.relX = self.sub.relX - itCorr / 2
×
788
   end
789
   -- Determine width and height
UNCOV
790
   self.width = maxLength(
×
UNCOV
791
      self.base and self.base.width or SILE.types.length(0),
×
UNCOV
792
      self.sub and self.sub.width or SILE.types.length(0),
×
UNCOV
793
      self.sup and self.sup.width or SILE.types.length(0)
×
794
   )
UNCOV
795
   if self.sup then
×
UNCOV
796
      self.height = 0 - self.sup.relY + self.sup.height
×
797
   else
UNCOV
798
      self.height = self.base and self.base.height or 0
×
799
   end
UNCOV
800
   if self.sub then
×
UNCOV
801
      self.depth = self.sub.relY + self.sub.depth
×
802
   else
803
      self.depth = self.base and self.base.depth or 0
×
804
   end
805
end
806

UNCOV
807
function elements.underOver:calculateItalicsCorrection ()
×
UNCOV
808
   local lastGid = getRightMostGlyphId(self.base)
×
UNCOV
809
   if lastGid > 0 then
×
UNCOV
810
      local mathMetrics = self:getMathMetrics()
×
UNCOV
811
      if mathMetrics.italicsCorrection[lastGid] then
×
812
         local c = mathMetrics.italicsCorrection[lastGid]
×
813
         -- If this is a big operator, and we are in display style, then the
814
         -- base glyph may be bigger than the font size. We need to adjust the
815
         -- italic correction accordingly.
816
         if self.base.atom == atomType.bigOperator and isDisplayMode(self.mode) then
×
817
            c = c * (self.base and self.base.font.size / self.font.size or 1.0)
×
818
         end
819
         return c
×
820
      end
821
   end
UNCOV
822
   return 0
×
823
end
824

UNCOV
825
function elements.underOver.output (_, _, _, _) end
×
826

827
-- terminal is the base class for leaf node
UNCOV
828
elements.terminal = pl.class(elements.mbox)
×
UNCOV
829
elements.terminal._type = "Terminal"
×
830

UNCOV
831
function elements.terminal:_init ()
×
UNCOV
832
   elements.mbox._init(self)
×
833
end
834

UNCOV
835
function elements.terminal.styleChildren (_) end
×
836

UNCOV
837
function elements.terminal.shape (_) end
×
838

UNCOV
839
elements.space = pl.class(elements.terminal)
×
UNCOV
840
elements.space._type = "Space"
×
841

UNCOV
842
function elements.space:_init ()
×
843
   elements.terminal._init(self)
×
844
end
845

UNCOV
846
function elements.space:__tostring ()
×
847
   return self._type
×
848
      .. "(width="
×
849
      .. tostring(self.width)
×
850
      .. ", height="
×
851
      .. tostring(self.height)
×
852
      .. ", depth="
×
853
      .. tostring(self.depth)
×
854
      .. ")"
×
855
end
856

857
local function getStandardLength (value)
UNCOV
858
   if type(value) == "string" then
×
UNCOV
859
      local direction = 1
×
UNCOV
860
      if value:sub(1, 1) == "-" then
×
UNCOV
861
         value = value:sub(2, -1)
×
UNCOV
862
         direction = -1
×
863
      end
UNCOV
864
      if value == "thin" then
×
UNCOV
865
         return SILE.types.length("3mu") * direction
×
UNCOV
866
      elseif value == "med" then
×
UNCOV
867
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
×
UNCOV
868
      elseif value == "thick" then
×
UNCOV
869
         return SILE.types.length("5mu plus 5mu") * direction
×
870
      end
871
   end
UNCOV
872
   return SILE.types.length(value)
×
873
end
874

UNCOV
875
function elements.space:_init (width, height, depth)
×
UNCOV
876
   elements.terminal._init(self)
×
UNCOV
877
   self.width = getStandardLength(width)
×
UNCOV
878
   self.height = getStandardLength(height)
×
UNCOV
879
   self.depth = getStandardLength(depth)
×
880
end
881

UNCOV
882
function elements.space:shape ()
×
UNCOV
883
   self.width = self.width:absolute() * self:getScaleDown()
×
UNCOV
884
   self.height = self.height:absolute() * self:getScaleDown()
×
UNCOV
885
   self.depth = self.depth:absolute() * self:getScaleDown()
×
886
end
887

UNCOV
888
function elements.space.output (_) end
×
889

890
-- text node. For any actual text output
UNCOV
891
elements.text = pl.class(elements.terminal)
×
UNCOV
892
elements.text._type = "Text"
×
893

UNCOV
894
function elements.text:__tostring ()
×
895
   return self._type
×
896
      .. "(atom="
×
897
      .. tostring(self.atom)
×
898
      .. ", kind="
×
899
      .. tostring(self.kind)
×
900
      .. ", script="
×
901
      .. tostring(self.script)
×
902
      .. (self.stretchy and ", stretchy" or "")
×
903
      .. (self.largeop and ", largeop" or "")
×
904
      .. ', text="'
×
905
      .. (self.originalText or self.text)
×
906
      .. '")'
×
907
end
908

UNCOV
909
function elements.text:_init (kind, attributes, script, text)
×
UNCOV
910
   elements.terminal._init(self)
×
UNCOV
911
   if not (kind == "number" or kind == "identifier" or kind == "operator" or kind == "string") then
×
912
      SU.error("Unknown text node kind '" .. kind .. "'; should be one of: number, identifier, operator, string")
×
913
   end
UNCOV
914
   self.kind = kind
×
UNCOV
915
   self.script = script
×
UNCOV
916
   self.text = text
×
UNCOV
917
   if self.script ~= "upright" then
×
UNCOV
918
      local converted = ""
×
UNCOV
919
      for _, uchr in luautf8.codes(self.text) do
×
UNCOV
920
         local dst_char = luautf8.char(uchr)
×
UNCOV
921
         if uchr >= 0x41 and uchr <= 0x5A then -- Latin capital letter
×
UNCOV
922
            dst_char = luautf8.char(mathScriptConversionTable.capital[self.script](uchr))
×
UNCOV
923
         elseif uchr >= 0x61 and uchr <= 0x7A then -- Latin non-capital letter
×
UNCOV
924
            dst_char = luautf8.char(mathScriptConversionTable.small[self.script](uchr))
×
925
         end
UNCOV
926
         converted = converted .. dst_char
×
927
      end
UNCOV
928
      self.originalText = self.text
×
UNCOV
929
      self.text = converted
×
930
   end
UNCOV
931
   if self.kind == "operator" then
×
UNCOV
932
      if self.text == "-" then
×
UNCOV
933
         self.text = "−"
×
934
      end
935
   end
UNCOV
936
   for attribute, value in pairs(attributes) do
×
UNCOV
937
      self[attribute] = value
×
938
   end
939
end
940

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

UNCOV
1016
function elements.text:stretchyReshape (depth, height)
×
1017
   -- Required depth+height of stretched glyph, in font units
UNCOV
1018
   local mathMetrics = self:getMathMetrics()
×
UNCOV
1019
   local upem = mathMetrics.unitsPerEm
×
UNCOV
1020
   local sz = self.font.size
×
UNCOV
1021
   local requiredAdvance = (depth + height):tonumber() * upem / sz
×
UNCOV
1022
   SU.debug("math", "stretch: rA =", requiredAdvance)
×
1023
   -- Choose variant of the closest size. The criterion we use is to have
1024
   -- an advance measurement as close as possible as the required one.
1025
   -- The advance measurement is simply the depth+height of the glyph.
1026
   -- Therefore, the selected glyph may be smaller or bigger than
1027
   -- required.  TODO: implement assembly of stretchable glyphs form
1028
   -- their parts for cases when the biggest variant is not big enough.
1029
   -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
UNCOV
1030
   local glyphs = pl.tablex.deepcopy(self.value.items)
×
UNCOV
1031
   local constructions = self:getMathMetrics().mathVariants.vertGlyphConstructions[glyphs[1].gid]
×
UNCOV
1032
   if constructions then
×
UNCOV
1033
      local variants = constructions.mathGlyphVariantRecord
×
UNCOV
1034
      SU.debug("math", "stretch: variants =", variants)
×
1035
      local closest
1036
      local closestI
UNCOV
1037
      local m = requiredAdvance - (self.depth + self.height):tonumber() * upem / sz
×
UNCOV
1038
      SU.debug("math", "stretch: m =", m)
×
UNCOV
1039
      for i, v in ipairs(variants) do
×
UNCOV
1040
         local diff = math.abs(v.advanceMeasurement - requiredAdvance)
×
UNCOV
1041
         SU.debug("math", "stretch: diff =", diff)
×
UNCOV
1042
         if diff < m then
×
UNCOV
1043
            closest = v
×
UNCOV
1044
            closestI = i
×
UNCOV
1045
            m = diff
×
1046
         end
1047
      end
UNCOV
1048
      SU.debug("math", "stretch: closestI =", closestI)
×
UNCOV
1049
      if closest then
×
1050
         -- Now we have to re-shape the glyph chain. We will assume there
1051
         -- is only one glyph.
1052
         -- TODO: this code is probably wrong when the vertical
1053
         -- variants have a different width than the original, because
1054
         -- the shaping phase is already done. Need to do better.
UNCOV
1055
         glyphs[1].gid = closest.variantGlyph
×
UNCOV
1056
         local face = SILE.font.cache(self.font, SILE.shaper.getFace)
×
UNCOV
1057
         local dimen = hb.get_glyph_dimensions(face, self.font.size, closest.variantGlyph)
×
UNCOV
1058
         glyphs[1].width = dimen.width
×
UNCOV
1059
         glyphs[1].height = dimen.height
×
UNCOV
1060
         glyphs[1].depth = dimen.depth
×
UNCOV
1061
         glyphs[1].glyphAdvance = dimen.glyphAdvance
×
UNCOV
1062
         self.width = SILE.types.length(dimen.glyphAdvance)
×
UNCOV
1063
         self.depth = SILE.types.length(dimen.depth)
×
UNCOV
1064
         self.height = SILE.types.length(dimen.height)
×
UNCOV
1065
         SILE.shaper:preAddNodes(glyphs, self.value)
×
UNCOV
1066
         self.value.items = glyphs
×
UNCOV
1067
         self.value.glyphString = { glyphs[1].gid }
×
1068
      end
1069
   end
1070
end
1071

UNCOV
1072
function elements.text:output (x, y, line)
×
UNCOV
1073
   if not self.value.glyphString then
×
1074
      return
×
1075
   end
1076
   local compensatedY
UNCOV
1077
   if isDisplayMode(self.mode) and self.atom == atomType.bigOperator and self.value.items[1].fontDepth then
×
UNCOV
1078
      compensatedY = SILE.types.length(y.length + self.value.items[1].depth - self.value.items[1].fontDepth)
×
1079
   else
UNCOV
1080
      compensatedY = y
×
1081
   end
UNCOV
1082
   SILE.outputter:setCursor(scaleWidth(x, line), compensatedY.length)
×
UNCOV
1083
   SILE.outputter:setFont(self.font)
×
1084
   -- There should be no stretch or shrink on the width of a text
1085
   -- element.
UNCOV
1086
   local width = self.width.length
×
UNCOV
1087
   SILE.outputter:drawHbox(self.value, width)
×
1088
end
1089

UNCOV
1090
elements.fraction = pl.class(elements.mbox)
×
UNCOV
1091
elements.fraction._type = "Fraction"
×
1092

UNCOV
1093
function elements.fraction:__tostring ()
×
1094
   return self._type .. "(" .. tostring(self.numerator) .. ", " .. tostring(self.denominator) .. ")"
×
1095
end
1096

UNCOV
1097
function elements.fraction:_init (numerator, denominator)
×
UNCOV
1098
   elements.mbox._init(self)
×
UNCOV
1099
   self.numerator = numerator
×
UNCOV
1100
   self.denominator = denominator
×
UNCOV
1101
   table.insert(self.children, numerator)
×
UNCOV
1102
   table.insert(self.children, denominator)
×
1103
end
1104

UNCOV
1105
function elements.fraction:styleChildren ()
×
UNCOV
1106
   self.numerator.mode = getNumeratorMode(self.mode)
×
UNCOV
1107
   self.denominator.mode = getDenominatorMode(self.mode)
×
1108
end
1109

UNCOV
1110
function elements.fraction:shape ()
×
1111
   -- MathML Core 3.3.2: "To avoid visual confusion between the fraction bar
1112
   -- and another adjacent items (e.g. minus sign or another fraction's bar),"
1113
   -- By convention, here we use 1px = 1/96in = 0.75pt.
1114
   -- Note that PlainTeX would likely use \nulldelimiterspace (default 1.2pt)
1115
   -- but it would depend on the surrounding context, and might be far too
1116
   -- much in some cases, so we stick to MathML's suggested padding.
NEW
1117
   self.padding = SILE.types.length(0.75)
×
1118

1119
   -- Determine relative abscissas and width
1120
   local widest, other
UNCOV
1121
   if self.denominator.width > self.numerator.width then
×
UNCOV
1122
      widest, other = self.denominator, self.numerator
×
1123
   else
UNCOV
1124
      widest, other = self.numerator, self.denominator
×
1125
   end
NEW
1126
   widest.relX = self.padding
×
NEW
1127
   other.relX = self.padding + (widest.width - other.width) / 2
×
NEW
1128
   self.width = widest.width + 2 * self.padding
×
1129
   -- Determine relative ordinates and height
UNCOV
1130
   local constants = self:getMathMetrics().constants
×
UNCOV
1131
   local scaleDown = self:getScaleDown()
×
UNCOV
1132
   self.axisHeight = constants.axisHeight * scaleDown
×
UNCOV
1133
   self.ruleThickness = constants.fractionRuleThickness * scaleDown
×
1134

1135
   local numeratorGapMin, denominatorGapMin, numeratorShiftUp, denominatorShiftDown
UNCOV
1136
   if isDisplayMode(self.mode) then
×
NEW
1137
      numeratorGapMin = constants.fractionNumDisplayStyleGapMin * scaleDown
×
NEW
1138
      denominatorGapMin = constants.fractionDenomDisplayStyleGapMin * scaleDown
×
NEW
1139
      numeratorShiftUp = constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
×
NEW
1140
      denominatorShiftDown = constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
×
1141
   else
NEW
1142
      numeratorGapMin = constants.fractionNumeratorGapMin * scaleDown
×
NEW
1143
      denominatorGapMin = constants.fractionDenominatorGapMin * scaleDown
×
NEW
1144
      numeratorShiftUp = constants.fractionNumeratorShiftUp * scaleDown
×
NEW
1145
      denominatorShiftDown = constants.fractionDenominatorShiftDown * scaleDown
×
1146
   end
1147

NEW
1148
   self.numerator.relY = -self.axisHeight
×
NEW
1149
      - self.ruleThickness / 2
×
NEW
1150
      - SILE.types.length(
×
NEW
1151
         math.max(
×
NEW
1152
            (numeratorGapMin + self.numerator.depth):tonumber(),
×
NEW
1153
            numeratorShiftUp - self.axisHeight - self.ruleThickness / 2
×
1154
         )
1155
      )
NEW
1156
   self.denominator.relY = -self.axisHeight
×
NEW
1157
      + self.ruleThickness / 2
×
NEW
1158
      + SILE.types.length(
×
NEW
1159
         math.max(
×
NEW
1160
            (denominatorGapMin + self.denominator.height):tonumber(),
×
NEW
1161
            denominatorShiftDown + self.axisHeight - self.ruleThickness / 2
×
1162
         )
1163
      )
UNCOV
1164
   self.height = self.numerator.height - self.numerator.relY
×
UNCOV
1165
   self.depth = self.denominator.relY + self.denominator.depth
×
1166
end
1167

UNCOV
1168
function elements.fraction:output (x, y, line)
×
UNCOV
1169
   SILE.outputter:drawRule(
×
NEW
1170
      scaleWidth(x + self.padding, line),
×
UNCOV
1171
      y.length - self.axisHeight - self.ruleThickness / 2,
×
NEW
1172
      scaleWidth(self.width - 2 * self.padding, line),
×
1173
      self.ruleThickness
1174
   )
1175
end
1176

1177
local function newSubscript (spec)
UNCOV
1178
   return elements.subscript(spec.base, spec.sub, spec.sup)
×
1179
end
1180

1181
local function newUnderOver (spec)
UNCOV
1182
   return elements.underOver(spec.base, spec.sub, spec.sup)
×
1183
end
1184

1185
-- TODO replace with penlight equivalent
1186
local function mapList (f, l)
UNCOV
1187
   local ret = {}
×
UNCOV
1188
   for i, x in ipairs(l) do
×
UNCOV
1189
      ret[i] = f(i, x)
×
1190
   end
UNCOV
1191
   return ret
×
1192
end
1193

UNCOV
1194
elements.mtr = pl.class(elements.mbox)
×
1195
-- elements.mtr._type = "" -- TODO why not set?
1196

UNCOV
1197
function elements.mtr:_init (children)
×
UNCOV
1198
   self.children = children
×
1199
end
1200

UNCOV
1201
function elements.mtr:styleChildren ()
×
UNCOV
1202
   for _, c in ipairs(self.children) do
×
UNCOV
1203
      c.mode = self.mode
×
1204
   end
1205
end
1206

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

UNCOV
1209
function elements.mtr.output (_) end
×
1210

UNCOV
1211
elements.table = pl.class(elements.mbox)
×
UNCOV
1212
elements.table._type = "table" -- TODO why case difference?
×
1213

UNCOV
1214
function elements.table:_init (children, options)
×
UNCOV
1215
   elements.mbox._init(self)
×
UNCOV
1216
   self.children = children
×
UNCOV
1217
   self.options = options
×
UNCOV
1218
   self.nrows = #self.children
×
UNCOV
1219
   self.ncols = math.max(pl.utils.unpack(mapList(function (_, row)
×
UNCOV
1220
      return #row.children
×
UNCOV
1221
   end, self.children)))
×
UNCOV
1222
   SU.debug("math", "self.ncols =", self.ncols)
×
UNCOV
1223
   self.rowspacing = self.options.rowspacing and SILE.types.length(self.options.rowspacing) or SILE.types.length("7pt")
×
UNCOV
1224
   self.columnspacing = self.options.columnspacing and SILE.types.length(self.options.columnspacing)
×
UNCOV
1225
      or SILE.types.length("6pt")
×
1226
   -- Pad rows that do not have enough cells by adding cells to the
1227
   -- right.
UNCOV
1228
   for i, row in ipairs(self.children) do
×
UNCOV
1229
      for j = 1, (self.ncols - #row.children) do
×
1230
         SU.debug("math", "padding i =", i, "j =", j)
×
1231
         table.insert(row.children, elements.stackbox("H", {}))
×
1232
         SU.debug("math", "size", #row.children)
×
1233
      end
1234
   end
UNCOV
1235
   if options.columnalign then
×
UNCOV
1236
      local l = {}
×
UNCOV
1237
      for w in string.gmatch(options.columnalign, "[^%s]+") do
×
UNCOV
1238
         if not (w == "left" or w == "center" or w == "right") then
×
1239
            SU.error("Invalid specifier in `columnalign` attribute: " .. w)
×
1240
         end
UNCOV
1241
         table.insert(l, w)
×
1242
      end
1243
      -- Pad with last value of l if necessary
UNCOV
1244
      for _ = 1, (self.ncols - #l), 1 do
×
1245
         table.insert(l, l[#l])
×
1246
      end
1247
      -- On the contrary, remove excess values in l if necessary
UNCOV
1248
      for _ = 1, (#l - self.ncols), 1 do
×
1249
         table.remove(l)
×
1250
      end
UNCOV
1251
      self.options.columnalign = l
×
1252
   else
UNCOV
1253
      self.options.columnalign = pl.List.range(1, self.ncols):map(function (_)
×
UNCOV
1254
         return "center"
×
1255
      end)
1256
   end
1257
end
1258

UNCOV
1259
function elements.table:styleChildren ()
×
UNCOV
1260
   if self.mode == mathMode.display and self.options.displaystyle ~= "false" then
×
UNCOV
1261
      for _, c in ipairs(self.children) do
×
UNCOV
1262
         c.mode = mathMode.display
×
1263
      end
1264
   else
UNCOV
1265
      for _, c in ipairs(self.children) do
×
UNCOV
1266
         c.mode = mathMode.text
×
1267
      end
1268
   end
1269
end
1270

UNCOV
1271
function elements.table:shape ()
×
1272
   -- Determine the height (resp. depth) of each row, which is the max
1273
   -- height (resp. depth) among its elements. Then we only need to add it to
1274
   -- the table's height and center every cell vertically.
UNCOV
1275
   for _, row in ipairs(self.children) do
×
UNCOV
1276
      row.height = SILE.types.length(0)
×
UNCOV
1277
      row.depth = SILE.types.length(0)
×
UNCOV
1278
      for _, cell in ipairs(row.children) do
×
UNCOV
1279
         row.height = maxLength(row.height, cell.height)
×
UNCOV
1280
         row.depth = maxLength(row.depth, cell.depth)
×
1281
      end
1282
   end
UNCOV
1283
   self.vertSize = SILE.types.length(0)
×
UNCOV
1284
   for i, row in ipairs(self.children) do
×
1285
      self.vertSize = self.vertSize
×
UNCOV
1286
         + row.height
×
UNCOV
1287
         + row.depth
×
UNCOV
1288
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
×
1289
   end
UNCOV
1290
   local rowHeightSoFar = SILE.types.length(0)
×
UNCOV
1291
   for i, row in ipairs(self.children) do
×
UNCOV
1292
      row.relY = rowHeightSoFar + row.height - self.vertSize
×
1293
      rowHeightSoFar = rowHeightSoFar
×
UNCOV
1294
         + row.height
×
UNCOV
1295
         + row.depth
×
UNCOV
1296
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
×
1297
   end
UNCOV
1298
   self.width = SILE.types.length(0)
×
UNCOV
1299
   local thisColRelX = SILE.types.length(0)
×
1300
   -- For every column...
UNCOV
1301
   for i = 1, self.ncols do
×
1302
      -- Determine its width
UNCOV
1303
      local columnWidth = SILE.types.length(0)
×
UNCOV
1304
      for j = 1, self.nrows do
×
UNCOV
1305
         if self.children[j].children[i].width > columnWidth then
×
UNCOV
1306
            columnWidth = self.children[j].children[i].width
×
1307
         end
1308
      end
1309
      -- Use it to align the contents of every cell as required.
UNCOV
1310
      for j = 1, self.nrows do
×
UNCOV
1311
         local cell = self.children[j].children[i]
×
UNCOV
1312
         if self.options.columnalign[i] == "left" then
×
UNCOV
1313
            cell.relX = thisColRelX
×
UNCOV
1314
         elseif self.options.columnalign[i] == "center" then
×
UNCOV
1315
            cell.relX = thisColRelX + (columnWidth - cell.width) / 2
×
UNCOV
1316
         elseif self.options.columnalign[i] == "right" then
×
UNCOV
1317
            cell.relX = thisColRelX + (columnWidth - cell.width)
×
1318
         else
1319
            SU.error("invalid columnalign parameter")
×
1320
         end
1321
      end
UNCOV
1322
      thisColRelX = thisColRelX + columnWidth + (i == self.ncols and SILE.types.length(0) or self.columnspacing) -- Spacing
×
1323
   end
UNCOV
1324
   self.width = thisColRelX
×
1325
   -- Center myself vertically around the axis, and update relative Ys of rows accordingly
UNCOV
1326
   local axisHeight = self:getMathMetrics().constants.axisHeight * self:getScaleDown()
×
UNCOV
1327
   self.height = self.vertSize / 2 + axisHeight
×
UNCOV
1328
   self.depth = self.vertSize / 2 - axisHeight
×
UNCOV
1329
   for _, row in ipairs(self.children) do
×
UNCOV
1330
      row.relY = row.relY + self.vertSize / 2 - axisHeight
×
1331
      -- Also adjust width
UNCOV
1332
      row.width = self.width
×
1333
   end
1334
end
1335

UNCOV
1336
function elements.table.output (_) end
×
1337

UNCOV
1338
elements.sqrt = pl.class(elements.mbox)
×
UNCOV
1339
elements.sqrt._type = "Sqrt"
×
1340

UNCOV
1341
function elements.sqrt:__tostring ()
×
1342
   return self._type .. "(" .. tostring(self.radicand) .. ")"
×
1343
end
1344

UNCOV
1345
function elements.sqrt:_init (radicand)
×
1346
   elements.mbox._init(self)
×
1347
   self.radicand = radicand
×
1348
   table.insert(self.children, radicand)
×
1349
   self.relX = SILE.types.length(0) -- x position relative to its parent box
×
1350
   self.relY = SILE.types.length(0) -- y position relative to its parent box
×
1351
end
1352

UNCOV
1353
function elements.sqrt:styleChildren ()
×
1354
   self.radicand.mode = self.mode
×
1355
end
1356

UNCOV
1357
function elements.sqrt:shape ()
×
1358
   local mathMetrics = self:getMathMetrics()
×
1359
   local scaleDown = self:getScaleDown()
×
1360
   local constants = mathMetrics.constants
×
1361

1362
   self.radicalRuleThickness = constants.radicalRuleThickness * scaleDown
×
1363
   if self.mode == mathMode.display or self.mode == mathMode.displayCramped then
×
1364
      self.radicalVerticalGap = constants.radicalDisplayStyleVerticalGap * scaleDown
×
1365
   else
1366
      self.radicalVerticalGap = constants.radicalVerticalGap * scaleDown
×
1367
   end
1368
   self.extraAscender = constants.radicalExtraAscender * scaleDown
×
1369

1370
   -- HACK: More or less ad hoc values, see output method for more details
1371
   self.symbolWidth = SILE.shaper:measureChar("√").width
×
1372
   self.symbolHeight = SILE.types.length("1.1ex"):tonumber() * scaleDown
×
1373

1374
   self.width = self.radicand.width + SILE.types.length(self.symbolWidth)
×
1375
   self.height = self.radicand.height + self.radicalVerticalGap + self.extraAscender
×
1376
   self.depth = self.radicand.depth
×
1377
   self.radicand:shape()
×
1378
   self.radicand.relX = self.symbolWidth
×
1379
end
1380

1381
local function _r (number)
1382
   -- Lua 5.3+ formats floats as 1.0 and integers as 1
1383
   -- Also some PDF readers do not like double precision.
1384
   return math.floor(number) == number and math.floor(number) or tonumber(string.format("%.5f", number))
×
1385
end
1386

UNCOV
1387
function elements.sqrt:output (x, y, line)
×
1388
   -- HACK FIXME:
1389
   -- OpenType might say we need to assemble the radical sign from parts.
1390
   -- Frankly, it's much easier to just draw it as a graphic :-)
1391
   -- Hence, here we use a PDF graphic operators to draw a nice radical sign.
1392
   -- Some values here are ad hoc, but they look good.
1393
   local h = self.height:tonumber()
×
1394
   local d = self.depth:tonumber()
×
1395
   local sw = self.symbolWidth
×
1396
   local dh = h - self.symbolHeight
×
1397
   local symbol = {
×
1398
      _r(self.radicalRuleThickness),
×
1399
      "w", -- line width
1400
      2,
1401
      "j", -- round line joins
1402
      _r(sw),
×
1403
      _r(self.extraAscender),
×
1404
      "m",
1405
      _r(sw * 0.4),
×
1406
      _r(h + d),
×
1407
      "l",
1408
      _r(sw * 0.15),
×
1409
      _r(dh),
×
1410
      "l",
1411
      0,
1412
      _r(dh + 0.5),
×
1413
      "l",
1414
      "S",
1415
   }
1416
   local svg = table.concat(symbol, " ")
×
1417
   SILE.outputter:drawSVG(svg, x, y, sw, h, 1)
×
1418
   -- And now we just need to draw the bar over the radicand
1419
   SILE.outputter:drawRule(
×
1420
      self.symbolWidth + scaleWidth(x, line),
×
1421
      y.length - scaleWidth(self.radicand.height, line) - self.radicalVerticalGap - self.radicalRuleThickness / 2,
×
1422
      scaleWidth(self.radicand.width, line),
×
1423
      self.radicalRuleThickness
1424
   )
1425
end
1426

UNCOV
1427
elements.mathMode = mathMode
×
UNCOV
1428
elements.atomType = atomType
×
UNCOV
1429
elements.scriptType = scriptType
×
UNCOV
1430
elements.mathVariantToScriptType = mathVariantToScriptType
×
UNCOV
1431
elements.symbolDefaults = symbolDefaults
×
UNCOV
1432
elements.newSubscript = newSubscript
×
UNCOV
1433
elements.newUnderOver = newUnderOver
×
1434

UNCOV
1435
return elements
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc