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

sile-typesetter / sile / 11989223534

23 Nov 2024 05:54PM UTC coverage: 64.386% (-4.2%) from 68.563%
11989223534

push

github

web-flow
Merge 144192218 into f0ddaed08

3 of 12 new or added lines in 1 file covered. (25.0%)

1080 existing lines in 35 files now uncovered.

12771 of 19835 relevant lines covered (64.39%)

4344.53 hits per line

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

68.22
/packages/math/texlike.lua
1
local atoms = require("packages.math.atoms")
1✔
2
local syms = require("packages.math.unicode-symbols")
1✔
3
local bits = require("core.parserbits")
1✔
4

5
local epnf = require("epnf")
1✔
6
local lpeg = require("lpeg")
1✔
7

8
local operatorDict = syms.operatorDict
1✔
9
local symbols = syms.symbols
1✔
10

11
-- Grammar to parse TeX-like math
12
-- luacheck: push ignore
13
-- stylua: ignore start
14
---@diagnostic disable: undefined-global, unused-local, lowercase-global
15
local mathGrammar = function (_ENV)
16
   local _ = WS^0
1✔
17
   local eol = S"\r\n"
1✔
18
   local digit = R("09")
1✔
19
   local natural = (
20
      -- TeX doesn't really knows what a number in a formula is.
21
      -- It handles any sequence of "ordinary" characters, including period(s):
22
      -- See for instance The TeXbook, p. 132.
23
      -- When later converting to MathML, we'll ideally want <mn>0.0123</mn>
24
      -- instead of, say, <mn>0</mn><mo>.</mo><mn>0123</mn> (not only wrong
25
      -- in essence, but also taking the risk of using a <mo> operator, then
26
      -- considered as a punctuation, thus inserting a space)
27
      -- We cannot be general, but checking MathJax and TeMML's behavior, they
28
      -- are not general either in this regard.
29
         digit^0 * P(".")^-1 * digit^1 + -- Decimal number (ex: 1.23, 0.23, .23)
1✔
30
         digit^1 -- Integer (digits only, ex: 123)
1✔
31
      ) / tostring
1✔
32
   local pos_natural = R("19") * digit^0 / tonumber
1✔
33
   local ctrl_word = R("AZ", "az")^1
1✔
34
   local ctrl_symbol = P(1) - S"{}\\"
1✔
35
   local ctrl_sequence_name = C(ctrl_word + ctrl_symbol) / 1
1✔
36
   local comment = (
37
         P"%" *
1✔
38
         P(1-eol)^0 *
1✔
39
         eol^-1
1✔
40
      )
41
   local utf8cont = R("\128\191")
1✔
42
   local utf8code = lpeg.R("\0\127")
1✔
43
      + lpeg.R("\194\223") * utf8cont
1✔
44
      + lpeg.R("\224\239") * utf8cont * utf8cont
1✔
45
      + lpeg.R("\240\244") * utf8cont * utf8cont * utf8cont
1✔
46
   -- Identifiers inside \mo and \mi tags
47
   local sileID = C(bits.identifier + P(1)) / 1
1✔
48
   local mathMLID = (utf8code - S"\\{}%")^1 / function (...)
1✔
49
         local ret = ""
×
50
         local t = {...}
×
51
         for _,b in ipairs(t) do
×
52
         ret = ret .. b
×
53
         end
54
         return ret
×
55
      end
56
   local group = P"{" * V"mathlist" * (P"}" + E("`}` expected"))
2✔
57
   -- Simple amsmath-like \text command (no embedded math)
58
   local textgroup = P"{" * C((1-P"}")^1) * (P"}" + E("`}` expected"))
2✔
59
   local element_no_infix =
60
      V"def" +
1✔
61
      V"text" + -- Important: before command
1✔
62
      V"command" +
1✔
63
      group +
1✔
64
      V"argument" +
1✔
65
      V"atom"
1✔
66
   local element =
67
      V"supsub" +
1✔
68
      V"subsup" +
1✔
69
      V"sup" +
1✔
70
      V"sub" +
1✔
71
      element_no_infix
1✔
72
   local sep = S",;" * _
1✔
73
   local quotedString = (P'"' * C((1-P'"')^1) * P'"')
1✔
74
   local value = ( quotedString + (1-S",;]")^1 )
1✔
75
   local pair = Cg(sileID * _ * "=" * _ * C(value)) * sep^-1 / function (...)
1✔
76
      local t = {...}; return t[1], t[#t]
92✔
77
   end
78
   local list = Cf(Ct"" * pair^0, rawset)
1✔
79
   local parameters = (
80
         P"[" *
1✔
81
         list *
1✔
82
         P"]"
1✔
83
      )^-1 / function (a)
1✔
84
            return type(a)=="table" and a or {}
105✔
85
         end
86
   local dim2_arg_inner = Ct(V"mathlist" * (P"&" * V"mathlist")^0) /
1✔
87
      function (t)
88
         t.id = "mathlist"
×
89
         return t
×
90
      end
91
   local dim2_arg =
92
      Cg(P"{" *
2✔
93
         dim2_arg_inner *
1✔
94
         (P"\\\\" * dim2_arg_inner)^1 *
1✔
95
         (P"}" + E("`}` expected"))
2✔
96
         ) / function (...)
×
97
            local t = {...}
×
98
            -- Remove the last mathlist if empty. This way,
99
            -- `inner1 \\ inner2 \\` is the same as `inner1 \\ inner2`.
100
            if not t[#t][1] or not t[#t][1][1] then table.remove(t) end
×
101
            return pl.utils.unpack(t)
×
102
         end
103

104
   local dim2_arg_inner = Ct(V"mathlist" * (P"&" * V"mathlist")^0) /
1✔
105
      function (t)
UNCOV
106
         t.id = "mathlist"
×
UNCOV
107
         return t
×
108
      end
109
   local dim2_arg =
110
      Cg(P"{" *
2✔
111
         dim2_arg_inner *
1✔
112
         (P"\\\\" * dim2_arg_inner)^1 *
1✔
113
         (P"}" + E("`}` expected"))
2✔
114
         ) / function (...)
×
UNCOV
115
         local t = {...}
×
116
         -- Remove the last mathlist if empty. This way,
117
         -- `inner1 \\ inner2 \\` is the same as `inner1 \\ inner2`.
UNCOV
118
         if not t[#t][1] or not t[#t][1][1] then table.remove(t) end
×
UNCOV
119
         return pl.utils.unpack(t)
×
120
         end
121

122
   -- TeX uses the regular asterisk (* = U+002A) in superscripts or subscript:
123
   -- The TeXbook exercice 18.32 (p. 179, 330) for instance.
124
   -- Fonts usually have the asterisk raised too high, so using the Unicode
125
   -- asterisk operator U+2217 looks better (= \ast in TeX).
126
   local astop = P"*" / luautf8.char(0x2217)
1✔
127
   -- TeX interprets apostrophes as primes in math mode:
128
   -- The TeXbook p. 130 expands ' to ^\prime commands and repeats the \prime
129
   -- for multiple apostrophes.
130
   -- The TeXbook p. 134: "Then there is the character ', which we know is used
131
   -- as an abbreviation for \prime superscripts."
132
   -- (So we are really sure superscript primes are really the intended meaning.)
133
   -- Here we use the Unicode characters for primes, but the intent is the same.
134
   local primes = (
135
         P"''''" / luautf8.char(0x2057) + -- quadruple prime
1✔
136
         P"'''" / luautf8.char(0x2034) + -- triple prime
1✔
137
         P"''" / luautf8.char(0x2033) + -- double prime
1✔
138
         P"'" / luautf8.char(0x2032) -- prime
1✔
139
      ) / function (s)
×
UNCOV
140
            return { id="atom", s }
×
141
         end
142
   local primes_sup = (
143
         primes * _ * P"^" * _ * element_no_infix / function (p, e)
1✔
144
            -- Combine the prime with the superscript in the same mathlist
UNCOV
145
            if e.id == "mathlist" then
×
UNCOV
146
               table.insert(e, 1, p)
×
UNCOV
147
               return e
×
148
            end
UNCOV
149
            return { id="mathlist", p, e }
×
150
         end
151
         + primes -- or standalone primes
1✔
152
      )
153

154
   START "math"
1✔
155
   math = V"mathlist" * EOF"Unexpected character at end of math code"
2✔
156
   mathlist = (comment + (WS * _) + element)^0
1✔
157
   supsub = element_no_infix * _ * primes_sup                  * _ *  P"_" * _ * element_no_infix +
1✔
158
            element_no_infix * _ * P"^" * _ * element_no_infix * _ *  P"_" * _ * element_no_infix
1✔
159
   subsup = element_no_infix * _ * P"_" * _ * element_no_infix * primes_sup +
1✔
160
            element_no_infix * _ * P"_" * _ * element_no_infix * _ * P"^" * _ * element_no_infix
1✔
161
   sup =  element_no_infix * _ * primes_sup +
1✔
162
          element_no_infix * _ * P"^" * _ * element_no_infix
1✔
163
   sub = element_no_infix * _ * P"_" * _ * element_no_infix
1✔
164
   atom = natural + astop + C(utf8code - S"\\{}%^_&'") +
1✔
165
      (P"\\{" + P"\\}") / function (s) return string.sub(s, -1) end
1✔
166
   text = (
×
167
         P"\\text" *
1✔
168
         Cg(parameters, "options") *
1✔
169
         textgroup
1✔
170
      )
1✔
171
   command = (
×
172
         P"\\" *
1✔
173
         Cg(ctrl_sequence_name, "command") *
1✔
174
         Cg(parameters, "options") *
1✔
175
         (dim2_arg + group^0)
1✔
176
      )
1✔
177
   def = P"\\def" * _ * P"{" *
1✔
178
      Cg(ctrl_sequence_name, "command-name") * P"}" * _ *
1✔
179
      --P"[" * Cg(digit^1, "arity") * P"]" * _ *
180
      P"{" * V"mathlist" * P"}"
1✔
181
   argument = P"#" * Cg(pos_natural, "index")
1✔
182
end
183
-- luacheck: pop
184
-- stylua: ignore end
185
---@diagnostic enable: undefined-global, unused-local, lowercase-global
186

187
local mathParser = epnf.define(mathGrammar)
1✔
188

189
local commands = {}
1✔
190

191
-- A command type is a type for each argument it takes: either string or MathML
192
-- tree. If a command has no type, it is assumed to take only trees.
193
-- Tags like <mi>, <mo>, <mn> take a string, and this needs to be propagated in
194
-- commands that use them.
195

196
local objType = {
1✔
197
   tree = 1,
198
   str = 2,
199
}
200

201
local function inferArgTypes_aux (accumulator, typeRequired, body)
202
   if type(body) == "table" then
410✔
203
      if body.id == "argument" then
410✔
204
         local ret = accumulator
20✔
205
         table.insert(ret, body.index, typeRequired)
20✔
206
         return ret
20✔
207
      elseif body.id == "command" then
390✔
208
         if commands[body.command] then
87✔
209
            local cmdArgTypes = commands[body.command][1]
69✔
210
            if #cmdArgTypes ~= #body then
69✔
211
               SU.error(
×
212
                  "Wrong number of arguments ("
213
                     .. #body
×
214
                     .. ") for command "
×
215
                     .. body.command
×
216
                     .. " (should be "
×
217
                     .. #cmdArgTypes
×
218
                     .. ")"
×
219
               )
220
            else
221
               for i = 1, #cmdArgTypes do
128✔
222
                  accumulator = inferArgTypes_aux(accumulator, cmdArgTypes[i], body[i])
118✔
223
               end
224
            end
225
            return accumulator
69✔
226
         elseif body.command == "mi" or body.command == "mo" or body.command == "mn" then
18✔
227
            if #body ~= 1 then
×
228
               SU.error("Wrong number of arguments (" .. #body .. ") for command " .. body.command .. " (should be 1)")
×
229
            end
230
            accumulator = inferArgTypes_aux(accumulator, objType.str, body[1])
×
231
            return accumulator
×
232
         else
233
            -- Not a macro, recurse on children assuming tree type for all
234
            -- arguments
235
            for _, child in ipairs(body) do
28✔
236
               accumulator = inferArgTypes_aux(accumulator, objType.tree, child)
20✔
237
            end
238
            return accumulator
18✔
239
         end
240
      elseif body.id == "atom" then
303✔
241
         return accumulator
154✔
242
      else
243
         -- Simply recurse on children
244
         for _, child in ipairs(body) do
410✔
245
            accumulator = inferArgTypes_aux(accumulator, typeRequired, child)
522✔
246
         end
247
         return accumulator
149✔
248
      end
249
   else
250
      SU.error("invalid argument to inferArgTypes_aux")
×
251
   end
252
end
253

254
local inferArgTypes = function (body)
255
   return inferArgTypes_aux({}, objType.tree, body)
80✔
256
end
257

258
local function registerCommand (name, argTypes, func)
259
   commands[name] = { argTypes, func }
83✔
260
end
261

262
-- Computes func(func(... func(init, k1, v1), k2, v2)..., k_n, v_n), i.e. applies
263
-- func on every key-value pair in the table. Keys with numeric indices are
264
-- processed in order. This is an important property for MathML compilation below.
265
local function fold_pairs (func, table)
266
   local accumulator = {}
142✔
267
   for k, v in pl.utils.kpairs(table) do
1,210✔
268
      accumulator = func(v, k, accumulator)
784✔
269
   end
270
   for i, v in ipairs(table) do
395✔
271
      accumulator = func(v, i, accumulator)
506✔
272
   end
273
   return accumulator
142✔
274
end
275

276
local function forall (pred, list)
277
   for _, x in ipairs(list) do
3✔
278
      if not pred(x) then
4✔
279
         return false
2✔
280
      end
281
   end
282
   return true
1✔
283
end
284

285
local compileToStr = function (argEnv, mathlist)
286
   if #mathlist == 1 and mathlist.id == "atom" then
10✔
287
      -- List is a single atom
288
      return mathlist[1]
×
289
   elseif #mathlist == 1 and mathlist[1].id == "argument" then
10✔
UNCOV
290
      return argEnv[mathlist[1].index]
×
291
   elseif mathlist.id == "argument" then
10✔
292
      return argEnv[mathlist.index]
×
293
   else
294
      local ret = ""
10✔
295
      for _, elt in ipairs(mathlist) do
24✔
296
         if elt.id == "atom" then
14✔
297
            ret = ret .. elt[1]
14✔
UNCOV
298
         elseif elt.id == "command" and symbols[elt.command] then
×
UNCOV
299
            ret = ret .. symbols[elt.command]
×
300
         else
301
            SU.error("Encountered non-character token in command that takes a string")
×
302
         end
303
      end
304
      return ret
10✔
305
   end
306
end
307

308
local function isOperatorKind (tree, typeOfAtom)
309
   if not tree then
78✔
310
      return false -- safeguard
×
311
   end
312
   if tree.command ~= "mo" then
78✔
313
      return false
32✔
314
   end
315
   -- Case \mo[atom=xxx]{ops}
316
   -- E.g. \mo[atom=op]{lim}
317
   if tree.options and tree.options.atom then
46✔
UNCOV
318
      return atoms.types[tree.options.atom] == typeOfAtom
×
319
   end
320
   -- Case \mo{ops} where ops is registered with the resquested type
321
   -- E.g. \mo{∑) or \sum
322
   if tree[1] and operatorDict[tree[1]] and operatorDict[tree[1]].atom then
46✔
323
      return operatorDict[tree[1]].atom == typeOfAtom
42✔
324
   end
325
   return false
4✔
326
end
327

328
local function isMoveableLimits (tree)
329
   if tree.command ~= "mo" then
4✔
330
      return false
4✔
331
   end
UNCOV
332
   if tree.options and SU.boolean(tree.options.movablelimits, false) then
×
UNCOV
333
      return true
×
334
   end
UNCOV
335
   if tree[1] and operatorDict[tree[1]] and operatorDict[tree[1]].forms then
×
336
      -- Leap of faith: We have not idea yet which form the operator will take
337
      -- in the final MathML.
338
      -- In the MathML operator dictionary, some operators have a movablelimits
339
      -- in some forms and not in others.
340
      -- Ex. \Join (U+2A1D) and \bigtriangleleft (U+2A1E) have it prefix but not
341
      -- infix, for some unspecified reason (?).
342
      -- Assume that if at least one form has movablelimits, the operator is
343
      -- considered to have movablelimits "in general".
UNCOV
344
      for _, form in pairs(operatorDict[tree[1]].forms) do
×
UNCOV
345
         if SU.boolean(form.movablelimits, false) then
×
UNCOV
346
            return true
×
347
         end
348
      end
349
   end
UNCOV
350
   return false
×
351
end
352
local function isCloseOperator (tree)
353
   return isOperatorKind(tree, atoms.types.close)
38✔
354
end
355
local function isOpeningOperator (tree)
356
   return isOperatorKind(tree, atoms.types.open)
40✔
357
end
358

359
local function isAccentSymbol (symbol)
360
   return operatorDict[symbol] and operatorDict[symbol].atom == atoms.types.accent
8✔
361
end
362

363
local function compileToMathML_aux (_, arg_env, tree)
364
   if type(tree) == "string" then
174✔
365
      return tree
32✔
366
   end
367
   local function compile_and_insert (child, key, accumulator)
368
      if type(key) ~= "number" then
645✔
369
         accumulator[key] = child
392✔
370
         return accumulator
392✔
371
      -- Compile all children, except if this node is a macro definition (no
372
      -- evaluation "under lambda") or the application of a registered macro
373
      -- (since evaluating the nodes depends on the macro's signature, it is more
374
      -- complex and done below)..
375
      elseif tree.id == "def" or (tree.id == "command" and commands[tree.command]) then
253✔
376
         -- Conserve unevaluated child
377
         table.insert(accumulator, child)
90✔
378
      else
379
         -- Compile next child
380
         local comp = compileToMathML_aux(nil, arg_env, child)
163✔
381
         if comp then
163✔
382
            if comp.id == "wrapper" then
83✔
383
               -- Insert all children of the wrapper node
UNCOV
384
               for _, inner_child in ipairs(comp) do
×
UNCOV
385
                  table.insert(accumulator, inner_child)
×
386
               end
387
            else
388
               table.insert(accumulator, comp)
83✔
389
            end
390
         end
391
      end
392
      return accumulator
253✔
393
   end
394
   tree = fold_pairs(compile_and_insert, tree)
284✔
395
   if tree.id == "math" then
142✔
396
      tree.command = "math"
3✔
397
      -- If the outermost `mrow` contains only other `mrow`s, remove it
398
      -- (allowing vertical stacking).
399
      if forall(function (c)
6✔
400
         return c.command == "mrow"
2✔
401
      end, tree[1]) then
6✔
402
         tree[1].command = "math"
1✔
403
         return tree[1]
1✔
404
      end
405
   elseif tree.id == "mathlist" then
139✔
406
      -- Turn mathlist into `mrow` except if it has exactly one `mtr` or `mtd`
407
      -- child.
408
      -- Note that `def`s have already been compiled away at this point.
409
      if #tree == 1 then
5✔
UNCOV
410
         if tree[1].command == "mtr" or tree[1].command == "mtd" then
×
411
            return tree[1]
×
412
         else
UNCOV
413
            tree.command = "mrow"
×
414
         end
415
      else
416
         -- Re-wrap content from opening to closing operator in an implicit mrow,
417
         -- so stretchy operators apply to the correct span of content.
418
         local children = {}
5✔
419
         local stack = {}
5✔
420
         for _, child in ipairs(tree) do
45✔
421
            if isOpeningOperator(child) then
80✔
422
               table.insert(stack, children)
6✔
423
               local mrow = {
6✔
424
                  command = "mrow",
425
                  is_paired = true, -- Internal flag to mark this re-wrapped mrow
426
                  options = {},
6✔
427
                  child,
6✔
428
               }
429
               table.insert(children, mrow)
6✔
430
               children = mrow
6✔
431
            elseif isCloseOperator(child) then
68✔
432
               table.insert(children, child)
6✔
433
               if #stack > 0 then
6✔
434
                  children = table.remove(stack)
12✔
435
               end
436
            elseif
×
437
               (child.command == "msubsup" or child.command == "msub" or child.command == "msup")
28✔
438
               and isCloseOperator(child[1]) -- child[1] is the base
8✔
439
            then
440
               if #stack > 0 then
×
441
                  -- Special case for closing operator with sub/superscript:
442
                  -- (....)^i must be interpreted as {(....)}^i, not as (...{)}^i
443
                  -- Push the closing operator into the mrow
444
                  table.insert(children, child[1])
×
445
                  -- Move the mrow into the msubsup, replacing the closing operator
446
                  child[1] = children
×
447
                  -- And insert the msubsup into the parent
448
                  children = table.remove(stack)
×
449
                  children[#children] = child
×
450
               else
451
                  table.insert(children, child)
×
452
               end
453
            else
454
               table.insert(children, child)
28✔
455
            end
456
         end
457
         tree = #stack > 0 and stack[1] or children
5✔
458
         tree.command = "mrow"
5✔
459
      end
460
   elseif tree.id == "atom" then
134✔
461
      local codepoints = {}
32✔
462
      for _, cp in luautf8.codes(tree[1]) do
64✔
463
         table.insert(codepoints, cp)
32✔
464
      end
465
      local cp = codepoints[1]
32✔
466
      if
467
         #codepoints == 1
32✔
468
         and ( -- If length of UTF-8 string is 1
×
469
            cp >= SU.codepoint("A") and cp <= SU.codepoint("Z")
78✔
470
            or cp >= SU.codepoint("a") and cp <= SU.codepoint("z")
78✔
471
            or cp >= SU.codepoint("Α") and cp <= SU.codepoint("Ω")
60✔
472
            or cp >= SU.codepoint("α") and cp <= SU.codepoint("ω")
60✔
473
            or cp == SU.codepoint("ϑ")
52✔
474
            or cp == SU.codepoint("ϕ")
52✔
475
            or cp == SU.codepoint("ϰ")
52✔
476
            or cp == SU.codepoint("ϱ")
52✔
477
            or cp == SU.codepoint("ϖ")
52✔
478
            or cp == SU.codepoint("ϵ")
52✔
479
         )
480
      then
481
         tree.command = "mi"
6✔
482
      elseif lpeg.match(lpeg.R("09") ^ 1, tree[1]) then
26✔
483
         tree.command = "mn"
2✔
484
      else
485
         tree.command = "mo"
24✔
486
      end
487
      tree.options = {}
32✔
488
   -- Translate TeX-like sub/superscripts to `munderover` or `msubsup`,
489
   -- depending on whether the base is an operator with moveable limits.
490
   elseif tree.id == "sup" and isMoveableLimits(tree[1]) then
106✔
491
      tree.command = "mover"
×
492
   elseif tree.id == "sub" and isMoveableLimits(tree[1]) then
102✔
UNCOV
493
      tree.command = "munder"
×
494
   elseif tree.id == "subsup" and isMoveableLimits(tree[1]) then
102✔
UNCOV
495
      tree.command = "munderover"
×
496
   elseif tree.id == "supsub" and isMoveableLimits(tree[1]) then
102✔
497
      tree.command = "munderover"
×
498
      local tmp = tree[2]
×
499
      tree[2] = tree[3]
×
500
      tree[3] = tmp
×
501
   elseif tree.id == "sup" then
102✔
502
      tree.command = "msup"
4✔
503
   elseif tree.id == "sub" then
98✔
UNCOV
504
      tree.command = "msub"
×
505
   elseif tree.id == "subsup" then
98✔
UNCOV
506
      tree.command = "msubsup"
×
507
   elseif tree.id == "supsub" then
98✔
UNCOV
508
      tree.command = "msubsup"
×
UNCOV
509
      local tmp = tree[2]
×
UNCOV
510
      tree[2] = tree[3]
×
UNCOV
511
      tree[3] = tmp
×
512
   elseif tree.id == "def" then
98✔
513
      local commandName = tree["command-name"]
80✔
514
      local argTypes = inferArgTypes(tree[1])
80✔
515
      registerCommand(commandName, argTypes, function (compiledArgs)
160✔
UNCOV
516
         return compileToMathML_aux(nil, compiledArgs, tree[1])
×
517
      end)
518
      return nil
80✔
519
   elseif tree.id == "text" then
18✔
520
      tree.command = "mtext"
×
521
   elseif tree.id == "command" and commands[tree.command] then
18✔
522
      local argTypes = commands[tree.command][1]
10✔
523
      local cmdFun = commands[tree.command][2]
10✔
524
      local applicationTree = tree
10✔
525
      local cmdName = tree.command
10✔
526
      if #applicationTree ~= #argTypes then
10✔
527
         SU.error(
×
528
            "Wrong number of arguments ("
529
               .. #applicationTree
×
530
               .. ") for command "
×
531
               .. cmdName
×
532
               .. " (should be "
×
533
               .. #argTypes
×
534
               .. ")"
×
535
         )
536
      end
537
      -- Compile every argument
538
      local compiledArgs = {}
10✔
539
      for i, arg in pairs(applicationTree) do
60✔
540
         if type(i) == "number" then
50✔
541
            if argTypes[i] == objType.tree then
10✔
UNCOV
542
               table.insert(compiledArgs, compileToMathML_aux(nil, arg_env, arg))
×
543
            else
544
               local x = compileToStr(arg_env, arg)
10✔
545
               table.insert(compiledArgs, x)
10✔
546
            end
547
         else
548
            -- Not an argument but an attribute. Add it to the compiled
549
            -- argument tree as-is
550
            compiledArgs[i] = applicationTree[i]
40✔
551
         end
552
      end
553
      local res = cmdFun(compiledArgs)
10✔
554
      if res.command == "mrow" then
10✔
555
         -- Mark the outer mrow to be unwrapped in the parent
UNCOV
556
         res.id = "wrapper"
×
557
      end
558
      return res
10✔
559
   elseif tree.id == "command" and symbols[tree.command] then
8✔
560
      local atom = { id = "atom", [1] = symbols[tree.command] }
8✔
561
      if isAccentSymbol(symbols[tree.command]) and #tree > 0 then
16✔
562
         -- LaTeX-style accents \vec{v} = <mover accent="true"><mi>v</mi><mo>→</mo></mover>
563
         local accent = {
×
564
            id = "command",
565
            command = "mover",
566
            options = {
×
567
               accent = "true",
568
            },
569
         }
570
         accent[1] = compileToMathML_aux(nil, arg_env, tree[1])
×
571
         accent[2] = compileToMathML_aux(nil, arg_env, atom)
×
572
         tree = accent
×
573
      elseif #tree > 0 then
8✔
574
         -- Play cool with LaTeX-style commands that don't take arguments:
575
         -- Edge case for non-accent symbols so we don't loose bracketed groups
576
         -- that might have been seen as command arguments.
577
         -- Ex. \langle{x}\rangle (without space after \langle)
578
         local sym = compileToMathML_aux(nil, arg_env, atom)
×
579
         -- Compile all children in-place
580
         for i, child in ipairs(tree) do
×
581
            tree[i] = compileToMathML_aux(nil, arg_env, child)
×
582
         end
583
         -- Insert symbol at the beginning,
584
         -- And add a wrapper mrow to be unwrapped in the parent.
585
         table.insert(tree, 1, sym)
×
586
         tree.command = "mrow"
×
587
         tree.id = "wrapper"
×
588
      else
589
         tree = compileToMathML_aux(nil, arg_env, atom)
16✔
590
      end
UNCOV
591
   elseif tree.id == "argument" then
×
UNCOV
592
      if arg_env[tree.index] then
×
UNCOV
593
         return arg_env[tree.index]
×
594
      else
595
         SU.error("Argument #" .. tree.index .. " has escaped its scope (probably not fully applied command).")
×
596
      end
597
   end
598
   tree.id = nil
51✔
599
   return tree
51✔
600
end
601

602
local function printMathML (tree)
603
   if type(tree) == "string" then
×
604
      return tree
×
605
   end
606
   local result = "\\" .. tree.command
×
607
   if tree.options then
×
608
      local options = {}
×
609
      for k, v in pairs(tree.options) do
×
610
         table.insert(options, k .. "=" .. tostring(v))
×
611
      end
612
      if #options > 0 then
×
613
         result = result .. "[" .. table.concat(options, ", ") .. "]"
×
614
      end
615
   end
616
   if #tree > 0 then
×
617
      result = result .. "{"
×
618
      for _, child in ipairs(tree) do
×
619
         result = result .. printMathML(child)
×
620
      end
621
      result = result .. "}"
×
622
   end
623
   return result
×
624
end
625

626
local function compileToMathML (_, arg_env, tree)
627
   local result = compileToMathML_aux(_, arg_env, tree)
3✔
628
   SU.debug("texmath", function ()
6✔
629
      return "Resulting MathML: " .. printMathML(result)
×
630
   end)
631
   return result
3✔
632
end
633

634
local function convertTexlike (_, content)
635
   local ret = epnf.parsestring(mathParser, content[1])
3✔
636
   SU.debug("texmath", function ()
6✔
637
      return "Parsed TeX math: " .. pl.pretty.write(ret)
×
638
   end)
639
   return ret
3✔
640
end
641

642
registerCommand("mi", { [1] = objType.str }, function (x)
2✔
643
   return x
8✔
644
end)
645
registerCommand("mo", { [1] = objType.str }, function (x)
2✔
646
   return x
2✔
647
end)
648
registerCommand("mn", { [1] = objType.str }, function (x)
2✔
UNCOV
649
   return x
×
650
end)
651

652
compileToMathML(
2✔
653
   nil,
1✔
654
   {},
655
   convertTexlike(nil, {
1✔
656
      [==[
×
657
  \def{frac}{\mfrac{#1}{#2}}
658
  \def{sqrt}{\msqrt{#1}}
659
  \def{bi}{\mi[mathvariant=bold-italic]{#1}}
660
  \def{dsi}{\mi[mathvariant=double-struck]{#1}}
661
  \def{vec}{\mover[accent=true]{#1}{\rightarrow}}
662

663
  % From amsmath:
664
  \def{to}{\mo[atom=bin]{→}}
665
  \def{lim}{\mo[atom=op, movablelimits=true]{lim}}
666
  \def{gcd}{\mo[atom=op, movablelimits=true]{gcd}}
667
  \def{sup}{\mo[atom=op, movablelimits=true]{sup}}
668
  \def{inf}{\mo[atom=op, movablelimits=true]{inf}}
669
  \def{max}{\mo[atom=op, movablelimits=true]{max}}
670
  \def{min}{\mo[atom=op, movablelimits=true]{min}}
671
  % Those use U+202F NARROW NO-BREAK SPACE in their names
672
  \def{limsup}{\mo[atom=op, movablelimits=true]{lim sup}}
673
  \def{liminf}{\mo[atom=op, movablelimits=true]{lim inf}}
674
  \def{projlim}{\mo[atom=op, movablelimits=true]{proj lim}}
675
  \def{injlim}{\mo[atom=op, movablelimits=true]{inj lim}}
676

677
  % Other pre-defined operators from the TeXbook, p. 162:
678
  % TeX of course defines them with \mathop, so we use atom=op here.
679
  % MathML would use a <mi> here.
680
  % But we use a <mo> so the atom type is handled
681
  \def{arccos}{\mo[atom=op]{arccos}}
682
  \def{arcsin}{\mo[atom=op]{arcsin}}
683
  \def{arctan}{\mo[atom=op]{arctan}}
684
  \def{arg}{\mo[atom=op]{arg}}
685
  \def{cos}{\mo[atom=op]{cos}}
686
  \def{cosh}{\mo[atom=op]{cosh}}
687
  \def{cot}{\mo[atom=op]{cot}}
688
  \def{coth}{\mo[atom=op]{coth}}
689
  \def{csc}{\mo[atom=op]{csc}}
690
  \def{deg}{\mo[atom=op]{deg}}
691
  \def{det}{\mo[atom=op]{det}}
692
  \def{dim}{\mo[atom=op]{dim}}
693
  \def{exp}{\mo[atom=op]{exp}}
694
  \def{hom}{\mo[atom=op]{hom}}
695
  \def{ker}{\mo[atom=op]{ker}}
696
  \def{lg}{\mo[atom=op]{lg}}
697
  \def{ln}{\mo[atom=op]{ln}}
698
  \def{log}{\mo[atom=op]{log}}
699
  \def{Pr}{\mo[atom=op]{Pr}}
700
  \def{sec}{\mo[atom=op]{sec}}
701
  \def{sin}{\mo[atom=op]{sin}}
702
  \def{sinh}{\mo[atom=op]{sinh}}
703
  \def{tan}{\mo[atom=op]{tan}}
704
  \def{tanh}{\mo[atom=op]{tanh}}
705

706
  % Standard spaces gleaned from plain TeX
707
  \def{thinspace}{\mspace[width=thin]}
708
  \def{negthinspace}{\mspace[width=-thin]}
709
  \def{,}{\thinspace}
710
  \def{!}{\negthinspace}
711
  \def{medspace}{\mspace[width=med]}
712
  \def{negmedspace}{\mspace[width=-med]}
713
  \def{>}{\medspace}
714
  \def{thickspace}{\mspace[width=thick]}
715
  \def{negthickspace}{\mspace[width=-thick]}
716
  \def{;}{\thickspace}
717
  \def{enspace}{\mspace[width=1en]}
718
  \def{enskip}{\enspace}
719
  \def{quad}{\mspace[width=1em]}
720
  \def{qquad}{\mspace[width=2em]}
721

722
  % MathML says a single-character identifier must be in italic by default.
723
  % TeX however has the following Greek capital macros rendered in upright shape.
724
  % It so common that you've probably never seen Γ(x) written with an italic gamma.
725
  \def{Gamma}{\mi[mathvariant=normal]{Γ}}
726
  \def{Delta}{\mi[mathvariant=normal]{Δ}}
727
  \def{Theta}{\mi[mathvariant=normal]{Θ}}
728
  \def{Lambda}{\mi[mathvariant=normal]{Λ}}
729
  \def{Xi}{\mi[mathvariant=normal]{Ξ}}
730
  \def{Pi}{\mi[mathvariant=normal]{Π}}
731
  \def{Sigma}{\mi[mathvariant=normal]{Σ}}
732
  \def{Upsilon}{\mi[mathvariant=normal]{Υ}}
733
  \def{Phi}{\mi[mathvariant=normal]{Φ}}
734
  \def{Psi}{\mi[mathvariant=normal]{Ψ}}
735
  \def{Omega}{\mi[mathvariant=normal]{Ω}}
736
  % Some calligraphic (script), fraktur, double-struck styles:
737
  % Convenience for compatibility with LaTeX.
738
  \def{mathcal}{\mi[mathvariant=script]{#1}}
739
  \def{mathfrak}{\mi[mathvariant=fraktur]{#1}}
740
  \def{mathbb}{\mi[mathvariant=double-struck]{#1}}
741
  % Some style-switching commands for compatibility with LaTeX math.
742
  % Caveat emptor: LaTeX would allow these to apply to a whole formula.
743
  % We can't do that in MathML, as mathvariant applies to token elements only.
744
  % Also note that LaTeX and related packages may have many more such commands.
745
  % We only provide a few common ('historical') ones here.
746
  \def{mathrm}{\mi[mathvariant=normal]{#1}}
747
  \def{mathbf}{\mi[mathvariant=bold]{#1}}
748
  \def{mathit}{\mi[mathvariant=italic]{#1}}
749
  \def{mathsf}{\mi[mathvariant=sans-serif]{#1}}
750
  \def{mathtt}{\mi[mathvariant=monospace]{#1}}
751

752
  % Modulus operator forms
753
  % See Michael Downes & Barbara Beeton, "Short Math Guide for LaTeX"
754
  % American Mathematical Society (v2.0, 2017), §7.1 p. 18
755
  \def{bmod}{\mo[atom=bin]{mod}}
756
  \def{pmod}{\quad(\mo[atom=ord]{mod}\>#1)}
757
  \def{mod}{\quad \mo[atom=ord]{mod}\>#1}
758
  \def{pod}{\quad(#1)}
759

760
  % Phantom commands from TeX/LaTeX
761
  \def{phantom}{\mphantom{#1}}
762
  \def{hphantom}{\mpadded[height=0, depth=0]{\mphantom{#1}}}
763
  \def{vphantom}{\mpadded[width=0]{\mphantom{#1}}}
764
]==],
765
   })
766
)
767

768
return { convertTexlike, compileToMathML }
1✔
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