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

sile-typesetter / sile / 3847017599

pending completion
3847017599

push

github

GitHub
Merge 95a1ed2ab into 13df3c1f5

56 of 56 new or added lines in 7 files covered. (100.0%)

9886 of 15388 relevant lines covered (64.24%)

6234.56 hits per line

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

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

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

9
local elements = {}
10✔
10

11
local mathMode = {
10✔
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 = {
10✔
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
59✔
42
    attr == "bold" and scriptType.bold or
37✔
43
    attr == "italic" and scriptType.italic or
25✔
44
    attr == "bold-italic" and scriptType.boldItalic or
25✔
45
    attr == "double-struck" and scriptType.doubleStruck or
23✔
46
    SU.error("Invalid value \""..attr.."\" for option mathvariant")
59✔
47
end
48

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

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

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

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

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

93
local mathCache = {}
10✔
94

95
local function retrieveMathTable(font)
96
  local key = SILE.font._key(font)
3,566✔
97
  if not mathCache[key] then
3,566✔
98
    SU.debug("math", "Loading math font", key)
32✔
99
    local face = SILE.font.cache(font, SILE.shaper.getFace)
32✔
100
    if not face then
32✔
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"))
32✔
104
    local upem = ot.parseHead(hb.get_table(face, "head")).unitsPerEm
64✔
105
    if mathTable == nil then
32✔
106
      SU.error("You must use a math font for math rendering.")
×
107
    end
108
    local constants = {}
32✔
109
    for k, v in pairs(mathTable.mathConstants) do
1,824✔
110
      if type(v) == "table" then v = v.value end
1,792✔
111
      if k:sub(-9) == "ScaleDown" then constants[k] = v / 100
3,584✔
112
      else
113
        constants[k] = v * font.size / upem
1,728✔
114
      end
115
    end
116
    local italicsCorrection = {}
32✔
117
    for k, v in pairs(mathTable.mathItalicsCorrection) do
13,024✔
118
      italicsCorrection[k] = v.value * font.size / upem
12,992✔
119
    end
120
    mathCache[key] = {
32✔
121
      constants = constants,
32✔
122
      italicsCorrection = italicsCorrection,
32✔
123
      mathVariants = mathTable.mathVariants,
32✔
124
      unitsPerEm = upem
32✔
125
    }
32✔
126
  end
127
  return mathCache[key]
3,566✔
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
66✔
134
    return mathMode.script
31✔
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
99✔
147
      or mode == mathMode.displayCramped or mode == mathMode.textCramped then
50✔
148
      return mathMode.scriptCramped
80✔
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
33✔
157
    return mathMode.text
10✔
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
33✔
176
    return mathMode.textCramped
10✔
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
274✔
186
    node = node.children[#(node.children)]
12✔
187
  end
188
  if node and node:is_a(elements.text) then
250✔
189
    return node.value.glyphString[#(node.value.glyphString)]
123✔
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 = {...}
2,411✔
199
  local m
200
  for i, v in ipairs(arg) do
7,477✔
201
    if i == 1 then
5,066✔
202
      m = v
2,411✔
203
    else
204
      if v.length:tonumber() > m.length:tonumber() then
7,965✔
205
        m = v
611✔
206
      end
207
    end
208
  end
209
  return m
2,411✔
210
end
211

212
local function scaleWidth(length, line)
213
  local number = length.length
1,052✔
214
  if line.ratio and line.ratio < 0 and length.shrink:tonumber() > 0 then
1,052✔
215
    number = number + length.shrink * line.ratio
×
216
  elseif line.ratio and line.ratio > 0 and length.stretch:tonumber() > 0 then
2,104✔
217
    number = number + length.stretch * line.ratio
1,174✔
218
  end
219
  return number
1,052✔
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 framwork
229
elements.mbox = pl.class(nodefactory.hbox)
20✔
230
elements.mbox._type = "Mbox"
10✔
231

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

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

254
function elements.mbox.styleChildren (_)
20✔
255
  SU.error("styleChildren is a virtual function that need to be overriden by its child classes")
×
256
end
257

258
function elements.mbox.shape (_, _, _)
20✔
259
  SU.error("shape is a virtual function that need to be overriden by its child classes")
×
260
end
261

262
function elements.mbox.output (_, _, _, _)
20✔
263
  SU.error("output is a virtual function that need to be overriden by its child classes")
×
264
end
265

266
function elements.mbox:getMathMetrics ()
20✔
267
  return retrieveMathTable(self.font)
3,566✔
268
end
269

270
function elements.mbox:getScaleDown ()
20✔
271
  local constants = self:getMathMetrics().constants
4,390✔
272
  local scaleDown
273
  if isScriptMode(self.mode) then
4,390✔
274
    scaleDown = constants.scriptPercentScaleDown
405✔
275
  elseif isScriptScriptMode(self.mode) then
3,580✔
276
    scaleDown = constants.scriptScriptPercentScaleDown
181✔
277
  else
278
    scaleDown = 1
1,609✔
279
  end
280
  return scaleDown
2,195✔
281
end
282

283
  -- Determine the mode of its descendants
284
function elements.mbox:styleDescendants ()
20✔
285
  self:styleChildren()
1,761✔
286
  for _, n in ipairs(self.children) do
3,460✔
287
    if n then n:styleDescendants() end
1,699✔
288
  end
289
end
290

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

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

319
local spaceKind = {
10✔
320
  thin = "thin",
321
  med = "med",
322
  thick = "thick",
323
}
324

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

377
-- _stackbox stacks its content one, either horizontally or vertically
378
elements.stackbox = pl.class(elements.mbox)
20✔
379
elements.stackbox._type = "Stackbox"
10✔
380

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

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

399
function elements.stackbox:styleChildren ()
20✔
400
  for _, n in ipairs(self.children) do
1,404✔
401
    n.mode = self.mode
1,043✔
402
  end
403
  if self.direction == "H" then
361✔
404
    -- Insert spaces according to the atom type, following Knuth's guidelines
405
    -- in the TeXbook
406
    local spaces = {}
299✔
407
    for i = 1, #self.children-1 do
987✔
408
      local v = self.children[i]
688✔
409
      local v2 = self.children[i + 1]
688✔
410
      if spacingRules[v.atom] and spacingRules[v.atom][v2.atom] then
688✔
411
        local rule = spacingRules[v.atom][v2.atom]
334✔
412
        if not (rule.notScript and (isScriptMode(self.mode) or isScriptScriptMode(self.mode))) then
894✔
413
          spaces[i+1] = rule[1]
238✔
414
        end
415
      end
416
    end
417
    local spaceIdx = {}
299✔
418
    for i, _ in pairs(spaces) do
537✔
419
      table.insert(spaceIdx, i)
238✔
420
    end
421
    table.sort(spaceIdx, function(a, b) return a > b end)
658✔
422
    for _, idx in ipairs(spaceIdx) do
537✔
423
      local hsp = elements.space(spaces[idx], 0, 0)
238✔
424
      table.insert(self.children, idx, hsp)
238✔
425
    end
426
  end
427
end
428

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

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

489
function elements.stackbox.output (_, _, _, _) end
371✔
490

491
elements.subscript = pl.class(elements.mbox)
20✔
492
elements.subscript._type = "Subscript"
10✔
493

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

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

510
function elements.subscript:styleChildren ()
20✔
511
  if self.base then self.base.mode = self.mode end
102✔
512
  if self.sub then self.sub.mode = getSubscriptMode(self.mode) end
176✔
513
  if self.sup then self.sup.mode = getSuperscriptMode(self.mode) end
144✔
514
end
515

516
function elements.subscript:calculateItalicsCorrection ()
20✔
517
  local lastGid = getRightMostGlyphId(self.base)
102✔
518
  if lastGid > 0 then
102✔
519
    local mathMetrics = self:getMathMetrics()
98✔
520
    if mathMetrics.italicsCorrection[lastGid] then
98✔
521
      return mathMetrics.italicsCorrection[lastGid]
55✔
522
    end
523
  end
524
  return 0
47✔
525
end
526

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

617
function elements.subscript.output (_, _, _, _) end
112✔
618

619
elements.underOver = pl.class(elements.subscript)
20✔
620
elements.underOver._type = "UnderOver"
10✔
621

622
function elements.underOver:__tostring ()
20✔
623
  return self._type .. "(" .. tostring(self.base) .. ", " ..
×
624
    tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
×
625
end
626

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

640
function elements.underOver:styleChildren ()
20✔
641
  if self.base then self.base.mode = self.mode end
25✔
642
  if self.sub then self.sub.mode = getSubscriptMode(self.mode) end
50✔
643
  if self.sup then self.sup.mode = getSuperscriptMode(self.mode) end
49✔
644
end
645

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

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

743
function elements.underOver.output (_, _, _, _) end
35✔
744

745
-- terminal is the base class for leaf node
746
elements.terminal = pl.class(elements.mbox)
20✔
747
elements.terminal._type = "Terminal"
10✔
748

749
function elements.terminal:_init ()
20✔
750
  elements.mbox._init(self)
1,220✔
751
end
752

753
function elements.terminal.styleChildren (_) end
1,230✔
754

755
function elements.terminal.shape (_) end
10✔
756

757
elements.space = pl.class(elements.terminal)
20✔
758
elements.space._type = "Space"
10✔
759

760
function elements.space:_init ()
20✔
761
  elements.terminal._init(self)
×
762
end
763

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

770
local function getStandardLength (value)
771
  if type(value) == "string" then
888✔
772
    local direction = 1
296✔
773
    if value:sub(1, 1) == "-" then
592✔
774
      value = value:sub(2, -1)
20✔
775
      direction = -1
10✔
776
    end
777
    if value == "thin" then
296✔
778
      return SILE.length("3mu") * direction
195✔
779
    elseif value == "med" then
231✔
780
      return SILE.length("4mu plus 2mu minus 4mu") * direction
288✔
781
    elseif value == "thick" then
135✔
782
      return SILE.length("5mu plus 5mu") * direction
333✔
783
    end
784
  end
785
  return SILE.length(value)
616✔
786
end
787

788
function elements.space:_init (width, height, depth)
20✔
789
  elements.terminal._init(self)
296✔
790
  self.width = getStandardLength(width)
592✔
791
  self.height = getStandardLength(height)
592✔
792
  self.depth = getStandardLength(depth)
592✔
793
end
794

795
function elements.space:shape ()
20✔
796
  self.width = self.width:absolute() * self:getScaleDown()
1,184✔
797
  self.height = self.height:absolute() * self:getScaleDown()
1,184✔
798
  self.depth = self.depth:absolute() * self:getScaleDown()
1,184✔
799
end
800

801
function elements.space.output (_) end
306✔
802

803
-- text node. For any actual text output
804
elements.text = pl.class(elements.terminal)
20✔
805
elements.text._type = "Text"
10✔
806

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

816
function elements.text:_init (kind, attributes, script, text)
20✔
817
  elements.terminal._init(self)
924✔
818
  if not (kind == "number" or kind == "identifier" or kind == "operator") then
924✔
819
    SU.error("Unknown text node kind '"..kind.."'; should be one of: number, identifier, operator.")
×
820
  end
821
  self.kind = kind
924✔
822
  self.script = script
924✔
823
  self.text = text
924✔
824
  if self.script ~= 'upright' then
924✔
825
    local converted = ""
924✔
826
    for _, uchr in luautf8.codes(self.text) do
2,095✔
827
      local dst_char = luautf8.char(uchr)
1,171✔
828
      if uchr >= 0x41 and uchr <= 0x5A then -- Latin capital letter
1,171✔
829
        dst_char = luautf8.char(mathScriptConversionTable.capital[self.script](uchr))
108✔
830
      elseif uchr >= 0x61 and uchr <= 0x7A then -- Latin non-capital letter
1,117✔
831
        dst_char = luautf8.char(mathScriptConversionTable.small[self.script](uchr))
1,038✔
832
      end
833
      converted = converted..dst_char
1,171✔
834
    end
835
    self.originalText = self.text
924✔
836
    self.text = converted
924✔
837
  end
838
  if self.kind == 'operator' then
924✔
839
    if self.text == "-" then
368✔
840
      self.text = "−"
4✔
841
    end
842
  end
843
  for attribute,value in pairs(attributes) do
1,366✔
844
    self[attribute] = value
442✔
845
  end
846
end
847

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

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

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

999
elements.fraction = pl.class(elements.mbox)
20✔
1000
elements.fraction._type = "Fraction"
10✔
1001

1002
function elements.fraction:__tostring ()
20✔
1003
  return self._type .. "(" .. tostring(self.numerator) .. ", " ..
×
1004
    tostring(self.denominator) .. ")"
×
1005
end
1006

1007
function elements.fraction:_init (numerator, denominator)
20✔
1008
  elements.mbox._init(self)
33✔
1009
  self.numerator = numerator
33✔
1010
  self.denominator = denominator
33✔
1011
  table.insert(self.children, numerator)
33✔
1012
  table.insert(self.children, denominator)
33✔
1013
end
1014

1015
function elements.fraction:styleChildren ()
20✔
1016
  self.numerator.mode = getNumeratorMode(self.mode)
66✔
1017
  self.denominator.mode = getDenominatorMode(self.mode)
66✔
1018
end
1019

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

1064
function elements.fraction:output (x, y, line)
20✔
1065
  SILE.outputter:drawRule(
66✔
1066
    scaleWidth(x, line),
33✔
1067
    y.length - self.axisHeight - self.ruleThickness / 2,
66✔
1068
    scaleWidth(self.width, line), self.ruleThickness)
66✔
1069
end
1070

1071
local function newSubscript (spec)
1072
    return elements.subscript(spec.base, spec.sub, spec.sup)
102✔
1073
end
1074

1075
local function newUnderOver (spec)
1076
  return elements.underOver(spec.base, spec.sub, spec.sup)
25✔
1077
end
1078

1079
-- TODO replace with penlight equivalent
1080
local function mapList (f, l)
1081
  local ret = {}
8✔
1082
  for i, x in ipairs(l) do
32✔
1083
    ret[i] = f(i, x)
48✔
1084
  end
1085
  return ret
8✔
1086
end
1087

1088
elements.mtr = pl.class(elements.mbox)
20✔
1089
-- elements.mtr._type = "" -- TODO why not set?
1090

1091
function elements.mtr:_init (children)
20✔
1092
  self.children = children
12✔
1093
end
1094

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

1101
function elements.mtr.shape (_) end -- done by parent table
22✔
1102

1103
function elements.mtr.output (_) end
22✔
1104

1105
elements.table = pl.class(elements.mbox)
20✔
1106
elements.table._type = "table" -- TODO why case diference?
10✔
1107

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

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

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

1225
function elements.table.output (_) end
18✔
1226

1227
elements.mathMode = mathMode
10✔
1228
elements.atomType = atomType
10✔
1229
elements.scriptType = scriptType
10✔
1230
elements.mathVariantToScriptType = mathVariantToScriptType
10✔
1231
elements.symbolDefaults = symbolDefaults
10✔
1232
elements.newSubscript = newSubscript
10✔
1233
elements.newUnderOver = newUnderOver
10✔
1234

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

© 2026 Coveralls, Inc