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

sile-typesetter / sile / 6691426968

30 Oct 2023 09:11AM UTC coverage: 62.266% (-12.4%) from 74.636%
6691426968

push

github

alerque
chore(release): 0.14.13

9777 of 15702 relevant lines covered (62.27%)

6531.11 hits per line

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

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

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

9
local elements = {}
7✔
10

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

49
local function isDisplayMode(mode)
50
  return mode <= 1
1,544✔
51
end
52

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

57
local function isScriptMode(mode)
58
  return mode == mathMode.script or mode == mathMode.scriptCramped
1,998✔
59
end
60

61
local function isScriptScriptMode(mode)
62
  return mode == mathMode.scriptScript or mode == mathMode.scriptScriptCramped
1,576✔
63
end
64

65
local mathScriptConversionTable = {
7✔
66
  capital = {
7✔
67
    [scriptType.upright] = function(codepoint) return codepoint end,
15✔
68
    [scriptType.bold] = function(codepoint)
7✔
69
      return codepoint + 0x1D400 - 0x41
12✔
70
    end,
71
    [scriptType.italic] = function(codepoint) return codepoint + 0x1D434 - 0x41 end,
21✔
72
    [scriptType.boldItalic] = function(codepoint) return codepoint + 0x1D468 - 0x41 end,
7✔
73
    [scriptType.doubleStruck] = function(codepoint)
7✔
74
      return codepoint == 0x43 and 0x2102 or
18✔
75
        codepoint == 0x48 and 0x210D or
18✔
76
        codepoint == 0x4E and 0x2115 or
18✔
77
        codepoint == 0x50 and 0x2119 or
12✔
78
        codepoint == 0x51 and 0x211A or
12✔
79
        codepoint == 0x52 and 0x211D or
6✔
80
        codepoint == 0x5A and 0x2124 or
×
81
        codepoint + 0x1D538 - 0x41
18✔
82
    end
83
  },
7✔
84
  small = {
7✔
85
    [scriptType.upright] = function(codepoint) return codepoint end,
306✔
86
    [scriptType.bold] = function(codepoint) return codepoint + 0x1D41A - 0x61 end,
13✔
87
    [scriptType.italic] = function(codepoint) return codepoint == 0x68 and 0x210E or codepoint + 0x1D44E - 0x61 end,
133✔
88
    [scriptType.boldItalic] = function(codepoint) return codepoint + 0x1D482 - 0x61 end,
7✔
89
    [scriptType.doubleStruck] = function(codepoint) return codepoint + 0x1D552 - 0x61 end,
19✔
90
  }
7✔
91
}
92

93
local mathCache = {}
7✔
94

95
local function retrieveMathTable(font)
96
  local key = SILE.font._key(font)
2,870✔
97
  if not mathCache[key] then
2,870✔
98
    SU.debug("math", "Loading math font", key)
27✔
99
    local face = SILE.font.cache(font, SILE.shaper.getFace)
27✔
100
    if not face then
27✔
101
      SU.error("Could not find requested font ".. font .." or any suitable substitutes")
×
102
    end
103
    local mathTable = ot.parseMath(hb.get_table(face, "MATH"))
27✔
104
    local upem = ot.parseHead(hb.get_table(face, "head")).unitsPerEm
54✔
105
    if mathTable == nil then
27✔
106
      SU.error("You must use a math font for math rendering.")
×
107
    end
108
    local constants = {}
27✔
109
    for k, v in pairs(mathTable.mathConstants) do
1,539✔
110
      if type(v) == "table" then v = v.value end
1,512✔
111
      if k:sub(-9) == "ScaleDown" then constants[k] = v / 100
3,024✔
112
      else
113
        constants[k] = v * font.size / upem
1,458✔
114
      end
115
    end
116
    local italicsCorrection = {}
27✔
117
    for k, v in pairs(mathTable.mathItalicsCorrection) do
10,989✔
118
      italicsCorrection[k] = v.value * font.size / upem
10,962✔
119
    end
120
    mathCache[key] = {
27✔
121
      constants = constants,
27✔
122
      italicsCorrection = italicsCorrection,
27✔
123
      mathVariants = mathTable.mathVariants,
27✔
124
      unitsPerEm = upem
27✔
125
    }
27✔
126
  end
127
  return mathCache[key]
2,870✔
128
end
129

130
-- Style transition functions for superscript and subscript
131
local function getSuperscriptMode(mode)
132
  -- D, T -> S
133
  if mode == mathMode.display or mode == mathMode.text then
64✔
134
    return mathMode.script
29✔
135
  -- D', T' -> S'
136
  elseif mode == mathMode.displayCramped or mode == mathMode.textCramped then
35✔
137
    return mathMode.scriptCramped
29✔
138
  -- S, SS -> SS
139
  elseif mode == mathMode.script or mode == mathMode.scriptScript then
6✔
140
    return mathMode.scriptScript
3✔
141
  -- S', SS' -> SS'
142
  else return mathMode.scriptScriptCramped end
3✔
143
end
144
local function getSubscriptMode(mode)
145
  -- D, T, D', T' -> S'
146
  if mode == mathMode.display or mode == mathMode.text
84✔
147
      or mode == mathMode.displayCramped or mode == mathMode.textCramped then
50✔
148
      return mathMode.scriptCramped
65✔
149
  -- S, SS, S', SS' -> SS'
150
  else return mathMode.scriptScriptCramped end
19✔
151
end
152

153
-- Style transition functions for fraction (numerator and denominator)
154
local function getNumeratorMode(mode)
155
  -- D -> T
156
  if mode == mathMode.display then
32✔
157
    return mathMode.text
9✔
158
  -- D' -> T'
159
  elseif mode == mathMode.displayCramped then
23✔
160
    return mathMode.textCramped
×
161
  -- T -> S
162
  elseif mode == mathMode.text then
23✔
163
    return mathMode.script
×
164
  -- T' -> S'
165
  elseif mode == mathMode.textCramped then
23✔
166
    return mathMode.scriptCramped
13✔
167
  -- S, SS -> SS
168
  elseif mode == mathMode.script or mode == mathMode.scriptScript then
10✔
169
    return mathMode.scriptScript
×
170
  -- S', SS' -> SS'
171
  else return mathMode.scriptScriptCramped end
10✔
172
end
173
local function getDenominatorMode(mode)
174
  -- D, D' -> T'
175
  if mode == mathMode.display or mode == mathMode.displayCramped then
32✔
176
    return mathMode.textCramped
9✔
177
  -- T, T' -> S'
178
  elseif mode == mathMode.text or mode == mathMode.textCramped then
23✔
179
    return mathMode.scriptCramped
13✔
180
  -- S, SS, S', SS' -> SS'
181
  else return mathMode.scriptScriptCramped end
10✔
182
end
183

184
local function getRightMostGlyphId(node)
185
  while node and node:is_a(elements.stackbox) and node.direction == 'H' do
236✔
186
    node = node.children[#(node.children)]
10✔
187
  end
188
  if node and node:is_a(elements.text) then
216✔
189
    return node.value.glyphString[#(node.value.glyphString)]
106✔
190
  else
191
    return 0
4✔
192
  end
193
end
194

195
-- Compares two SILE length, without considering shrink or stretch values, and
196
-- returns the biggest.
197
local function maxLength(...)
198
  local arg = {...}
1,888✔
199
  local m
200
  for i, v in ipairs(arg) do
5,875✔
201
    if i == 1 then
3,987✔
202
      m = v
1,888✔
203
    else
204
      if v.length:tonumber() > m.length:tonumber() then
6,297✔
205
        m = v
482✔
206
      end
207
    end
208
  end
209
  return m
1,888✔
210
end
211

212
local function scaleWidth(length, line)
213
  local number = length.length
847✔
214
  if line.ratio and line.ratio < 0 and length.shrink:tonumber() > 0 then
847✔
215
    number = number + length.shrink * line.ratio
×
216
  elseif line.ratio and line.ratio > 0 and length.stretch:tonumber() > 0 then
1,694✔
217
    number = number + length.stretch * line.ratio
954✔
218
  end
219
  return number
847✔
220
end
221

222
-- math box, box with a horizontal shift value and could contain zero or more
223
-- mbox'es (or its child classes) the entire math environment itself is
224
-- a top-level mbox.
225
-- Typesetting of mbox evolves four steps:
226
--   1. Determine the mode for each mbox according to their parent.
227
--   2. Shape the mbox hierarchy from leaf to top. Get the shape and relative position.
228
--   3. Convert mbox into _nnode's to put in SILE's typesetting framework
229
elements.mbox = pl.class(nodefactory.hbox)
14✔
230
elements.mbox._type = "Mbox"
7✔
231

232
function elements.mbox:__tostring ()
14✔
233
  return self._type
×
234
end
235

236
function elements.mbox:_init ()
14✔
237
  nodefactory.hbox._init(self)
1,382✔
238
  self.font = {}
1,382✔
239
  self.children = {} -- The child nodes
1,382✔
240
  self.relX = SILE.length(0) -- x position relative to its parent box
2,764✔
241
  self.relY = SILE.length(0) -- y position relative to its parent box
2,764✔
242
  self.value = {}
1,382✔
243
  self.mode = mathMode.display
1,382✔
244
  self.atom = atomType.ordinary
1,382✔
245
  local font = {
1,382✔
246
    family = SILE.settings:get("math.font.family"),
2,764✔
247
    size = SILE.settings:get("math.font.size"),
2,764✔
248
    style = SILE.settings:get("math.font.style"),
2,764✔
249
    weight = SILE.settings:get("math.font.weight")
2,764✔
250
  }
251
  local filename = SILE.settings:get("math.font.filename")
1,382✔
252
  if filename and filename ~= "" then font.filename = filename end
1,382✔
253
  self.font = SILE.font.loadDefaults(font)
2,764✔
254
end
255

256
function elements.mbox.styleChildren (_)
14✔
257
  SU.error("styleChildren is a virtual function that need to be overridden by its child classes")
×
258
end
259

260
function elements.mbox.shape (_, _, _)
14✔
261
  SU.error("shape is a virtual function that need to be overridden by its child classes")
×
262
end
263

264
function elements.mbox.output (_, _, _, _)
14✔
265
  SU.error("output is a virtual function that need to be overridden by its child classes")
×
266
end
267

268
function elements.mbox:getMathMetrics ()
14✔
269
  return retrieveMathTable(self.font)
2,870✔
270
end
271

272
function elements.mbox:getScaleDown ()
14✔
273
  local constants = self:getMathMetrics().constants
3,500✔
274
  local scaleDown
275
  if isScriptMode(self.mode) then
3,500✔
276
    scaleDown = constants.scriptPercentScaleDown
366✔
277
  elseif isScriptScriptMode(self.mode) then
2,768✔
278
    scaleDown = constants.scriptScriptPercentScaleDown
181✔
279
  else
280
    scaleDown = 1
1,203✔
281
  end
282
  return scaleDown
1,750✔
283
end
284

285
  -- Determine the mode of its descendants
286
function elements.mbox:styleDescendants ()
14✔
287
  self:styleChildren()
1,394✔
288
  for _, n in ipairs(self.children) do
2,745✔
289
    if n then n:styleDescendants() end
1,351✔
290
  end
291
end
292

293
  -- shapeTree shapes the mbox and all its descendants in a recursive fashion
294
  -- The inner-most leaf nodes determine their shape first, and then propagate to their parents
295
  -- During the process, each node will determine its size by (width, height, depth)
296
  -- and (relX, relY) which the relative position to its parent
297
function elements.mbox:shapeTree ()
14✔
298
  for _, n in ipairs(self.children) do
2,745✔
299
    if n then n:shapeTree() end
1,351✔
300
  end
301
  self:shape()
1,394✔
302
end
303

304
  -- Output the node and all its descendants
305
function elements.mbox:outputTree (x, y, line)
14✔
306
  self:output(x, y, line)
1,394✔
307
  local debug = SILE.settings:get("math.debug.boxes")
1,394✔
308
  if debug and not (self:is_a(elements.space)) then
1,394✔
309
    SILE.outputter:setCursor(scaleWidth(x, line), y.length)
×
310
    SILE.outputter:debugHbox(
×
311
      { height = self.height.length,
×
312
        depth = self.depth.length },
313
      scaleWidth(self.width, line)
×
314
    )
315
  end
316
  for _, n in ipairs(self.children) do
2,745✔
317
    if n then n:outputTree(x + n.relX, y + n.relY, line) end
4,053✔
318
  end
319
end
320

321
local spaceKind = {
7✔
322
  thin = "thin",
323
  med = "med",
324
  thick = "thick",
325
}
326

327
-- Indexed by left atom
328
local spacingRules = {
7✔
329
  [atomType.ordinary] = {
7✔
330
    [atomType.bigOperator] = {spaceKind.thin},
7✔
331
    [atomType.binaryOperator] = {spaceKind.med, notScript = true},
7✔
332
    [atomType.relationalOperator] = {spaceKind.thick, notScript = true},
7✔
333
    [atomType.inner] = {spaceKind.thin, notScript = true}
7✔
334
  },
7✔
335
  [atomType.bigOperator] = {
7✔
336
    [atomType.ordinary] = {spaceKind.thin},
7✔
337
    [atomType.bigOperator] = {spaceKind.thin},
7✔
338
    [atomType.relationalOperator] = {spaceKind.thick, notScript = true},
7✔
339
    [atomType.inner] = {spaceKind.thin, notScript = true},
7✔
340
  },
7✔
341
  [atomType.binaryOperator] = {
7✔
342
    [atomType.ordinary] = {spaceKind.med, notScript = true},
7✔
343
    [atomType.bigOperator] = {spaceKind.med, notScript = true},
7✔
344
    [atomType.openingSymbol] = {spaceKind.med, notScript = true},
7✔
345
    [atomType.inner] = {spaceKind.med, notScript = true}
7✔
346
  },
7✔
347
  [atomType.relationalOperator] = {
7✔
348
    [atomType.ordinary] = {spaceKind.thick, notScript = true},
7✔
349
    [atomType.bigOperator] = {spaceKind.thick, notScript = true},
7✔
350
    [atomType.openingSymbol] = {spaceKind.thick, notScript = true},
7✔
351
    [atomType.inner] = {spaceKind.thick, notScript = true}
7✔
352
  },
7✔
353
  [atomType.closeSymbol] = {
7✔
354
    [atomType.bigOperator] = {spaceKind.thin},
7✔
355
    [atomType.binaryOperator] = {spaceKind.med, notScript = true},
7✔
356
    [atomType.relationalOperator] = {spaceKind.thick, notScript = true},
7✔
357
    [atomType.inner] = {spaceKind.thin, notScript = true}
7✔
358
  },
7✔
359
  [atomType.punctuationSymbol] = {
7✔
360
    [atomType.ordinary] = {spaceKind.thin, notScript = true},
7✔
361
    [atomType.bigOperator] = {spaceKind.thin, notScript = true},
7✔
362
    [atomType.relationalOperator] = {spaceKind.thin, notScript = true},
7✔
363
    [atomType.openingSymbol] = {spaceKind.thin, notScript = true},
7✔
364
    [atomType.closeSymbol] = {spaceKind.thin, notScript = true},
7✔
365
    [atomType.punctuationSymbol] = {spaceKind.thin, notScript = true},
7✔
366
    [atomType.inner] = {spaceKind.thin, notScript = true}
7✔
367
  },
7✔
368
  [atomType.inner] = {
7✔
369
    [atomType.ordinary] = {spaceKind.thin, notScript = true},
7✔
370
    [atomType.bigOperator] = {spaceKind.thin},
7✔
371
    [atomType.binaryOperator] = {spaceKind.med, notScript = true},
7✔
372
    [atomType.relationalOperator] = {spaceKind.thick, notScript = true},
7✔
373
    [atomType.openingSymbol] = {spaceKind.thin, notScript = true},
7✔
374
    [atomType.punctuationSymbol] = {spaceKind.thin, notScript = true},
7✔
375
    [atomType.inner] = {spaceKind.thin, notScript = true}
7✔
376
  }
7✔
377
}
378

379
-- _stackbox stacks its content one, either horizontally or vertically
380
elements.stackbox = pl.class(elements.mbox)
14✔
381
elements.stackbox._type = "Stackbox"
7✔
382

383
function elements.stackbox:__tostring ()
14✔
384
  local result = self.direction.."Box("
×
385
  for i, n in ipairs(self.children) do
×
386
    result = result..(i == 1 and "" or ", ")..tostring(n)
×
387
  end
388
  result = result..")"
×
389
  return result
×
390
end
391

392
function elements.stackbox:_init (direction, children)
14✔
393
  elements.mbox._init(self)
262✔
394
  if not (direction == "H" or direction == "V") then
262✔
395
    SU.error("Wrong direction '"..direction.."'; should be H or V")
×
396
  end
397
  self.direction = direction
262✔
398
  self.children = children
262✔
399
end
400

401
function elements.stackbox:styleChildren ()
14✔
402
  for _, n in ipairs(self.children) do
1,065✔
403
    n.mode = self.mode
803✔
404
  end
405
  if self.direction == "H" then
262✔
406
    -- Insert spaces according to the atom type, following Knuth's guidelines
407
    -- in the TeXbook
408
    local spaces = {}
219✔
409
    for i = 1, #self.children-1 do
766✔
410
      local v = self.children[i]
547✔
411
      local v2 = self.children[i + 1]
547✔
412
      if spacingRules[v.atom] and spacingRules[v.atom][v2.atom] then
547✔
413
        local rule = spacingRules[v.atom][v2.atom]
272✔
414
        if not (rule.notScript and (isScriptMode(self.mode) or isScriptScriptMode(self.mode))) then
712✔
415
          spaces[i+1] = rule[1]
178✔
416
        end
417
      end
418
    end
419
    local spaceIdx = {}
219✔
420
    for i, _ in pairs(spaces) do
397✔
421
      table.insert(spaceIdx, i)
178✔
422
    end
423
    table.sort(spaceIdx, function(a, b) return a > b end)
491✔
424
    for _, idx in ipairs(spaceIdx) do
397✔
425
      local hsp = elements.space(spaces[idx], 0, 0)
178✔
426
      table.insert(self.children, idx, hsp)
178✔
427
    end
428
  end
429
end
430

431
function elements.stackbox:shape ()
14✔
432
  -- For a horizontal stackbox (i.e. mrow):
433
  -- 1. set self.height and self.depth to max element height & depth
434
  -- 2. handle stretchy operators
435
  -- 3. set self.width
436
  -- For a vertical stackbox:
437
  -- 1. set self.width to max element width
438
  -- 2. set self.height
439
  -- And finally set children's relative coordinates
440
  self.height = SILE.length(0)
524✔
441
  self.depth = SILE.length(0)
524✔
442
  if self.direction == "H" then
262✔
443
    for i, n in ipairs(self.children) do
1,157✔
444
      n.relY = SILE.length(0)
1,876✔
445
      self.height = i == 1 and n.height or maxLength(self.height, n.height)
1,663✔
446
      self.depth = i == 1 and n.depth or maxLength(self.depth, n.depth)
1,663✔
447
    end
448
    -- Handle stretchy operators
449
    for _, elt in ipairs(self.children) do
1,157✔
450
      if elt.is_a(elements.text) and elt.kind == 'operator'
1,876✔
451
          and elt.stretchy then
230✔
452
        elt:stretchyReshape(self.depth, self.height)
64✔
453
      end
454
    end
455
    -- Set self.width
456
    self.width = SILE.length(0)
438✔
457
    for i, n in ipairs(self.children) do
1,157✔
458
      n.relX = self.width
938✔
459
      self.width = i == 1 and n.width or self.width + n.width
1,663✔
460
    end
461
  else -- self.direction == "V"
462
    for i, n in ipairs(self.children) do
86✔
463
      n.relX = SILE.length(0)
86✔
464
      self.width = i == 1 and n.width or maxLength(self.width, n.width)
43✔
465
    end
466
    -- Set self.height and self.depth
467
    for i, n in ipairs(self.children) do
86✔
468
      self.depth = i == 1 and n.depth or self.depth + n.depth
43✔
469
    end
470
    for i = 1, #self.children do
86✔
471
      local n = self.children[i]
43✔
472
      if i == 1 then
43✔
473
        self.height = n.height
43✔
474
        self.depth = n.depth
43✔
475
      elseif i > 1 then
×
476
        n.relY = self.children[i - 1].relY + self.children[i - 1].depth + n.height
×
477
        self.depth = self.depth + n.height + n.depth
×
478
      end
479
    end
480
  end
481
end
482

483
-- Despite of its name, this function actually output the whole tree of nodes recursively.
484
function elements.stackbox:outputYourself (typesetter, line)
14✔
485
  local mathX = typesetter.frame.state.cursorX
43✔
486
  local mathY = typesetter.frame.state.cursorY
43✔
487
  self:outputTree(self.relX + mathX, self.relY + mathY, line)
129✔
488
  typesetter.frame:advanceWritingDirection(scaleWidth(self.width, line))
86✔
489
end
490

491
function elements.stackbox.output (_, _, _, _) end
269✔
492

493
elements.subscript = pl.class(elements.mbox)
14✔
494
elements.subscript._type = "Subscript"
7✔
495

496
function elements.subscript:__tostring ()
14✔
497
  return (self.sub and "Subscript" or "Superscript") .. "(" ..
×
498
    tostring(self.base) .. ", " .. tostring(self.sub or self.super) .. ")"
×
499
end
500

501
function elements.subscript:_init (base, sub, sup)
14✔
502
  elements.mbox._init(self)
86✔
503
  self.base = base
86✔
504
  self.sub = sub
86✔
505
  self.sup = sup
86✔
506
  if self.base then table.insert(self.children, self.base) end
86✔
507
  if self.sub then table.insert(self.children, self.sub) end
86✔
508
  if self.sup then table.insert(self.children, self.sup) end
86✔
509
  self.atom = self.base.atom
86✔
510
end
511

512
function elements.subscript:styleChildren ()
14✔
513
  if self.base then self.base.mode = self.mode end
86✔
514
  if self.sub then self.sub.mode = getSubscriptMode(self.mode) end
146✔
515
  if self.sup then self.sup.mode = getSuperscriptMode(self.mode) end
126✔
516
end
517

518
function elements.subscript:calculateItalicsCorrection ()
14✔
519
  local lastGid = getRightMostGlyphId(self.base)
86✔
520
  if lastGid > 0 then
86✔
521
    local mathMetrics = self:getMathMetrics()
82✔
522
    if mathMetrics.italicsCorrection[lastGid] then
82✔
523
      return mathMetrics.italicsCorrection[lastGid]
44✔
524
    end
525
  end
526
  return 0
42✔
527
end
528

529
function elements.subscript:shape ()
14✔
530
  local mathMetrics = self:getMathMetrics()
101✔
531
  local constants = mathMetrics.constants
101✔
532
  local scaleDown = self:getScaleDown()
101✔
533
  if self.base then
101✔
534
    self.base.relX = SILE.length(0)
202✔
535
    self.base.relY = SILE.length(0)
202✔
536
    -- Use widthForSubscript of base, if available
537
    self.width = self.base.widthForSubscript or self.base.width
101✔
538
  else
539
    self.width = SILE.length(0)
×
540
  end
541
  local itCorr = self:calculateItalicsCorrection() * scaleDown
202✔
542
  local subShift
543
  local supShift
544
  if self.sub then
101✔
545
    if self.isUnderOver or self.base.largeop then
75✔
546
      -- Ad hoc correction on integral limits, following LuaTeX's
547
      -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
548
      subShift = -itCorr
33✔
549
    else
550
      subShift = 0
42✔
551
    end
552
    self.sub.relX = self.width + subShift
150✔
553
    self.sub.relY = SILE.length(math.max(
150✔
554
      constants.subscriptShiftDown * scaleDown,
75✔
555
      --self.base.depth + constants.subscriptBaselineDropMin * scaleDown,
556
      (self.sub.height - constants.subscriptTopMax * scaleDown):tonumber()
150✔
557
    ))
75✔
558
    if (self:is_a(elements.underOver)
75✔
559
        or self:is_a(elements.stackbox) or self.base.largeop) then
135✔
560
      self.sub.relY = maxLength(self.sub.relY,
66✔
561
        self.base.depth + constants.subscriptBaselineDropMin*scaleDown)
66✔
562
    end
563
  end
564
  if self.sup then
101✔
565
    if self.isUnderOver or self.base.largeop then
55✔
566
      -- Ad hoc correction on integral limits, following LuaTeX's
567
      -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
568
      supShift = 0
21✔
569
    else
570
      supShift = itCorr
34✔
571
    end
572
    self.sup.relX = self.width + supShift
110✔
573
    self.sup.relY = SILE.length(math.max(
110✔
574
      isCrampedMode(self.mode)
55✔
575
      and constants.superscriptShiftUpCramped * scaleDown
55✔
576
      or constants.superscriptShiftUp * scaleDown, -- or cramped
55✔
577
      --self.base.height - constants.superscriptBaselineDropMax * scaleDown,
578
      (self.sup.depth + constants.superscriptBottomMin * scaleDown):tonumber()
110✔
579
    )) * (-1)
110✔
580
    if (self:is_a(elements.underOver)
55✔
581
        or self:is_a(elements.stackbox) or self.base.largeop) then
95✔
582
      self.sup.relY = maxLength(
42✔
583
        (0-self.sup.relY),
21✔
584
        self.base.height - constants.superscriptBaselineDropMax
21✔
585
        * scaleDown) * (-1)
63✔
586
      end
587
  end
588
  if self.sub and self.sup then
101✔
589
    local gap = self.sub.relY - self.sub.height - self.sup.relY - self.sup.depth
87✔
590
    if gap.length:tonumber() < constants.subSuperscriptGapMin * scaleDown then
58✔
591
      -- The following adjustment comes directly from Appendix G of he
592
      -- TeXbook (rule 18e).
593
      self.sub.relY = constants.subSuperscriptGapMin * scaleDown
8✔
594
        + self.sub.height + self.sup.relY + self.sup.depth
32✔
595
      local psi = constants.superscriptBottomMaxWithSubscript*scaleDown
8✔
596
        + self.sup.relY + self.sup.depth
16✔
597
      if psi:tonumber() > 0 then
16✔
598
        self.sup.relY = self.sup.relY - psi
16✔
599
        self.sub.relY = self.sub.relY - psi
16✔
600
      end
601
    end
602
  end
603
  self.width = self.width + maxLength(
202✔
604
    self.sub and self.sub.width + subShift or SILE.length(0),
176✔
605
    self.sup and self.sup.width + supShift or SILE.length(0)
156✔
606
  ) + constants.spaceAfterScript * scaleDown
303✔
607
  self.height = maxLength(
202✔
608
    self.base and self.base.height or SILE.length(0),
101✔
609
    self.sub and (self.sub.height - self.sub.relY) or SILE.length(0),
176✔
610
    self.sup and (self.sup.height - self.sup.relY) or SILE.length(0)
156✔
611
  )
101✔
612
  self.depth = maxLength(
202✔
613
    self.base and self.base.depth or SILE.length(0),
101✔
614
    self.sub and (self.sub.depth + self.sub.relY) or SILE.length(0),
176✔
615
    self.sup and (self.sup.depth + self.sup.relY) or SILE.length(0)
156✔
616
  )
101✔
617
end
618

619
function elements.subscript.output (_, _, _, _) end
93✔
620

621
elements.underOver = pl.class(elements.subscript)
14✔
622
elements.underOver._type = "UnderOver"
7✔
623

624
function elements.underOver:__tostring ()
14✔
625
  return self._type .. "(" .. tostring(self.base) .. ", " ..
×
626
    tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
×
627
end
628

629
function elements.underOver:_init (base, sub, sup)
14✔
630
  elements.mbox._init(self)
24✔
631
  self.atom = base.atom
24✔
632
  self.base = base
24✔
633
  self.sub = sub
24✔
634
  self.sup = sup
24✔
635
  if self.sup then table.insert(self.children, self.sup) end
24✔
636
  if self.base then
24✔
637
    table.insert(self.children, self.base)
24✔
638
  end
639
  if self.sub then table.insert(self.children, self.sub) end
24✔
640
end
641

642
function elements.underOver:styleChildren ()
14✔
643
  if self.base then self.base.mode = self.mode end
24✔
644
  if self.sub then self.sub.mode = getSubscriptMode(self.mode) end
48✔
645
  if self.sup then self.sup.mode = getSuperscriptMode(self.mode) end
48✔
646
end
647

648
function elements.underOver:shape ()
14✔
649
  if not (self.mode == mathMode.display
24✔
650
    or self.mode == mathMode.displayCramped) then
15✔
651
    self.isUnderOver = true
15✔
652
    elements.subscript.shape(self)
15✔
653
    return
15✔
654
  end
655
  local constants = self:getMathMetrics().constants
18✔
656
  local scaleDown = self:getScaleDown()
9✔
657
  -- Determine relative Ys
658
  if self.base then
9✔
659
    self.base.relY = SILE.length(0)
18✔
660
  end
661
  if self.sub then
9✔
662
    self.sub.relY = self.base.depth + SILE.length(math.max(
18✔
663
    (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
18✔
664
    constants.lowerLimitBaselineDropMin * scaleDown))
27✔
665
  end
666
  if self.sup then
9✔
667
    self.sup.relY = 0 - self.base.height - SILE.length(math.max(
27✔
668
    (constants.upperLimitGapMin * scaleDown + self.sup.depth):tonumber(),
18✔
669
    constants.upperLimitBaselineRiseMin * scaleDown))
27✔
670
  end
671
  -- Determine relative Xs based on widest symbol
672
  local widest, a, b
673
  if self.sub and self.sub.width > self.base.width then
9✔
674
    if self.sup and self.sub.width > self.sup.width then
6✔
675
      widest = self.sub
6✔
676
      a = self.base
6✔
677
      b = self.sup
6✔
678
    elseif self.sup then
×
679
      widest = self.sup
×
680
      a = self.base
×
681
      b = self.sub
×
682
    else
683
      widest = self.sub
×
684
      a = self.base
×
685
      b = nil
×
686
    end
687
  else
688
    if self.sup and self.base.width > self.sup.width then
3✔
689
      widest = self.base
3✔
690
      a = self.sub
3✔
691
      b = self.sup
3✔
692
    elseif self.sup then
×
693
      widest = self.sup
×
694
      a = self.base
×
695
      b = self.sub
×
696
    else
697
      widest = self.base
×
698
      a = self.sub
×
699
      b = nil
×
700
    end
701
  end
702
  widest.relX = SILE.length(0)
18✔
703
  local c = widest.width / 2
9✔
704
  if a then a.relX = c - a.width / 2 end
27✔
705
  if b then b.relX = c - b.width / 2 end
27✔
706
  local itCorr = self:calculateItalicsCorrection() * scaleDown
18✔
707
  if self.sup then self.sup.relX = self.sup.relX + itCorr / 2 end
18✔
708
  if self.sub then self.sub.relX = self.sub.relX - itCorr / 2 end
18✔
709
  -- Determine width and height
710
  self.width = maxLength(
18✔
711
  self.base and self.base.width or SILE.length(0),
9✔
712
  self.sub and self.sub.width or SILE.length(0),
9✔
713
  self.sup and self.sup.width or SILE.length(0)
9✔
714
  )
9✔
715
  if self.sup then
9✔
716
    self.height = 0 - self.sup.relY + self.sup.height
27✔
717
  else
718
    self.height = self.base and self.base.height or 0
×
719
  end
720
  if self.sub then
9✔
721
    self.depth = self.sub.relY + self.sub.depth
18✔
722
  else
723
    self.depth = self.base and self.base.depth or 0
×
724
  end
725
end
726

727
function elements.underOver:calculateItalicsCorrection ()
14✔
728
  local lastGid = getRightMostGlyphId(self.base)
24✔
729
  if lastGid > 0 then
24✔
730
    local mathMetrics = self:getMathMetrics()
24✔
731
    if mathMetrics.italicsCorrection[lastGid] then
24✔
732
      local c = mathMetrics.italicsCorrection[lastGid]
×
733
      -- If this is a big operator, and we are in display style, then the
734
      -- base glyph may be bigger than the font size. We need to adjust the
735
      -- italic correction accordingly.
736
      if self.base.atom == atomType.bigOperator and isDisplayMode(self.mode) then
×
737
        c = c * (self.base and self.base.font.size / self.font.size or 1.0)
×
738
      end
739
      return c
×
740
    end
741
  end
742
  return 0
24✔
743
end
744

745
function elements.underOver.output (_, _, _, _) end
31✔
746

747
-- terminal is the base class for leaf node
748
elements.terminal = pl.class(elements.mbox)
14✔
749
elements.terminal._type = "Terminal"
7✔
750

751
function elements.terminal:_init ()
14✔
752
  elements.mbox._init(self)
974✔
753
end
754

755
function elements.terminal.styleChildren (_) end
981✔
756

757
function elements.terminal.shape (_) end
7✔
758

759
elements.space = pl.class(elements.terminal)
14✔
760
elements.space._type = "Space"
7✔
761

762
function elements.space:_init ()
14✔
763
  elements.terminal._init(self)
×
764
end
765

766
function elements.space:__tostring ()
14✔
767
  return self._type .. "(width=" .. tostring(self.width) ..
×
768
  ", height=" .. tostring(self.height) ..
×
769
  ", depth=" .. tostring(self.depth) .. ")"
×
770
end
771

772
local function getStandardLength (value)
773
  if type(value) == "string" then
702✔
774
    local direction = 1
234✔
775
    if value:sub(1, 1) == "-" then
468✔
776
      value = value:sub(2, -1)
20✔
777
      direction = -1
10✔
778
    end
779
    if value == "thin" then
234✔
780
      return SILE.length("3mu") * direction
162✔
781
    elseif value == "med" then
180✔
782
      return SILE.length("4mu plus 2mu minus 4mu") * direction
249✔
783
    elseif value == "thick" then
97✔
784
      return SILE.length("5mu plus 5mu") * direction
225✔
785
    end
786
  end
787
  return SILE.length(value)
490✔
788
end
789

790
function elements.space:_init (width, height, depth)
14✔
791
  elements.terminal._init(self)
234✔
792
  self.width = getStandardLength(width)
468✔
793
  self.height = getStandardLength(height)
468✔
794
  self.depth = getStandardLength(depth)
468✔
795
end
796

797
function elements.space:shape ()
14✔
798
  self.width = self.width:absolute() * self:getScaleDown()
936✔
799
  self.height = self.height:absolute() * self:getScaleDown()
936✔
800
  self.depth = self.depth:absolute() * self:getScaleDown()
936✔
801
end
802

803
function elements.space.output (_) end
241✔
804

805
-- text node. For any actual text output
806
elements.text = pl.class(elements.terminal)
14✔
807
elements.text._type = "Text"
7✔
808

809
function elements.text:__tostring ()
14✔
810
  return self._type .. "(atom=" .. tostring(self.atom) ..
×
811
      ", kind=" .. tostring(self.kind) ..
×
812
      ", script=" .. tostring(self.script) ..
×
813
      (self.stretchy and ", stretchy" or "") ..
×
814
      (self.largeop and ", largeop" or "") ..
×
815
      ", text=\"" .. (self.originalText or self.text) .. "\")"
×
816
end
817

818
function elements.text:_init (kind, attributes, script, text)
14✔
819
  elements.terminal._init(self)
740✔
820
  if not (kind == "number" or kind == "identifier" or kind == "operator") then
740✔
821
    SU.error("Unknown text node kind '"..kind.."'; should be one of: number, identifier, operator.")
×
822
  end
823
  self.kind = kind
740✔
824
  self.script = script
740✔
825
  self.text = text
740✔
826
  if self.script ~= 'upright' then
740✔
827
    local converted = ""
740✔
828
    for _, uchr in luautf8.codes(self.text) do
1,705✔
829
      local dst_char = luautf8.char(uchr)
965✔
830
      if uchr >= 0x41 and uchr <= 0x5A then -- Latin capital letter
965✔
831
        dst_char = luautf8.char(mathScriptConversionTable.capital[self.script](uchr))
104✔
832
      elseif uchr >= 0x61 and uchr <= 0x7A then -- Latin non-capital letter
913✔
833
        dst_char = luautf8.char(mathScriptConversionTable.small[self.script](uchr))
886✔
834
      end
835
      converted = converted..dst_char
965✔
836
    end
837
    self.originalText = self.text
740✔
838
    self.text = converted
740✔
839
  end
840
  if self.kind == 'operator' then
740✔
841
    if self.text == "-" then
292✔
842
      self.text = "−"
×
843
    end
844
  end
845
  for attribute,value in pairs(attributes) do
1,100✔
846
    self[attribute] = value
360✔
847
  end
848
end
849

850
function elements.text:shape ()
14✔
851
  self.font.size = self.font.size * self:getScaleDown()
1,480✔
852
  local face = SILE.font.cache(self.font, SILE.shaper.getFace)
740✔
853
  local mathMetrics = self:getMathMetrics()
740✔
854
  local glyphs = SILE.shaper:shapeToken(self.text, self.font)
740✔
855
  -- Use bigger variants for big operators in display style
856
  if isDisplayMode(self.mode) and self.largeop then
1,480✔
857
    -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
858
    glyphs = pl.tablex.deepcopy(glyphs)
36✔
859
    local constructions = mathMetrics.mathVariants
18✔
860
      .vertGlyphConstructions[glyphs[1].gid]
18✔
861
    if constructions then
18✔
862
      local displayVariants = constructions.mathGlyphVariantRecord
18✔
863
      -- We select the biggest variant. TODO: we should probably select the
864
      -- first variant that is higher than displayOperatorMinHeight.
865
      local biggest
866
      local m = 0
18✔
867
      for _, v in ipairs(displayVariants) do
54✔
868
        if v.advanceMeasurement > m then
36✔
869
          biggest = v
36✔
870
          m = v.advanceMeasurement
36✔
871
        end
872
      end
873
      if biggest then
18✔
874
        glyphs[1].gid = biggest.variantGlyph
18✔
875
        local dimen = hb.get_glyph_dimensions(face,
36✔
876
          self.font.size, biggest.variantGlyph)
18✔
877
        glyphs[1].width = dimen.width
18✔
878
        glyphs[1].glyphAdvance = dimen.glyphAdvance
18✔
879
        --[[ I am told (https://github.com/alif-type/xits/issues/90) that,
880
        in fact, the relative height and depth of display-style big operators
881
        in the font is not relevant, as these should be centered around the
882
        axis. So the following code does that, while conserving their
883
        vertical size (distance from top to bottom). ]]
884
        local axisHeight = mathMetrics.constants.axisHeight * self:getScaleDown()
36✔
885
        local y_size = dimen.height + dimen.depth
18✔
886
        glyphs[1].height = y_size / 2 + axisHeight
18✔
887
        glyphs[1].depth = y_size / 2 - axisHeight
18✔
888
        -- We still need to store the font's height and depth somewhere,
889
        -- because that's what will be used to draw the glyph, and we will need
890
        -- to artificially compensate for that.
891
        glyphs[1].fontHeight = dimen.height
18✔
892
        glyphs[1].fontDepth = dimen.depth
18✔
893
      end
894
    end
895
  end
896
  SILE.shaper:preAddNodes(glyphs, self.value)
740✔
897
  self.value.items = glyphs
740✔
898
  self.value.glyphString = {}
740✔
899
  if glyphs and #glyphs > 0 then
740✔
900
    for i = 1, #glyphs do
1,705✔
901
      table.insert(self.value.glyphString, glyphs[i].gid)
965✔
902
    end
903
    self.width = SILE.length(0)
1,480✔
904
    self.widthForSubscript = SILE.length(0)
1,480✔
905
    for i = #glyphs, 1, -1 do
1,705✔
906
      self.width = self.width + glyphs[i].glyphAdvance
1,930✔
907
    end
908
    -- Store width without italic correction somewhere
909
    self.widthForSubscript = self.width
740✔
910
    local itCorr = mathMetrics.italicsCorrection[glyphs[#glyphs].gid]
740✔
911
    if itCorr then
740✔
912
      self.width = self.width + itCorr * self:getScaleDown()
432✔
913
    end
914
    for i = 1, #glyphs do
1,705✔
915
      self.height = i == 1 and SILE.length(glyphs[i].height) or SILE.length(math.max(self.height:tonumber(), glyphs[i].height))
2,155✔
916
      self.depth = i == 1 and SILE.length(glyphs[i].depth) or SILE.length(math.max(self.depth:tonumber(), glyphs[i].depth))
2,155✔
917
    end
918
  else
919
    self.width = SILE.length(0)
×
920
    self.height = SILE.length(0)
×
921
    self.depth = SILE.length(0)
×
922
  end
923
end
924

925
function elements.text:stretchyReshape (depth, height)
14✔
926
  -- Required depth+height of stretched glyph, in font units
927
  local mathMetrics = self:getMathMetrics()
64✔
928
  local upem = mathMetrics.unitsPerEm
64✔
929
  local sz = self.font.size
64✔
930
  local requiredAdvance = (depth + height):tonumber() * upem/sz
192✔
931
  SU.debug("math", "stretch: rA =", requiredAdvance)
64✔
932
  -- Choose variant of the closest size. The criterion we use is to have
933
  -- an advance measurement as close as possible as the required one.
934
  -- The advance measurement is simply the depth+height of the glyph.
935
  -- Therefore, the selected glyph may be smaller or bigger than
936
  -- required.  TODO: implement assembly of stretchable glyphs form
937
  -- their parts for cases when the biggest variant is not big enough.
938
  -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
939
  local glyphs = pl.tablex.deepcopy(self.value.items)
64✔
940
  local constructions = self:getMathMetrics().mathVariants
128✔
941
    .vertGlyphConstructions[glyphs[1].gid]
64✔
942
  if constructions then
64✔
943
    local variants = constructions.mathGlyphVariantRecord
64✔
944
    SU.debug("math", "stretch: variants =", variants)
64✔
945
    local closest
946
    local closestI
947
    local m = requiredAdvance - (self.depth+self.height):tonumber() * upem/sz
192✔
948
    SU.debug("math", "stretch: m =", m)
64✔
949
    for i,v in ipairs(variants) do
896✔
950
      local diff = math.abs(v.advanceMeasurement - requiredAdvance)
832✔
951
      SU.debug("math", "stretch: diff =", diff)
832✔
952
      if diff < m then
832✔
953
        closest = v
100✔
954
        closestI = i
100✔
955
        m = diff
100✔
956
      end
957
    end
958
    SU.debug("math", "stretch: closestI =", tostring(closestI))
64✔
959
    if closest then
64✔
960
      -- Now we have to re-shape the glyph chain. We will assume there
961
      -- is only one glyph.
962
      -- TODO: this code is probably wrong when the vertical
963
      -- variants have a different width than the original, because
964
      -- the shaping phase is already done. Need to do better.
965
      glyphs[1].gid = closest.variantGlyph
56✔
966
      local face = SILE.font.cache(self.font, SILE.shaper.getFace)
56✔
967
      local dimen = hb.get_glyph_dimensions(face,
112✔
968
        self.font.size, closest.variantGlyph)
56✔
969
      glyphs[1].width = dimen.width
56✔
970
      glyphs[1].height = dimen.height
56✔
971
      glyphs[1].depth = dimen.depth
56✔
972
      glyphs[1].glyphAdvance = dimen.glyphAdvance
56✔
973
      self.width = SILE.length(dimen.glyphAdvance)
112✔
974
      self.depth = SILE.length(dimen.depth)
112✔
975
      self.height = SILE.length(dimen.height)
112✔
976
      SILE.shaper:preAddNodes(glyphs, self.value)
56✔
977
      self.value.items = glyphs
56✔
978
      self.value.glyphString = {glyphs[1].gid}
56✔
979
    end
980
  end
981
end
982

983
function elements.text:output (x, y, line)
14✔
984
  if not self.value.glyphString then return end
740✔
985
  local compensatedY
986
  if isDisplayMode(self.mode)
740✔
987
      and self.atom == atomType.bigOperator
740✔
988
      and self.value.items[1].fontDepth then
9✔
989
    compensatedY = SILE.length(y.length + self.value.items[1].depth - self.value.items[1].fontDepth)
36✔
990
  else
991
    compensatedY = y
731✔
992
  end
993
  SILE.outputter:setCursor(scaleWidth(x, line), compensatedY.length)
1,480✔
994
  SILE.outputter:setFont(self.font)
740✔
995
  -- There should be no stretch or shrink on the width of a text
996
  -- element.
997
  local width = self.width.length
740✔
998
  SILE.outputter:drawHbox(self.value, width)
740✔
999
end
1000

1001
elements.fraction = pl.class(elements.mbox)
14✔
1002
elements.fraction._type = "Fraction"
7✔
1003

1004
function elements.fraction:__tostring ()
14✔
1005
  return self._type .. "(" .. tostring(self.numerator) .. ", " ..
×
1006
    tostring(self.denominator) .. ")"
×
1007
end
1008

1009
function elements.fraction:_init (numerator, denominator)
14✔
1010
  elements.mbox._init(self)
32✔
1011
  self.numerator = numerator
32✔
1012
  self.denominator = denominator
32✔
1013
  table.insert(self.children, numerator)
32✔
1014
  table.insert(self.children, denominator)
32✔
1015
end
1016

1017
function elements.fraction:styleChildren ()
14✔
1018
  self.numerator.mode = getNumeratorMode(self.mode)
64✔
1019
  self.denominator.mode = getDenominatorMode(self.mode)
64✔
1020
end
1021

1022
function elements.fraction:shape ()
14✔
1023
  -- Determine relative abscissas and width
1024
  local widest, other
1025
  if self.denominator.width > self.numerator.width then
32✔
1026
    widest, other = self.denominator, self.numerator
27✔
1027
  else
1028
    widest, other = self.numerator, self.denominator
5✔
1029
  end
1030
  widest.relX = SILE.length(0)
64✔
1031
  other.relX = (widest.width - other.width) / 2
96✔
1032
  self.width = widest.width
32✔
1033
  -- Determine relative ordinates and height
1034
  local constants = self:getMathMetrics().constants
64✔
1035
  local scaleDown = self:getScaleDown()
32✔
1036
  self.axisHeight = constants.axisHeight * scaleDown
32✔
1037
  self.ruleThickness = constants.fractionRuleThickness * scaleDown
32✔
1038
  if isDisplayMode(self.mode) then
64✔
1039
    self.numerator.relY = -self.axisHeight - self.ruleThickness/2 - SILE.length(math.max(
18✔
1040
      (constants.fractionNumDisplayStyleGapMin*scaleDown + self.numerator.depth):tonumber(),
18✔
1041
      constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
9✔
1042
        - self.axisHeight - self.ruleThickness/2))
27✔
1043
  else
1044
    self.numerator.relY = -self.axisHeight - self.ruleThickness/2 - SILE.length(math.max(
46✔
1045
      (constants.fractionNumeratorGapMin*scaleDown + self.numerator.depth):tonumber(),
46✔
1046
      constants.fractionNumeratorShiftUp * scaleDown - self.axisHeight
23✔
1047
        - self.ruleThickness/2))
69✔
1048
  end
1049
  if isDisplayMode(self.mode) then
64✔
1050
    self.denominator.relY = -self.axisHeight + self.ruleThickness/2 + SILE.length(math.max(
18✔
1051
      (constants.fractionDenomDisplayStyleGapMin * scaleDown
9✔
1052
        + self.denominator.height):tonumber(),
18✔
1053
      constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
9✔
1054
        + self.axisHeight - self.ruleThickness/2))
27✔
1055
  else
1056
    self.denominator.relY = -self.axisHeight + self.ruleThickness/2 + SILE.length(math.max(
46✔
1057
      (constants.fractionDenominatorGapMin * scaleDown
23✔
1058
        + self.denominator.height):tonumber(),
46✔
1059
      constants.fractionDenominatorShiftDown * scaleDown
23✔
1060
        + self.axisHeight - self.ruleThickness/2))
69✔
1061
  end
1062
  self.height = self.numerator.height - self.numerator.relY
64✔
1063
  self.depth = self.denominator.relY + self.denominator.depth
64✔
1064
end
1065

1066
function elements.fraction:output (x, y, line)
14✔
1067
  SILE.outputter:drawRule(
64✔
1068
    scaleWidth(x, line),
32✔
1069
    y.length - self.axisHeight - self.ruleThickness / 2,
64✔
1070
    scaleWidth(self.width, line), self.ruleThickness)
64✔
1071
end
1072

1073
local function newSubscript (spec)
1074
    return elements.subscript(spec.base, spec.sub, spec.sup)
86✔
1075
end
1076

1077
local function newUnderOver (spec)
1078
  return elements.underOver(spec.base, spec.sub, spec.sup)
24✔
1079
end
1080

1081
-- TODO replace with penlight equivalent
1082
local function mapList (f, l)
1083
  local ret = {}
4✔
1084
  for i, x in ipairs(l) do
16✔
1085
    ret[i] = f(i, x)
24✔
1086
  end
1087
  return ret
4✔
1088
end
1089

1090
elements.mtr = pl.class(elements.mbox)
14✔
1091
-- elements.mtr._type = "" -- TODO why not set?
1092

1093
function elements.mtr:_init (children)
14✔
1094
  self.children = children
12✔
1095
end
1096

1097
function elements.mtr:styleChildren ()
14✔
1098
  for _, c in ipairs(self.children) do
48✔
1099
    c.mode = self.mode
36✔
1100
  end
1101
end
1102

1103
function elements.mtr.shape (_) end -- done by parent table
19✔
1104

1105
function elements.mtr.output (_) end
19✔
1106

1107
elements.table = pl.class(elements.mbox)
14✔
1108
elements.table._type = "table" -- TODO why case difference?
7✔
1109

1110
function elements.table:_init (children, options)
14✔
1111
  elements.mbox._init(self)
4✔
1112
  self.children = children
4✔
1113
  self.options = options
4✔
1114
  self.nrows = #self.children
4✔
1115
  self.ncols = math.max(table.unpack(mapList(function(_, row)
12✔
1116
    return #row.children end, self.children)))
20✔
1117
  SU.debug("math", "self.ncols =", self.ncols)
4✔
1118
  self.rowspacing = self.options.rowspacing and SILE.length(self.options.rowspacing)
4✔
1119
    or SILE.length("7pt")
8✔
1120
  self.columnspacing = self.options.columnspacing and SILE.length(self.options.columnspacing)
4✔
1121
    or SILE.length("6pt")
8✔
1122
  -- Pad rows that do not have enough cells by adding cells to the
1123
  -- right.
1124
  for i,row in ipairs(self.children) do
16✔
1125
    for j = 1, (self.ncols - #row.children) do
12✔
1126
      SU.debug("math", "padding i =", i, "j =", j)
×
1127
      table.insert(row.children, elements.stackbox('H', {}))
×
1128
      SU.debug("math", "size", #row.children)
×
1129
    end
1130
  end
1131
  if options.columnalign then
4✔
1132
    local l = {}
2✔
1133
    for w in string.gmatch(options.columnalign, "[^%s]+") do
8✔
1134
      if not (w == "left" or w == "center" or w == "right") then
6✔
1135
        SU.error("Invalid specifier in `columnalign` attribute: "..w)
×
1136
      end
1137
      table.insert(l, w)
6✔
1138
    end
1139
    -- Pad with last value of l if necessary
1140
    for _ = 1, (self.ncols - #l), 1 do
2✔
1141
      table.insert(l, l[#l])
×
1142
    end
1143
    -- On the contrary, remove excess values in l if necessary
1144
    for _ = 1, (#l - self.ncols), 1 do
2✔
1145
      table.remove(l)
×
1146
    end
1147
    self.options.columnalign = l
2✔
1148
  else
1149
    self.options.columnalign = pl.List.range(1, self.ncols):map(function(_) return "center" end)
11✔
1150
  end
1151
end
1152

1153
function elements.table:styleChildren ()
14✔
1154
  if self.mode == mathMode.display and self.options.displaystyle ~= "false" then
4✔
1155
    for _,c in ipairs(self.children) do
8✔
1156
      c.mode = mathMode.display
6✔
1157
    end
1158
  else
1159
    for _,c in ipairs(self.children) do
8✔
1160
      c.mode = mathMode.text
6✔
1161
    end
1162
  end
1163
end
1164

1165
function elements.table:shape ()
14✔
1166
  -- Determine the height (resp. depth) of each row, which is the max
1167
  -- height (resp. depth) among its elements. Then we only need to add it to
1168
  -- the table's height and center every cell vertically.
1169
  for _,row in ipairs(self.children) do
16✔
1170
    row.height = SILE.length(0)
24✔
1171
    row.depth = SILE.length(0)
24✔
1172
    for _,cell in ipairs(row.children) do
48✔
1173
      row.height = maxLength(row.height, cell.height)
72✔
1174
      row.depth = maxLength(row.depth, cell.depth)
72✔
1175
    end
1176
  end
1177
  self.vertSize = SILE.length(0)
8✔
1178
  for i, row in ipairs(self.children) do
16✔
1179
    self.vertSize = self.vertSize + row.height + row.depth +
24✔
1180
      (i == self.nrows and SILE.length(0) or self.rowspacing) -- Spacing
28✔
1181
  end
1182
  local rowHeightSoFar = SILE.length(0)
4✔
1183
  for i, row in ipairs(self.children) do
16✔
1184
    row.relY = rowHeightSoFar + row.height - self.vertSize
36✔
1185
    rowHeightSoFar = rowHeightSoFar + row.height + row.depth +
24✔
1186
      (i == self.nrows and SILE.length(0) or self.rowspacing) -- Spacing
16✔
1187
  end
1188
  self.width = SILE.length(0)
8✔
1189
  local thisColRelX = SILE.length(0)
4✔
1190
  -- For every column...
1191
  for i = 1,self.ncols do
16✔
1192
    -- Determine its width
1193
    local columnWidth = SILE.length(0)
12✔
1194
    for j = 1,self.nrows do
48✔
1195
      if self.children[j].children[i].width > columnWidth then
36✔
1196
        columnWidth = self.children[j].children[i].width
16✔
1197
      end
1198
    end
1199
    -- Use it to align the contents of every cell as required.
1200
    for j = 1,self.nrows do
48✔
1201
      local cell = self.children[j].children[i]
36✔
1202
      if self.options.columnalign[i] == "left" then
36✔
1203
        cell.relX = thisColRelX
6✔
1204
      elseif self.options.columnalign[i] == "center" then
30✔
1205
        cell.relX = thisColRelX + (columnWidth - cell.width) / 2
96✔
1206
      elseif self.options.columnalign[i] == "right" then
6✔
1207
        cell.relX = thisColRelX + (columnWidth - cell.width)
18✔
1208
      else
1209
        SU.error("invalid columnalign parameter")
×
1210
      end
1211
    end
1212
    thisColRelX = thisColRelX + columnWidth +
12✔
1213
      (i == self.ncols and SILE.length(0) or self.columnspacing) -- Spacing
16✔
1214
  end
1215
  self.width = thisColRelX
4✔
1216
  -- Center myself vertically around the axis, and update relative Ys of rows accordingly
1217
  local axisHeight = self:getMathMetrics().constants.axisHeight * self:getScaleDown()
12✔
1218
  self.height = self.vertSize / 2 + axisHeight
12✔
1219
  self.depth = self.vertSize / 2 - axisHeight
12✔
1220
  for _,row in ipairs(self.children) do
16✔
1221
    row.relY = row.relY + self.vertSize / 2 - axisHeight
48✔
1222
    -- Also adjust width
1223
    row.width = self.width
12✔
1224
  end
1225
end
1226

1227
function elements.table.output (_) end
11✔
1228

1229
elements.mathMode = mathMode
7✔
1230
elements.atomType = atomType
7✔
1231
elements.scriptType = scriptType
7✔
1232
elements.mathVariantToScriptType = mathVariantToScriptType
7✔
1233
elements.symbolDefaults = symbolDefaults
7✔
1234
elements.newSubscript = newSubscript
7✔
1235
elements.newUnderOver = newUnderOver
7✔
1236

1237
return elements
7✔
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