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

sile-typesetter / sile / 11826759401

13 Nov 2024 10:16PM UTC coverage: 61.013% (-2.4%) from 63.4%
11826759401

push

github

web-flow
Merge pull request #2165 from Omikhleia/feat-bevelled-fractions

feat(math): Support MathML bevelled fractions

7 of 38 new or added lines in 2 files covered. (18.42%)

581 existing lines in 28 files now uncovered.

11155 of 18283 relevant lines covered (61.01%)

2604.98 hits per line

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

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

4
local epnf = require("epnf")
10✔
5
local lpeg = require("lpeg")
10✔
6

7
local atomType = syms.atomType
10✔
8
local symbolDefaults = syms.symbolDefaults
10✔
9
local symbols = syms.symbols
10✔
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
10✔
17
   local eol = S"\r\n"
10✔
18
   local digit = R("09")
10✔
19
   local natural = digit^1 / tostring
10✔
20
   local pos_natural = R("19") * digit^0 / tonumber
10✔
21
   local ctrl_word = R("AZ", "az")^1
10✔
22
   local ctrl_symbol = P(1) - S"{}\\"
10✔
23
   local ctrl_sequence_name = C(ctrl_word + ctrl_symbol) / 1
10✔
24
   local comment = (
25
         P"%" *
10✔
26
         P(1-eol)^0 *
10✔
27
         eol^-1
10✔
28
      )
29
   local utf8cont = R("\128\191")
10✔
30
   local utf8code = lpeg.R("\0\127")
10✔
31
      + lpeg.R("\194\223") * utf8cont
10✔
32
      + lpeg.R("\224\239") * utf8cont * utf8cont
10✔
33
      + lpeg.R("\240\244") * utf8cont * utf8cont * utf8cont
10✔
34
   -- Identifiers inside \mo and \mi tags
35
   local sileID = C(bits.identifier + P(1)) / 1
10✔
36
   local mathMLID = (utf8code - S"\\{}%")^1 / function (...)
10✔
37
         local ret = ""
×
38
         local t = {...}
×
39
         for _,b in ipairs(t) do
×
40
         ret = ret .. b
×
41
         end
42
         return ret
×
43
      end
44
   local group = P"{" * V"mathlist" * (P"}" + E("`}` expected"))
20✔
45
   -- Simple amsmath-like \text command (no embedded math)
46
   local textgroup = P"{" * C((1-P"}")^1) * (P"}" + E("`}` expected"))
20✔
47
   local element_no_infix =
48
      V"def" +
10✔
49
      V"text" + -- Important: before command
10✔
50
      V"command" +
10✔
51
      group +
10✔
52
      V"argument" +
10✔
53
      V"atom"
10✔
54
   local element =
55
      V"supsub" +
10✔
56
      V"subsup" +
10✔
57
      V"sup" +
10✔
58
      V"sub" +
10✔
59
      element_no_infix
10✔
60
   local sep = S",;" * _
10✔
61
   local quotedString = (P'"' * C((1-P'"')^1) * P'"')
10✔
62
   local value = ( quotedString + (1-S",;]")^1 )
10✔
63
   local pair = Cg(sileID * _ * "=" * _ * C(value)) * sep^-1 / function (...)
10✔
64
      local t = {...}; return t[1], t[#t]
469✔
65
   end
66
   local list = Cf(Ct"" * pair^0, rawset)
10✔
67
   local parameters = (
68
         P"[" *
10✔
69
         list *
10✔
70
         P"]"
10✔
71
      )^-1 / function (a)
10✔
72
            return type(a)=="table" and a or {}
802✔
73
         end
74
   local dim2_arg_inner = Ct(V"mathlist" * (P"&" * V"mathlist")^0) /
10✔
75
      function (t)
76
         t.id = "mathlist"
×
77
         return t
×
78
      end
79
   local dim2_arg =
80
      Cg(P"{" *
20✔
81
         dim2_arg_inner *
10✔
82
         (P"\\\\" * dim2_arg_inner)^1 *
10✔
83
         (P"}" + E("`}` expected"))
20✔
84
         ) / function (...)
×
85
            local t = {...}
×
86
            -- Remove the last mathlist if empty. This way,
87
            -- `inner1 \\ inner2 \\` is the same as `inner1 \\ inner2`.
88
            if not t[#t][1] or not t[#t][1][1] then table.remove(t) end
×
89
            return pl.utils.unpack(t)
×
90
         end
91

92
   local dim2_arg_inner = Ct(V"mathlist" * (P"&" * V"mathlist")^0) /
10✔
93
      function (t)
94
         t.id = "mathlist"
16✔
95
         return t
16✔
96
      end
97
   local dim2_arg =
98
      Cg(P"{" *
20✔
99
         dim2_arg_inner *
10✔
100
         (P"\\\\" * dim2_arg_inner)^1 *
10✔
101
         (P"}" + E("`}` expected"))
20✔
102
         ) / function (...)
×
103
         local t = {...}
4✔
104
         -- Remove the last mathlist if empty. This way,
105
         -- `inner1 \\ inner2 \\` is the same as `inner1 \\ inner2`.
106
         if not t[#t][1] or not t[#t][1][1] then table.remove(t) end
4✔
107
         return pl.utils.unpack(t)
4✔
108
         end
109

110
   START "math"
10✔
111
   math = V"mathlist" * EOF"Unexpected character at end of math code"
20✔
112
   mathlist = (comment + (WS * _) + element)^0
10✔
113
   supsub = element_no_infix * _ * P"^" * _ * element_no_infix * _ *
10✔
114
      P"_" * _ * element_no_infix
10✔
115
   subsup = element_no_infix * _ * P"_" * _ * element_no_infix * _ *
10✔
116
      P"^" * _ * element_no_infix
10✔
117
   sup = element_no_infix * _ * P"^" * _ * element_no_infix
10✔
118
   sub = element_no_infix * _ * P"_" * _ * element_no_infix
10✔
119
   atom = natural + C(utf8code - S"\\{}%^_&") +
10✔
120
      (P"\\{" + P"\\}") / function (s) return string.sub(s, -1) end
10✔
121
   text = (
×
122
         P"\\text" *
10✔
123
         Cg(parameters, "options") *
10✔
124
         textgroup
10✔
125
      )
10✔
126
   command = (
×
127
         P"\\" *
10✔
128
         Cg(ctrl_sequence_name, "command") *
10✔
129
         Cg(parameters, "options") *
10✔
130
         (dim2_arg + group^0)
10✔
131
      )
10✔
132
   def = P"\\def" * _ * P"{" *
10✔
133
      Cg(ctrl_sequence_name, "command-name") * P"}" * _ *
10✔
134
      --P"[" * Cg(digit^1, "arity") * P"]" * _ *
135
      P"{" * V"mathlist" * P"}"
10✔
136
   argument = P"#" * Cg(pos_natural, "index")
10✔
137
end
138
-- luacheck: pop
139
-- stylua: ignore end
140
---@diagnostic enable: undefined-global, unused-local, lowercase-global
141

142
local mathParser = epnf.define(mathGrammar)
10✔
143

144
local commands = {}
10✔
145

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

151
local objType = {
10✔
152
   tree = 1,
153
   str = 2,
154
}
155

156
local function inferArgTypes_aux (accumulator, typeRequired, body)
157
   if type(body) == "table" then
2,390✔
158
      if body.id == "argument" then
2,390✔
159
         local ret = accumulator
176✔
160
         table.insert(ret, body.index, typeRequired)
176✔
161
         return ret
176✔
162
      elseif body.id == "command" then
2,214✔
163
         if commands[body.command] then
568✔
164
            local cmdArgTypes = commands[body.command][1]
407✔
165
            if #cmdArgTypes ~= #body then
407✔
166
               SU.error(
×
167
                  "Wrong number of arguments ("
168
                     .. #body
×
169
                     .. ") for command "
×
170
                     .. body.command
×
171
                     .. " (should be "
×
172
                     .. #cmdArgTypes
×
173
                     .. ")"
×
174
               )
175
            else
176
               for i = 1, #cmdArgTypes do
754✔
177
                  accumulator = inferArgTypes_aux(accumulator, cmdArgTypes[i], body[i])
694✔
178
               end
179
            end
180
            return accumulator
407✔
181
         elseif body.command == "mi" or body.command == "mo" or body.command == "mn" then
161✔
182
            if #body ~= 1 then
×
183
               SU.error("Wrong number of arguments (" .. #body .. ") for command " .. body.command .. " (should be 1)")
×
184
            end
185
            accumulator = inferArgTypes_aux(accumulator, objType.str, body[1])
×
186
            return accumulator
×
187
         else
188
            -- Not a macro, recurse on children assuming tree type for all
189
            -- arguments
190
            for _, child in ipairs(body) do
243✔
191
               accumulator = inferArgTypes_aux(accumulator, objType.tree, child)
164✔
192
            end
193
            return accumulator
161✔
194
         end
195
      elseif body.id == "atom" then
1,646✔
196
         return accumulator
678✔
197
      else
198
         -- Simply recurse on children
199
         for _, child in ipairs(body) do
2,392✔
200
            accumulator = inferArgTypes_aux(accumulator, typeRequired, child)
2,848✔
201
         end
202
         return accumulator
968✔
203
      end
204
   else
205
      SU.error("invalid argument to inferArgTypes_aux")
×
206
   end
207
end
208

209
local inferArgTypes = function (body)
210
   return inferArgTypes_aux({}, objType.tree, body)
537✔
211
end
212

213
local function registerCommand (name, argTypes, func)
214
   commands[name] = { argTypes, func }
577✔
215
end
216

217
-- Computes func(func(... func(init, k1, v1), k2, v2)..., k_n, v_n), i.e. applies
218
-- func on every key-value pair in the table. Keys with numeric indices are
219
-- processed in order. This is an important property for MathML compilation below.
220
local function fold_pairs (func, table)
221
   local accumulator = {}
1,683✔
222
   for k, v in pl.utils.kpairs(table) do
14,117✔
223
      accumulator = func(v, k, accumulator)
9,068✔
224
   end
225
   for i, v in ipairs(table) do
4,207✔
226
      accumulator = func(v, i, accumulator)
5,048✔
227
   end
228
   return accumulator
1,683✔
229
end
230

231
local function forall (pred, list)
232
   for _, x in ipairs(list) do
51✔
233
      if not pred(x) then
82✔
234
         return false
40✔
235
      end
236
   end
237
   return true
10✔
238
end
239

240
local compileToStr = function (argEnv, mathlist)
241
   if #mathlist == 1 and mathlist.id == "atom" then
113✔
242
      -- List is a single atom
243
      return mathlist[1]
×
244
   elseif #mathlist == 1 and mathlist[1].id == "argument" then
113✔
245
      return argEnv[mathlist[1].index]
3✔
246
   elseif mathlist.id == "argument" then
110✔
247
      return argEnv[mathlist.index]
×
248
   else
249
      local ret = ""
110✔
250
      for _, elt in ipairs(mathlist) do
435✔
251
         if elt.id == "atom" then
325✔
252
            ret = ret .. elt[1]
325✔
253
         elseif elt.id == "command" and symbols[elt.command] then
×
254
            ret = ret .. symbols[elt.command]
×
255
         else
256
            SU.error("Encountered non-character token in command that takes a string")
×
257
         end
258
      end
259
      return ret
110✔
260
   end
261
end
262

263
local function isOperatorKind (tree, typeOfAtom, typeOfSymbol)
264
   if not tree then
1,026✔
265
      return false -- safeguard
×
266
   end
267
   if tree.command ~= "mo" then
1,026✔
268
      return false
742✔
269
   end
270
   -- Case \mo[atom=big]{ops}
271
   -- E.g. \mo[atom=big]{lim}
272
   if tree.options and tree.options.atom == typeOfAtom then
284✔
273
      return true
4✔
274
   end
275
   -- Case \mo{ops} where ops is registered with the resquested type
276
   -- E.g. \mo{∑) or \sum
277
   if tree[1] and symbolDefaults[tree[1]] and symbolDefaults[tree[1]].atom == typeOfSymbol then
280✔
278
      return true
38✔
279
   end
280
   return false
242✔
281
end
282

283
local function isMoveableLimits (tree)
284
   if tree.command ~= "mo" then
59✔
285
      return false
44✔
286
   end
287
   if tree.options and SU.boolean(tree.options.movablelimits, false) then
30✔
UNCOV
288
      return true
×
289
   end
290
   if tree[1] and symbolDefaults[tree[1]] and SU.boolean(symbolDefaults[tree[1]].movablelimits, false) then
30✔
291
      return true
9✔
292
   end
293
   return false
6✔
294
end
295
local function isCloseOperator (tree)
296
   return isOperatorKind(tree, "close", atomType.closeSymbol)
521✔
297
end
298
local function isOpeningOperator (tree)
299
   return isOperatorKind(tree, "open", atomType.openingSymbol)
505✔
300
end
301

302
local function isAccentSymbol (symbol)
303
   return symbolDefaults[symbol] and symbolDefaults[symbol].atom == atomType.accentSymbol
46✔
304
end
305

306
local function compileToMathML_aux (_, arg_env, tree)
307
   if type(tree) == "string" then
2,060✔
308
      return tree
377✔
309
   end
310
   local function compile_and_insert (child, key, accumulator)
311
      if type(key) ~= "number" then
7,058✔
312
         accumulator[key] = child
4,534✔
313
         return accumulator
4,534✔
314
      -- Compile all children, except if this node is a macro definition (no
315
      -- evaluation "under lambda") or the application of a registered macro
316
      -- (since evaluating the nodes depends on the macro's signature, it is more
317
      -- complex and done below)..
318
      elseif tree.id == "def" or (tree.id == "command" and commands[tree.command]) then
2,524✔
319
         -- Conserve unevaluated child
320
         table.insert(accumulator, child)
687✔
321
      else
322
         -- Compile next child
323
         local comp = compileToMathML_aux(nil, arg_env, child)
1,837✔
324
         if comp then
1,837✔
325
            if comp.id == "wrapper" then
1,300✔
326
               -- Insert all children of the wrapper node
327
               for _, inner_child in ipairs(comp) do
181✔
328
                  table.insert(accumulator, inner_child)
91✔
329
               end
330
            else
331
               table.insert(accumulator, comp)
1,210✔
332
            end
333
         end
334
      end
335
      return accumulator
2,524✔
336
   end
337
   tree = fold_pairs(compile_and_insert, tree)
3,366✔
338
   if tree.id == "math" then
1,683✔
339
      tree.command = "math"
50✔
340
      -- If the outermost `mrow` contains only other `mrow`s, remove it
341
      -- (allowing vertical stacking).
342
      if forall(function (c)
100✔
343
         return c.command == "mrow"
41✔
344
      end, tree[1]) then
100✔
345
         tree[1].command = "math"
10✔
346
         return tree[1]
10✔
347
      end
348
   elseif tree.id == "mathlist" then
1,633✔
349
      -- Turn mathlist into `mrow` except if it has exactly one `mtr` or `mtd`
350
      -- child.
351
      -- Note that `def`s have already been compiled away at this point.
352
      if #tree == 1 then
297✔
353
         if tree[1].command == "mtr" or tree[1].command == "mtd" then
191✔
UNCOV
354
            return tree[1]
×
355
         else
356
            tree.command = "mrow"
191✔
357
         end
358
      else
359
         -- Re-wrap content from opening to closing operator in an implicit mrow,
360
         -- so stretchy operators apply to the correct span of content.
361
         local children = {}
106✔
362
         local stack = {}
106✔
363
         for _, child in ipairs(tree) do
611✔
364
            if isOpeningOperator(child) then
1,010✔
365
               table.insert(stack, children)
22✔
366
               local mrow = {
22✔
367
                  command = "mrow",
368
                  options = {},
22✔
369
                  child,
22✔
370
               }
371
               table.insert(children, mrow)
22✔
372
               children = mrow
22✔
373
            elseif isCloseOperator(child) then
966✔
374
               table.insert(children, child)
20✔
375
               if #stack > 0 then
20✔
376
                  children = table.remove(stack)
40✔
377
               end
UNCOV
378
            elseif
×
379
               (child.command == "msubsup" or child.command == "msub" or child.command == "msup")
463✔
380
               and isCloseOperator(child[1]) -- child[1] is the base
76✔
381
            then
382
               if #stack > 0 then
×
383
                  -- Special case for closing operator with sub/superscript:
384
                  -- (....)^i must be interpreted as {(....)}^i, not as (...{)}^i
385
                  -- Push the closing operator into the mrow
UNCOV
386
                  table.insert(children, child[1])
×
387
                  -- Move the mrow into the msubsup, replacing the closing operator
UNCOV
388
                  child[1] = children
×
389
                  -- And insert the msubsup into the parent
UNCOV
390
                  children = table.remove(stack)
×
UNCOV
391
                  children[#children] = child
×
392
               else
UNCOV
393
                  table.insert(children, child)
×
394
               end
395
            else
396
               table.insert(children, child)
463✔
397
            end
398
         end
399
         tree = #stack > 0 and stack[1] or children
106✔
400
         tree.command = "mrow"
106✔
401
      end
402
   elseif tree.id == "atom" then
1,336✔
403
      local codepoints = {}
377✔
404
      for _, cp in luautf8.codes(tree[1]) do
766✔
405
         table.insert(codepoints, cp)
389✔
406
      end
407
      local cp = codepoints[1]
377✔
408
      if
409
         #codepoints == 1
377✔
UNCOV
410
         and ( -- If length of UTF-8 string is 1
×
411
            cp >= SU.codepoint("A") and cp <= SU.codepoint("Z")
887✔
412
            or cp >= SU.codepoint("a") and cp <= SU.codepoint("z")
869✔
413
            or cp >= SU.codepoint("Α") and cp <= SU.codepoint("Ω")
575✔
414
            or cp >= SU.codepoint("α") and cp <= SU.codepoint("ω")
575✔
415
            or cp == SU.codepoint("ϑ")
496✔
416
            or cp == SU.codepoint("ϕ")
496✔
417
            or cp == SU.codepoint("ϰ")
496✔
418
            or cp == SU.codepoint("ϱ")
496✔
419
            or cp == SU.codepoint("ϖ")
496✔
420
            or cp == SU.codepoint("ϵ")
496✔
421
         )
422
      then
423
         tree.command = "mi"
117✔
424
      elseif lpeg.match(lpeg.R("09") ^ 1, tree[1]) then
260✔
425
         tree.command = "mn"
104✔
426
      else
427
         tree.command = "mo"
156✔
428
      end
429
      tree.options = {}
377✔
430
   -- Translate TeX-like sub/superscripts to `munderover` or `msubsup`,
431
   -- depending on whether the base is a big operator
432
   elseif tree.id == "sup" and isMoveableLimits(tree[1]) then
971✔
UNCOV
433
      tree.command = "mover"
×
434
   elseif tree.id == "sub" and isMoveableLimits(tree[1]) then
992✔
435
      tree.command = "munder"
1✔
436
   elseif tree.id == "subsup" and isMoveableLimits(tree[1]) then
972✔
437
      tree.command = "munderover"
8✔
438
   elseif tree.id == "supsub" and isMoveableLimits(tree[1]) then
950✔
UNCOV
439
      tree.command = "munderover"
×
UNCOV
440
      local tmp = tree[2]
×
UNCOV
441
      tree[2] = tree[3]
×
UNCOV
442
      tree[3] = tmp
×
443
   elseif tree.id == "sup" then
950✔
444
      tree.command = "msup"
12✔
445
   elseif tree.id == "sub" then
938✔
446
      tree.command = "msub"
32✔
447
   elseif tree.id == "subsup" then
906✔
448
      tree.command = "msubsup"
6✔
449
   elseif tree.id == "supsub" then
900✔
UNCOV
450
      tree.command = "msubsup"
×
UNCOV
451
      local tmp = tree[2]
×
UNCOV
452
      tree[2] = tree[3]
×
UNCOV
453
      tree[3] = tmp
×
454
   elseif tree.id == "def" then
900✔
455
      local commandName = tree["command-name"]
537✔
456
      local argTypes = inferArgTypes(tree[1])
537✔
457
      registerCommand(commandName, argTypes, function (compiledArgs)
1,074✔
458
         return compileToMathML_aux(nil, compiledArgs, tree[1])
90✔
459
      end)
460
      return nil
537✔
461
   elseif tree.id == "text" then
363✔
UNCOV
462
      tree.command = "mtext"
×
463
   elseif tree.id == "command" and commands[tree.command] then
363✔
464
      local argTypes = commands[tree.command][1]
201✔
465
      local cmdFun = commands[tree.command][2]
201✔
466
      local applicationTree = tree
201✔
467
      local cmdName = tree.command
201✔
468
      if #applicationTree ~= #argTypes then
201✔
UNCOV
469
         SU.error(
×
470
            "Wrong number of arguments ("
UNCOV
471
               .. #applicationTree
×
UNCOV
472
               .. ") for command "
×
UNCOV
473
               .. cmdName
×
UNCOV
474
               .. " (should be "
×
UNCOV
475
               .. #argTypes
×
UNCOV
476
               .. ")"
×
477
         )
478
      end
479
      -- Compile every argument
480
      local compiledArgs = {}
201✔
481
      for i, arg in pairs(applicationTree) do
1,155✔
482
         if type(i) == "number" then
954✔
483
            if argTypes[i] == objType.tree then
150✔
484
               table.insert(compiledArgs, compileToMathML_aux(nil, arg_env, arg))
74✔
485
            else
486
               local x = compileToStr(arg_env, arg)
113✔
487
               table.insert(compiledArgs, x)
113✔
488
            end
489
         else
490
            -- Not an argument but an attribute. Add it to the compiled
491
            -- argument tree as-is
492
            compiledArgs[i] = applicationTree[i]
804✔
493
         end
494
      end
495
      local res = cmdFun(compiledArgs)
201✔
496
      if res.command == "mrow" then
201✔
497
         -- Mark the outer mrow to be unwrapped in the parent
498
         res.id = "wrapper"
90✔
499
      end
500
      return res
201✔
501
   elseif tree.id == "command" and symbols[tree.command] then
162✔
502
      local atom = { id = "atom", [1] = symbols[tree.command] }
46✔
503
      if isAccentSymbol(symbols[tree.command]) and #tree > 0 then
92✔
504
         -- LaTeX-style accents \vec{v} = <mover accent="true"><mi>v</mi><mo>→</mo></mover>
505
         local accent = {
×
506
            id = "command",
507
            command = "mover",
508
            options = {
×
509
               accent = "true",
510
            },
511
         }
512
         accent[1] = compileToMathML_aux(nil, arg_env, tree[1])
×
513
         accent[2] = compileToMathML_aux(nil, arg_env, atom)
×
514
         tree = accent
×
515
      elseif #tree > 0 then
46✔
516
         -- Play cool with LaTeX-style commands that don't take arguments:
517
         -- Edge case for non-accent symbols so we don't loose bracketed groups
518
         -- that might have been seen as command arguments.
519
         -- Ex. \langle{x}\rangle (without space after \langle)
UNCOV
520
         local sym = compileToMathML_aux(nil, arg_env, atom)
×
521
         -- Compile all children in-place
522
         for i, child in ipairs(tree) do
×
UNCOV
523
            tree[i] = compileToMathML_aux(nil, arg_env, child)
×
524
         end
525
         -- Insert symbol at the beginning,
526
         -- And add a wrapper mrow to be unwrapped in the parent.
UNCOV
527
         table.insert(tree, 1, sym)
×
UNCOV
528
         tree.command = "mrow"
×
UNCOV
529
         tree.id = "wrapper"
×
530
      else
531
         tree = compileToMathML_aux(nil, arg_env, atom)
92✔
532
      end
533
   elseif tree.id == "argument" then
116✔
534
      if arg_env[tree.index] then
37✔
535
         return arg_env[tree.index]
37✔
536
      else
537
         SU.error("Argument #" .. tree.index .. " has escaped its scope (probably not fully applied command).")
×
538
      end
539
   end
540
   tree.id = nil
898✔
541
   return tree
898✔
542
end
543

544
local function printMathML (tree)
545
   if type(tree) == "string" then
×
546
      return tree
×
547
   end
548
   local result = "\\" .. tree.command
×
UNCOV
549
   if tree.options then
×
550
      local options = {}
×
UNCOV
551
      for k, v in pairs(tree.options) do
×
UNCOV
552
         table.insert(options, k .. "=" .. tostring(v))
×
553
      end
UNCOV
554
      if #options > 0 then
×
UNCOV
555
         result = result .. "[" .. table.concat(options, ", ") .. "]"
×
556
      end
557
   end
UNCOV
558
   if #tree > 0 then
×
UNCOV
559
      result = result .. "{"
×
UNCOV
560
      for _, child in ipairs(tree) do
×
UNCOV
561
         result = result .. printMathML(child)
×
562
      end
UNCOV
563
      result = result .. "}"
×
564
   end
UNCOV
565
   return result
×
566
end
567

568
local function compileToMathML (_, arg_env, tree)
569
   local result = compileToMathML_aux(_, arg_env, tree)
50✔
570
   SU.debug("texmath", function ()
100✔
UNCOV
571
      return "Resulting MathML: " .. printMathML(result)
×
572
   end)
573
   return result
50✔
574
end
575

576
local function convertTexlike (_, content)
577
   local ret = epnf.parsestring(mathParser, content[1])
50✔
578
   SU.debug("texmath", function ()
100✔
UNCOV
579
      return "Parsed TeX math: " .. pl.pretty.write(ret)
×
580
   end)
581
   return ret
50✔
582
end
583

584
registerCommand("%", {}, function ()
20✔
585
   return { "%", command = "mo", options = {} }
1✔
586
end)
587
registerCommand("mi", { [1] = objType.str }, function (x)
20✔
588
   return x
90✔
589
end)
590
registerCommand("mo", { [1] = objType.str }, function (x)
20✔
591
   return x
19✔
592
end)
593
registerCommand("mn", { [1] = objType.str }, function (x)
20✔
594
   return x
1✔
595
end)
596

597
compileToMathML(
20✔
598
   nil,
10✔
599
   {},
600
   convertTexlike(nil, {
10✔
UNCOV
601
      [==[
×
602
  \def{frac}{\mfrac{#1}{#2}}
603
  \def{sqrt}{\msqrt{#1}}
604
  \def{bi}{\mi[mathvariant=bold-italic]{#1}}
605
  \def{dsi}{\mi[mathvariant=double-struck]{#1}}
606

607
  \def{lim}{\mo[movablelimits=true]{lim}}
608

609
  % From amsmath:
610
  \def{to}{\mo[atom=bin]{→}}
611
  \def{gcd}{\mo[movablelimits=true]{gcd}}
612
  \def{sup}{\mo[movablelimits=true]{sup}}
613
  \def{inf}{\mo[movablelimits=true]{inf}}
614
  \def{max}{\mo[movablelimits=true]{max}}
615
  \def{min}{\mo[movablelimits=true]{min}}
616
  % Those use U+202F NARROW NO-BREAK SPACE in their names
617
  \def{limsup}{\mo[movablelimits=true]{lim sup}}
618
  \def{liminf}{\mo[movablelimits=true]{lim inf}}
619
  \def{projlim}{\mo[movablelimits=true]{proj lim}}
620
  \def{injlim}{\mo[movablelimits=true]{inj lim}}
621

622
  % Standard spaces gleaned from plain TeX
623
  \def{thinspace}{\mspace[width=thin]}
624
  \def{negthinspace}{\mspace[width=-thin]}
625
  \def{,}{\thinspace}
626
  \def{!}{\negthinspace}
627
  \def{medspace}{\mspace[width=med]}
628
  \def{negmedspace}{\mspace[width=-med]}
629
  \def{>}{\medspace}
630
  \def{thickspace}{\mspace[width=thick]}
631
  \def{negthickspace}{\mspace[width=-thick]}
632
  \def{;}{\thickspace}
633
  \def{enspace}{\mspace[width=1en]}
634
  \def{enskip}{\enspace}
635
  \def{quad}{\mspace[width=1em]}
636
  \def{qquad}{\mspace[width=2em]}
637

638
  % MathML says a single-character identifier must be in italic by default.
639
  % TeX however has the following Greek capital macros rendered in upright shape.
640
  % It so common that you've probably never seen Γ(x) written with an italic gamma.
641
  \def{Gamma}{\mi[mathvariant=normal]{Γ}}
642
  \def{Delta}{\mi[mathvariant=normal]{Δ}}
643
  \def{Theta}{\mi[mathvariant=normal]{Θ}}
644
  \def{Lambda}{\mi[mathvariant=normal]{Λ}}
645
  \def{Xi}{\mi[mathvariant=normal]{Ξ}}
646
  \def{Pi}{\mi[mathvariant=normal]{Π}}
647
  \def{Sigma}{\mi[mathvariant=normal]{Σ}}
648
  \def{Upsilon}{\mi[mathvariant=normal]{Υ}}
649
  \def{Phi}{\mi[mathvariant=normal]{Φ}}
650
  \def{Psi}{\mi[mathvariant=normal]{Ψ}}
651
  \def{Omega}{\mi[mathvariant=normal]{Ω}}
652
  % Some calligraphic (script), fraktur, double-struck styles:
653
  % Convenience for compatibility with LaTeX.
654
  \def{mathcal}{\mi[mathvariant=script]{#1}}
655
  \def{mathfrak}{\mi[mathvariant=fraktur]{#1}}
656
  \def{mathbb}{\mi[mathvariant=double-struck]{#1}}
657
  % Some style-switching commands for compatibility with LaTeX math.
658
  % Caveat emptor: LaTeX would allow these to apply to a whole formula.
659
  % We can't do that in MathML, as mathvariant applies to token elements only.
660
  % Also note that LaTeX and related packages may have many more such commands.
661
  % We only provide a few common ('historical') ones here.
662
  \def{mathrm}{\mi[mathvariant=normal]{#1}}
663
  \def{mathbf}{\mi[mathvariant=bold]{#1}}
664
  \def{mathit}{\mi[mathvariant=italic]{#1}}
665
  \def{mathsf}{\mi[mathvariant=sans-serif]{#1}}
666
  \def{mathtt}{\mi[mathvariant=monospace]{#1}}
667

668
  % Modulus operator forms
669
  \def{bmod}{\mo{mod}}
670
  \def{pmod}{\quad(\mo{mod} #1)}
671

672
  % Phantom commands from TeX/LaTeX
673
  \def{phantom}{\mphantom{#1}}
674
  \def{hphantom}{\mpadded[height=0, depth=0]{\mphantom{#1}}}
675
  \def{vphantom}{\mpadded[width=0]{\mphantom{#1}}}
676
  %\mphantom[special=v]{#1}}}
677
]==],
678
   })
679
)
680

681
return { convertTexlike, compileToMathML }
10✔
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