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

sile-typesetter / sile / 11124789710

01 Oct 2024 11:57AM UTC coverage: 29.567% (-31.4%) from 60.926%
11124789710

push

github

web-flow
Merge pull request #2105 from Omikhleia/refactor-collated-sort

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

5252 existing lines in 53 files now uncovered.

5048 of 17073 relevant lines covered (29.57%)

1856.13 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(([[You must use a math font for math rendering.
×
125

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

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

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

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

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

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

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

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

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

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

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

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

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

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

324
-- Determine the mode of its descendants
UNCOV
325
function elements.mbox:styleDescendants ()
×
UNCOV
326
   self:styleChildren()
×
UNCOV
327
   for _, n in ipairs(self.children) do
×
UNCOV
328
      if n then
×
UNCOV
329
         n:styleDescendants()
×
330
      end
331
   end
332
end
333

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
1109
function elements.fraction:shape ()
×
1110
   -- Determine relative abscissas and width
1111
   local widest, other
UNCOV
1112
   if self.denominator.width > self.numerator.width then
×
UNCOV
1113
      widest, other = self.denominator, self.numerator
×
1114
   else
UNCOV
1115
      widest, other = self.numerator, self.denominator
×
1116
   end
UNCOV
1117
   widest.relX = SILE.types.length(0)
×
UNCOV
1118
   other.relX = (widest.width - other.width) / 2
×
UNCOV
1119
   self.width = widest.width
×
1120
   -- Determine relative ordinates and height
UNCOV
1121
   local constants = self:getMathMetrics().constants
×
UNCOV
1122
   local scaleDown = self:getScaleDown()
×
UNCOV
1123
   self.axisHeight = constants.axisHeight * scaleDown
×
UNCOV
1124
   self.ruleThickness = constants.fractionRuleThickness * scaleDown
×
UNCOV
1125
   if isDisplayMode(self.mode) then
×
UNCOV
1126
      self.numerator.relY = -self.axisHeight
×
UNCOV
1127
         - self.ruleThickness / 2
×
UNCOV
1128
         - SILE.types.length(
×
UNCOV
1129
            math.max(
×
UNCOV
1130
               (constants.fractionNumDisplayStyleGapMin * scaleDown + self.numerator.depth):tonumber(),
×
UNCOV
1131
               constants.fractionNumeratorDisplayStyleShiftUp * scaleDown - self.axisHeight - self.ruleThickness / 2
×
1132
            )
1133
         )
1134
   else
UNCOV
1135
      self.numerator.relY = -self.axisHeight
×
UNCOV
1136
         - self.ruleThickness / 2
×
UNCOV
1137
         - SILE.types.length(
×
UNCOV
1138
            math.max(
×
UNCOV
1139
               (constants.fractionNumeratorGapMin * scaleDown + self.numerator.depth):tonumber(),
×
UNCOV
1140
               constants.fractionNumeratorShiftUp * scaleDown - self.axisHeight - self.ruleThickness / 2
×
1141
            )
1142
         )
1143
   end
UNCOV
1144
   if isDisplayMode(self.mode) then
×
UNCOV
1145
      self.denominator.relY = -self.axisHeight
×
UNCOV
1146
         + self.ruleThickness / 2
×
UNCOV
1147
         + SILE.types.length(
×
UNCOV
1148
            math.max(
×
UNCOV
1149
               (constants.fractionDenomDisplayStyleGapMin * scaleDown + self.denominator.height):tonumber(),
×
UNCOV
1150
               constants.fractionDenominatorDisplayStyleShiftDown * scaleDown + self.axisHeight - self.ruleThickness / 2
×
1151
            )
1152
         )
1153
   else
UNCOV
1154
      self.denominator.relY = -self.axisHeight
×
UNCOV
1155
         + self.ruleThickness / 2
×
UNCOV
1156
         + SILE.types.length(
×
UNCOV
1157
            math.max(
×
UNCOV
1158
               (constants.fractionDenominatorGapMin * scaleDown + self.denominator.height):tonumber(),
×
UNCOV
1159
               constants.fractionDenominatorShiftDown * scaleDown + self.axisHeight - self.ruleThickness / 2
×
1160
            )
1161
         )
1162
   end
UNCOV
1163
   self.height = self.numerator.height - self.numerator.relY
×
UNCOV
1164
   self.depth = self.denominator.relY + self.denominator.depth
×
1165
end
1166

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
1337
elements.mathMode = mathMode
×
UNCOV
1338
elements.atomType = atomType
×
UNCOV
1339
elements.scriptType = scriptType
×
UNCOV
1340
elements.mathVariantToScriptType = mathVariantToScriptType
×
UNCOV
1341
elements.symbolDefaults = symbolDefaults
×
UNCOV
1342
elements.newSubscript = newSubscript
×
UNCOV
1343
elements.newUnderOver = newUnderOver
×
1344

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

© 2026 Coveralls, Inc