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

sile-typesetter / sile / 12273436894

11 Dec 2024 09:28AM UTC coverage: 31.554% (-39.1%) from 70.614%
12273436894

push

github

web-flow
Merge pull request #2195 from alerque/autoconf-upstream

Sync autoconf macros with new form submitted upstream

6226 of 19731 relevant lines covered (31.55%)

2807.64 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.getFace)
×
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
   }
218
   local filename = SILE.settings:get("math.font.filename")
×
219
   if filename and filename ~= "" then
×
220
      font.filename = filename
×
221
   end
222
   self.font = SILE.font.loadDefaults(font)
×
223
end
224

225
function elements.mbox.styleChildren (_)
×
226
   SU.error("styleChildren is a virtual function that need to be overridden by its child classes")
×
227
end
228

229
function elements.mbox.shape (_, _, _)
×
230
   SU.error("shape is a virtual function that need to be overridden by its child classes")
×
231
end
232

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

237
function elements.mbox:getMathMetrics ()
×
238
   return retrieveMathTable(self.font)
×
239
end
240

241
function elements.mbox:getScaleDown ()
×
242
   local constants = self:getMathMetrics().constants
×
243
   local scaleDown
244
   if isScriptMode(self.mode) then
×
245
      scaleDown = constants.scriptPercentScaleDown
×
246
   elseif isScriptScriptMode(self.mode) then
×
247
      scaleDown = constants.scriptScriptPercentScaleDown
×
248
   else
249
      scaleDown = 1
×
250
   end
251
   return scaleDown
×
252
end
253

254
-- Determine the mode of its descendants
255
function elements.mbox:styleDescendants ()
×
256
   self:styleChildren()
×
257
   for _, n in ipairs(self.children) do
×
258
      if n then
×
259
         n:styleDescendants()
×
260
      end
261
   end
262
end
263

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

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

292
local spaceKind = {
×
293
   thin = "thin",
294
   med = "med",
295
   thick = "thick",
296
}
297

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

394
-- _stackbox stacks its content one, either horizontally or vertically
395
elements.stackbox = pl.class(elements.mbox)
×
396
elements.stackbox._type = "Stackbox"
×
397

398
function elements.stackbox:__tostring ()
×
399
   local result = self.direction .. "Box("
×
400
   for i, n in ipairs(self.children) do
×
401
      result = result .. (i == 1 and "" or ", ") .. tostring(n)
×
402
   end
403
   result = result .. ")"
×
404
   return result
×
405
end
406

407
function elements.stackbox:_init (direction, children)
×
408
   elements.mbox._init(self)
×
409
   if not (direction == "H" or direction == "V") then
×
410
      SU.error("Wrong direction '" .. direction .. "'; should be H or V")
×
411
   end
412
   self.direction = direction
×
413
   self.children = children
×
414
end
415

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

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

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

547
function elements.stackbox.output (_, _, _, _) end
×
548

549
elements.phantom = pl.class(elements.stackbox) -- inherit from stackbox
×
550
elements.phantom._type = "Phantom"
×
551

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

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

570
elements.subscript = pl.class(elements.mbox)
×
571
elements.subscript._type = "Subscript"
×
572

573
function elements.subscript:__tostring ()
×
574
   return (self.sub and "Subscript" or "Superscript")
×
575
      .. "("
×
576
      .. tostring(self.base)
×
577
      .. ", "
×
578
      .. tostring(self.sub or self.super)
×
579
      .. ")"
×
580
end
581

582
function elements.subscript:_init (base, sub, sup)
×
583
   elements.mbox._init(self)
×
584
   self.base = base
×
585
   self.sub = sub
×
586
   self.sup = sup
×
587
   if self.base then
×
588
      table.insert(self.children, self.base)
×
589
   end
590
   if self.sub then
×
591
      table.insert(self.children, self.sub)
×
592
   end
593
   if self.sup then
×
594
      table.insert(self.children, self.sup)
×
595
   end
596
   self.atom = self.base.atom
×
597
end
598

599
function elements.subscript:styleChildren ()
×
600
   if self.base then
×
601
      self.base.mode = self.mode
×
602
   end
603
   if self.sub then
×
604
      self.sub.mode = getSubscriptMode(self.mode)
×
605
   end
606
   if self.sup then
×
607
      self.sup.mode = getSuperscriptMode(self.mode)
×
608
   end
609
end
610

611
function elements.subscript:calculateItalicsCorrection ()
×
612
   local lastGid = getRightMostGlyphId(self.base)
×
613
   if lastGid > 0 then
×
614
      local mathMetrics = self:getMathMetrics()
×
615
      if mathMetrics.italicsCorrection[lastGid] then
×
616
         return mathMetrics.italicsCorrection[lastGid]
×
617
      end
618
   end
619
   return 0
×
620
end
621

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

716
function elements.subscript.output (_, _, _, _) end
×
717

718
elements.underOver = pl.class(elements.subscript)
×
719
elements.underOver._type = "UnderOver"
×
720

721
function elements.underOver:__tostring ()
×
722
   return self._type .. "(" .. tostring(self.base) .. ", " .. tostring(self.sub) .. ", " .. tostring(self.sup) .. ")"
×
723
end
724

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

735
local function getAccentMode (mode)
736
   -- Size unchanged but leave display mode
737
   -- See MathML Core §3.4.3
738
   if mode == mathMode.display then
×
739
      return mathMode.text
×
740
   end
741
   if mode == mathMode.displayCramped then
×
742
      return mathMode.textCramped
×
743
   end
744
   return mode
×
745
end
746

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

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

784
function elements.underOver:styleChildren ()
×
785
   if self.base then
×
786
      self.base.mode = self.mode
×
787
   end
788
   if self.sub then
×
789
      self.sub.mode = self.attributes.accentunder and getAccentMode(self.mode) or getSubscriptMode(self.mode)
×
790
   end
791
   if self.sup then
×
792
      self.sup.mode = self.attributes.accent and getAccentMode(self.mode) or getSuperscriptMode(self.mode)
×
793
   end
794
end
795

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

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

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

978
function elements.underOver.output (_, _, _, _) end
×
979

980
-- terminal is the base class for leaf node
981
elements.terminal = pl.class(elements.mbox)
×
982
elements.terminal._type = "Terminal"
×
983

984
function elements.terminal:_init ()
×
985
   elements.mbox._init(self)
×
986
end
987

988
function elements.terminal.styleChildren (_) end
×
989

990
function elements.terminal.shape (_) end
×
991

992
elements.space = pl.class(elements.terminal)
×
993
elements.space._type = "Space"
×
994

995
function elements.space:_init ()
×
996
   elements.terminal._init(self)
×
997
end
998

999
function elements.space:__tostring ()
×
1000
   return self._type
×
1001
      .. "(width="
×
1002
      .. tostring(self.width)
×
1003
      .. ", height="
×
1004
      .. tostring(self.height)
×
1005
      .. ", depth="
×
1006
      .. tostring(self.depth)
×
1007
      .. ")"
×
1008
end
1009

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

1028
function elements.space:_init (width, height, depth)
×
1029
   elements.terminal._init(self)
×
1030
   self.width = getStandardLength(width)
×
1031
   self.height = getStandardLength(height)
×
1032
   self.depth = getStandardLength(depth)
×
1033
end
1034

1035
function elements.space:shape ()
×
1036
   self.width = self.width:absolute() * self:getScaleDown()
×
1037
   self.height = self.height:absolute() * self:getScaleDown()
×
1038
   self.depth = self.depth:absolute() * self:getScaleDown()
×
1039
end
1040

1041
function elements.space.output (_) end
×
1042

1043
-- text node. For any actual text output
1044
elements.text = pl.class(elements.terminal)
×
1045
elements.text._type = "Text"
×
1046

1047
function elements.text:__tostring ()
×
1048
   return self._type
×
1049
      .. "(atom="
×
1050
      .. tostring(self.atom)
×
1051
      .. ", kind="
×
1052
      .. tostring(self.kind)
×
1053
      .. ", script="
×
1054
      .. tostring(self.script)
×
1055
      .. (SU.boolean(self.stretchy, false) and ", stretchy" or "")
×
1056
      .. (SU.boolean(self.largeop, false) and ", largeop" or "")
×
1057
      .. ', text="'
×
1058
      .. (self.originalText or self.text)
×
1059
      .. '")'
×
1060
end
1061

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

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

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

1187
function elements.text:_reshapeGlyph (glyph, closestVariant, sz)
×
1188
   local face = SILE.font.cache(self.font, SILE.shaper.getFace)
×
1189
   local dimen = hb.get_glyph_dimensions(face, sz, closestVariant.variantGlyph)
×
1190
   glyph.gid = closestVariant.variantGlyph
×
1191
   glyph.width, glyph.height, glyph.depth, glyph.glyphAdvance =
×
1192
      dimen.width, dimen.height, dimen.depth, dimen.glyphAdvance
×
1193
   return dimen
×
1194
end
1195

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

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

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

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

1306
elements.fraction = pl.class(elements.mbox)
×
1307
elements.fraction._type = "Fraction"
×
1308

1309
function elements.fraction:__tostring ()
×
1310
   return self._type .. "(" .. tostring(self.numerator) .. ", " .. tostring(self.denominator) .. ")"
×
1311
end
1312

1313
function elements.fraction:_init (attributes, numerator, denominator)
×
1314
   elements.mbox._init(self)
×
1315
   self.numerator = numerator
×
1316
   self.denominator = denominator
×
1317
   self.attributes = attributes
×
1318
   table.insert(self.children, numerator)
×
1319
   table.insert(self.children, denominator)
×
1320
end
1321

1322
function elements.fraction:styleChildren ()
×
1323
   self.numerator.mode = getNumeratorMode(self.mode)
×
1324
   self.denominator.mode = getDenominatorMode(self.mode)
×
1325
end
1326

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

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

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

1361
   local numeratorGapMin, denominatorGapMin, numeratorShiftUp, denominatorShiftDown
1362
   if isDisplayMode(self.mode) then
×
1363
      numeratorGapMin = constants.fractionNumDisplayStyleGapMin * scaleDown
×
1364
      denominatorGapMin = constants.fractionDenomDisplayStyleGapMin * scaleDown
×
1365
      numeratorShiftUp = constants.fractionNumeratorDisplayStyleShiftUp * scaleDown
×
1366
      denominatorShiftDown = constants.fractionDenominatorDisplayStyleShiftDown * scaleDown
×
1367
   else
1368
      numeratorGapMin = constants.fractionNumeratorGapMin * scaleDown
×
1369
      denominatorGapMin = constants.fractionDenominatorGapMin * scaleDown
×
1370
      numeratorShiftUp = constants.fractionNumeratorShiftUp * scaleDown
×
1371
      denominatorShiftDown = constants.fractionDenominatorShiftDown * scaleDown
×
1372
   end
1373

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

1394
function elements.fraction:output (x, y, line)
×
1395
   if self.ruleThickness > 0 then
×
1396
      SILE.outputter:drawRule(
×
1397
         scaleWidth(x + self.padding, line),
×
1398
         y.length - self.axisHeight - self.ruleThickness / 2,
×
1399
         scaleWidth(self.width - 2 * self.padding, line),
×
1400
         self.ruleThickness
1401
      )
1402
   end
1403
end
1404

1405
local function newSubscript (spec)
1406
   return elements.subscript(spec.base, spec.sub, spec.sup)
×
1407
end
1408

1409
local function newUnderOver (spec)
1410
   return elements.underOver(spec.attributes, spec.base, spec.sub, spec.sup)
×
1411
end
1412

1413
-- TODO replace with penlight equivalent
1414
local function mapList (f, l)
1415
   local ret = {}
×
1416
   for i, x in ipairs(l) do
×
1417
      ret[i] = f(i, x)
×
1418
   end
1419
   return ret
×
1420
end
1421

1422
elements.mtr = pl.class(elements.mbox)
×
1423
-- elements.mtr._type = "" -- TODO why not set?
1424

1425
function elements.mtr:_init (children)
×
1426
   self.children = children
×
1427
end
1428

1429
function elements.mtr:styleChildren ()
×
1430
   for _, c in ipairs(self.children) do
×
1431
      c.mode = self.mode
×
1432
   end
1433
end
1434

1435
function elements.mtr.shape (_) end -- done by parent table
×
1436

1437
function elements.mtr.output (_) end
×
1438

1439
elements.table = pl.class(elements.mbox)
×
1440
elements.table._type = "table" -- TODO why case difference?
×
1441

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

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

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

1564
function elements.table.output (_) end
×
1565

1566
local function getRadicandMode (mode)
1567
   -- Not too sure if we should do something special/
1568
   return mode
×
1569
end
1570

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

1584
elements.sqrt = pl.class(elements.mbox)
×
1585
elements.sqrt._type = "Sqrt"
×
1586

1587
function elements.sqrt:__tostring ()
×
1588
   return self._type .. "(" .. tostring(self.radicand) .. (self.degree and ", " .. tostring(self.degree) or "") .. ")"
×
1589
end
1590

1591
function elements.sqrt:_init (radicand, degree)
×
1592
   elements.mbox._init(self)
×
1593
   self.radicand = radicand
×
1594
   if degree then
×
1595
      self.degree = degree
×
1596
      table.insert(self.children, degree)
×
1597
   end
1598
   table.insert(self.children, radicand)
×
1599
   self.relX = SILE.types.length()
×
1600
   self.relY = SILE.types.length()
×
1601
end
1602

1603
function elements.sqrt:styleChildren ()
×
1604
   self.radicand.mode = getRadicandMode(self.mode)
×
1605
   if self.degree then
×
1606
      self.degree.mode = getDegreeMode(self.mode)
×
1607
   end
1608
end
1609

1610
function elements.sqrt:shape ()
×
1611
   local mathMetrics = self:getMathMetrics()
×
1612
   local scaleDown = self:getScaleDown()
×
1613
   local constants = mathMetrics.constants
×
1614

1615
   self.radicalRuleThickness = constants.radicalRuleThickness * scaleDown
×
1616
   if self.mode == mathMode.display or self.mode == mathMode.displayCramped then
×
1617
      self.radicalVerticalGap = constants.radicalDisplayStyleVerticalGap * scaleDown
×
1618
   else
1619
      self.radicalVerticalGap = constants.radicalVerticalGap * scaleDown
×
1620
   end
1621
   self.extraAscender = constants.radicalExtraAscender * scaleDown
×
1622

1623
   -- HACK: We draw own own radical sign in the output() method.
1624
   -- Derive dimensions for the radical sign (more or less ad hoc).
1625
   -- Note: In TeX, the radical sign extends a lot below the baseline,
1626
   -- and MathML Core also has a lot of layout text about it.
1627
   -- Not only it doesn't look good, but it's not very clear vs. OpenType.
1628
   local radicalGlyph = SILE.shaper:measureChar("√")
×
1629
   local ratio = (self.radicand.height:tonumber() + self.radicand.depth:tonumber())
×
1630
      / (radicalGlyph.height + radicalGlyph.depth)
×
1631
   local vertAdHocOffset = (ratio > 1 and math.log(ratio) or 0) * self.radicalVerticalGap
×
1632
   self.symbolHeight = SILE.types.length(radicalGlyph.height) * scaleDown
×
1633
   self.symbolDepth = (SILE.types.length(radicalGlyph.depth) + vertAdHocOffset) * scaleDown
×
1634
   self.symbolWidth = (SILE.types.length(radicalGlyph.width) + vertAdHocOffset) * scaleDown
×
1635

1636
   -- Adjust the height of the radical sign if the radicand is higher
1637
   self.symbolHeight = self.radicand.height > self.symbolHeight and self.radicand.height or self.symbolHeight
×
1638
   -- Compute the (max-)height of the short leg of the radical sign
1639
   self.symbolShortHeight = self.symbolHeight * constants.radicalDegreeBottomRaisePercent
×
1640

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

1661
local function _r (number)
1662
   -- Lua 5.3+ formats floats as 1.0 and integers as 1
1663
   -- Also some PDF readers do not like double precision.
1664
   return math.floor(number) == number and math.floor(number) or tonumber(string.format("%.5f", number))
×
1665
end
1666

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

1713
elements.padded = pl.class(elements.mbox)
×
1714
elements.padded._type = "Padded"
×
1715

1716
function elements.padded:__tostring ()
×
1717
   return self._type .. "(" .. tostring(self.impadded) .. ")"
×
1718
end
1719

1720
function elements.padded:_init (attributes, impadded)
×
1721
   elements.mbox._init(self)
×
1722
   self.impadded = impadded
×
1723
   self.attributes = attributes or {}
×
1724
   table.insert(self.children, impadded)
×
1725
end
1726

1727
function elements.padded:styleChildren ()
×
1728
   self.impadded.mode = self.mode
×
1729
end
1730

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

1755
function elements.padded.output (_, _, _, _) end
×
1756

1757
-- Bevelled fractions are not part of MathML Core, and MathML4 does not
1758
-- exactly specify how to compute the layout.
1759
elements.bevelledFraction = pl.class(elements.fraction) -- Inherit from fraction
×
1760
elements.fraction._type = "BevelledFraction"
×
1761

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

1777
   self.ruleThickness = self.attributes.linethickness
×
1778
         and SU.cast("measurement", self.attributes.linethickness):tonumber()
×
1779
      or constants.fractionRuleThickness * scaleDown
×
1780
   self.numerator.relX = SILE.types.length(0)
×
1781
   self.numerator.relY = SILE.types.length(-vSkewUp)
×
1782
   self.denominator.relX = self.numerator.width + hSkew
×
1783
   self.denominator.relY = SILE.types.length(vSkewDown)
×
1784
   self.width = self.numerator.width + self.denominator.width + hSkew
×
1785
   self.height = maxLength(self.numerator.height + vSkewUp, self.denominator.height - vSkewDown)
×
1786
   self.depth = maxLength(self.numerator.depth - vSkewUp, self.denominator.depth + vSkewDown)
×
1787
   self.barWidth = SILE.types.length(hSkew)
×
1788
   self.barX = self.numerator.relX + self.numerator.width
×
1789
end
1790

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

1814
elements.mathMode = mathMode
×
1815
elements.newSubscript = newSubscript
×
1816
elements.newUnderOver = newUnderOver
×
1817

1818
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