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

sile-typesetter / sile / 11986197070

23 Nov 2024 09:55AM UTC coverage: 60.594% (+25.5%) from 35.106%
11986197070

push

github

web-flow
Merge pull request #2174 from Omikhleia/fix-math-primes-asterisk

Fix math primes and asterisk

28 of 29 new or added lines in 3 files covered. (96.55%)

322 existing lines in 23 files now uncovered.

11105 of 18327 relevant lines covered (60.59%)

2636.66 hits per line

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

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

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

7
local atomType = syms.atomType
11✔
8
local symbolDefaults = syms.symbolDefaults
11✔
9
local symbols = syms.symbols
11✔
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
11✔
17
   local eol = S"\r\n"
11✔
18
   local digit = R("09")
11✔
19
   local natural = digit^1 / tostring
11✔
20
   local pos_natural = R("19") * digit^0 / tonumber
11✔
21
   local ctrl_word = R("AZ", "az")^1
11✔
22
   local ctrl_symbol = P(1) - S"{}\\"
11✔
23
   local ctrl_sequence_name = C(ctrl_word + ctrl_symbol) / 1
11✔
24
   local comment = (
25
         P"%" *
11✔
26
         P(1-eol)^0 *
11✔
27
         eol^-1
11✔
28
      )
29
   local utf8cont = R("\128\191")
11✔
30
   local utf8code = lpeg.R("\0\127")
11✔
31
      + lpeg.R("\194\223") * utf8cont
11✔
32
      + lpeg.R("\224\239") * utf8cont * utf8cont
11✔
33
      + lpeg.R("\240\244") * utf8cont * utf8cont * utf8cont
11✔
34
   -- Identifiers inside \mo and \mi tags
35
   local sileID = C(bits.identifier + P(1)) / 1
11✔
36
   local mathMLID = (utf8code - S"\\{}%")^1 / function (...)
11✔
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"))
22✔
45
   -- Simple amsmath-like \text command (no embedded math)
46
   local textgroup = P"{" * C((1-P"}")^1) * (P"}" + E("`}` expected"))
22✔
47
   local element_no_infix =
48
      V"def" +
11✔
49
      V"text" + -- Important: before command
11✔
50
      V"command" +
11✔
51
      group +
11✔
52
      V"argument" +
11✔
53
      V"atom"
11✔
54
   local element =
55
      V"supsub" +
11✔
56
      V"subsup" +
11✔
57
      V"sup" +
11✔
58
      V"sub" +
11✔
59
      element_no_infix
11✔
60
   local sep = S",;" * _
11✔
61
   local quotedString = (P'"' * C((1-P'"')^1) * P'"')
11✔
62
   local value = ( quotedString + (1-S",;]")^1 )
11✔
63
   local pair = Cg(sileID * _ * "=" * _ * C(value)) * sep^-1 / function (...)
11✔
64
      local t = {...}; return t[1], t[#t]
513✔
65
   end
66
   local list = Cf(Ct"" * pair^0, rawset)
11✔
67
   local parameters = (
68
         P"[" *
11✔
69
         list *
11✔
70
         P"]"
11✔
71
      )^-1 / function (a)
11✔
72
            return type(a)=="table" and a or {}
892✔
73
         end
74
   local dim2_arg_inner = Ct(V"mathlist" * (P"&" * V"mathlist")^0) /
11✔
75
      function (t)
76
         t.id = "mathlist"
×
77
         return t
×
78
      end
79
   local dim2_arg =
80
      Cg(P"{" *
22✔
81
         dim2_arg_inner *
11✔
82
         (P"\\\\" * dim2_arg_inner)^1 *
11✔
83
         (P"}" + E("`}` expected"))
22✔
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) /
11✔
93
      function (t)
94
         t.id = "mathlist"
16✔
95
         return t
16✔
96
      end
97
   local dim2_arg =
98
      Cg(P"{" *
22✔
99
         dim2_arg_inner *
11✔
100
         (P"\\\\" * dim2_arg_inner)^1 *
11✔
101
         (P"}" + E("`}` expected"))
22✔
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
   -- TeX uses the regular asterisk (* = U+002A) in superscripts or subscript:
111
   -- The TeXbook exercice 18.32 (p. 179, 330) for instance.
112
   -- Fonts usually have the asterisk raised too high, so using the Unicode
113
   -- asterisk operator U+2217 looks better (= \ast in TeX).
114
   local astop = P"*" / luautf8.char(0x2217)
11✔
115
   -- TeX interprets apostrophes as primes in math mode:
116
   -- The TeXbook p. 130 expands ' to ^\prime commands and repeats the \prime
117
   -- for multiple apostrophes.
118
   -- The TeXbook p. 134: "Then there is the character ', which we know is used
119
   -- as an abbreviation for \prime superscripts."
120
   -- (So we are really sure superscript primes are really the intended meaning.)
121
   -- Here we use the Unicode characters for primes, but the intent is the same.
122
   local primes = (
123
         P"''''" / luautf8.char(0x2057) + -- quadruple prime
11✔
124
         P"'''" / luautf8.char(0x2034) + -- triple prime
11✔
125
         P"''" / luautf8.char(0x2033) + -- double prime
11✔
126
         P"'" / luautf8.char(0x2032) -- prime
11✔
NEW
127
      ) / function (s)
×
128
            return { id="atom", s }
26✔
129
         end
130
   local primes_sup = (
131
         primes * _ * P"^" * _ * element_no_infix / function (p, e)
11✔
132
            -- Combine the prime with the superscript in the same mathlist
133
            if e.id == "mathlist" then
12✔
134
               table.insert(e, 1, p)
2✔
135
               return e
2✔
136
            end
137
            return { id="mathlist", p, e }
10✔
138
         end
139
         + primes -- or standalone primes
11✔
140
      )
141

142
   START "math"
11✔
143
   math = V"mathlist" * EOF"Unexpected character at end of math code"
22✔
144
   mathlist = (comment + (WS * _) + element)^0
11✔
145
   supsub = element_no_infix * _ * primes_sup                  * _ *  P"_" * _ * element_no_infix +
11✔
146
            element_no_infix * _ * P"^" * _ * element_no_infix * _ *  P"_" * _ * element_no_infix
11✔
147
   subsup = element_no_infix * _ * P"_" * _ * element_no_infix * primes_sup +
11✔
148
            element_no_infix * _ * P"_" * _ * element_no_infix * _ * P"^" * _ * element_no_infix
11✔
149
   sup =  element_no_infix * _ * primes_sup +
11✔
150
          element_no_infix * _ * P"^" * _ * element_no_infix
11✔
151
   sub = element_no_infix * _ * P"_" * _ * element_no_infix
11✔
152
   atom = natural + astop + C(utf8code - S"\\{}%^_&'") +
11✔
153
      (P"\\{" + P"\\}") / function (s) return string.sub(s, -1) end
11✔
154
   text = (
×
155
         P"\\text" *
11✔
156
         Cg(parameters, "options") *
11✔
157
         textgroup
11✔
158
      )
11✔
159
   command = (
×
160
         P"\\" *
11✔
161
         Cg(ctrl_sequence_name, "command") *
11✔
162
         Cg(parameters, "options") *
11✔
163
         (dim2_arg + group^0)
11✔
164
      )
11✔
165
   def = P"\\def" * _ * P"{" *
11✔
166
      Cg(ctrl_sequence_name, "command-name") * P"}" * _ *
11✔
167
      --P"[" * Cg(digit^1, "arity") * P"]" * _ *
168
      P"{" * V"mathlist" * P"}"
11✔
169
   argument = P"#" * Cg(pos_natural, "index")
11✔
170
end
171
-- luacheck: pop
172
-- stylua: ignore end
173
---@diagnostic enable: undefined-global, unused-local, lowercase-global
174

175
local mathParser = epnf.define(mathGrammar)
11✔
176

177
local commands = {}
11✔
178

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

184
local objType = {
11✔
185
   tree = 1,
186
   str = 2,
187
}
188

189
local function inferArgTypes_aux (accumulator, typeRequired, body)
190
   if type(body) == "table" then
2,625✔
191
      if body.id == "argument" then
2,625✔
192
         local ret = accumulator
193✔
193
         table.insert(ret, body.index, typeRequired)
193✔
194
         return ret
193✔
195
      elseif body.id == "command" then
2,432✔
196
         if commands[body.command] then
624✔
197
            local cmdArgTypes = commands[body.command][1]
447✔
198
            if #cmdArgTypes ~= #body then
447✔
199
               SU.error(
×
200
                  "Wrong number of arguments ("
201
                     .. #body
×
202
                     .. ") for command "
×
203
                     .. body.command
×
204
                     .. " (should be "
×
205
                     .. #cmdArgTypes
×
206
                     .. ")"
×
207
               )
208
            else
209
               for i = 1, #cmdArgTypes do
828✔
210
                  accumulator = inferArgTypes_aux(accumulator, cmdArgTypes[i], body[i])
762✔
211
               end
212
            end
213
            return accumulator
447✔
214
         elseif body.command == "mi" or body.command == "mo" or body.command == "mn" then
177✔
215
            if #body ~= 1 then
×
216
               SU.error("Wrong number of arguments (" .. #body .. ") for command " .. body.command .. " (should be 1)")
×
217
            end
218
            accumulator = inferArgTypes_aux(accumulator, objType.str, body[1])
×
219
            return accumulator
×
220
         else
221
            -- Not a macro, recurse on children assuming tree type for all
222
            -- arguments
223
            for _, child in ipairs(body) do
267✔
224
               accumulator = inferArgTypes_aux(accumulator, objType.tree, child)
180✔
225
            end
226
            return accumulator
177✔
227
         end
228
      elseif body.id == "atom" then
1,808✔
229
         return accumulator
745✔
230
      else
231
         -- Simply recurse on children
232
         for _, child in ipairs(body) do
2,627✔
233
            accumulator = inferArgTypes_aux(accumulator, typeRequired, child)
3,128✔
234
         end
235
         return accumulator
1,063✔
236
      end
237
   else
238
      SU.error("invalid argument to inferArgTypes_aux")
×
239
   end
240
end
241

242
local inferArgTypes = function (body)
243
   return inferArgTypes_aux({}, objType.tree, body)
590✔
244
end
245

246
local function registerCommand (name, argTypes, func)
247
   commands[name] = { argTypes, func }
634✔
248
end
249

250
-- Computes func(func(... func(init, k1, v1), k2, v2)..., k_n, v_n), i.e. applies
251
-- func on every key-value pair in the table. Keys with numeric indices are
252
-- processed in order. This is an important property for MathML compilation below.
253
local function fold_pairs (func, table)
254
   local accumulator = {}
2,018✔
255
   for k, v in pl.utils.kpairs(table) do
16,702✔
256
      accumulator = func(v, k, accumulator)
10,648✔
257
   end
258
   for i, v in ipairs(table) do
5,021✔
259
      accumulator = func(v, i, accumulator)
6,006✔
260
   end
261
   return accumulator
2,018✔
262
end
263

264
local function forall (pred, list)
265
   for _, x in ipairs(list) do
59✔
266
      if not pred(x) then
96✔
267
         return false
47✔
268
      end
269
   end
270
   return true
11✔
271
end
272

273
local compileToStr = function (argEnv, mathlist)
274
   if #mathlist == 1 and mathlist.id == "atom" then
113✔
275
      -- List is a single atom
276
      return mathlist[1]
×
277
   elseif #mathlist == 1 and mathlist[1].id == "argument" then
113✔
278
      return argEnv[mathlist[1].index]
3✔
279
   elseif mathlist.id == "argument" then
110✔
280
      return argEnv[mathlist.index]
×
281
   else
282
      local ret = ""
110✔
283
      for _, elt in ipairs(mathlist) do
435✔
284
         if elt.id == "atom" then
325✔
285
            ret = ret .. elt[1]
325✔
286
         elseif elt.id == "command" and symbols[elt.command] then
×
287
            ret = ret .. symbols[elt.command]
×
288
         else
289
            SU.error("Encountered non-character token in command that takes a string")
×
290
         end
291
      end
292
      return ret
110✔
293
   end
294
end
295

296
local function isOperatorKind (tree, typeOfAtom, typeOfSymbol)
297
   if not tree then
1,291✔
298
      return false -- safeguard
×
299
   end
300
   if tree.command ~= "mo" then
1,291✔
301
      return false
944✔
302
   end
303
   -- Case \mo[atom=big]{ops}
304
   -- E.g. \mo[atom=big]{lim}
305
   if tree.options and tree.options.atom == typeOfAtom then
347✔
306
      return true
4✔
307
   end
308
   -- Case \mo{ops} where ops is registered with the resquested type
309
   -- E.g. \mo{∑) or \sum
310
   if tree[1] and symbolDefaults[tree[1]] and symbolDefaults[tree[1]].atom == typeOfSymbol then
343✔
311
      return true
44✔
312
   end
313
   return false
299✔
314
end
315

316
local function isMoveableLimits (tree)
317
   if tree.command ~= "mo" then
96✔
318
      return false
81✔
319
   end
320
   if tree.options and SU.boolean(tree.options.movablelimits, false) then
30✔
321
      return true
×
322
   end
323
   if tree[1] and symbolDefaults[tree[1]] and SU.boolean(symbolDefaults[tree[1]].movablelimits, false) then
30✔
324
      return true
9✔
325
   end
326
   return false
6✔
327
end
328
local function isCloseOperator (tree)
329
   return isOperatorKind(tree, "close", atomType.closeSymbol)
670✔
330
end
331
local function isOpeningOperator (tree)
332
   return isOperatorKind(tree, "open", atomType.openingSymbol)
621✔
333
end
334

335
local function isAccentSymbol (symbol)
336
   return symbolDefaults[symbol] and symbolDefaults[symbol].atom == atomType.accentSymbol
57✔
337
end
338

339
local function compileToMathML_aux (_, arg_env, tree)
340
   if type(tree) == "string" then
2,528✔
341
      return tree
510✔
342
   end
343
   local function compile_and_insert (child, key, accumulator)
344
      if type(key) ~= "number" then
8,327✔
345
         accumulator[key] = child
5,324✔
346
         return accumulator
5,324✔
347
      -- Compile all children, except if this node is a macro definition (no
348
      -- evaluation "under lambda") or the application of a registered macro
349
      -- (since evaluating the nodes depends on the macro's signature, it is more
350
      -- complex and done below)..
351
      elseif tree.id == "def" or (tree.id == "command" and commands[tree.command]) then
3,003✔
352
         -- Conserve unevaluated child
353
         table.insert(accumulator, child)
740✔
354
      else
355
         -- Compile next child
356
         local comp = compileToMathML_aux(nil, arg_env, child)
2,263✔
357
         if comp then
2,263✔
358
            if comp.id == "wrapper" then
1,673✔
359
               -- Insert all children of the wrapper node
360
               for _, inner_child in ipairs(comp) do
227✔
361
                  table.insert(accumulator, inner_child)
114✔
362
               end
363
            else
364
               table.insert(accumulator, comp)
1,560✔
365
            end
366
         end
367
      end
368
      return accumulator
3,003✔
369
   end
370
   tree = fold_pairs(compile_and_insert, tree)
4,036✔
371
   if tree.id == "math" then
2,018✔
372
      tree.command = "math"
58✔
373
      -- If the outermost `mrow` contains only other `mrow`s, remove it
374
      -- (allowing vertical stacking).
375
      if forall(function (c)
116✔
376
         return c.command == "mrow"
48✔
377
      end, tree[1]) then
116✔
378
         tree[1].command = "math"
11✔
379
         return tree[1]
11✔
380
      end
381
   elseif tree.id == "mathlist" then
1,960✔
382
      -- Turn mathlist into `mrow` except if it has exactly one `mtr` or `mtd`
383
      -- child.
384
      -- Note that `def`s have already been compiled away at this point.
385
      if #tree == 1 then
344✔
386
         if tree[1].command == "mtr" or tree[1].command == "mtd" then
215✔
387
            return tree[1]
×
388
         else
389
            tree.command = "mrow"
215✔
390
         end
391
      else
392
         -- Re-wrap content from opening to closing operator in an implicit mrow,
393
         -- so stretchy operators apply to the correct span of content.
394
         local children = {}
129✔
395
         local stack = {}
129✔
396
         for _, child in ipairs(tree) do
750✔
397
            if isOpeningOperator(child) then
1,242✔
398
               table.insert(stack, children)
25✔
399
               local mrow = {
25✔
400
                  command = "mrow",
401
                  options = {},
25✔
402
                  child,
25✔
403
               }
404
               table.insert(children, mrow)
25✔
405
               children = mrow
25✔
406
            elseif isCloseOperator(child) then
1,192✔
407
               table.insert(children, child)
23✔
408
               if #stack > 0 then
23✔
409
                  children = table.remove(stack)
46✔
410
               end
411
            elseif
×
412
               (child.command == "msubsup" or child.command == "msub" or child.command == "msup")
573✔
413
               and isCloseOperator(child[1]) -- child[1] is the base
148✔
414
            then
415
               if #stack > 0 then
×
416
                  -- Special case for closing operator with sub/superscript:
417
                  -- (....)^i must be interpreted as {(....)}^i, not as (...{)}^i
418
                  -- Push the closing operator into the mrow
419
                  table.insert(children, child[1])
×
420
                  -- Move the mrow into the msubsup, replacing the closing operator
421
                  child[1] = children
×
422
                  -- And insert the msubsup into the parent
423
                  children = table.remove(stack)
×
424
                  children[#children] = child
×
425
               else
426
                  table.insert(children, child)
×
427
               end
428
            else
429
               table.insert(children, child)
573✔
430
            end
431
         end
432
         tree = #stack > 0 and stack[1] or children
129✔
433
         tree.command = "mrow"
129✔
434
      end
435
   elseif tree.id == "atom" then
1,616✔
436
      local codepoints = {}
510✔
437
      for _, cp in luautf8.codes(tree[1]) do
1,032✔
438
         table.insert(codepoints, cp)
522✔
439
      end
440
      local cp = codepoints[1]
510✔
441
      if
442
         #codepoints == 1
510✔
443
         and ( -- If length of UTF-8 string is 1
×
444
            cp >= SU.codepoint("A") and cp <= SU.codepoint("Z")
1,240✔
445
            or cp >= SU.codepoint("a") and cp <= SU.codepoint("z")
1,220✔
446
            or cp >= SU.codepoint("Α") and cp <= SU.codepoint("Ω")
788✔
447
            or cp >= SU.codepoint("α") and cp <= SU.codepoint("ω")
788✔
448
            or cp == SU.codepoint("ϑ")
670✔
449
            or cp == SU.codepoint("ϕ")
670✔
450
            or cp == SU.codepoint("ϰ")
670✔
451
            or cp == SU.codepoint("ϱ")
670✔
452
            or cp == SU.codepoint("ϖ")
670✔
453
            or cp == SU.codepoint("ϵ")
670✔
454
         )
455
      then
456
         tree.command = "mi"
163✔
457
      elseif lpeg.match(lpeg.R("09") ^ 1, tree[1]) then
347✔
458
         tree.command = "mn"
137✔
459
      else
460
         tree.command = "mo"
210✔
461
      end
462
      tree.options = {}
510✔
463
   -- Translate TeX-like sub/superscripts to `munderover` or `msubsup`,
464
   -- depending on whether the base is a big operator
465
   elseif tree.id == "sup" and isMoveableLimits(tree[1]) then
1,137✔
466
      tree.command = "mover"
×
467
   elseif tree.id == "sub" and isMoveableLimits(tree[1]) then
1,139✔
468
      tree.command = "munder"
1✔
469
   elseif tree.id == "subsup" and isMoveableLimits(tree[1]) then
1,123✔
470
      tree.command = "munderover"
8✔
471
   elseif tree.id == "supsub" and isMoveableLimits(tree[1]) then
1,111✔
472
      tree.command = "munderover"
×
473
      local tmp = tree[2]
×
474
      tree[2] = tree[3]
×
475
      tree[3] = tmp
×
476
   elseif tree.id == "sup" then
1,097✔
477
      tree.command = "msup"
31✔
478
   elseif tree.id == "sub" then
1,066✔
479
      tree.command = "msub"
32✔
480
   elseif tree.id == "subsup" then
1,034✔
481
      tree.command = "msubsup"
10✔
482
   elseif tree.id == "supsub" then
1,024✔
483
      tree.command = "msubsup"
14✔
484
      local tmp = tree[2]
14✔
485
      tree[2] = tree[3]
14✔
486
      tree[3] = tmp
14✔
487
   elseif tree.id == "def" then
1,010✔
488
      local commandName = tree["command-name"]
590✔
489
      local argTypes = inferArgTypes(tree[1])
590✔
490
      registerCommand(commandName, argTypes, function (compiledArgs)
1,180✔
491
         return compileToMathML_aux(nil, compiledArgs, tree[1])
113✔
492
      end)
493
      return nil
590✔
494
   elseif tree.id == "text" then
420✔
495
      tree.command = "mtext"
×
496
   elseif tree.id == "command" and commands[tree.command] then
420✔
497
      local argTypes = commands[tree.command][1]
224✔
498
      local cmdFun = commands[tree.command][2]
224✔
499
      local applicationTree = tree
224✔
500
      local cmdName = tree.command
224✔
501
      if #applicationTree ~= #argTypes then
224✔
502
         SU.error(
×
503
            "Wrong number of arguments ("
504
               .. #applicationTree
×
505
               .. ") for command "
×
506
               .. cmdName
×
507
               .. " (should be "
×
508
               .. #argTypes
×
509
               .. ")"
×
510
         )
511
      end
512
      -- Compile every argument
513
      local compiledArgs = {}
224✔
514
      for i, arg in pairs(applicationTree) do
1,270✔
515
         if type(i) == "number" then
1,046✔
516
            if argTypes[i] == objType.tree then
150✔
517
               table.insert(compiledArgs, compileToMathML_aux(nil, arg_env, arg))
74✔
518
            else
519
               local x = compileToStr(arg_env, arg)
113✔
520
               table.insert(compiledArgs, x)
113✔
521
            end
522
         else
523
            -- Not an argument but an attribute. Add it to the compiled
524
            -- argument tree as-is
525
            compiledArgs[i] = applicationTree[i]
896✔
526
         end
527
      end
528
      local res = cmdFun(compiledArgs)
224✔
529
      if res.command == "mrow" then
224✔
530
         -- Mark the outer mrow to be unwrapped in the parent
531
         res.id = "wrapper"
113✔
532
      end
533
      return res
224✔
534
   elseif tree.id == "command" and symbols[tree.command] then
196✔
535
      local atom = { id = "atom", [1] = symbols[tree.command] }
57✔
536
      if isAccentSymbol(symbols[tree.command]) and #tree > 0 then
114✔
537
         -- LaTeX-style accents \vec{v} = <mover accent="true"><mi>v</mi><mo>→</mo></mover>
538
         local accent = {
×
539
            id = "command",
540
            command = "mover",
541
            options = {
×
542
               accent = "true",
543
            },
544
         }
545
         accent[1] = compileToMathML_aux(nil, arg_env, tree[1])
×
546
         accent[2] = compileToMathML_aux(nil, arg_env, atom)
×
547
         tree = accent
×
548
      elseif #tree > 0 then
57✔
549
         -- Play cool with LaTeX-style commands that don't take arguments:
550
         -- Edge case for non-accent symbols so we don't loose bracketed groups
551
         -- that might have been seen as command arguments.
552
         -- Ex. \langle{x}\rangle (without space after \langle)
553
         local sym = compileToMathML_aux(nil, arg_env, atom)
×
554
         -- Compile all children in-place
555
         for i, child in ipairs(tree) do
×
556
            tree[i] = compileToMathML_aux(nil, arg_env, child)
×
557
         end
558
         -- Insert symbol at the beginning,
559
         -- And add a wrapper mrow to be unwrapped in the parent.
560
         table.insert(tree, 1, sym)
×
561
         tree.command = "mrow"
×
562
         tree.id = "wrapper"
×
563
      else
564
         tree = compileToMathML_aux(nil, arg_env, atom)
114✔
565
      end
566
   elseif tree.id == "argument" then
139✔
567
      if arg_env[tree.index] then
37✔
568
         return arg_env[tree.index]
37✔
569
      else
570
         SU.error("Argument #" .. tree.index .. " has escaped its scope (probably not fully applied command).")
×
571
      end
572
   end
573
   tree.id = nil
1,156✔
574
   return tree
1,156✔
575
end
576

577
local function printMathML (tree)
578
   if type(tree) == "string" then
×
579
      return tree
×
580
   end
581
   local result = "\\" .. tree.command
×
582
   if tree.options then
×
583
      local options = {}
×
584
      for k, v in pairs(tree.options) do
×
585
         table.insert(options, k .. "=" .. tostring(v))
×
586
      end
587
      if #options > 0 then
×
588
         result = result .. "[" .. table.concat(options, ", ") .. "]"
×
589
      end
590
   end
591
   if #tree > 0 then
×
592
      result = result .. "{"
×
593
      for _, child in ipairs(tree) do
×
594
         result = result .. printMathML(child)
×
595
      end
596
      result = result .. "}"
×
597
   end
598
   return result
×
599
end
600

601
local function compileToMathML (_, arg_env, tree)
602
   local result = compileToMathML_aux(_, arg_env, tree)
58✔
603
   SU.debug("texmath", function ()
116✔
604
      return "Resulting MathML: " .. printMathML(result)
×
605
   end)
606
   return result
58✔
607
end
608

609
local function convertTexlike (_, content)
610
   local ret = epnf.parsestring(mathParser, content[1])
58✔
611
   SU.debug("texmath", function ()
116✔
612
      return "Parsed TeX math: " .. pl.pretty.write(ret)
×
613
   end)
614
   return ret
58✔
615
end
616

617
registerCommand("%", {}, function ()
22✔
618
   return { "%", command = "mo", options = {} }
1✔
619
end)
620
registerCommand("mi", { [1] = objType.str }, function (x)
22✔
621
   return x
90✔
622
end)
623
registerCommand("mo", { [1] = objType.str }, function (x)
22✔
624
   return x
19✔
625
end)
626
registerCommand("mn", { [1] = objType.str }, function (x)
22✔
627
   return x
1✔
628
end)
629

630
compileToMathML(
22✔
631
   nil,
11✔
632
   {},
633
   convertTexlike(nil, {
11✔
634
      [==[
×
635
  \def{frac}{\mfrac{#1}{#2}}
636
  \def{sqrt}{\msqrt{#1}}
637
  \def{bi}{\mi[mathvariant=bold-italic]{#1}}
638
  \def{dsi}{\mi[mathvariant=double-struck]{#1}}
639

640
  \def{lim}{\mo[movablelimits=true]{lim}}
641

642
  % From amsmath:
643
  \def{to}{\mo[atom=bin]{→}}
644
  \def{gcd}{\mo[movablelimits=true]{gcd}}
645
  \def{sup}{\mo[movablelimits=true]{sup}}
646
  \def{inf}{\mo[movablelimits=true]{inf}}
647
  \def{max}{\mo[movablelimits=true]{max}}
648
  \def{min}{\mo[movablelimits=true]{min}}
649
  % Those use U+202F NARROW NO-BREAK SPACE in their names
650
  \def{limsup}{\mo[movablelimits=true]{lim sup}}
651
  \def{liminf}{\mo[movablelimits=true]{lim inf}}
652
  \def{projlim}{\mo[movablelimits=true]{proj lim}}
653
  \def{injlim}{\mo[movablelimits=true]{inj lim}}
654

655
  % Standard spaces gleaned from plain TeX
656
  \def{thinspace}{\mspace[width=thin]}
657
  \def{negthinspace}{\mspace[width=-thin]}
658
  \def{,}{\thinspace}
659
  \def{!}{\negthinspace}
660
  \def{medspace}{\mspace[width=med]}
661
  \def{negmedspace}{\mspace[width=-med]}
662
  \def{>}{\medspace}
663
  \def{thickspace}{\mspace[width=thick]}
664
  \def{negthickspace}{\mspace[width=-thick]}
665
  \def{;}{\thickspace}
666
  \def{enspace}{\mspace[width=1en]}
667
  \def{enskip}{\enspace}
668
  \def{quad}{\mspace[width=1em]}
669
  \def{qquad}{\mspace[width=2em]}
670

671
  % MathML says a single-character identifier must be in italic by default.
672
  % TeX however has the following Greek capital macros rendered in upright shape.
673
  % It so common that you've probably never seen Γ(x) written with an italic gamma.
674
  \def{Gamma}{\mi[mathvariant=normal]{Γ}}
675
  \def{Delta}{\mi[mathvariant=normal]{Δ}}
676
  \def{Theta}{\mi[mathvariant=normal]{Θ}}
677
  \def{Lambda}{\mi[mathvariant=normal]{Λ}}
678
  \def{Xi}{\mi[mathvariant=normal]{Ξ}}
679
  \def{Pi}{\mi[mathvariant=normal]{Π}}
680
  \def{Sigma}{\mi[mathvariant=normal]{Σ}}
681
  \def{Upsilon}{\mi[mathvariant=normal]{Υ}}
682
  \def{Phi}{\mi[mathvariant=normal]{Φ}}
683
  \def{Psi}{\mi[mathvariant=normal]{Ψ}}
684
  \def{Omega}{\mi[mathvariant=normal]{Ω}}
685
  % Some calligraphic (script), fraktur, double-struck styles:
686
  % Convenience for compatibility with LaTeX.
687
  \def{mathcal}{\mi[mathvariant=script]{#1}}
688
  \def{mathfrak}{\mi[mathvariant=fraktur]{#1}}
689
  \def{mathbb}{\mi[mathvariant=double-struck]{#1}}
690
  % Some style-switching commands for compatibility with LaTeX math.
691
  % Caveat emptor: LaTeX would allow these to apply to a whole formula.
692
  % We can't do that in MathML, as mathvariant applies to token elements only.
693
  % Also note that LaTeX and related packages may have many more such commands.
694
  % We only provide a few common ('historical') ones here.
695
  \def{mathrm}{\mi[mathvariant=normal]{#1}}
696
  \def{mathbf}{\mi[mathvariant=bold]{#1}}
697
  \def{mathit}{\mi[mathvariant=italic]{#1}}
698
  \def{mathsf}{\mi[mathvariant=sans-serif]{#1}}
699
  \def{mathtt}{\mi[mathvariant=monospace]{#1}}
700

701
  % Modulus operator forms
702
  \def{bmod}{\mo{mod}}
703
  \def{pmod}{\quad(\mo{mod} #1)}
704

705
  % Phantom commands from TeX/LaTeX
706
  \def{phantom}{\mphantom{#1}}
707
  \def{hphantom}{\mpadded[height=0, depth=0]{\mphantom{#1}}}
708
  \def{vphantom}{\mpadded[width=0]{\mphantom{#1}}}
709
  %\mphantom[special=v]{#1}}}
710
]==],
711
   })
712
)
713

714
return { convertTexlike, compileToMathML }
11✔
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