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

sile-typesetter / sile / 15507594683

07 Jun 2025 11:54AM UTC coverage: 30.951% (-30.4%) from 61.309%
15507594683

push

github

alerque
chore(tooling): Add post-checkout hook to clear makedeps on branch switch

6363 of 20558 relevant lines covered (30.95%)

3445.44 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("types.node")
×
2
local hb = require("justenoughharfbuzz")
×
3
local ot = require("core.opentype-parser")
×
4
local atoms = require("packages.math.atoms")
×
5
local mathvariants = require("packages.math.unicode-mathvariants")
×
6
local convertMathVariantScript = mathvariants.convertMathVariantScript
×
7

8
local elements = {}
×
9

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

21
local function isDisplayMode (mode)
22
   return mode <= 1
×
23
end
24

25
local function isCrampedMode (mode)
26
   return mode % 2 == 1
×
27
end
28

29
local function isScriptMode (mode)
30
   return mode == mathMode.script or mode == mathMode.scriptCramped
×
31
end
32

33
local function isScriptScriptMode (mode)
34
   return mode == mathMode.scriptScript or mode == mathMode.scriptScriptCramped
×
35
end
36

37
local mathCache = {}
×
38

39
local function retrieveMathTable (font)
40
   local key = SILE.font._key(font)
×
41
   if not mathCache[key] then
×
42
      SU.debug("math", "Loading math font", key)
×
43
      local face = SILE.font.cache(font, SILE.shaper:_getFaceCallback())
×
44
      if not face then
×
45
         SU.error("Could not find requested font " .. font .. " or any suitable substitutes")
×
46
      end
47
      local fontHasMathTable, rawMathTable, mathTableParsable, mathTable
48
      fontHasMathTable, rawMathTable = pcall(hb.get_table, face, "MATH")
×
49
      if fontHasMathTable then
×
50
         mathTableParsable, mathTable = pcall(ot.parseMath, rawMathTable)
×
51
      end
52
      if not fontHasMathTable or not mathTableParsable then
×
53
         SU.error(([[
×
54
            You must use a math font for math rendering
55

56
            The math table in '%s' could not be %s.
57
         ]]):format(face.filename, fontHasMathTable and "parsed" or "loaded"))
×
58
      end
59
      local upem = ot.parseHead(hb.get_table(face, "head")).unitsPerEm
×
60
      local constants = {}
×
61
      for k, v in pairs(mathTable.mathConstants) do
×
62
         if type(v) == "table" then
×
63
            v = v.value
×
64
         end
65
         if k:sub(-9) == "ScaleDown" then
×
66
            constants[k] = v / 100
×
67
         else
68
            constants[k] = v * font.size / upem
×
69
         end
70
      end
71
      local italicsCorrection = {}
×
72
      for k, v in pairs(mathTable.mathItalicsCorrection) do
×
73
         italicsCorrection[k] = v.value * font.size / upem
×
74
      end
75
      mathCache[key] = {
×
76
         constants = constants,
77
         italicsCorrection = italicsCorrection,
78
         mathVariants = mathTable.mathVariants,
79
         unitsPerEm = upem,
80
      }
81
   end
82
   return mathCache[key]
×
83
end
84

85
-- Style transition functions for superscript and subscript
86
local function getSuperscriptMode (mode)
87
   -- D, T -> S
88
   if mode == mathMode.display or mode == mathMode.text then
×
89
      return mathMode.script
×
90
   -- D', T' -> S'
91
   elseif mode == mathMode.displayCramped or mode == mathMode.textCramped then
×
92
      return mathMode.scriptCramped
×
93
   -- S, SS -> SS
94
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
×
95
      return mathMode.scriptScript
×
96
   -- S', SS' -> SS'
97
   else
98
      return mathMode.scriptScriptCramped
×
99
   end
100
end
101
local function getSubscriptMode (mode)
102
   -- D, T, D', T' -> S'
103
   if
104
      mode == mathMode.display
×
105
      or mode == mathMode.text
×
106
      or mode == mathMode.displayCramped
×
107
      or mode == mathMode.textCramped
×
108
   then
109
      return mathMode.scriptCramped
×
110
   -- S, SS, S', SS' -> SS'
111
   else
112
      return mathMode.scriptScriptCramped
×
113
   end
114
end
115

116
-- Style transition functions for fraction (numerator and denominator)
117
local function getNumeratorMode (mode)
118
   -- D -> T
119
   if mode == mathMode.display then
×
120
      return mathMode.text
×
121
   -- D' -> T'
122
   elseif mode == mathMode.displayCramped then
×
123
      return mathMode.textCramped
×
124
   -- T -> S
125
   elseif mode == mathMode.text then
×
126
      return mathMode.script
×
127
   -- T' -> S'
128
   elseif mode == mathMode.textCramped then
×
129
      return mathMode.scriptCramped
×
130
   -- S, SS -> SS
131
   elseif mode == mathMode.script or mode == mathMode.scriptScript then
×
132
      return mathMode.scriptScript
×
133
   -- S', SS' -> SS'
134
   else
135
      return mathMode.scriptScriptCramped
×
136
   end
137
end
138
local function getDenominatorMode (mode)
139
   -- D, D' -> T'
140
   if mode == mathMode.display or mode == mathMode.displayCramped then
×
141
      return mathMode.textCramped
×
142
   -- T, T' -> S'
143
   elseif mode == mathMode.text or mode == mathMode.textCramped then
×
144
      return mathMode.scriptCramped
×
145
   -- S, SS, S', SS' -> SS'
146
   else
147
      return mathMode.scriptScriptCramped
×
148
   end
149
end
150

151
local function getRightMostGlyphId (node)
152
   while node and node:is_a(elements.stackbox) and node.direction == "H" do
×
153
      node = node.children[#node.children]
×
154
   end
155
   if node and node:is_a(elements.text) then
×
156
      return node.value.glyphString[#node.value.glyphString]
×
157
   else
158
      return 0
×
159
   end
160
end
161

162
-- Compares two SILE.types.length, without considering shrink or stretch values, and
163
-- returns the biggest.
164
local function maxLength (...)
165
   local arg = { ... }
×
166
   local m
167
   for i, v in ipairs(arg) do
×
168
      if i == 1 then
×
169
         m = v
×
170
      else
171
         if v.length:tonumber() > m.length:tonumber() then
×
172
            m = v
×
173
         end
174
      end
175
   end
176
   return m
×
177
end
178

179
local function scaleWidth (length, line)
180
   local number = length.length
×
181
   if line.ratio and line.ratio < 0 and length.shrink:tonumber() > 0 then
×
182
      number = number + length.shrink * line.ratio
×
183
   elseif line.ratio and line.ratio > 0 and length.stretch:tonumber() > 0 then
×
184
      number = number + length.stretch * line.ratio
×
185
   end
186
   return number
×
187
end
188

189
-- math box, box with a horizontal shift value and could contain zero or more
190
-- mbox'es (or its child classes) the entire math environment itself is
191
-- a top-level mbox.
192
-- Typesetting of mbox evolves four steps:
193
--   1. Determine the mode for each mbox according to their parent.
194
--   2. Shape the mbox hierarchy from leaf to top. Get the shape and relative position.
195
--   3. Convert mbox into _nnode's to put in SILE's typesetting framework
196
elements.mbox = pl.class(nodefactory.hbox)
×
197
elements.mbox._type = "Mbox"
×
198

199
function elements.mbox:__tostring ()
×
200
   return self._type
×
201
end
202

203
function elements.mbox:_init ()
×
204
   nodefactory.hbox._init(self)
×
205
   self.font = {}
×
206
   self.children = {} -- The child nodes
×
207
   self.relX = SILE.types.length(0) -- x position relative to its parent box
×
208
   self.relY = SILE.types.length(0) -- y position relative to its parent box
×
209
   self.value = {}
×
210
   self.mode = mathMode.display
×
211
   self.atom = atoms.types.ord
×
212
   local font = {
×
213
      family = SILE.settings:get("math.font.family"),
214
      size = SILE.settings:get("math.font.size"),
215
      style = SILE.settings:get("math.font.style"),
216
      weight = SILE.settings:get("math.font.weight"),
217
      -- https://learn.microsoft.com/en-us/typography/opentype/spec/math#opentype-layout-tags-used-with-the-math-table
218
      --   "Script tag to be used for features in math layout.
219
      --   The only language system supported with this tag is the default language system."
220
      -- Thus, needed for the ssty feature in superscript/subscript to work properly.
221
      script = "math",
222
   }
223
   local filename = SILE.settings:get("math.font.filename")
×
224
   if filename and filename ~= "" then
×
225
      font.filename = filename
×
226
   end
227
   self.font = SILE.font.loadDefaults(font)
×
228
end
229

230
function elements.mbox:styleChildren ()
×
231
   SU.error("styleChildren is a virtual function that need to be overridden by its child classes")
×
232
end
233

234
function elements.mbox:shape (_, _)
×
235
   SU.error("shape is a virtual function that need to be overridden by its child classes")
×
236
end
237

238
function elements.mbox:output (_, _, _)
×
239
   SU.error("output is a virtual function that need to be overridden by its child classes")
×
240
end
241

242
function elements.mbox:getMathMetrics ()
×
243
   return retrieveMathTable(self.font)
×
244
end
245

246
function elements.mbox:getScaleDown ()
×
247
   local constants = self:getMathMetrics().constants
×
248
   local scaleDown
249
   if isScriptMode(self.mode) then
×
250
      scaleDown = constants.scriptPercentScaleDown
×
251
   elseif isScriptScriptMode(self.mode) then
×
252
      scaleDown = constants.scriptScriptPercentScaleDown
×
253
   else
254
      scaleDown = 1
×
255
   end
256
   return scaleDown
×
257
end
258

259
-- Determine the mode of its descendants
260
function elements.mbox:styleDescendants ()
×
261
   self:styleChildren()
×
262
   for _, n in ipairs(self.children) do
×
263
      if n then
×
264
         n:styleDescendants()
×
265
      end
266
   end
267
end
268

269
-- shapeTree shapes the mbox and all its descendants in a recursive fashion
270
-- The inner-most leaf nodes determine their shape first, and then propagate to their parents
271
-- During the process, each node will determine its size by (width, height, depth)
272
-- and (relX, relY) which the relative position to its parent
273
function elements.mbox:shapeTree ()
×
274
   for _, n in ipairs(self.children) do
×
275
      if n then
×
276
         n:shapeTree()
×
277
      end
278
   end
279
   self:shape()
×
280
end
281

282
-- Output the node and all its descendants
283
function elements.mbox:outputTree (x, y, line)
×
284
   self:output(x, y, line)
×
285
   local debug = SILE.settings:get("math.debug.boxes")
×
286
   if debug and not (self:is_a(elements.space)) then
×
287
      SILE.outputter:setCursor(scaleWidth(x, line), y.length)
×
288
      SILE.outputter:debugHbox({ height = self.height.length, depth = self.depth.length }, scaleWidth(self.width, line))
×
289
   end
290
   for _, n in ipairs(self.children) do
×
291
      if n then
×
292
         n:outputTree(x + n.relX, y + n.relY, line)
×
293
      end
294
   end
295
end
296

297
local spaceKind = {
×
298
   thin = "thin",
299
   med = "med",
300
   thick = "thick",
301
}
302

303
-- Spacing table indexed by left atom, as in TeXbook p. 170.
304
-- Notes
305
--  - the "notScript" key is used to prevent spaces in script and scriptscript modes
306
--    (= parenthesized non-zero value in The TeXbook's table).
307
--  - Cases commented are as expected, just listed for clarity and completeness.
308
--    (= no space i.e. 0 in in The TeXbook's table)
309
--  - Cases marked as impossible are not expected to happen (= stars in the TeXbook):
310
--    "... such cases never arise, because binary atoms must be preceded and followed
311
--    by atoms compatible with the nature of binary operations."
312
--    This must be understood with the context explained onp. 133:
313
--     "... binary operations are treated as ordinary symbols if they don’t occur
314
--     between two quantities that they can operate on." (a rule which notably helps
315
--     addressing binary atoms used as unary operators.)
316
local spacingRules = {
×
317
   [atoms.types.ord] = {
×
318
      -- [atoms.types.ord] = nil
319
      [atoms.types.op] = { spaceKind.thin },
×
320
      [atoms.types.bin] = { spaceKind.med, notScript = true },
×
321
      [atoms.types.rel] = { spaceKind.thick, notScript = true },
×
322
      -- [atoms.types.open] = nil
323
      -- [atoms.types.close] = nil
324
      -- [atoms.types.punct] = nil
325
      [atoms.types.inner] = { spaceKind.thin, notScript = true },
×
326
   },
327
   [atoms.types.op] = {
×
328
      [atoms.types.ord] = { spaceKind.thin },
×
329
      [atoms.types.op] = { spaceKind.thin },
×
330
      [atoms.types.bin] = { impossible = true },
×
331
      [atoms.types.rel] = { spaceKind.thick, notScript = true },
×
332
      -- [atoms.types.open] = nil
333
      -- [atoms.types.close] = nil
334
      -- [atoms.types.punct] = nil
335
      [atoms.types.inner] = { spaceKind.thin, notScript = true },
×
336
   },
337
   [atoms.types.bin] = {
×
338
      [atoms.types.ord] = { spaceKind.med, notScript = true },
×
339
      [atoms.types.op] = { spaceKind.med, notScript = true },
×
340
      [atoms.types.bin] = { impossible = true },
×
341
      [atoms.types.rel] = { impossible = true },
×
342
      [atoms.types.open] = { spaceKind.med, notScript = true },
×
343
      [atoms.types.close] = { impossible = true },
×
344
      [atoms.types.punct] = { impossible = true },
×
345
      [atoms.types.inner] = { spaceKind.med, notScript = true },
×
346
   },
347
   [atoms.types.rel] = {
×
348
      [atoms.types.ord] = { spaceKind.thick, notScript = true },
×
349
      [atoms.types.op] = { spaceKind.thick, notScript = true },
×
350
      [atoms.types.bin] = { impossible = true },
×
351
      -- [atoms.types.rel] = nil
352
      [atoms.types.open] = { spaceKind.thick, notScript = true },
×
353
      -- [atoms.types.close] = nil
354
      -- [atoms.types.punct] = nil
355
      [atoms.types.inner] = { spaceKind.thick, notScript = true },
×
356
   },
357
   [atoms.types.open] = {
×
358
      -- [atoms.types.ord] = nil
359
      -- [atoms.types.op] = nil
360
      [atoms.types.bin] = { impossible = true },
×
361
      -- [atoms.types.rel] = nil
362
      -- [atoms.types.open] = nil
363
      -- [atoms.types.close] = nil
364
      -- [atoms.types.punct] = nil
365
      -- [atoms.types.inner] = nil
366
   },
367
   [atoms.types.close] = {
×
368
      -- [atoms.types.ord] = nil
369
      [atoms.types.op] = { spaceKind.thin },
×
370
      [atoms.types.bin] = { spaceKind.med, notScript = true },
×
371
      [atoms.types.rel] = { spaceKind.thick, notScript = true },
×
372
      -- [atoms.types.open] = nil
373
      -- [atoms.types.close] = nil
374
      -- [atoms.types.punct] = nil
375
      [atoms.types.inner] = { spaceKind.thin, notScript = true },
×
376
   },
377
   [atoms.types.punct] = {
×
378
      [atoms.types.ord] = { spaceKind.thin, notScript = true },
×
379
      [atoms.types.op] = { spaceKind.thin, notScript = true },
×
380
      [atoms.types.bin] = { impossible = true },
×
381
      [atoms.types.rel] = { spaceKind.thin, notScript = true },
×
382
      [atoms.types.open] = { spaceKind.thin, notScript = true },
×
383
      [atoms.types.close] = { spaceKind.thin, notScript = true },
×
384
      [atoms.types.punct] = { spaceKind.thin, notScript = true },
×
385
      [atoms.types.inner] = { spaceKind.thin, notScript = true },
×
386
   },
387
   [atoms.types.inner] = {
×
388
      [atoms.types.ord] = { spaceKind.thin, notScript = true },
×
389
      [atoms.types.op] = { spaceKind.thin },
×
390
      [atoms.types.bin] = { spaceKind.med, notScript = true },
×
391
      [atoms.types.rel] = { spaceKind.thick, notScript = true },
×
392
      [atoms.types.open] = { spaceKind.thin, notScript = true },
×
393
      [atoms.types.punct] = { spaceKind.thin, notScript = true },
×
394
      -- [atoms.types.close] = nil
395
      [atoms.types.inner] = { spaceKind.thin, notScript = true },
×
396
   },
397
}
398

399
-- _stackbox stacks its content one, either horizontally or vertically
400
elements.stackbox = pl.class(elements.mbox)
×
401
elements.stackbox._type = "Stackbox"
×
402

403
function elements.stackbox:__tostring ()
×
404
   local result = self.direction .. "Box("
×
405
   for i, n in ipairs(self.children) do
×
406
      result = result .. (i == 1 and "" or ", ") .. tostring(n)
×
407
   end
408
   result = result .. ")"
×
409
   return result
×
410
end
411

412
function elements.stackbox:_init (direction, children)
×
413
   elements.mbox._init(self)
×
414
   if not (direction == "H" or direction == "V") then
×
415
      SU.error("Wrong direction '" .. direction .. "'; should be H or V")
×
416
   end
417
   self.direction = direction
×
418
   self.children = children
×
419
end
420

421
function elements.stackbox:styleChildren ()
×
422
   for _, n in ipairs(self.children) do
×
423
      n.mode = self.mode
×
424
   end
425
   if self.direction == "H" then
×
426
      -- Insert spaces according to the atom type, following Knuth's guidelines
427
      -- in The TeXbook, p. 170 (amended with p. 133 for binary operators)
428
      -- FIXME: This implementation is not using the atom form and the MathML logic (lspace/rspace).
429
      -- (This is notably unsatisfactory for <mphantom> elements)
430
      local spaces = {}
×
431
      if #self.children >= 1 then
×
432
         -- An interpretation of the TeXbook p. 133 for binary operator exceptions:
433
         -- A binary operator at the beginning of the expression is treated as an ordinary atom
434
         -- (so as to be considered as a unary operator, without more context).
435
         local v = self.children[1]
×
436
         if v.atom == atoms.types.bin then
×
437
            v.atom = atoms.types.ord
×
438
         end
439
      end
440
      for i = 1, #self.children - 1 do
×
441
         local v = self.children[i]
×
442
         local v2 = self.children[i + 1]
×
443
         -- Handle re-wrapped paired open/close symbols
444
         v = v.is_paired and v.children[#v.children] or v
×
445
         v2 = v2.is_paired and v2.children[1] or v2
×
446
         if spacingRules[v.atom] and spacingRules[v.atom][v2.atom] then
×
447
            local rule = spacingRules[v.atom][v2.atom]
×
448
            if rule.impossible then
×
449
               -- Another interpretation of the TeXbook p. 133 for binary operator exceptions:
450
               if v2.atom == atoms.types.bin then
×
451
                  -- If a binary atom follows an atom that is not compatible with it, make it an ordinary.
452
                  -- (so as to be conidered as a unary operator).
453
                  -- Typical case: "a = -b" (ord rel bin ord), "a + -b" (ord bin bin ord)
454
                  v2.atom = atoms.types.ord
×
455
               else
456
                  -- If a binary atom precedes an atom that is not compatible with it, make it an ordinary.
457
                  -- Quite unusual case (bin, rel/close/punct) unlikely to happen in practice.
458
                  -- (Not seen in 80+ test formulas)
459
                  -- We might address it a bit late here, the preceding atom has already based its spacing
460
                  -- on the binary atom... but this might not be a big deal.
461
                  -- (i.e. rather than add an extra look-ahead just for this case).
462
                  -- Artificial example: "a + = b" (ord bin rel ord)
463
                  v.atom = atoms.types.ord
×
464
               end
465
               rule = spacingRules[v.atom][v2.atom]
×
466
               if rule and rule.impossible then
×
467
                  -- Should not occur if we did our table based on the TeXbook correctly?
468
                  -- We can still handle it by ignoring the rule: no spacing sounds logical.
469
                  -- But let's have a warning so it might be investigated further.
470
                  SU.warn("Impossible spacing rule for (" .. v.atom .. ", " .. v2.atom .. "), please report this issue")
×
471
                  rule = nil
×
472
               end
473
            end
474
            if rule and not (rule.notScript and (isScriptMode(self.mode) or isScriptScriptMode(self.mode))) then
×
475
               spaces[i + 1] = rule[1]
×
476
            end
477
         end
478
      end
479
      local spaceIdx = {}
×
480
      for i, _ in pairs(spaces) do
×
481
         table.insert(spaceIdx, i)
×
482
      end
483
      table.sort(spaceIdx, function (a, b)
×
484
         return a > b
×
485
      end)
486
      for _, idx in ipairs(spaceIdx) do
×
487
         local hsp = elements.space(spaces[idx], 0, 0)
×
488
         table.insert(self.children, idx, hsp)
×
489
      end
490
   end
491
end
492

493
function elements.stackbox:shape ()
×
494
   -- For a horizontal stackbox (i.e. mrow):
495
   -- 1. set self.height and self.depth to max element height & depth
496
   -- 2. handle stretchy operators
497
   -- 3. set self.width
498
   -- For a vertical stackbox:
499
   -- 1. set self.width to max element width
500
   -- 2. set self.height
501
   -- And finally set children's relative coordinates
502
   self.height = SILE.types.length(0)
×
503
   self.depth = SILE.types.length(0)
×
504
   if self.direction == "H" then
×
505
      for i, n in ipairs(self.children) do
×
506
         n.relY = SILE.types.length(0)
×
507
         self.height = i == 1 and n.height or maxLength(self.height, n.height)
×
508
         self.depth = i == 1 and n.depth or maxLength(self.depth, n.depth)
×
509
      end
510
      -- Handle stretchy operators
511
      for _, elt in ipairs(self.children) do
×
512
         if elt:is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
×
513
            elt:_vertStretchyReshape(self.depth, self.height)
×
514
         end
515
      end
516
      -- Set self.width
517
      self.width = SILE.types.length(0)
×
518
      for i, n in ipairs(self.children) do
×
519
         n.relX = self.width
×
520
         self.width = i == 1 and n.width or self.width + n.width
×
521
      end
522
   else -- self.direction == "V"
523
      for i, n in ipairs(self.children) do
×
524
         n.relX = SILE.types.length(0)
×
525
         self.width = i == 1 and n.width or maxLength(self.width, n.width)
×
526
      end
527
      -- Set self.height and self.depth
528
      for i, n in ipairs(self.children) do
×
529
         self.depth = i == 1 and n.depth or self.depth + n.depth
×
530
      end
531
      for i = 1, #self.children do
×
532
         local n = self.children[i]
×
533
         if i == 1 then
×
534
            self.height = n.height
×
535
            self.depth = n.depth
×
536
         elseif i > 1 then
×
537
            n.relY = self.children[i - 1].relY + self.children[i - 1].depth + n.height
×
538
            self.depth = self.depth + n.height + n.depth
×
539
         end
540
      end
541
   end
542
end
543

544
-- Despite of its name, this function actually output the whole tree of nodes recursively.
545
function elements.stackbox:outputYourself (typesetter, line)
×
546
   local mathX = typesetter.frame.state.cursorX
×
547
   local mathY = typesetter.frame.state.cursorY
×
548
   self:outputTree(self.relX + mathX, self.relY + mathY, line)
×
549
   typesetter.frame:advanceWritingDirection(scaleWidth(self.width, line))
×
550
end
551

552
function elements.stackbox:output (_, _, _) end
×
553

554
elements.phantom = pl.class(elements.stackbox) -- inherit from stackbox
×
555
elements.phantom._type = "Phantom"
×
556

557
function elements.phantom:_init (children)
×
558
   -- MathML core 3.3.7:
559
   -- "Its layout algorithm is the same as the mrow element".
560
   -- Also not the MathML states that <mphantom> is sort of legacy, "implemented
561
   -- for compatibility with full MathML. Authors whose only target is MathML
562
   -- Core are encouraged to use CSS for styling."
563
   -- The thing is that we don't have CSS in SILE, so supporting <mphantom> is
564
   -- a must.
565
   elements.stackbox._init(self, "H", children)
×
566
end
567

568
function elements.phantom:output (_, _, _)
×
569
   -- Note the trick here: when the tree is rendered, the node's output
570
   -- function is invoked, then all its children's output functions.
571
   -- So we just cancel the list of children here, before it's rendered.
572
   self.children = {}
×
573
end
574

575
elements.subscript = pl.class(elements.mbox)
×
576
elements.subscript._type = "Subscript"
×
577

578
function elements.subscript:__tostring ()
×
579
   return (self.sub and "Subscript" or "Superscript")
×
580
      .. "("
×
581
      .. tostring(self.base)
×
582
      .. ", "
×
583
      .. tostring(self.sub or self.super)
×
584
      .. ")"
×
585
end
586

587
function elements.subscript:_init (base, sub, sup)
×
588
   elements.mbox._init(self)
×
589
   self.base = base
×
590
   self.sub = sub
×
591
   self.sup = sup
×
592
   if self.base then
×
593
      table.insert(self.children, self.base)
×
594
   end
595
   if self.sub then
×
596
      table.insert(self.children, self.sub)
×
597
   end
598
   if self.sup then
×
599
      table.insert(self.children, self.sup)
×
600
   end
601
   self.atom = self.base.atom
×
602
end
603

604
function elements.subscript:styleChildren ()
×
605
   if self.base then
×
606
      self.base.mode = self.mode
×
607
   end
608
   if self.sub then
×
609
      self.sub.mode = getSubscriptMode(self.mode)
×
610
   end
611
   if self.sup then
×
612
      self.sup.mode = getSuperscriptMode(self.mode)
×
613
   end
614
end
615

616
function elements.subscript:calculateItalicsCorrection ()
×
617
   local lastGid = getRightMostGlyphId(self.base)
×
618
   if lastGid > 0 then
×
619
      local mathMetrics = self:getMathMetrics()
×
620
      if mathMetrics.italicsCorrection[lastGid] then
×
621
         return mathMetrics.italicsCorrection[lastGid]
×
622
      end
623
   end
624
   return 0
×
625
end
626

627
function elements.subscript:shape ()
×
628
   local mathMetrics = self:getMathMetrics()
×
629
   local constants = mathMetrics.constants
×
630
   local scaleDown = self:getScaleDown()
×
631
   if self.base then
×
632
      self.base.relX = SILE.types.length(0)
×
633
      self.base.relY = SILE.types.length(0)
×
634
      -- Use widthForSubscript of base, if available
635
      self.width = self.base.widthForSubscript or self.base.width
×
636
   else
637
      self.width = SILE.types.length(0)
×
638
   end
639
   local itCorr = self:calculateItalicsCorrection() * scaleDown
×
640
   local isBaseSymbol = not self.base or self.base:is_a(elements.terminal)
×
641
   local isBaseLargeOp = SU.boolean(self.base and self.base.largeop, false)
×
642
   local subShift
643
   local supShift
644
   if self.sub then
×
645
      if self.isUnderOver or isBaseLargeOp then
×
646
         -- Ad hoc correction on integral limits, following LuaTeX's
647
         -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
648
         subShift = -itCorr
×
649
      else
650
         subShift = 0
×
651
      end
652
      self.sub.relX = self.width + subShift
×
653
      self.sub.relY = SILE.types.length(
×
654
         math.max(
×
655
            constants.subscriptShiftDown * scaleDown,
×
656
            isBaseSymbol and 0 -- TeX (σ19) is more finicky than MathML Core
×
657
               or (self.base.depth + constants.subscriptBaselineDropMin * scaleDown):tonumber(),
×
658
            (self.sub.height - constants.subscriptTopMax * scaleDown):tonumber()
×
659
         )
660
      )
661
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or isBaseLargeOp then
×
662
         self.sub.relY = maxLength(self.sub.relY, self.base.depth + constants.subscriptBaselineDropMin * scaleDown)
×
663
      end
664
   end
665
   if self.sup then
×
666
      if self.isUnderOver or isBaseLargeOp then
×
667
         -- Ad hoc correction on integral limits, following LuaTeX's
668
         -- `\mathnolimitsmode=0` (see LuaTeX Reference Manual).
669
         supShift = 0
×
670
      else
671
         supShift = itCorr
×
672
      end
673
      self.sup.relX = self.width + supShift
×
674
      self.sup.relY = SILE.types.length(
×
675
         math.max(
×
676
            isCrampedMode(self.mode) and constants.superscriptShiftUpCramped * scaleDown
×
677
               or constants.superscriptShiftUp * scaleDown,
×
678
            isBaseSymbol and 0 -- TeX (σ18) is more finicky than MathML Core
×
679
               or (self.base.height - constants.superscriptBaselineDropMax * scaleDown):tonumber(),
×
680
            (self.sup.depth + constants.superscriptBottomMin * scaleDown):tonumber()
×
681
         )
682
      ) * -1
×
683
      if self:is_a(elements.underOver) or self:is_a(elements.stackbox) or isBaseLargeOp then
×
684
         self.sup.relY = maxLength(
×
685
            (0 - self.sup.relY),
×
686
            self.base.height - constants.superscriptBaselineDropMax * scaleDown
×
687
         ) * -1
×
688
      end
689
   end
690
   if self.sub and self.sup then
×
691
      local gap = self.sub.relY - self.sub.height - self.sup.relY - self.sup.depth
×
692
      if gap.length:tonumber() < constants.subSuperscriptGapMin * scaleDown then
×
693
         -- The following adjustment comes directly from Appendix G of he
694
         -- TeXbook (rule 18e).
695
         self.sub.relY = constants.subSuperscriptGapMin * scaleDown + self.sub.height + self.sup.relY + self.sup.depth
×
696
         local psi = constants.superscriptBottomMaxWithSubscript * scaleDown + self.sup.relY + self.sup.depth
×
697
         if psi:tonumber() > 0 then
×
698
            self.sup.relY = self.sup.relY - psi
×
699
            self.sub.relY = self.sub.relY - psi
×
700
         end
701
      end
702
   end
703
   self.width = self.width
×
704
      + maxLength(
×
705
         self.sub and self.sub.width + subShift or SILE.types.length(0),
×
706
         self.sup and self.sup.width + supShift or SILE.types.length(0)
×
707
      )
708
      + constants.spaceAfterScript * scaleDown
×
709
   self.height = maxLength(
×
710
      self.base and self.base.height or SILE.types.length(0),
×
711
      self.sub and (self.sub.height - self.sub.relY) or SILE.types.length(0),
×
712
      self.sup and (self.sup.height - self.sup.relY) or SILE.types.length(0)
×
713
   )
714
   self.depth = maxLength(
×
715
      self.base and self.base.depth or SILE.types.length(0),
×
716
      self.sub and (self.sub.depth + self.sub.relY) or SILE.types.length(0),
×
717
      self.sup and (self.sup.depth + self.sup.relY) or SILE.types.length(0)
×
718
   )
719
end
720

721
function elements.subscript:output (_, _, _) end
×
722

723
elements.underOver = pl.class(elements.subscript)
×
724
elements.underOver._type = "UnderOver"
×
725

726
function elements.underOver:__tostring ()
×
727
   return self._type .. "(" .. tostring(self.base) .. ", " .. tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
×
728
end
729

730
local function isNotEmpty (element)
731
   -- The MathML test suite uses <munderover> with an empty <mrow> as sub/sup.
732
   -- I don't know why they didn't use a <munder> or <mover> instead...
733
   -- But the expectation is to behave as if the empty element was not there,
734
   -- so that height and depth are not affected by the axis height.
735
   -- See notably:
736
   --   MathML3 "complex1" torture test: Maxwell's Equations (vectors in fractions)
737
   return element and (element:is_a(elements.terminal) or #element.children > 0)
×
738
end
739

740
local function getAccentMode (mode)
741
   -- Size unchanged but leave display mode
742
   -- See MathML Core §3.4.3
743
   if mode == mathMode.display then
×
744
      return mathMode.text
×
745
   end
746
   if mode == mathMode.displayCramped then
×
747
      return mathMode.textCramped
×
748
   end
749
   return mode
×
750
end
751

752
local function unwrapSingleElementMrow (elt)
753
   -- CODE SMELL.
754
   -- For \overset or \underset in LaTeX, MathML would use <mover> or <munder>.
755
   -- It would need to inherit the base's atom type, especially if the later is an operator
756
   -- (binary, relational etc.), which is a fairly common case, e.g.
757
   --   \overset{R}{=} (equality with a R above the equal in some Ramanujan summations),
758
   -- but we can't remove 1-element mrow's in the math typesetter, or have them inherit
759
   -- their base's atom type here above, because it breaks tables for some reasons
760
   -- that I couldn't figure out.
761
   if elt:is_a(elements.stackbox) and elt.direction == "H" and #elt.children == 1 then
×
762
      return unwrapSingleElementMrow(elt.children[1])
×
763
   else
764
      return elt
×
765
   end
766
end
767

768
function elements.underOver:_init (attributes, base, sub, sup)
×
769
   elements.mbox._init(self)
×
770
   base = unwrapSingleElementMrow(base)
×
771
   self.atom = base.atom
×
772
   self.attributes = attributes or {}
×
773
   self.attributes.accent = SU.boolean(self.attributes.accent, false)
×
774
   self.attributes.accentunder = SU.boolean(self.attributes.accentunder, false)
×
775
   self.base = base
×
776
   self.sub = isNotEmpty(sub) and sub or nil
×
777
   self.sup = isNotEmpty(sup) and sup or nil
×
778
   if self.sup then
×
779
      table.insert(self.children, self.sup)
×
780
   end
781
   if self.base then
×
782
      table.insert(self.children, self.base)
×
783
   end
784
   if self.sub then
×
785
      table.insert(self.children, self.sub)
×
786
   end
787
end
788

789
function elements.underOver:styleChildren ()
×
790
   if self.base then
×
791
      self.base.mode = self.mode
×
792
   end
793
   if self.sub then
×
794
      self.sub.mode = self.attributes.accentunder and getAccentMode(self.mode) or getSubscriptMode(self.mode)
×
795
   end
796
   if self.sup then
×
797
      self.sup.mode = self.attributes.accent and getAccentMode(self.mode) or getSuperscriptMode(self.mode)
×
798
   end
799
end
800

801
function elements.underOver:_stretchyReshapeToBase (part)
×
802
   -- FIXME: Big leap of faith here.
803
   -- MathML Core only mentions stretching along the inline axis in 3.4.2.2,
804
   -- i.e. under the section on <mover>, <munder>, <munderover>.
805
   -- So we are "somewhat" good here, but... the algorithm is totally unclear
806
   -- to me and seems to imply a lot of recursion and reshaping.
807
   -- The implementation below is NOT general and only works for the cases
808
   -- I checked:
809
   --   Mozilla MathML tests: braces in f19, f22
810
   --   Personal tests: vectors in d19, d22, d23
811
   --   Joe Javawaski's tests: braces in 8a, 8b
812
   --   MathML3 "complex1" torture test: Maxwell's Equations (vectors in fractions)
813
   if #part.children == 0 then
×
814
      local elt = part
×
815
      if elt:is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
×
816
         elt:_horizStretchyReshape(self.base.width)
×
817
      end
818
   elseif part:is_a(elements.underOver) then
×
819
      -- Big assumption here: only considering one level of stacked under/over.
820
      local hasStretched = false
×
821
      for _, elt in ipairs(part.children) do
×
822
         if elt:is_a(elements.text) and elt.kind == "operator" and SU.boolean(elt.stretchy, false) then
×
823
            local stretched = elt:_horizStretchyReshape(self.base.width)
×
824
            if stretched then
×
825
               hasStretched = true
×
826
            end
827
         end
828
      end
829
      if hasStretched then
×
830
         -- We need to re-calculate the shape so positions are re-calculated on each
831
         -- of its own parts.
832
         -- (Added after seeing that Mozilla test f19 was not rendering correctly.)
833
         part:shape()
×
834
      end
835
   end
836
end
837

838
function elements.underOver:shape ()
×
839
   local constants = self:getMathMetrics().constants
×
840
   local scaleDown = self:getScaleDown()
×
841
   local isMovableLimits = SU.boolean(self.base and self.base.movablelimits, false)
×
842
   local itCorr = self:calculateItalicsCorrection() * scaleDown
×
843
   if not (self.mode == mathMode.display or self.mode == mathMode.displayCramped) and isMovableLimits then
×
844
      -- When the base is a movable limit, the under/over scripts are not placed under/over the base,
845
      -- but other to the right of it, when display mode is not used.
846
      -- Notable effects:
847
      --   Mozilla MathML test 19 (on "k times" > overbrace > base)
848
      --   Maxwell's Equations in MathML3 Test Suite "complex1" (on the vectors in fractions)
849
      self.isUnderOver = true
×
850
      elements.subscript.shape(self)
×
851
      return
×
852
   end
853
   -- Determine relative Ys
854
   if self.base then
×
855
      self.base.relY = SILE.types.length(0)
×
856
   end
857
   if self.sub then
×
858
      self:_stretchyReshapeToBase(self.sub)
×
859
      -- TODO These rules are incomplete and even wrong if we were to fully implement MathML Core.
860
      if self.attributes.accentunder then
×
861
         self.sub.relY = self.base.depth
×
862
            + SILE.types.length(
×
863
               (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber()
×
864
               -- We assume that the accent is aligned on the base.
865
            )
866
      else
867
         self.sub.relY = self.base.depth
×
868
            + SILE.types.length(
×
869
               math.max(
×
870
                  (self.sub.height + constants.lowerLimitGapMin * scaleDown):tonumber(),
×
871
                  constants.lowerLimitBaselineDropMin * scaleDown
×
872
               )
873
            )
874
      end
875
   end
876
   if self.sup then
×
877
      self:_stretchyReshapeToBase(self.sup)
×
878
      -- TODO These rules are incomplete if we were to fully implement MathML Core.
879
      if self.attributes.accent then
×
880
         self.sup.relY = 0 - self.base.height
×
881
         -- MathML Core wants to align on the accentBaseHeight...
882
         local overShift = math.max(0, constants.accentBaseHeight * scaleDown - self.base.height:tonumber())
×
883
         self.sup.relY = self.sup.relY - SILE.types.length(overShift)
×
884
         -- HACK: .... but improperly dimensioned accents can overshoot the base glyph.
885
         -- So we try some guesswork to correct this.
886
         -- Typically some non-combining symbols are in this case...
887
         local heuristics = 0.5 * constants.flattenedAccentBaseHeight + 0.5 * constants.accentBaseHeight
×
888
         if self.sup.height > SILE.types.length(heuristics * scaleDown) then
×
889
            self.sup.relY = self.sup.relY + SILE.types.length(constants.accentBaseHeight * scaleDown)
×
890
         end
891
      else
892
         self.sup.relY = 0
×
893
            - self.base.height
×
894
            - SILE.types.length(
×
895
               math.max(
×
896
                  (constants.upperLimitGapMin * scaleDown + self.sup.depth):tonumber(),
×
897
                  constants.upperLimitBaselineRiseMin * scaleDown
×
898
               )
899
            )
900
      end
901
   end
902
   -- Determine relative Xs based on widest symbol
903
   local widest, a, b
904
   if self.sub and self.sub.width > self.base.width then
×
905
      if self.sup and self.sub.width > self.sup.width then
×
906
         widest = self.sub
×
907
         a = self.base
×
908
         b = self.sup
×
909
      elseif self.sup then
×
910
         widest = self.sup
×
911
         a = self.base
×
912
         b = self.sub
×
913
      else
914
         widest = self.sub
×
915
         a = self.base
×
916
         b = nil
×
917
      end
918
   else
919
      if self.sup and self.base.width > self.sup.width then
×
920
         widest = self.base
×
921
         a = self.sub
×
922
         b = self.sup
×
923
      elseif self.sup then
×
924
         widest = self.sup
×
925
         a = self.base
×
926
         b = self.sub
×
927
      else
928
         widest = self.base
×
929
         a = self.sub
×
930
         b = nil
×
931
      end
932
   end
933
   widest.relX = SILE.types.length(0)
×
934
   local c = widest.width / 2
×
935
   if a then
×
936
      a.relX = c - a.width / 2
×
937
   end
938
   if b then
×
939
      b.relX = c - b.width / 2
×
940
   end
941
   if self.sup then
×
942
      self.sup.relX = self.sup.relX + itCorr / 2
×
943
   end
944
   if self.sub then
×
945
      self.sub.relX = self.sub.relX - itCorr / 2
×
946
   end
947
   -- Determine width and height
948
   self.width = maxLength(
×
949
      self.base and self.base.width or SILE.types.length(0),
×
950
      self.sub and self.sub.width or SILE.types.length(0),
×
951
      self.sup and self.sup.width or SILE.types.length(0)
×
952
   )
953
   if self.sup then
×
954
      self.height = 0 - self.sup.relY + self.sup.height
×
955
   else
956
      self.height = self.base and self.base.height or 0
×
957
   end
958
   if self.sub then
×
959
      self.depth = self.sub.relY + self.sub.depth
×
960
   else
961
      self.depth = self.base and self.base.depth or 0
×
962
   end
963
end
964

965
function elements.underOver:calculateItalicsCorrection ()
×
966
   local lastGid = getRightMostGlyphId(self.base)
×
967
   if lastGid > 0 then
×
968
      local mathMetrics = self:getMathMetrics()
×
969
      if mathMetrics.italicsCorrection[lastGid] then
×
970
         local c = mathMetrics.italicsCorrection[lastGid]
×
971
         -- If this is a big operator, and we are in display style, then the
972
         -- base glyph may be bigger than the font size. We need to adjust the
973
         -- italic correction accordingly.
974
         if SU.boolean(self.base.largeop) and isDisplayMode(self.mode) then
×
975
            c = c * (self.base and self.base.font.size / self.font.size or 1.0)
×
976
         end
977
         return c
×
978
      end
979
   end
980
   return 0
×
981
end
982

983
function elements.underOver:output (_, _, _) end
×
984

985
-- terminal is the base class for leaf node
986
elements.terminal = pl.class(elements.mbox)
×
987
elements.terminal._type = "Terminal"
×
988

989
function elements.terminal:_init ()
×
990
   elements.mbox._init(self)
×
991
end
992

993
function elements.terminal:styleChildren () end
×
994

995
function elements.terminal:shape () end
×
996

997
elements.space = pl.class(elements.terminal)
×
998
elements.space._type = "Space"
×
999

1000
function elements.space:_init ()
×
1001
   elements.terminal._init(self)
×
1002
end
1003

1004
function elements.space:__tostring ()
×
1005
   return self._type
×
1006
      .. "(width="
×
1007
      .. tostring(self.width)
×
1008
      .. ", height="
×
1009
      .. tostring(self.height)
×
1010
      .. ", depth="
×
1011
      .. tostring(self.depth)
×
1012
      .. ")"
×
1013
end
1014

1015
local function getStandardLength (value)
1016
   if type(value) == "string" then
×
1017
      local direction = 1
×
1018
      if value:sub(1, 1) == "-" then
×
1019
         value = value:sub(2, -1)
×
1020
         direction = -1
×
1021
      end
1022
      if value == "thin" then
×
1023
         return SILE.types.length("3mu") * direction
×
1024
      elseif value == "med" then
×
1025
         return SILE.types.length("4mu plus 2mu minus 4mu") * direction
×
1026
      elseif value == "thick" then
×
1027
         return SILE.types.length("5mu plus 5mu") * direction
×
1028
      end
1029
   end
1030
   return SILE.types.length(value)
×
1031
end
1032

1033
function elements.space:_init (width, height, depth)
×
1034
   elements.terminal._init(self)
×
1035
   self.width = getStandardLength(width)
×
1036
   self.height = getStandardLength(height)
×
1037
   self.depth = getStandardLength(depth)
×
1038
end
1039

1040
function elements.space:shape ()
×
1041
   self.width = self.width:absolute() * self:getScaleDown()
×
1042
   self.height = self.height:absolute() * self:getScaleDown()
×
1043
   self.depth = self.depth:absolute() * self:getScaleDown()
×
1044
end
1045

1046
function elements.space:output (_, _, _) end
×
1047

1048
-- text node. For any actual text output
1049
elements.text = pl.class(elements.terminal)
×
1050
elements.text._type = "Text"
×
1051

1052
function elements.text:__tostring ()
×
1053
   return self._type
×
1054
      .. "(atom="
×
1055
      .. tostring(self.atom)
×
1056
      .. ", kind="
×
1057
      .. tostring(self.kind)
×
1058
      .. ", script="
×
1059
      .. tostring(self.script)
×
1060
      .. (SU.boolean(self.stretchy, false) and ", stretchy" or "")
×
1061
      .. (SU.boolean(self.largeop, false) and ", largeop" or "")
×
1062
      .. ', text="'
×
1063
      .. (self.originalText or self.text)
×
1064
      .. '")'
×
1065
end
1066

1067
function elements.text:_init (kind, attributes, script, text)
×
1068
   elements.terminal._init(self)
×
1069
   if not (kind == "number" or kind == "identifier" or kind == "operator" or kind == "string") then
×
1070
      SU.error("Unknown text node kind '" .. kind .. "'; should be one of: number, identifier, operator, string")
×
1071
   end
1072
   self.kind = kind
×
1073
   self.script = script
×
1074
   self.text = text
×
1075
   if self.script ~= "upright" then
×
1076
      local converted = convertMathVariantScript(self.text, self.script)
×
1077
      self.originalText = self.text
×
1078
      self.text = converted
×
1079
   end
1080
   if self.kind == "operator" then
×
1081
      if self.text == "-" then
×
1082
         self.text = "−"
×
1083
      end
1084
   end
1085
   for attribute, value in pairs(attributes) do
×
1086
      self[attribute] = value
×
1087
   end
1088
end
1089

1090
function elements.text:shape ()
×
1091
   self.font.size = self.font.size * self:getScaleDown()
×
1092
   if isScriptMode(self.mode) then
×
1093
      local scriptFeature = SILE.settings:get("math.font.script.feature")
×
1094
      if scriptFeature then
×
1095
         self.font.features = ("+%s=1"):format(scriptFeature)
×
1096
      end
1097
   elseif isScriptScriptMode(self.mode) then
×
1098
      local scriptFeature = SILE.settings:get("math.font.script.feature")
×
1099
      if scriptFeature then
×
1100
         self.font.features = ("+%s=2"):format(scriptFeature)
×
1101
      end
1102
   end
1103
   local face = SILE.font.cache(self.font, SILE.shaper:_getFaceCallback())
×
1104
   local mathMetrics = self:getMathMetrics()
×
1105
   local glyphs = SILE.shaper:shapeToken(self.text, self.font)
×
1106
   -- Use bigger variants for big operators in display style
1107
   if isDisplayMode(self.mode) and SU.boolean(self.largeop, false) then
×
1108
      -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
1109
      glyphs = pl.tablex.deepcopy(glyphs)
×
1110
      local constructions = mathMetrics.mathVariants.vertGlyphConstructions[glyphs[1].gid]
×
1111
      if constructions then
×
1112
         local displayVariants = constructions.mathGlyphVariantRecord
×
1113
         -- We select the biggest variant. TODO: we should probably select the
1114
         -- first variant that is higher than displayOperatorMinHeight.
1115
         local biggest
1116
         local m = 0
×
1117
         for _, v in ipairs(displayVariants) do
×
1118
            if v.advanceMeasurement > m then
×
1119
               biggest = v
×
1120
               m = v.advanceMeasurement
×
1121
            end
1122
         end
1123
         if biggest then
×
1124
            glyphs[1].gid = biggest.variantGlyph
×
1125
            local dimen = hb.get_glyph_dimensions(face, self.font.size, biggest.variantGlyph)
×
1126
            glyphs[1].width = dimen.width
×
1127
            glyphs[1].glyphAdvance = dimen.glyphAdvance
×
1128
            --[[ I am told (https://github.com/alif-type/xits/issues/90) that,
1129
        in fact, the relative height and depth of display-style big operators
1130
        in the font is not relevant, as these should be centered around the
1131
        axis. So the following code does that, while conserving their
1132
        vertical size (distance from top to bottom). ]]
1133
            local axisHeight = mathMetrics.constants.axisHeight * self:getScaleDown()
×
1134
            local y_size = dimen.height + dimen.depth
×
1135
            glyphs[1].height = y_size / 2 + axisHeight
×
1136
            glyphs[1].depth = y_size / 2 - axisHeight
×
1137
            -- We still need to store the font's height and depth somewhere,
1138
            -- because that's what will be used to draw the glyph, and we will need
1139
            -- to artificially compensate for that.
1140
            glyphs[1].fontHeight = dimen.height
×
1141
            glyphs[1].fontDepth = dimen.depth
×
1142
         end
1143
      end
1144
   end
1145
   SILE.shaper:preAddNodes(glyphs, self.value)
×
1146
   self.value.items = glyphs
×
1147
   self.value.glyphString = {}
×
1148
   if glyphs and #glyphs > 0 then
×
1149
      for i = 1, #glyphs do
×
1150
         table.insert(self.value.glyphString, glyphs[i].gid)
×
1151
      end
1152
      self.width = SILE.types.length(0)
×
1153
      self.widthForSubscript = SILE.types.length(0)
×
1154
      for i = #glyphs, 1, -1 do
×
1155
         self.width = self.width + glyphs[i].glyphAdvance
×
1156
      end
1157
      -- Store width without italic correction somewhere
1158
      self.widthForSubscript = self.width
×
1159
      local itCorr = mathMetrics.italicsCorrection[glyphs[#glyphs].gid]
×
1160
      if itCorr then
×
1161
         self.width = self.width + itCorr * self:getScaleDown()
×
1162
      end
1163
      for i = 1, #glyphs do
×
1164
         self.height = i == 1 and SILE.types.length(glyphs[i].height)
×
1165
            or SILE.types.length(math.max(self.height:tonumber(), glyphs[i].height))
×
1166
         self.depth = i == 1 and SILE.types.length(glyphs[i].depth)
×
1167
            or SILE.types.length(math.max(self.depth:tonumber(), glyphs[i].depth))
×
1168
      end
1169
   else
1170
      self.width = SILE.types.length(0)
×
1171
      self.height = SILE.types.length(0)
×
1172
      self.depth = SILE.types.length(0)
×
1173
   end
1174
end
1175

1176
function elements.text:findClosestVariant (variants, requiredAdvance, currentAdvance)
×
1177
   local closest
1178
   local closestI
1179
   local m = requiredAdvance - currentAdvance
×
1180
   for i, variant in ipairs(variants) do
×
1181
      local diff = math.abs(variant.advanceMeasurement - requiredAdvance)
×
1182
      SU.debug("math", "stretch: diff =", diff)
×
1183
      if diff < m then
×
1184
         closest = variant
×
1185
         closestI = i
×
1186
         m = diff
×
1187
      end
1188
   end
1189
   return closest, closestI
×
1190
end
1191

1192
function elements.text:_reshapeGlyph (glyph, closestVariant, sz)
×
1193
   local face = SILE.font.cache(self.font, SILE.shaper:_getFaceCallback())
×
1194
   local dimen = hb.get_glyph_dimensions(face, sz, closestVariant.variantGlyph)
×
1195
   glyph.gid = closestVariant.variantGlyph
×
1196
   glyph.width, glyph.height, glyph.depth, glyph.glyphAdvance =
×
1197
      dimen.width, dimen.height, dimen.depth, dimen.glyphAdvance
×
1198
   return dimen
×
1199
end
1200

1201
function elements.text:_stretchyReshape (target, direction)
×
1202
   -- direction is the required direction of stretching: true for vertical, false for horizontal
1203
   -- target is the required dimension of the stretched glyph, in font units
1204
   local mathMetrics = self:getMathMetrics()
×
1205
   local upem = mathMetrics.unitsPerEm
×
1206
   local sz = self.font.size
×
1207
   local requiredAdvance = target:tonumber() * upem / sz
×
1208
   SU.debug("math", "stretch: rA =", requiredAdvance)
×
1209
   -- Choose variant of the closest size. The criterion we use is to have
1210
   -- an advance measurement as close as possible as the required one.
1211
   -- The advance measurement is simply the dimension of the glyph.
1212
   -- Therefore, the selected glyph may be smaller or bigger than
1213
   -- required.
1214
   -- TODO: implement assembly of stretchable glyphs from their parts for cases
1215
   -- when the biggest variant is not big enough.
1216
   -- We copy the glyph list to avoid modifying the shaper's cache. Yes.
1217
   local glyphs = pl.tablex.deepcopy(self.value.items)
×
1218
   local glyphConstructions = direction and mathMetrics.mathVariants.vertGlyphConstructions
×
1219
      or mathMetrics.mathVariants.horizGlyphConstructions
×
1220
   local constructions = glyphConstructions[glyphs[1].gid]
×
1221
   if constructions then
×
1222
      local variants = constructions.mathGlyphVariantRecord
×
1223
      SU.debug("math", "stretch: variants =", variants)
×
1224
      local currentAdvance = (direction and (self.depth + self.height):tonumber() or self.width:tonumber()) * upem / sz
×
1225
      local closest, closestI = self:findClosestVariant(variants, requiredAdvance, currentAdvance)
×
1226
      SU.debug("math", "stretch: closestI =", closestI)
×
1227
      if closest then
×
1228
         -- Now we have to re-shape the glyph chain. We will assume there
1229
         -- is only one glyph.
1230
         -- TODO: this code is probably wrong when the vertical
1231
         -- variants have a different width than the original, because
1232
         -- the shaping phase is already done. Need to do better.
1233
         local dimen = self:_reshapeGlyph(glyphs[1], closest, sz)
×
1234
         self.width, self.depth, self.height =
×
1235
            SILE.types.length(dimen.glyphAdvance), SILE.types.length(dimen.depth), SILE.types.length(dimen.height)
×
1236
         SILE.shaper:preAddNodes(glyphs, self.value)
×
1237
         self.value.items = glyphs
×
1238
         self.value.glyphString = { glyphs[1].gid }
×
1239
         return true
×
1240
      end
1241
   end
1242
   return false
×
1243
end
1244

1245
function elements.text:_vertStretchyReshape (depth, height)
×
1246
   local hasStretched = self:_stretchyReshape(depth + height, true)
×
1247
   if hasStretched then
×
1248
      -- RESCALING HACK: see output routine
1249
      -- We only do it if the scaling logic found constructions on the vertical block axis.
1250
      -- It's a dirty hack until we properly implement assembly of glyphs in the case we couldn't
1251
      -- find a big enough variant.
1252
      self.vertExpectedSz = height + depth
×
1253
      self.vertScalingRatio = (depth + height):tonumber() / (self.height:tonumber() + self.depth:tonumber())
×
1254
      self.height = height
×
1255
      self.depth = depth
×
1256
   end
1257
   return hasStretched
×
1258
end
1259

1260
function elements.text:_horizStretchyReshape (width)
×
1261
   local hasStretched = self:_stretchyReshape(width, false)
×
1262
   if not hasStretched and width:tonumber() < self.width:tonumber() then
×
1263
      -- Never shrink glyphs, it looks ugly
1264
      return false
×
1265
   end
1266
   -- But if stretching couldn't be done, it will be ugly anyway, so we will force
1267
   -- a re-scaling of the glyph.
1268
   -- (So it slightly different from the vertical case, 'cause MathML just has one stretchy
1269
   -- attribute, whether for stretching on the vertical (block) or horizontal (inline) axis,
1270
   -- and we cannot know which axis is meant unless we implement yet another mapping table
1271
   -- as the one in the MathML Core appendices. Frankly, how many non-normative appendices
1272
   -- do we need to implement MathML correctly?)
1273
   -- RESCALING HACK: see output routine
1274
   self.horizScalingRatio = width:tonumber() / self.width:tonumber()
×
1275
   self.width = width
×
1276
   return true
×
1277
end
1278

1279
function elements.text:output (x, y, line)
×
1280
   if not self.value.glyphString then
×
1281
      return
×
1282
   end
1283
   local compensatedY
1284
   if isDisplayMode(self.mode) and SU.boolean(self.largeop, false) and self.value.items[1].fontDepth then
×
1285
      compensatedY = SILE.types.length(y.length + self.value.items[1].depth - self.value.items[1].fontDepth)
×
1286
   else
1287
      compensatedY = y
×
1288
   end
1289
   SILE.outputter:setCursor(scaleWidth(x, line), compensatedY.length)
×
1290
   SILE.outputter:setFont(self.font)
×
1291
   -- There should be no stretch or shrink on the width of a text
1292
   -- element.
1293
   local width = self.width.length
×
1294
   -- HACK: For stretchy operators, MathML Core and OpenType define how to build large glyphs
1295
   -- from an assembly of smaller ones. It's fairly complex and idealistic...
1296
   -- Anyhow, we do not have that yet, so we just stretch the glyph artificially.
1297
   -- There are cases where this will not look very good.
1298
   -- Call that a compromise, so that long vectors or large matrices look "decent" without assembly.
1299
   if SILE.outputter.scaleFn and (self.horizScalingRatio or self.vertScalingRatio) then
×
1300
      local xratio = self.horizScalingRatio or 1
×
1301
      local yratio = self.vertScalingRatio or 1
×
1302
      SU.debug("math", "fake glyph stretch: xratio =", xratio, "yratio =", yratio)
×
1303
      SILE.outputter:scaleFn(x, y, xratio, yratio, function ()
×
1304
         SILE.outputter:drawHbox(self.value, width)
×
1305
      end)
1306
   else
1307
      SILE.outputter:drawHbox(self.value, width)
×
1308
   end
1309
end
1310

1311
elements.fraction = pl.class(elements.mbox)
×
1312
elements.fraction._type = "Fraction"
×
1313

1314
function elements.fraction:__tostring ()
×
1315
   return self._type .. "(" .. tostring(self.numerator) .. ", " .. tostring(self.denominator) .. ")"
×
1316
end
1317

1318
function elements.fraction:_init (attributes, numerator, denominator)
×
1319
   elements.mbox._init(self)
×
1320
   self.numerator = numerator
×
1321
   self.denominator = denominator
×
1322
   self.attributes = attributes
×
1323
   table.insert(self.children, numerator)
×
1324
   table.insert(self.children, denominator)
×
1325
end
1326

1327
function elements.fraction:styleChildren ()
×
1328
   self.numerator.mode = getNumeratorMode(self.mode)
×
1329
   self.denominator.mode = getDenominatorMode(self.mode)
×
1330
end
1331

1332
function elements.fraction:shape ()
×
1333
   -- MathML Core 3.3.2: "To avoid visual confusion between the fraction bar
1334
   -- and another adjacent items (e.g. minus sign or another fraction's bar),
1335
   -- a default 1-pixel space is added around the element."
1336
   -- Note that PlainTeX would likely use \nulldelimiterspace (default 1.2pt)
1337
   -- but it would depend on the surrounding context, and might be far too
1338
   -- much in some cases, so we stick to MathML's suggested padding.
1339
   self.padding = SILE.types.length("1px"):absolute()
×
1340

1341
   -- Determine relative abscissas and width
1342
   local widest, other
1343
   if self.denominator.width > self.numerator.width then
×
1344
      widest, other = self.denominator, self.numerator
×
1345
   else
1346
      widest, other = self.numerator, self.denominator
×
1347
   end
1348
   widest.relX = self.padding
×
1349
   other.relX = self.padding + (widest.width - other.width) / 2
×
1350
   self.width = widest.width + 2 * self.padding
×
1351
   -- Determine relative ordinates and height
1352
   local constants = self:getMathMetrics().constants
×
1353
   local scaleDown = self:getScaleDown()
×
1354
   self.axisHeight = constants.axisHeight * scaleDown
×
1355
   self.ruleThickness = self.attributes.linethickness
×
1356
         and SU.cast("measurement", self.attributes.linethickness):tonumber()
×
1357
      or constants.fractionRuleThickness * scaleDown
×
1358

1359
   -- MathML Core 3.3.2.2 ("Fraction with zero line thickness") uses
1360
   -- stack(DisplayStyle)GapMin, stackTop(DisplayStyle)ShiftUp and stackBottom(DisplayStyle)ShiftDown.
1361
   -- TODO not implemented
1362
   -- The most common use cases for zero line thickness are:
1363
   --  - Binomial coefficients
1364
   --  - Stacked subscript/superscript on big operators such as sums.
1365

1366
   local numeratorGapMin, denominatorGapMin, numeratorShiftUp, denominatorShiftDown
1367
   if isDisplayMode(self.mode) then
×
1368
      numeratorGapMin = constants.fractionNumDisplayStyleGapMin * scaleDown
×
1369
      denominatorGapMin = constants.fractionDenomDisplayStyleGapMin * scaleDown
×
1370
      numeratorShiftUp = constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
×
1371
      denominatorShiftDown = constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
×
1372
   else
1373
      numeratorGapMin = constants.fractionNumeratorGapMin * scaleDown
×
1374
      denominatorGapMin = constants.fractionDenominatorGapMin * scaleDown
×
1375
      numeratorShiftUp = constants.fractionNumeratorShiftUp * scaleDown
×
1376
      denominatorShiftDown = constants.fractionDenominatorShiftDown * scaleDown
×
1377
   end
1378

1379
   self.numerator.relY = -self.axisHeight
×
1380
      - self.ruleThickness / 2
×
1381
      - SILE.types.length(
×
1382
         math.max(
×
1383
            (numeratorGapMin + self.numerator.depth):tonumber(),
×
1384
            numeratorShiftUp - self.axisHeight - self.ruleThickness / 2
×
1385
         )
1386
      )
1387
   self.denominator.relY = -self.axisHeight
×
1388
      + self.ruleThickness / 2
×
1389
      + SILE.types.length(
×
1390
         math.max(
×
1391
            (denominatorGapMin + self.denominator.height):tonumber(),
×
1392
            denominatorShiftDown + self.axisHeight - self.ruleThickness / 2
×
1393
         )
1394
      )
1395
   self.height = self.numerator.height - self.numerator.relY
×
1396
   self.depth = self.denominator.relY + self.denominator.depth
×
1397
end
1398

1399
function elements.fraction:output (x, y, line)
×
1400
   if self.ruleThickness > 0 then
×
1401
      SILE.outputter:drawRule(
×
1402
         scaleWidth(x + self.padding, line),
×
1403
         y.length - self.axisHeight - self.ruleThickness / 2,
×
1404
         scaleWidth(self.width - 2 * self.padding, line),
×
1405
         self.ruleThickness
1406
      )
1407
   end
1408
end
1409

1410
local function newSubscript (spec)
1411
   return elements.subscript(spec.base, spec.sub, spec.sup)
×
1412
end
1413

1414
local function newUnderOver (spec)
1415
   return elements.underOver(spec.attributes, spec.base, spec.sub, spec.sup)
×
1416
end
1417

1418
-- TODO replace with penlight equivalent
1419
local function mapList (f, l)
1420
   local ret = {}
×
1421
   for i, x in ipairs(l) do
×
1422
      ret[i] = f(i, x)
×
1423
   end
1424
   return ret
×
1425
end
1426

1427
elements.mtr = pl.class(elements.mbox)
×
1428
-- elements.mtr._type = "" -- TODO why not set?
1429

1430
function elements.mtr:_init (children)
×
1431
   self.children = children
×
1432
end
1433

1434
function elements.mtr:styleChildren ()
×
1435
   for _, c in ipairs(self.children) do
×
1436
      c.mode = self.mode
×
1437
   end
1438
end
1439

1440
function elements.mtr:shape () end -- done by parent table
×
1441

1442
function elements.mtr:output () end
×
1443

1444
elements.table = pl.class(elements.mbox)
×
1445
elements.table._type = "table" -- TODO why case difference?
×
1446

1447
function elements.table:_init (children, options)
×
1448
   elements.mbox._init(self)
×
1449
   self.children = children
×
1450
   self.options = options
×
1451
   self.nrows = #self.children
×
1452
   self.ncols = math.max(pl.utils.unpack(mapList(function (_, row)
×
1453
      return #row.children
×
1454
   end, self.children)))
×
1455
   SU.debug("math", "self.ncols =", self.ncols)
×
1456
   local spacing = SILE.settings:get("math.font.size") * 0.6 -- arbitrary ratio of the current math font size
×
1457
   self.rowspacing = self.options.rowspacing and SILE.types.length(self.options.rowspacing) or spacing
×
1458
   self.columnspacing = self.options.columnspacing and SILE.types.length(self.options.columnspacing) or spacing
×
1459
   -- Pad rows that do not have enough cells by adding cells to the
1460
   -- right.
1461
   for i, row in ipairs(self.children) do
×
1462
      for j = 1, (self.ncols - #row.children) do
×
1463
         SU.debug("math", "padding i =", i, "j =", j)
×
1464
         table.insert(row.children, elements.stackbox("H", {}))
×
1465
         SU.debug("math", "size", #row.children)
×
1466
      end
1467
   end
1468
   if options.columnalign then
×
1469
      local l = {}
×
1470
      for w in string.gmatch(options.columnalign, "[^%s]+") do
×
1471
         if not (w == "left" or w == "center" or w == "right") then
×
1472
            SU.error("Invalid specifier in `columnalign` attribute: " .. w)
×
1473
         end
1474
         table.insert(l, w)
×
1475
      end
1476
      -- Pad with last value of l if necessary
1477
      for _ = 1, (self.ncols - #l), 1 do
×
1478
         table.insert(l, l[#l])
×
1479
      end
1480
      -- On the contrary, remove excess values in l if necessary
1481
      for _ = 1, (#l - self.ncols), 1 do
×
1482
         table.remove(l)
×
1483
      end
1484
      self.options.columnalign = l
×
1485
   else
1486
      self.options.columnalign = pl.List.range(1, self.ncols):map(function (_)
×
1487
         return "center"
×
1488
      end)
1489
   end
1490
end
1491

1492
function elements.table:styleChildren ()
×
1493
   if self.mode == mathMode.display and self.options.displaystyle ~= "false" then
×
1494
      for _, c in ipairs(self.children) do
×
1495
         c.mode = mathMode.display
×
1496
      end
1497
   else
1498
      for _, c in ipairs(self.children) do
×
1499
         c.mode = mathMode.text
×
1500
      end
1501
   end
1502
end
1503

1504
function elements.table:shape ()
×
1505
   -- Determine the height (resp. depth) of each row, which is the max
1506
   -- height (resp. depth) among its elements. Then we only need to add it to
1507
   -- the table's height and center every cell vertically.
1508
   for _, row in ipairs(self.children) do
×
1509
      row.height = SILE.types.length(0)
×
1510
      row.depth = SILE.types.length(0)
×
1511
      for _, cell in ipairs(row.children) do
×
1512
         row.height = maxLength(row.height, cell.height)
×
1513
         row.depth = maxLength(row.depth, cell.depth)
×
1514
      end
1515
   end
1516
   self.vertSize = SILE.types.length(0)
×
1517
   for i, row in ipairs(self.children) do
×
1518
      self.vertSize = self.vertSize
×
1519
         + row.height
×
1520
         + row.depth
×
1521
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
×
1522
   end
1523
   local rowHeightSoFar = SILE.types.length(0)
×
1524
   for i, row in ipairs(self.children) do
×
1525
      row.relY = rowHeightSoFar + row.height - self.vertSize
×
1526
      rowHeightSoFar = rowHeightSoFar
×
1527
         + row.height
×
1528
         + row.depth
×
1529
         + (i == self.nrows and SILE.types.length(0) or self.rowspacing) -- Spacing
×
1530
   end
1531
   self.width = SILE.types.length(0)
×
1532
   local thisColRelX = SILE.types.length(0)
×
1533
   -- For every column...
1534
   for i = 1, self.ncols do
×
1535
      -- Determine its width
1536
      local columnWidth = SILE.types.length(0)
×
1537
      for j = 1, self.nrows do
×
1538
         if self.children[j].children[i].width > columnWidth then
×
1539
            columnWidth = self.children[j].children[i].width
×
1540
         end
1541
      end
1542
      -- Use it to align the contents of every cell as required.
1543
      for j = 1, self.nrows do
×
1544
         local cell = self.children[j].children[i]
×
1545
         if self.options.columnalign[i] == "left" then
×
1546
            cell.relX = thisColRelX
×
1547
         elseif self.options.columnalign[i] == "center" then
×
1548
            cell.relX = thisColRelX + (columnWidth - cell.width) / 2
×
1549
         elseif self.options.columnalign[i] == "right" then
×
1550
            cell.relX = thisColRelX + (columnWidth - cell.width)
×
1551
         else
1552
            SU.error("invalid columnalign parameter")
×
1553
         end
1554
      end
1555
      thisColRelX = thisColRelX + columnWidth + (i == self.ncols and SILE.types.length(0) or self.columnspacing) -- Spacing
×
1556
   end
1557
   self.width = thisColRelX
×
1558
   -- Center myself vertically around the axis, and update relative Ys of rows accordingly
1559
   local axisHeight = self:getMathMetrics().constants.axisHeight * self:getScaleDown()
×
1560
   self.height = self.vertSize / 2 + axisHeight
×
1561
   self.depth = self.vertSize / 2 - axisHeight
×
1562
   for _, row in ipairs(self.children) do
×
1563
      row.relY = row.relY + self.vertSize / 2 - axisHeight
×
1564
      -- Also adjust width
1565
      row.width = self.width
×
1566
   end
1567
end
1568

1569
function elements.table:output () end
×
1570

1571
local function getRadicandMode (mode)
1572
   -- Not too sure if we should do something special/
1573
   return mode
×
1574
end
1575

1576
local function getDegreeMode (mode)
1577
   -- 2 levels smaller, up to scriptScript evntually.
1578
   -- Not too sure if we should do something else.
1579
   if mode == mathMode.display then
×
1580
      return mathMode.scriptScript
×
1581
   elseif mode == mathMode.displayCramped then
×
1582
      return mathMode.scriptScriptCramped
×
1583
   elseif mode == mathMode.text or mode == mathMode.script or mode == mathMode.scriptScript then
×
1584
      return mathMode.scriptScript
×
1585
   end
1586
   return mathMode.scriptScriptCramped
×
1587
end
1588

1589
elements.sqrt = pl.class(elements.mbox)
×
1590
elements.sqrt._type = "Sqrt"
×
1591

1592
function elements.sqrt:__tostring ()
×
1593
   return self._type .. "(" .. tostring(self.radicand) .. (self.degree and ", " .. tostring(self.degree) or "") .. ")"
×
1594
end
1595

1596
function elements.sqrt:_init (radicand, degree)
×
1597
   elements.mbox._init(self)
×
1598
   self.radicand = radicand
×
1599
   if degree then
×
1600
      self.degree = degree
×
1601
      table.insert(self.children, degree)
×
1602
   end
1603
   table.insert(self.children, radicand)
×
1604
   self.relX = SILE.types.length()
×
1605
   self.relY = SILE.types.length()
×
1606
end
1607

1608
function elements.sqrt:styleChildren ()
×
1609
   self.radicand.mode = getRadicandMode(self.mode)
×
1610
   if self.degree then
×
1611
      self.degree.mode = getDegreeMode(self.mode)
×
1612
   end
1613
end
1614

1615
function elements.sqrt:shape ()
×
1616
   local mathMetrics = self:getMathMetrics()
×
1617
   local scaleDown = self:getScaleDown()
×
1618
   local constants = mathMetrics.constants
×
1619

1620
   self.radicalRuleThickness = constants.radicalRuleThickness * scaleDown
×
1621
   if self.mode == mathMode.display or self.mode == mathMode.displayCramped then
×
1622
      self.radicalVerticalGap = constants.radicalDisplayStyleVerticalGap * scaleDown
×
1623
   else
1624
      self.radicalVerticalGap = constants.radicalVerticalGap * scaleDown
×
1625
   end
1626
   self.extraAscender = constants.radicalExtraAscender * scaleDown
×
1627

1628
   -- HACK: We draw own own radical sign in the output() method.
1629
   -- Derive dimensions for the radical sign (more or less ad hoc).
1630
   -- Note: In TeX, the radical sign extends a lot below the baseline,
1631
   -- and MathML Core also has a lot of layout text about it.
1632
   -- Not only it doesn't look good, but it's not very clear vs. OpenType.
1633
   local radicalGlyph, found = SILE.shaper:measureChar("√")
×
1634
   if not found then
×
1635
      SU.error("Math font does not contain a square root glyph")
×
1636
   end
1637
   local ratio = (self.radicand.height:tonumber() + self.radicand.depth:tonumber())
×
1638
      / (radicalGlyph.height + radicalGlyph.depth)
×
1639
   local vertAdHocOffset = (ratio > 1 and math.log(ratio) or 0) * self.radicalVerticalGap
×
1640
   self.symbolHeight = SILE.types.length(radicalGlyph.height) * scaleDown
×
1641
   self.symbolDepth = (SILE.types.length(radicalGlyph.depth) + vertAdHocOffset) * scaleDown
×
1642
   self.symbolWidth = (SILE.types.length(radicalGlyph.width) + vertAdHocOffset) * scaleDown
×
1643

1644
   -- Adjust the height of the radical sign if the radicand is higher
1645
   self.symbolHeight = self.radicand.height > self.symbolHeight and self.radicand.height or self.symbolHeight
×
1646
   -- Compute the (max-)height of the short leg of the radical sign
1647
   self.symbolShortHeight = self.symbolHeight * constants.radicalDegreeBottomRaisePercent
×
1648

1649
   self.offsetX = SILE.types.length()
×
1650
   if self.degree then
×
1651
      -- Position the degree
1652
      self.degree.relY = -constants.radicalDegreeBottomRaisePercent * self.symbolHeight
×
1653
      -- Adjust the height of the short leg of the radical sign to ensure the degree is not too close
1654
      -- (empirically use radicalExtraAscender)
1655
      self.symbolShortHeight = self.symbolShortHeight - constants.radicalExtraAscender * scaleDown
×
1656
      -- Compute the width adjustment for the degree
1657
      self.offsetX = self.degree.width
×
1658
         + constants.radicalKernBeforeDegree * scaleDown
×
1659
         + constants.radicalKernAfterDegree * scaleDown
×
1660
   end
1661
   -- Position the radicand
1662
   self.radicand.relX = self.symbolWidth + self.offsetX
×
1663
   -- Compute the dimensions of the whole radical
1664
   self.width = self.radicand.width + self.symbolWidth + self.offsetX
×
1665
   self.height = self.symbolHeight + self.radicalVerticalGap + self.extraAscender
×
1666
   self.depth = self.radicand.depth
×
1667
end
1668

1669
local function _r (number)
1670
   -- Lua 5.3+ formats floats as 1.0 and integers as 1
1671
   -- Also some PDF readers do not like double precision.
1672
   return math.floor(number) == number and math.floor(number) or tonumber(string.format("%.5f", number))
×
1673
end
1674

1675
function elements.sqrt:output (x, y, line)
×
1676
   -- HACK:
1677
   -- OpenType might say we need to assemble the radical sign from parts.
1678
   -- Frankly, it's much easier to just draw it as a graphic :-)
1679
   -- Hence, here we use a PDF graphic operators to draw a nice radical sign.
1680
   -- Some values here are ad hoc, but they look good.
1681
   local h = self.height:tonumber()
×
1682
   local d = self.depth:tonumber()
×
1683
   local s0 = scaleWidth(self.offsetX, line):tonumber()
×
1684
   local sw = scaleWidth(self.symbolWidth, line):tonumber()
×
1685
   local dsh = h - self.symbolShortHeight:tonumber()
×
1686
   local dsd = self.symbolDepth:tonumber()
×
1687
   local symbol = {
×
1688
      _r(self.radicalRuleThickness),
×
1689
      "w", -- line width
1690
      1,
1691
      "j", -- round line joins
1692
      _r(sw + s0),
×
1693
      _r(self.extraAscender),
×
1694
      "m",
1695
      _r(s0 + sw * 0.90),
×
1696
      _r(self.extraAscender),
×
1697
      "l",
1698
      _r(s0 + sw * 0.4),
×
1699
      _r(h + d + dsd),
×
1700
      "l",
1701
      _r(s0 + sw * 0.2),
×
1702
      _r(dsh),
×
1703
      "l",
1704
      s0 + sw * 0.1,
×
1705
      _r(dsh + 0.5),
×
1706
      "l",
1707
      "S",
1708
   }
1709
   local svg = table.concat(symbol, " ")
×
1710
   local xscaled = scaleWidth(x, line)
×
1711
   SILE.outputter:drawSVG(svg, xscaled, y, sw, h, 1)
×
1712
   -- And now we just need to draw the bar over the radicand
1713
   SILE.outputter:drawRule(
×
1714
      s0 + self.symbolWidth + xscaled,
×
1715
      y.length - self.height + self.extraAscender - self.radicalRuleThickness / 2,
×
1716
      scaleWidth(self.radicand.width, line),
×
1717
      self.radicalRuleThickness
1718
   )
1719
end
1720

1721
elements.padded = pl.class(elements.mbox)
×
1722
elements.padded._type = "Padded"
×
1723

1724
function elements.padded:__tostring ()
×
1725
   return self._type .. "(" .. tostring(self.impadded) .. ")"
×
1726
end
1727

1728
function elements.padded:_init (attributes, impadded)
×
1729
   elements.mbox._init(self)
×
1730
   self.impadded = impadded
×
1731
   self.attributes = attributes or {}
×
1732
   table.insert(self.children, impadded)
×
1733
end
1734

1735
function elements.padded:styleChildren ()
×
1736
   self.impadded.mode = self.mode
×
1737
end
1738

1739
function elements.padded:shape ()
×
1740
   -- TODO MathML allows percentages font-relative units (em, ex) for padding
1741
   -- But our units work with font.size, not math.font.size (possibly adjusted by scaleDown)
1742
   -- so the expectations might not be met.
1743
   local width = self.attributes.width and SU.cast("measurement", self.attributes.width)
×
1744
   local height = self.attributes.height and SU.cast("measurement", self.attributes.height)
×
1745
   local depth = self.attributes.depth and SU.cast("measurement", self.attributes.depth)
×
1746
   local lspace = self.attributes.lspace and SU.cast("measurement", self.attributes.lspace)
×
1747
   local voffset = self.attributes.voffset and SU.cast("measurement", self.attributes.voffset)
×
1748
   -- Clamping for width, height, depth, lspace
1749
   width = width and (width:tonumber() > 0 and width or SILE.types.measurement())
×
1750
   height = height and (height:tonumber() > 0 and height or SILE.types.measurement())
×
1751
   depth = depth and (depth:tonumber() > 0 and depth or SILE.types.measurement())
×
1752
   lspace = lspace and (lspace:tonumber() > 0 and lspace or SILE.types.measurement())
×
1753
   -- No clamping for voffset
1754
   voffset = voffset or SILE.types.measurement(0)
×
1755
   -- Compute the dimensions
1756
   self.width = width and SILE.types.length(width) or self.impadded.width
×
1757
   self.height = height and SILE.types.length(height) or self.impadded.height
×
1758
   self.depth = depth and SILE.types.length(depth) or self.impadded.depth
×
1759
   self.impadded.relX = lspace and SILE.types.length(lspace) or SILE.types.length()
×
1760
   self.impadded.relY = voffset and SILE.types.length(voffset):negate() or SILE.types.length()
×
1761
end
1762

1763
function elements.padded:output (_, _, _) end
×
1764

1765
-- Bevelled fractions are not part of MathML Core, and MathML4 does not
1766
-- exactly specify how to compute the layout.
1767
elements.bevelledFraction = pl.class(elements.fraction) -- Inherit from fraction
×
1768
elements.fraction._type = "BevelledFraction"
×
1769

1770
function elements.bevelledFraction:shape ()
×
1771
   local constants = self:getMathMetrics().constants
×
1772
   local scaleDown = self:getScaleDown()
×
1773
   local hSkew = constants.skewedFractionHorizontalGap * scaleDown
×
1774
   -- OpenType has properties which are not totally explicit.
1775
   -- The definition of skewedFractionVerticalGap (and its value in fonts
1776
   -- such as Libertinus Math) seems to imply that it is measured from the
1777
   -- bottom of the numerator to the top of the denominator.
1778
   -- This does not seem to be a nice general layout.
1779
   -- So we will use superscriptShiftUp(Cramped) for the numerator:
1780
   local vSkewUp = isCrampedMode(self.mode) and constants.superscriptShiftUpCramped * scaleDown
×
1781
      or constants.superscriptShiftUp * scaleDown
×
1782
   -- And all good books say that the denominator should not be shifted down:
1783
   local vSkewDown = 0
×
1784

1785
   self.ruleThickness = self.attributes.linethickness
×
1786
         and SU.cast("measurement", self.attributes.linethickness):tonumber()
×
1787
      or constants.fractionRuleThickness * scaleDown
×
1788
   self.numerator.relX = SILE.types.length(0)
×
1789
   self.numerator.relY = SILE.types.length(-vSkewUp)
×
1790
   self.denominator.relX = self.numerator.width + hSkew
×
1791
   self.denominator.relY = SILE.types.length(vSkewDown)
×
1792
   self.width = self.numerator.width + self.denominator.width + hSkew
×
1793
   self.height = maxLength(self.numerator.height + vSkewUp, self.denominator.height - vSkewDown)
×
1794
   self.depth = maxLength(self.numerator.depth - vSkewUp, self.denominator.depth + vSkewDown)
×
1795
   self.barWidth = SILE.types.length(hSkew)
×
1796
   self.barX = self.numerator.relX + self.numerator.width
×
1797
end
1798

1799
function elements.bevelledFraction:output (x, y, line)
×
1800
   local h = self.height:tonumber()
×
1801
   local d = self.depth:tonumber()
×
1802
   local barwidth = scaleWidth(self.barWidth, line):tonumber()
×
1803
   local xscaled = scaleWidth(x + self.barX, line)
×
1804
   local rd = self.ruleThickness / 2
×
1805
   local symbol = {
×
1806
      _r(self.ruleThickness),
×
1807
      "w", -- line width
1808
      1,
1809
      "J", -- round line caps
1810
      _r(0),
×
1811
      _r(d + h - rd),
×
1812
      "m",
1813
      _r(barwidth),
×
1814
      _r(rd),
×
1815
      "l",
1816
      "S",
1817
   }
1818
   local svg = table.concat(symbol, " ")
×
1819
   SILE.outputter:drawSVG(svg, xscaled, y, barwidth, h, 1)
×
1820
end
1821

1822
elements.mathMode = mathMode
×
1823
elements.newSubscript = newSubscript
×
1824
elements.newUnderOver = newUnderOver
×
1825

1826
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

© 2025 Coveralls, Inc