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

sile-typesetter / sile / 6713098919

31 Oct 2023 10:21PM UTC coverage: 52.831% (-21.8%) from 74.636%
6713098919

push

github

web-flow
Merge d0a2a1ee9 into b185d4972

45 of 45 new or added lines in 3 files covered. (100.0%)

8173 of 15470 relevant lines covered (52.83%)

6562.28 hits per line

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

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

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

9
local elements = {}
3✔
10

11
local mathMode = {
3✔
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 = {
3✔
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
32✔
42
    attr == "bold" and scriptType.bold or
22✔
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")
32✔
47
end
48

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

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

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

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

65
local mathScriptConversionTable = {
3✔
66
  capital = {
3✔
67
    [scriptType.upright] = function(codepoint) return codepoint end,
5✔
68
    [scriptType.bold] = function(codepoint)
3✔
69
      return codepoint + 0x1D400 - 0x41
×
70
    end,
71
    [scriptType.italic] = function(codepoint) return codepoint + 0x1D434 - 0x41 end,
5✔
72
    [scriptType.boldItalic] = function(codepoint) return codepoint + 0x1D468 - 0x41 end,
3✔
73
    [scriptType.doubleStruck] = function(codepoint)
3✔
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
  },
3✔
84
  small = {
3✔
85
    [scriptType.upright] = function(codepoint) return codepoint end,
15✔
86
    [scriptType.bold] = function(codepoint) return codepoint + 0x1D41A - 0x61 end,
3✔
87
    [scriptType.italic] = function(codepoint) return codepoint == 0x68 and 0x210E or codepoint + 0x1D44E - 0x61 end,
51✔
88
    [scriptType.boldItalic] = function(codepoint) return codepoint + 0x1D482 - 0x61 end,
3✔
89
    [scriptType.doubleStruck] = function(codepoint) return codepoint + 0x1D552 - 0x61 end,
15✔
90
  }
3✔
91
}
92

93
local mathCache = {}
3✔
94

95
local function retrieveMathTable(font)
96
  local key = SILE.font._key(font)
966✔
97
  if not mathCache[key] then
966✔
98
    SU.debug("math", "Loading math font", key)
8✔
99
    local face = SILE.font.cache(font, SILE.shaper.getFace)
8✔
100
    if not face then
8✔
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"))
8✔
104
    local upem = ot.parseHead(hb.get_table(face, "head")).unitsPerEm
16✔
105
    if mathTable == nil then
8✔
106
      SU.error("You must use a math font for math rendering.")
×
107
    end
108
    local constants = {}
8✔
109
    for k, v in pairs(mathTable.mathConstants) do
456✔
110
      if type(v) == "table" then v = v.value end
448✔
111
      if k:sub(-9) == "ScaleDown" then constants[k] = v / 100
896✔
112
      else
113
        constants[k] = v * font.size / upem
432✔
114
      end
115
    end
116
    local italicsCorrection = {}
8✔
117
    for k, v in pairs(mathTable.mathItalicsCorrection) do
3,256✔
118
      italicsCorrection[k] = v.value * font.size / upem
3,248✔
119
    end
120
    mathCache[key] = {
8✔
121
      constants = constants,
8✔
122
      italicsCorrection = italicsCorrection,
8✔
123
      mathVariants = mathTable.mathVariants,
8✔
124
      unitsPerEm = upem
8✔
125
    }
8✔
126
  end
127
  return mathCache[key]
966✔
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
24✔
134
    return mathMode.script
12✔
135
  -- D', T' -> S'
136
  elseif mode == mathMode.displayCramped or mode == mathMode.textCramped then
12✔
137
    return mathMode.scriptCramped
12✔
138
  -- S, SS -> SS
139
  elseif mode == mathMode.script or mode == mathMode.scriptScript then
×
140
    return mathMode.scriptScript
×
141
  -- S', SS' -> SS'
142
  else return mathMode.scriptScriptCramped end
×
143
end
144
local function getSubscriptMode(mode)
145
  -- D, T, D', T' -> S'
146
  if mode == mathMode.display or mode == mathMode.text
22✔
147
      or mode == mathMode.displayCramped or mode == mathMode.textCramped then
6✔
148
      return mathMode.scriptCramped
22✔
149
  -- S, SS, S', SS' -> SS'
150
  else return mathMode.scriptScriptCramped end
×
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
×
157
    return mathMode.text
×
158
  -- D' -> T'
159
  elseif mode == mathMode.displayCramped then
×
160
    return mathMode.textCramped
×
161
  -- T -> S
162
  elseif mode == mathMode.text then
×
163
    return mathMode.script
×
164
  -- T' -> S'
165
  elseif mode == mathMode.textCramped then
×
166
    return mathMode.scriptCramped
×
167
  -- S, SS -> SS
168
  elseif mode == mathMode.script or mode == mathMode.scriptScript then
×
169
    return mathMode.scriptScript
×
170
  -- S', SS' -> SS'
171
  else return mathMode.scriptScriptCramped end
×
172
end
173
local function getDenominatorMode(mode)
174
  -- D, D' -> T'
175
  if mode == mathMode.display or mode == mathMode.displayCramped then
×
176
    return mathMode.textCramped
×
177
  -- T, T' -> S'
178
  elseif mode == mathMode.text or mode == mathMode.textCramped then
×
179
    return mathMode.scriptCramped
×
180
  -- S, SS, S', SS' -> SS'
181
  else return mathMode.scriptScriptCramped end
×
182
end
183

184
local function getRightMostGlyphId(node)
185
  while node and node:is_a(elements.stackbox) and node.direction == 'H' do
92✔
186
    node = node.children[#(node.children)]
10✔
187
  end
188
  if node and node:is_a(elements.text) then
72✔
189
    return node.value.glyphString[#(node.value.glyphString)]
34✔
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 = {...}
658✔
199
  local m
200
  for i, v in ipairs(arg) do
2,050✔
201
    if i == 1 then
1,392✔
202
      m = v
658✔
203
    else
204
      if v.length:tonumber() > m.length:tonumber() then
2,202✔
205
        m = v
182✔
206
      end
207
    end
208
  end
209
  return m
658✔
210
end
211

212
local function scaleWidth(length, line)
213
  local number = length.length
260✔
214
  if line.ratio and line.ratio < 0 and length.shrink:tonumber() > 0 then
260✔
215
    number = number + length.shrink * line.ratio
×
216
  elseif line.ratio and line.ratio > 0 and length.stretch:tonumber() > 0 then
520✔
217
    number = number + length.stretch * line.ratio
380✔
218
  end
219
  return number
260✔
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)
6✔
230
elements.mbox._type = "Mbox"
3✔
231

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

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

256
function elements.mbox.styleChildren (_)
6✔
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 (_, _, _)
6✔
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 (_, _, _, _)
6✔
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 ()
6✔
269
  return retrieveMathTable(self.font)
966✔
270
end
271

272
function elements.mbox:getScaleDown ()
6✔
273
  local constants = self:getMathMetrics().constants
1,144✔
274
  local scaleDown
275
  if isScriptMode(self.mode) then
1,144✔
276
    scaleDown = constants.scriptPercentScaleDown
70✔
277
  elseif isScriptScriptMode(self.mode) then
1,004✔
278
    scaleDown = constants.scriptScriptPercentScaleDown
×
279
  else
280
    scaleDown = 1
502✔
281
  end
282
  return scaleDown
572✔
283
end
284

285
  -- Determine the mode of its descendants
286
function elements.mbox:styleDescendants ()
6✔
287
  self:styleChildren()
456✔
288
  for _, n in ipairs(self.children) do
898✔
289
    if n then n:styleDescendants() end
442✔
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 ()
6✔
298
  for _, n in ipairs(self.children) do
898✔
299
    if n then n:shapeTree() end
442✔
300
  end
301
  self:shape()
456✔
302
end
303

304
  -- Output the node and all its descendants
305
function elements.mbox:outputTree (x, y, line)
6✔
306
  self:output(x, y, line)
456✔
307
  local debug = SILE.settings:get("math.debug.boxes")
456✔
308
  if debug and not (self:is_a(elements.space)) then
456✔
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
898✔
317
    if n then n:outputTree(x + n.relX, y + n.relY, line) end
1,326✔
318
  end
319
end
320

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

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

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

383
function elements.stackbox:__tostring ()
6✔
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)
6✔
393
  elements.mbox._init(self)
78✔
394
  if not (direction == "H" or direction == "V") then
78✔
395
    SU.error("Wrong direction '"..direction.."'; should be H or V")
×
396
  end
397
  self.direction = direction
78✔
398
  self.children = children
78✔
399
end
400

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

431
function elements.stackbox:shape ()
6✔
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)
156✔
441
  self.depth = SILE.length(0)
156✔
442
  if self.direction == "H" then
78✔
443
    for i, n in ipairs(self.children) do
360✔
444
      n.relY = SILE.length(0)
592✔
445
      self.height = i == 1 and n.height or maxLength(self.height, n.height)
532✔
446
      self.depth = i == 1 and n.depth or maxLength(self.depth, n.depth)
532✔
447
    end
448
    -- Handle stretchy operators
449
    for _, elt in ipairs(self.children) do
360✔
450
      if elt.is_a(elements.text) and elt.kind == 'operator'
592✔
451
          and elt.stretchy then
108✔
452
        elt:stretchyReshape(self.depth, self.height)
36✔
453
      end
454
    end
455
    -- Set self.width
456
    self.width = SILE.length(0)
128✔
457
    for i, n in ipairs(self.children) do
360✔
458
      n.relX = self.width
296✔
459
      self.width = i == 1 and n.width or self.width + n.width
532✔
460
    end
461
  else -- self.direction == "V"
462
    for i, n in ipairs(self.children) do
28✔
463
      n.relX = SILE.length(0)
28✔
464
      self.width = i == 1 and n.width or maxLength(self.width, n.width)
14✔
465
    end
466
    -- Set self.height and self.depth
467
    for i, n in ipairs(self.children) do
28✔
468
      self.depth = i == 1 and n.depth or self.depth + n.depth
14✔
469
    end
470
    for i = 1, #self.children do
28✔
471
      local n = self.children[i]
14✔
472
      if i == 1 then
14✔
473
        self.height = n.height
14✔
474
        self.depth = n.depth
14✔
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)
6✔
485
  local mathX = typesetter.frame.state.cursorX
14✔
486
  local mathY = typesetter.frame.state.cursorY
14✔
487
  self:outputTree(self.relX + mathX, self.relY + mathY, line)
42✔
488
  typesetter.frame:advanceWritingDirection(scaleWidth(self.width, line))
28✔
489
end
490

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

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

496
function elements.subscript:__tostring ()
6✔
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)
6✔
502
  elements.mbox._init(self)
38✔
503
  self.base = base
38✔
504
  self.sub = sub
38✔
505
  self.sup = sup
38✔
506
  if self.base then table.insert(self.children, self.base) end
38✔
507
  if self.sub then table.insert(self.children, self.sub) end
38✔
508
  if self.sup then table.insert(self.children, self.sup) end
38✔
509
  self.atom = self.base.atom
38✔
510
end
511

512
function elements.subscript:styleChildren ()
6✔
513
  if self.base then self.base.mode = self.mode end
38✔
514
  if self.sub then self.sub.mode = getSubscriptMode(self.mode) end
60✔
515
  if self.sup then self.sup.mode = getSuperscriptMode(self.mode) end
62✔
516
end
517

518
function elements.subscript:calculateItalicsCorrection ()
6✔
519
  local lastGid = getRightMostGlyphId(self.base)
38✔
520
  if lastGid > 0 then
38✔
521
    local mathMetrics = self:getMathMetrics()
34✔
522
    if mathMetrics.italicsCorrection[lastGid] then
34✔
523
      return mathMetrics.italicsCorrection[lastGid]
18✔
524
    end
525
  end
526
  return 0
20✔
527
end
528

529
function elements.subscript:shape ()
6✔
530
  local mathMetrics = self:getMathMetrics()
38✔
531
  local constants = mathMetrics.constants
38✔
532
  local scaleDown = self:getScaleDown()
38✔
533
  if self.base then
38✔
534
    self.base.relX = SILE.length(0)
76✔
535
    self.base.relY = SILE.length(0)
76✔
536
    -- Use widthForSubscript of base, if available
537
    self.width = self.base.widthForSubscript or self.base.width
38✔
538
  else
539
    self.width = SILE.length(0)
×
540
  end
541
  local itCorr = self:calculateItalicsCorrection() * scaleDown
76✔
542
  local subShift
543
  local supShift
544
  if self.sub then
38✔
545
    if self.isUnderOver or self.base.largeop then
22✔
546
      -- Ad hoc correction on integral limits, following LuaTeX's
547
      -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
548
      subShift = -itCorr
×
549
    else
550
      subShift = 0
22✔
551
    end
552
    self.sub.relX = self.width + subShift
44✔
553
    self.sub.relY = SILE.length(math.max(
44✔
554
      constants.subscriptShiftDown * scaleDown,
22✔
555
      --self.base.depth + constants.subscriptBaselineDropMin * scaleDown,
556
      (self.sub.height - constants.subscriptTopMax * scaleDown):tonumber()
44✔
557
    ))
22✔
558
    if (self:is_a(elements.underOver)
22✔
559
        or self:is_a(elements.stackbox) or self.base.largeop) then
44✔
560
      self.sub.relY = maxLength(self.sub.relY,
×
561
        self.base.depth + constants.subscriptBaselineDropMin*scaleDown)
×
562
    end
563
  end
564
  if self.sup then
38✔
565
    if self.isUnderOver or self.base.largeop then
24✔
566
      -- Ad hoc correction on integral limits, following LuaTeX's
567
      -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
568
      supShift = 0
×
569
    else
570
      supShift = itCorr
24✔
571
    end
572
    self.sup.relX = self.width + supShift
48✔
573
    self.sup.relY = SILE.length(math.max(
48✔
574
      isCrampedMode(self.mode)
24✔
575
      and constants.superscriptShiftUpCramped * scaleDown
24✔
576
      or constants.superscriptShiftUp * scaleDown, -- or cramped
24✔
577
      --self.base.height - constants.superscriptBaselineDropMax * scaleDown,
578
      (self.sup.depth + constants.superscriptBottomMin * scaleDown):tonumber()
48✔
579
    )) * (-1)
48✔
580
    if (self:is_a(elements.underOver)
24✔
581
        or self:is_a(elements.stackbox) or self.base.largeop) then
48✔
582
      self.sup.relY = maxLength(
×
583
        (0-self.sup.relY),
×
584
        self.base.height - constants.superscriptBaselineDropMax
×
585
        * scaleDown) * (-1)
×
586
      end
587
  end
588
  if self.sub and self.sup then
38✔
589
    local gap = self.sub.relY - self.sub.height - self.sup.relY - self.sup.depth
24✔
590
    if gap.length:tonumber() < constants.subSuperscriptGapMin * scaleDown then
16✔
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(
76✔
604
    self.sub and self.sub.width + subShift or SILE.length(0),
60✔
605
    self.sup and self.sup.width + supShift or SILE.length(0)
62✔
606
  ) + constants.spaceAfterScript * scaleDown
114✔
607
  self.height = maxLength(
76✔
608
    self.base and self.base.height or SILE.length(0),
38✔
609
    self.sub and (self.sub.height - self.sub.relY) or SILE.length(0),
60✔
610
    self.sup and (self.sup.height - self.sup.relY) or SILE.length(0)
62✔
611
  )
38✔
612
  self.depth = maxLength(
76✔
613
    self.base and self.base.depth or SILE.length(0),
38✔
614
    self.sub and (self.sub.depth + self.sub.relY) or SILE.length(0),
60✔
615
    self.sup and (self.sup.depth + self.sup.relY) or SILE.length(0)
62✔
616
  )
38✔
617
end
618

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

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

624
function elements.underOver:__tostring ()
6✔
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)
6✔
630
  elements.mbox._init(self)
×
631
  self.atom = base.atom
×
632
  self.base = base
×
633
  self.sub = sub
×
634
  self.sup = sup
×
635
  if self.sup then table.insert(self.children, self.sup) end
×
636
  if self.base then
×
637
    table.insert(self.children, self.base)
×
638
  end
639
  if self.sub then table.insert(self.children, self.sub) end
×
640
end
641

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

648
function elements.underOver:shape ()
6✔
649
  if not (self.mode == mathMode.display
×
650
    or self.mode == mathMode.displayCramped) then
×
651
    self.isUnderOver = true
×
652
    elements.subscript.shape(self)
×
653
    return
×
654
  end
655
  local constants = self:getMathMetrics().constants
×
656
  local scaleDown = self:getScaleDown()
×
657
  -- Determine relative Ys
658
  if self.base then
×
659
    self.base.relY = SILE.length(0)
×
660
  end
661
  if self.sub then
×
662
    self.sub.relY = self.base.depth + SILE.length(math.max(
×
663
    (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
×
664
    constants.lowerLimitBaselineDropMin * scaleDown))
×
665
  end
666
  if self.sup then
×
667
    self.sup.relY = 0 - self.base.height - SILE.length(math.max(
×
668
    (constants.upperLimitGapMin * scaleDown + self.sup.depth):tonumber(),
×
669
    constants.upperLimitBaselineRiseMin * scaleDown))
×
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
×
674
    if self.sup and self.sub.width > self.sup.width then
×
675
      widest = self.sub
×
676
      a = self.base
×
677
      b = self.sup
×
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
×
689
      widest = self.base
×
690
      a = self.sub
×
691
      b = self.sup
×
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)
×
703
  local c = widest.width / 2
×
704
  if a then a.relX = c - a.width / 2 end
×
705
  if b then b.relX = c - b.width / 2 end
×
706
  local itCorr = self:calculateItalicsCorrection() * scaleDown
×
707
  if self.sup then self.sup.relX = self.sup.relX + itCorr / 2 end
×
708
  if self.sub then self.sub.relX = self.sub.relX - itCorr / 2 end
×
709
  -- Determine width and height
710
  self.width = maxLength(
×
711
  self.base and self.base.width or SILE.length(0),
×
712
  self.sub and self.sub.width or SILE.length(0),
×
713
  self.sup and self.sup.width or SILE.length(0)
×
714
  )
715
  if self.sup then
×
716
    self.height = 0 - self.sup.relY + self.sup.height
×
717
  else
718
    self.height = self.base and self.base.height or 0
×
719
  end
720
  if self.sub then
×
721
    self.depth = self.sub.relY + self.sub.depth
×
722
  else
723
    self.depth = self.base and self.base.depth or 0
×
724
  end
725
end
726

727
function elements.underOver:calculateItalicsCorrection ()
6✔
728
  local lastGid = getRightMostGlyphId(self.base)
×
729
  if lastGid > 0 then
×
730
    local mathMetrics = self:getMathMetrics()
×
731
    if mathMetrics.italicsCorrection[lastGid] then
×
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
×
743
end
744

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

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

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

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

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

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

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

766
function elements.space:__tostring ()
6✔
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
234✔
774
    local direction = 1
78✔
775
    if value:sub(1, 1) == "-" then
156✔
776
      value = value:sub(2, -1)
×
777
      direction = -1
×
778
    end
779
    if value == "thin" then
78✔
780
      return SILE.length("3mu") * direction
54✔
781
    elseif value == "med" then
60✔
782
      return SILE.length("4mu plus 2mu minus 4mu") * direction
96✔
783
    elseif value == "thick" then
28✔
784
      return SILE.length("5mu plus 5mu") * direction
84✔
785
    end
786
  end
787
  return SILE.length(value)
156✔
788
end
789

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

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

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

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

809
function elements.text:__tostring ()
6✔
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)
6✔
819
  elements.terminal._init(self)
246✔
820
  if not (kind == "number" or kind == "identifier" or kind == "operator") then
246✔
821
    SU.error("Unknown text node kind '"..kind.."'; should be one of: number, identifier, operator.")
×
822
  end
823
  self.kind = kind
246✔
824
  self.script = script
246✔
825
  self.text = text
246✔
826
  if self.script ~= 'upright' then
246✔
827
    local converted = ""
246✔
828
    for _, uchr in luautf8.codes(self.text) do
512✔
829
      local dst_char = luautf8.char(uchr)
266✔
830
      if uchr >= 0x41 and uchr <= 0x5A then -- Latin capital letter
266✔
831
        dst_char = luautf8.char(mathScriptConversionTable.capital[self.script](uchr))
44✔
832
      elseif uchr >= 0x61 and uchr <= 0x7A then -- Latin non-capital letter
244✔
833
        dst_char = luautf8.char(mathScriptConversionTable.small[self.script](uchr))
144✔
834
      end
835
      converted = converted..dst_char
266✔
836
    end
837
    self.originalText = self.text
246✔
838
    self.text = converted
246✔
839
  end
840
  if self.kind == 'operator' then
246✔
841
    if self.text == "-" then
108✔
842
      self.text = "−"
×
843
    end
844
  end
845
  for attribute,value in pairs(attributes) do
384✔
846
    self[attribute] = value
138✔
847
  end
848
end
849

850
function elements.text:shape ()
6✔
851
  self.font.size = self.font.size * self:getScaleDown()
492✔
852
  local face = SILE.font.cache(self.font, SILE.shaper.getFace)
246✔
853
  local mathMetrics = self:getMathMetrics()
246✔
854
  local glyphs = SILE.shaper:shapeToken(self.text, self.font)
246✔
855
  -- Use bigger variants for big operators in display style
856
  if isDisplayMode(self.mode) and self.largeop then
492✔
857
    -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
858
    glyphs = pl.tablex.deepcopy(glyphs)
×
859
    local constructions = mathMetrics.mathVariants
×
860
      .vertGlyphConstructions[glyphs[1].gid]
×
861
    if constructions then
×
862
      local displayVariants = constructions.mathGlyphVariantRecord
×
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
×
867
      for _, v in ipairs(displayVariants) do
×
868
        if v.advanceMeasurement > m then
×
869
          biggest = v
×
870
          m = v.advanceMeasurement
×
871
        end
872
      end
873
      if biggest then
×
874
        glyphs[1].gid = biggest.variantGlyph
×
875
        local dimen = hb.get_glyph_dimensions(face,
×
876
          self.font.size, biggest.variantGlyph)
×
877
        glyphs[1].width = dimen.width
×
878
        glyphs[1].glyphAdvance = dimen.glyphAdvance
×
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()
×
885
        local y_size = dimen.height + dimen.depth
×
886
        glyphs[1].height = y_size / 2 + axisHeight
×
887
        glyphs[1].depth = y_size / 2 - axisHeight
×
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
×
892
        glyphs[1].fontDepth = dimen.depth
×
893
      end
894
    end
895
  end
896
  SILE.shaper:preAddNodes(glyphs, self.value)
246✔
897
  self.value.items = glyphs
246✔
898
  self.value.glyphString = {}
246✔
899
  if glyphs and #glyphs > 0 then
246✔
900
    for i = 1, #glyphs do
512✔
901
      table.insert(self.value.glyphString, glyphs[i].gid)
266✔
902
    end
903
    self.width = SILE.length(0)
492✔
904
    self.widthForSubscript = SILE.length(0)
492✔
905
    for i = #glyphs, 1, -1 do
512✔
906
      self.width = self.width + glyphs[i].glyphAdvance
532✔
907
    end
908
    -- Store width without italic correction somewhere
909
    self.widthForSubscript = self.width
246✔
910
    local itCorr = mathMetrics.italicsCorrection[glyphs[#glyphs].gid]
246✔
911
    if itCorr then
246✔
912
      self.width = self.width + itCorr * self:getScaleDown()
150✔
913
    end
914
    for i = 1, #glyphs do
512✔
915
      self.height = i == 1 and SILE.length(glyphs[i].height) or SILE.length(math.max(self.height:tonumber(), glyphs[i].height))
552✔
916
      self.depth = i == 1 and SILE.length(glyphs[i].depth) or SILE.length(math.max(self.depth:tonumber(), glyphs[i].depth))
552✔
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)
6✔
926
  -- Required depth+height of stretched glyph, in font units
927
  local mathMetrics = self:getMathMetrics()
36✔
928
  local upem = mathMetrics.unitsPerEm
36✔
929
  local sz = self.font.size
36✔
930
  local requiredAdvance = (depth + height):tonumber() * upem/sz
108✔
931
  SU.debug("math", "stretch: rA =", requiredAdvance)
36✔
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)
36✔
940
  local constructions = self:getMathMetrics().mathVariants
72✔
941
    .vertGlyphConstructions[glyphs[1].gid]
36✔
942
  if constructions then
36✔
943
    local variants = constructions.mathGlyphVariantRecord
36✔
944
    SU.debug("math", "stretch: variants =", variants)
36✔
945
    local closest
946
    local closestI
947
    local m = requiredAdvance - (self.depth+self.height):tonumber() * upem/sz
108✔
948
    SU.debug("math", "stretch: m =", m)
36✔
949
    for i,v in ipairs(variants) do
504✔
950
      local diff = math.abs(v.advanceMeasurement - requiredAdvance)
468✔
951
      SU.debug("math", "stretch: diff =", diff)
468✔
952
      if diff < m then
468✔
953
        closest = v
48✔
954
        closestI = i
48✔
955
        m = diff
48✔
956
      end
957
    end
958
    SU.debug("math", "stretch: closestI =", tostring(closestI))
36✔
959
    if closest then
36✔
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
36✔
966
      local face = SILE.font.cache(self.font, SILE.shaper.getFace)
36✔
967
      local dimen = hb.get_glyph_dimensions(face,
72✔
968
        self.font.size, closest.variantGlyph)
36✔
969
      glyphs[1].width = dimen.width
36✔
970
      glyphs[1].height = dimen.height
36✔
971
      glyphs[1].depth = dimen.depth
36✔
972
      glyphs[1].glyphAdvance = dimen.glyphAdvance
36✔
973
      self.width = SILE.length(dimen.glyphAdvance)
72✔
974
      self.depth = SILE.length(dimen.depth)
72✔
975
      self.height = SILE.length(dimen.height)
72✔
976
      SILE.shaper:preAddNodes(glyphs, self.value)
36✔
977
      self.value.items = glyphs
36✔
978
      self.value.glyphString = {glyphs[1].gid}
36✔
979
    end
980
  end
981
end
982

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

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

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

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

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

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

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

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

1077
local function newUnderOver (spec)
1078
  return elements.underOver(spec.base, spec.sub, spec.sup)
×
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)
6✔
1091
-- elements.mtr._type = "" -- TODO why not set?
1092

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

1097
function elements.mtr:styleChildren ()
6✔
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
15✔
1104

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

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

1110
function elements.table:_init (children, options)
6✔
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 ()
6✔
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 ()
6✔
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
7✔
1228

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

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