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

sile-typesetter / sile / 3849002768

pending completion
3849002768

push

github

GitHub
Merge f1334c9ed into 13df3c1f5

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

11 of 14758 relevant lines covered (0.07%)

0.0 hits per line

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

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

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

9
local elements = {}
×
10

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

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

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

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

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

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

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

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

93
local mathCache = {}
×
94

95
local function retrieveMathTable(font)
96
  local key = SILE.font._key(font)
×
97
  if not mathCache[key] then
×
98
    SU.debug("math", "Loading math font", key)
×
99
    local face = SILE.font.cache(font, SILE.shaper.getFace)
×
100
    if not face then
×
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"))
×
104
    local upem = ot.parseHead(hb.get_table(face, "head")).unitsPerEm
×
105
    if mathTable == nil then
×
106
      SU.error("You must use a math font for math rendering.")
×
107
    end
108
    local constants = {}
×
109
    for k, v in pairs(mathTable.mathConstants) do
×
110
      if type(v) == "table" then v = v.value end
×
111
      if k:sub(-9) == "ScaleDown" then constants[k] = v / 100
×
112
      else
113
        constants[k] = v * font.size / upem
×
114
      end
115
    end
116
    local italicsCorrection = {}
×
117
    for k, v in pairs(mathTable.mathItalicsCorrection) do
×
118
      italicsCorrection[k] = v.value * font.size / upem
×
119
    end
120
    mathCache[key] = {
×
121
      constants = constants,
122
      italicsCorrection = italicsCorrection,
123
      mathVariants = mathTable.mathVariants,
124
      unitsPerEm = upem
×
125
    }
126
  end
127
  return mathCache[key]
×
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
×
134
    return mathMode.script
×
135
  -- D', T' -> S'
136
  elseif mode == mathMode.displayCramped or mode == mathMode.textCramped then
×
137
    return mathMode.scriptCramped
×
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
×
147
      or mode == mathMode.displayCramped or mode == mathMode.textCramped then
×
148
      return mathMode.scriptCramped
×
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
×
186
    node = node.children[#(node.children)]
×
187
  end
188
  if node and node:is_a(elements.text) then
×
189
    return node.value.glyphString[#(node.value.glyphString)]
×
190
  else
191
    return 0
×
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 = {...}
×
199
  local m
200
  for i, v in ipairs(arg) do
×
201
    if i == 1 then
×
202
      m = v
×
203
    else
204
      if v.length:tonumber() > m.length:tonumber() then
×
205
        m = v
×
206
      end
207
    end
208
  end
209
  return m
×
210
end
211

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

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

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

254
function elements.mbox.styleChildren (_)
×
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 (_, _, _)
×
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 (_, _, _, _)
×
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 ()
×
267
  return retrieveMathTable(self.font)
×
268
end
269

270
function elements.mbox:getScaleDown ()
×
271
  local constants = self:getMathMetrics().constants
×
272
  local scaleDown
273
  if isScriptMode(self.mode) then
×
274
    scaleDown = constants.scriptPercentScaleDown
×
275
  elseif isScriptScriptMode(self.mode) then
×
276
    scaleDown = constants.scriptScriptPercentScaleDown
×
277
  else
278
    scaleDown = 1
×
279
  end
280
  return scaleDown
×
281
end
282

283
  -- Determine the mode of its descendants
284
function elements.mbox:styleDescendants ()
×
285
  self:styleChildren()
×
286
  for _, n in ipairs(self.children) do
×
287
    if n then n:styleDescendants() end
×
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 ()
×
296
  for _, n in ipairs(self.children) do
×
297
    if n then n:shapeTree() end
×
298
  end
299
  self:shape()
×
300
end
301

302
  -- Output the node and all its descendants
303
function elements.mbox:outputTree (x, y, line)
×
304
  self:output(x, y, line)
×
305
  local debug = SILE.settings:get("math.debug.boxes")
×
306
  if debug and not (self:is_a(elements.space)) then
×
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
×
315
    if n then n:outputTree(x + n.relX, y + n.relY, line) end
×
316
  end
317
end
318

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

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

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

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

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

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

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

491
elements.subscript = pl.class(elements.mbox)
×
492
elements.subscript._type = "Subscript"
×
493

494
function elements.subscript:__tostring ()
×
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)
×
500
  elements.mbox._init(self)
×
501
  self.base = base
×
502
  self.sub = sub
×
503
  self.sup = sup
×
504
  if self.base then table.insert(self.children, self.base) end
×
505
  if self.sub then table.insert(self.children, self.sub) end
×
506
  if self.sup then table.insert(self.children, self.sup) end
×
507
  self.atom = self.base.atom
×
508
end
509

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

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

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

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

619
elements.underOver = pl.class(elements.subscript)
×
620
elements.underOver._type = "UnderOver"
×
621

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

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

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

725
function elements.underOver:calculateItalicsCorrection ()
×
726
  local lastGid = getRightMostGlyphId(self.base)
×
727
  if lastGid > 0 then
×
728
    local mathMetrics = self:getMathMetrics()
×
729
    if mathMetrics.italicsCorrection[lastGid] then
×
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
×
741
end
742

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

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

749
function elements.terminal:_init ()
×
750
  elements.mbox._init(self)
×
751
end
752

753
function elements.terminal.styleChildren (_) end
×
754

755
function elements.terminal.shape (_) end
×
756

757
elements.space = pl.class(elements.terminal)
×
758
elements.space._type = "Space"
×
759

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

764
function elements.space:__tostring ()
×
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
×
772
    local direction = 1
×
773
    if value:sub(1, 1) == "-" then
×
774
      value = value:sub(2, -1)
×
775
      direction = -1
×
776
    end
777
    if value == "thin" then
×
778
      return SILE.length("3mu") * direction
×
779
    elseif value == "med" then
×
780
      return SILE.length("4mu plus 2mu minus 4mu") * direction
×
781
    elseif value == "thick" then
×
782
      return SILE.length("5mu plus 5mu") * direction
×
783
    end
784
  end
785
  return SILE.length(value)
×
786
end
787

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

795
function elements.space:shape ()
×
796
  self.width = self.width:absolute() * self:getScaleDown()
×
797
  self.height = self.height:absolute() * self:getScaleDown()
×
798
  self.depth = self.depth:absolute() * self:getScaleDown()
×
799
end
800

801
function elements.space.output (_) end
×
802

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

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

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

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

999
elements.fraction = pl.class(elements.mbox)
×
1000
elements.fraction._type = "Fraction"
×
1001

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

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

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

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

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

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

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

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

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

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

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

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

1103
function elements.mtr.output (_) end
×
1104

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

1108
function elements.table:_init (children, options)
×
1109
  elements.mbox._init(self)
×
1110
  self.children = children
×
1111
  self.options = options
×
1112
  self.nrows = #self.children
×
1113
  self.ncols = math.max(table.unpack(mapList(function(_, row)
×
1114
    return #row.children end, self.children)))
×
1115
  SU.debug("math", "self.ncols =", self.ncols)
×
1116
  self.rowspacing = self.options.rowspacing and SILE.length(self.options.rowspacing)
×
1117
    or SILE.length("7pt")
×
1118
  self.columnspacing = self.options.columnspacing and SILE.length(self.options.columnspacing)
×
1119
    or SILE.length("6pt")
×
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
×
1123
    for j = 1, (self.ncols - #row.children) do
×
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
×
1130
    local l = {}
×
1131
    for w in string.gmatch(options.columnalign, "[^%s]+") do
×
1132
      if not (w == "left" or w == "center" or w == "right") then
×
1133
        SU.error("Invalid specifier in `columnalign` attribute: "..w)
×
1134
      end
1135
      table.insert(l, w)
×
1136
    end
1137
    -- Pad with last value of l if necessary
1138
    for _ = 1, (self.ncols - #l), 1 do
×
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
×
1143
      table.remove(l)
×
1144
    end
1145
    self.options.columnalign = l
×
1146
  else
1147
    self.options.columnalign = pl.List.range(1, self.ncols):map(function(_) return "center" end)
×
1148
  end
1149
end
1150

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

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

1225
function elements.table.output (_) end
×
1226

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

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

© 2026 Coveralls, Inc