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

sile-typesetter / sile / 11573237387

29 Oct 2024 11:50AM UTC coverage: 57.701% (-2.2%) from 59.882%
11573237387

push

github

web-flow
Merge f9757d6cf into 8390534e6

10284 of 17823 relevant lines covered (57.7%)

4153.06 hits per line

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

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

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

9
local elements = {}
2✔
10

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

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

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

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

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

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

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

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

108
local mathCache = {}
2✔
109

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

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

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

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

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

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

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

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

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

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

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

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

308
function elements.mbox:getMathMetrics ()
4✔
309
   return retrieveMathTable(self.font)
862✔
310
end
311

312
function elements.mbox:getScaleDown ()
4✔
313
   local constants = self:getMathMetrics().constants
1,104✔
314
   local scaleDown
315
   if isScriptMode(self.mode) then
1,104✔
316
      scaleDown = constants.scriptPercentScaleDown
110✔
317
   elseif isScriptScriptMode(self.mode) then
884✔
318
      scaleDown = constants.scriptScriptPercentScaleDown
88✔
319
   else
320
      scaleDown = 1
354✔
321
   end
322
   return scaleDown
552✔
323
end
324

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

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

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

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

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

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

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

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

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

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

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

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

536
elements.subscript = pl.class(elements.mbox)
4✔
537
elements.subscript._type = "Subscript"
2✔
538

539
function elements.subscript:__tostring ()
4✔
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

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

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

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

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

674
function elements.subscript.output (_, _, _, _) end
22✔
675

676
elements.underOver = pl.class(elements.subscript)
4✔
677
elements.underOver._type = "UnderOver"
2✔
678

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

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

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

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

807
function elements.underOver:calculateItalicsCorrection ()
4✔
808
   local lastGid = getRightMostGlyphId(self.base)
×
809
   if lastGid > 0 then
×
810
      local mathMetrics = self:getMathMetrics()
×
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
822
   return 0
×
823
end
824

825
function elements.underOver.output (_, _, _, _) end
2✔
826

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

831
function elements.terminal:_init ()
4✔
832
   elements.mbox._init(self)
302✔
833
end
834

835
function elements.terminal.styleChildren (_) end
304✔
836

837
function elements.terminal.shape (_) end
2✔
838

839
elements.space = pl.class(elements.terminal)
4✔
840
elements.space._type = "Space"
2✔
841

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

846
function elements.space:__tostring ()
4✔
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)
858
   if type(value) == "string" then
312✔
859
      local direction = 1
104✔
860
      if value:sub(1, 1) == "-" then
208✔
861
         value = value:sub(2, -1)
20✔
862
         direction = -1
10✔
863
      end
864
      if value == "thin" then
104✔
865
         return SILE.types.length("3mu") * direction
72✔
866
      elseif value == "med" then
80✔
867
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
69✔
868
      elseif value == "thick" then
57✔
869
         return SILE.types.length("5mu plus 5mu") * direction
105✔
870
      end
871
   end
872
   return SILE.types.length(value)
230✔
873
end
874

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

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

888
function elements.space.output (_) end
106✔
889

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

894
function elements.text:__tostring ()
4✔
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

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

941
function elements.text:shape ()
4✔
942
   self.font.size = self.font.size * self:getScaleDown()
396✔
943
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
198✔
944
   local mathMetrics = self:getMathMetrics()
198✔
945
   local glyphs = SILE.shaper:shapeToken(self.text, self.font)
198✔
946
   -- Use bigger variants for big operators in display style
947
   if isDisplayMode(self.mode) and self.largeop then
396✔
948
      -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
949
      glyphs = pl.tablex.deepcopy(glyphs)
×
950
      local constructions = mathMetrics.mathVariants.vertGlyphConstructions[glyphs[1].gid]
×
951
      if constructions then
×
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
956
         local m = 0
×
957
         for _, v in ipairs(displayVariants) do
×
958
            if v.advanceMeasurement > m then
×
959
               biggest = v
×
960
               m = v.advanceMeasurement
×
961
            end
962
         end
963
         if biggest then
×
964
            glyphs[1].gid = biggest.variantGlyph
×
965
            local dimen = hb.get_glyph_dimensions(face, self.font.size, biggest.variantGlyph)
×
966
            glyphs[1].width = dimen.width
×
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). ]]
973
            local axisHeight = mathMetrics.constants.axisHeight * self:getScaleDown()
×
974
            local y_size = dimen.height + dimen.depth
×
975
            glyphs[1].height = y_size / 2 + axisHeight
×
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.
980
            glyphs[1].fontHeight = dimen.height
×
981
            glyphs[1].fontDepth = dimen.depth
×
982
         end
983
      end
984
   end
985
   SILE.shaper:preAddNodes(glyphs, self.value)
198✔
986
   self.value.items = glyphs
198✔
987
   self.value.glyphString = {}
198✔
988
   if glyphs and #glyphs > 0 then
198✔
989
      for i = 1, #glyphs do
601✔
990
         table.insert(self.value.glyphString, glyphs[i].gid)
403✔
991
      end
992
      self.width = SILE.types.length(0)
396✔
993
      self.widthForSubscript = SILE.types.length(0)
396✔
994
      for i = #glyphs, 1, -1 do
601✔
995
         self.width = self.width + glyphs[i].glyphAdvance
806✔
996
      end
997
      -- Store width without italic correction somewhere
998
      self.widthForSubscript = self.width
198✔
999
      local itCorr = mathMetrics.italicsCorrection[glyphs[#glyphs].gid]
198✔
1000
      if itCorr then
198✔
1001
         self.width = self.width + itCorr * self:getScaleDown()
66✔
1002
      end
1003
      for i = 1, #glyphs do
601✔
1004
         self.height = i == 1 and SILE.types.length(glyphs[i].height)
403✔
1005
            or SILE.types.length(math.max(self.height:tonumber(), glyphs[i].height))
813✔
1006
         self.depth = i == 1 and SILE.types.length(glyphs[i].depth)
403✔
1007
            or SILE.types.length(math.max(self.depth:tonumber(), glyphs[i].depth))
813✔
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

1016
function elements.text:stretchyReshape (depth, height)
4✔
1017
   -- Required depth+height of stretched glyph, in font units
1018
   local mathMetrics = self:getMathMetrics()
36✔
1019
   local upem = mathMetrics.unitsPerEm
36✔
1020
   local sz = self.font.size
36✔
1021
   local requiredAdvance = (depth + height):tonumber() * upem / sz
108✔
1022
   SU.debug("math", "stretch: rA =", requiredAdvance)
36✔
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.
1030
   local glyphs = pl.tablex.deepcopy(self.value.items)
36✔
1031
   local constructions = self:getMathMetrics().mathVariants.vertGlyphConstructions[glyphs[1].gid]
72✔
1032
   if constructions then
36✔
1033
      local variants = constructions.mathGlyphVariantRecord
36✔
1034
      SU.debug("math", "stretch: variants =", variants)
36✔
1035
      local closest
1036
      local closestI
1037
      local m = requiredAdvance - (self.depth + self.height):tonumber() * upem / sz
108✔
1038
      SU.debug("math", "stretch: m =", m)
36✔
1039
      for i, v in ipairs(variants) do
504✔
1040
         local diff = math.abs(v.advanceMeasurement - requiredAdvance)
468✔
1041
         SU.debug("math", "stretch: diff =", diff)
468✔
1042
         if diff < m then
468✔
1043
            closest = v
48✔
1044
            closestI = i
48✔
1045
            m = diff
48✔
1046
         end
1047
      end
1048
      SU.debug("math", "stretch: closestI =", closestI)
36✔
1049
      if closest then
36✔
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.
1055
         glyphs[1].gid = closest.variantGlyph
36✔
1056
         local face = SILE.font.cache(self.font, SILE.shaper.getFace)
36✔
1057
         local dimen = hb.get_glyph_dimensions(face, self.font.size, closest.variantGlyph)
36✔
1058
         glyphs[1].width = dimen.width
36✔
1059
         glyphs[1].height = dimen.height
36✔
1060
         glyphs[1].depth = dimen.depth
36✔
1061
         glyphs[1].glyphAdvance = dimen.glyphAdvance
36✔
1062
         self.width = SILE.types.length(dimen.glyphAdvance)
72✔
1063
         self.depth = SILE.types.length(dimen.depth)
72✔
1064
         self.height = SILE.types.length(dimen.height)
72✔
1065
         SILE.shaper:preAddNodes(glyphs, self.value)
36✔
1066
         self.value.items = glyphs
36✔
1067
         self.value.glyphString = { glyphs[1].gid }
36✔
1068
      end
1069
   end
1070
end
1071

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

1090
elements.fraction = pl.class(elements.mbox)
4✔
1091
elements.fraction._type = "Fraction"
2✔
1092

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

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

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

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

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

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

1181
local function newUnderOver (spec)
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)
1187
   local ret = {}
×
1188
   for i, x in ipairs(l) do
×
1189
      ret[i] = f(i, x)
×
1190
   end
1191
   return ret
×
1192
end
1193

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

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

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

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

1209
function elements.mtr.output (_) end
2✔
1210

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

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

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

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

1336
function elements.table.output (_) end
2✔
1337

1338
elements.sqrt = pl.class(elements.mbox)
4✔
1339
elements.sqrt._type = "Sqrt"
2✔
1340

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

1345
function elements.sqrt:_init (radicand)
4✔
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

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

1357
function elements.sqrt:shape ()
4✔
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

1387
function elements.sqrt:output (x, y, line)
4✔
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

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

1435
return elements
2✔
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