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

sile-typesetter / sile / 11574339945

29 Oct 2024 12:58PM UTC coverage: 34.309% (-33.9%) from 68.22%
11574339945

push

github

web-flow
Merge pull request #2142 from Omikhleia/fix-math-fraction-padding

fix(math): Fractions must have padding to avoid visual confusion

0 of 26 new or added lines in 1 file covered. (0.0%)

5779 existing lines in 69 files now uncovered.

6014 of 17529 relevant lines covered (34.31%)

4588.59 hits per line

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

0.0
/packages/math/texlike.lua
UNCOV
1
local syms = require("packages.math.unicode-symbols")
×
UNCOV
2
local bits = require("core.parserbits")
×
3

UNCOV
4
local epnf = require("epnf")
×
UNCOV
5
local lpeg = require("lpeg")
×
6

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

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

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

UNCOV
142
local mathParser = epnf.define(mathGrammar)
×
143

UNCOV
144
local commands = {}
×
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

UNCOV
151
local objType = {
×
152
   tree = 1,
153
   str = 2,
154
}
155

156
local function inferArgTypes_aux (accumulator, typeRequired, body)
UNCOV
157
   if type(body) == "table" then
×
UNCOV
158
      if body.id == "argument" then
×
UNCOV
159
         local ret = accumulator
×
UNCOV
160
         table.insert(ret, body.index, typeRequired)
×
UNCOV
161
         return ret
×
UNCOV
162
      elseif body.id == "command" then
×
UNCOV
163
         if commands[body.command] then
×
UNCOV
164
            local cmdArgTypes = commands[body.command][1]
×
UNCOV
165
            if #cmdArgTypes ~= #body then
×
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
UNCOV
176
               for i = 1, #cmdArgTypes do
×
UNCOV
177
                  accumulator = inferArgTypes_aux(accumulator, cmdArgTypes[i], body[i])
×
178
               end
179
            end
UNCOV
180
            return accumulator
×
UNCOV
181
         elseif body.command == "mi" or body.command == "mo" or body.command == "mn" then
×
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
UNCOV
190
            for _, child in ipairs(body) do
×
UNCOV
191
               accumulator = inferArgTypes_aux(accumulator, objType.tree, child)
×
192
            end
UNCOV
193
            return accumulator
×
194
         end
UNCOV
195
      elseif body.id == "atom" then
×
UNCOV
196
         return accumulator
×
197
      else
198
         -- Simply recurse on children
UNCOV
199
         for _, child in ipairs(body) do
×
UNCOV
200
            accumulator = inferArgTypes_aux(accumulator, typeRequired, child)
×
201
         end
UNCOV
202
         return accumulator
×
203
      end
204
   else
205
      SU.error("invalid argument to inferArgTypes_aux")
×
206
   end
207
end
208

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

213
local function registerCommand (name, argTypes, func)
UNCOV
214
   commands[name] = { argTypes, func }
×
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)
UNCOV
221
   local accumulator = {}
×
UNCOV
222
   for k, v in pl.utils.kpairs(table) do
×
UNCOV
223
      accumulator = func(v, k, accumulator)
×
224
   end
UNCOV
225
   for i, v in ipairs(table) do
×
UNCOV
226
      accumulator = func(v, i, accumulator)
×
227
   end
UNCOV
228
   return accumulator
×
229
end
230

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

240
local compileToStr = function (argEnv, mathlist)
UNCOV
241
   if #mathlist == 1 and mathlist.id == "atom" then
×
242
      -- List is a single atom
243
      return mathlist[1]
×
UNCOV
244
   elseif #mathlist == 1 and mathlist[1].id == "argument" then
×
UNCOV
245
      return argEnv[mathlist[1].index]
×
UNCOV
246
   elseif mathlist.id == "argument" then
×
247
      return argEnv[mathlist.index]
×
248
   else
UNCOV
249
      local ret = ""
×
UNCOV
250
      for _, elt in ipairs(mathlist) do
×
UNCOV
251
         if elt.id == "atom" then
×
UNCOV
252
            ret = ret .. elt[1]
×
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
UNCOV
259
      return ret
×
260
   end
261
end
262

263
local function isBigOperator (tree)
UNCOV
264
   if tree.command ~= "mo" then
×
UNCOV
265
      return false
×
266
   end
267
   -- Case \mo[atom=big]{ops}
268
   -- E.g. \mo[atom=big]{lim}
UNCOV
269
   if tree.options and tree.options.atom == "big" then
×
270
      return true
×
271
   end
272
   -- Case \mo{ops} where ops is registered as big operator (unicode-symbols)
273
   -- E.g. \mo{∑) or \sum
UNCOV
274
   if tree[1] and symbolDefaults[tree[1]] and symbolDefaults[tree[1]].atom == atomType.bigOperator then
×
UNCOV
275
      return true
×
276
   end
UNCOV
277
   return false
×
278
end
279

280
local function compileToMathML_aux (_, arg_env, tree)
UNCOV
281
   if type(tree) == "string" then
×
UNCOV
282
      return tree
×
283
   end
284
   local function compile_and_insert (child, key, accumulator)
UNCOV
285
      if type(key) ~= "number" then
×
UNCOV
286
         accumulator[key] = child
×
UNCOV
287
         return accumulator
×
288
      -- Compile all children, except if this node is a macro definition (no
289
      -- evaluation "under lambda") or the application of a registered macro
290
      -- (since evaluating the nodes depends on the macro's signature, it is more
291
      -- complex and done below)..
UNCOV
292
      elseif tree.id == "def" or (tree.id == "command" and commands[tree.command]) then
×
293
         -- Conserve unevaluated child
UNCOV
294
         table.insert(accumulator, child)
×
295
      else
296
         -- Compile next child
UNCOV
297
         local comp = compileToMathML_aux(nil, arg_env, child)
×
UNCOV
298
         if comp then
×
UNCOV
299
            if comp.id == "wrapper" then
×
300
               -- Insert all children of the wrapper node
UNCOV
301
               for _, inner_child in ipairs(comp) do
×
UNCOV
302
                  table.insert(accumulator, inner_child)
×
303
               end
304
            else
UNCOV
305
               table.insert(accumulator, comp)
×
306
            end
307
         end
308
      end
UNCOV
309
      return accumulator
×
310
   end
UNCOV
311
   tree = fold_pairs(compile_and_insert, tree)
×
UNCOV
312
   if tree.id == "math" then
×
UNCOV
313
      tree.command = "math"
×
314
      -- If the outermost `mrow` contains only other `mrow`s, remove it
315
      -- (allowing vertical stacking).
UNCOV
316
      if forall(function (c)
×
UNCOV
317
         return c.command == "mrow"
×
UNCOV
318
      end, tree[1]) then
×
UNCOV
319
         tree[1].command = "math"
×
UNCOV
320
         return tree[1]
×
321
      end
UNCOV
322
   elseif tree.id == "mathlist" then
×
323
      -- Turn mathlist into `mrow` except if it has exactly one `mtr` or `mtd`
324
      -- child.
325
      -- Note that `def`s have already been compiled away at this point.
UNCOV
326
      if #tree == 1 and (tree[1].command == "mtr" or tree[1].command == "mtd") then
×
327
         return tree[1]
×
328
      else
UNCOV
329
         tree.command = "mrow"
×
330
      end
UNCOV
331
      tree.command = "mrow"
×
UNCOV
332
   elseif tree.id == "atom" then
×
UNCOV
333
      local codepoints = {}
×
UNCOV
334
      for _, cp in luautf8.codes(tree[1]) do
×
UNCOV
335
         table.insert(codepoints, cp)
×
336
      end
UNCOV
337
      local cp = codepoints[1]
×
338
      if
UNCOV
339
         #codepoints == 1
×
340
         and ( -- If length of UTF-8 string is 1
×
UNCOV
341
            cp >= SU.codepoint("A") and cp <= SU.codepoint("Z")
×
UNCOV
342
            or cp >= SU.codepoint("a") and cp <= SU.codepoint("z")
×
UNCOV
343
            or cp >= SU.codepoint("Α") and cp <= SU.codepoint("Ω")
×
UNCOV
344
            or cp >= SU.codepoint("α") and cp <= SU.codepoint("ω")
×
345
         )
346
      then
UNCOV
347
         tree.command = "mi"
×
UNCOV
348
      elseif lpeg.match(lpeg.R("09") ^ 1, tree[1]) then
×
UNCOV
349
         tree.command = "mn"
×
350
      else
UNCOV
351
         tree.command = "mo"
×
352
      end
UNCOV
353
      tree.options = {}
×
354
   -- Translate TeX-like sub/superscripts to `munderover` or `msubsup`,
355
   -- depending on whether the base is a big operator
UNCOV
356
   elseif tree.id == "sup" and isBigOperator(tree[1]) then
×
357
      tree.command = "mover"
×
UNCOV
358
   elseif tree.id == "sub" and isBigOperator(tree[1]) then
×
UNCOV
359
      tree.command = "munder"
×
UNCOV
360
   elseif tree.id == "subsup" and isBigOperator(tree[1]) then
×
UNCOV
361
      tree.command = "munderover"
×
UNCOV
362
   elseif tree.id == "supsub" and isBigOperator(tree[1]) then
×
363
      tree.command = "munderover"
×
364
      local tmp = tree[2]
×
365
      tree[2] = tree[3]
×
366
      tree[3] = tmp
×
UNCOV
367
   elseif tree.id == "sup" then
×
UNCOV
368
      tree.command = "msup"
×
UNCOV
369
   elseif tree.id == "sub" then
×
UNCOV
370
      tree.command = "msub"
×
UNCOV
371
   elseif tree.id == "subsup" then
×
UNCOV
372
      tree.command = "msubsup"
×
UNCOV
373
   elseif tree.id == "supsub" then
×
374
      tree.command = "msubsup"
×
375
      local tmp = tree[2]
×
376
      tree[2] = tree[3]
×
377
      tree[3] = tmp
×
UNCOV
378
   elseif tree.id == "def" then
×
UNCOV
379
      local commandName = tree["command-name"]
×
UNCOV
380
      local argTypes = inferArgTypes(tree[1])
×
UNCOV
381
      registerCommand(commandName, argTypes, function (compiledArgs)
×
UNCOV
382
         return compileToMathML_aux(nil, compiledArgs, tree[1])
×
383
      end)
UNCOV
384
      return nil
×
UNCOV
385
   elseif tree.id == "text" then
×
386
      tree.command = "mtext"
×
UNCOV
387
   elseif tree.id == "command" and commands[tree.command] then
×
UNCOV
388
      local argTypes = commands[tree.command][1]
×
UNCOV
389
      local cmdFun = commands[tree.command][2]
×
UNCOV
390
      local applicationTree = tree
×
UNCOV
391
      local cmdName = tree.command
×
UNCOV
392
      if #applicationTree ~= #argTypes then
×
393
         SU.error(
×
394
            "Wrong number of arguments ("
395
               .. #applicationTree
×
396
               .. ") for command "
×
397
               .. cmdName
×
398
               .. " (should be "
×
399
               .. #argTypes
×
400
               .. ")"
×
401
         )
402
      end
403
      -- Compile every argument
UNCOV
404
      local compiledArgs = {}
×
UNCOV
405
      for i, arg in pairs(applicationTree) do
×
UNCOV
406
         if type(i) == "number" then
×
UNCOV
407
            if argTypes[i] == objType.tree then
×
UNCOV
408
               table.insert(compiledArgs, compileToMathML_aux(nil, arg_env, arg))
×
409
            else
UNCOV
410
               local x = compileToStr(arg_env, arg)
×
UNCOV
411
               table.insert(compiledArgs, x)
×
412
            end
413
         else
414
            -- Not an argument but an attribute. Add it to the compiled
415
            -- argument tree as-is
UNCOV
416
            compiledArgs[i] = applicationTree[i]
×
417
         end
418
      end
UNCOV
419
      local res = cmdFun(compiledArgs)
×
UNCOV
420
      if res.command == "mrow" then
×
421
         -- Mark the outer mrow to be unwrapped in the parent
UNCOV
422
         res.id = "wrapper"
×
423
      end
UNCOV
424
      return res
×
UNCOV
425
   elseif tree.id == "command" and symbols[tree.command] then
×
UNCOV
426
      local atom = { id = "atom", [1] = symbols[tree.command] }
×
UNCOV
427
      tree = compileToMathML_aux(nil, arg_env, atom)
×
UNCOV
428
   elseif tree.id == "argument" then
×
UNCOV
429
      if arg_env[tree.index] then
×
UNCOV
430
         return arg_env[tree.index]
×
431
      else
432
         SU.error("Argument #" .. tree.index .. " has escaped its scope (probably not fully applied command).")
×
433
      end
434
   end
UNCOV
435
   tree.id = nil
×
UNCOV
436
   return tree
×
437
end
438

439
local function printMathML (tree)
440
   if type(tree) == "string" then
×
441
      return tree
×
442
   end
443
   local result = "\\" .. tree.command
×
444
   if tree.options then
×
445
      local options = {}
×
446
      for k, v in pairs(tree.options) do
×
447
         table.insert(options, k .. "=" .. v)
×
448
      end
449
      if #options > 0 then
×
450
         result = result .. "[" .. table.concat(options, ", ") .. "]"
×
451
      end
452
   end
453
   if #tree > 0 then
×
454
      result = result .. "{"
×
455
      for _, child in ipairs(tree) do
×
456
         result = result .. printMathML(child)
×
457
      end
458
      result = result .. "}"
×
459
   end
460
   return result
×
461
end
462

463
local function compileToMathML (_, arg_env, tree)
UNCOV
464
   local result = compileToMathML_aux(_, arg_env, tree)
×
UNCOV
465
   SU.debug("texmath", function ()
×
466
      return "Resulting MathML: " .. printMathML(result)
×
467
   end)
UNCOV
468
   return result
×
469
end
470

471
local function convertTexlike (_, content)
UNCOV
472
   local ret = epnf.parsestring(mathParser, content[1])
×
UNCOV
473
   SU.debug("texmath", function ()
×
474
      return "Parsed TeX math: " .. pl.pretty.write(ret)
×
475
   end)
UNCOV
476
   return ret
×
477
end
478

UNCOV
479
registerCommand("%", {}, function ()
×
UNCOV
480
   return { "%", command = "mo", options = {} }
×
481
end)
UNCOV
482
registerCommand("mi", { [1] = objType.str }, function (x)
×
UNCOV
483
   return x
×
484
end)
UNCOV
485
registerCommand("mo", { [1] = objType.str }, function (x)
×
UNCOV
486
   return x
×
487
end)
UNCOV
488
registerCommand("mn", { [1] = objType.str }, function (x)
×
UNCOV
489
   return x
×
490
end)
491

UNCOV
492
compileToMathML(
×
493
   nil,
494
   {},
UNCOV
495
   convertTexlike(nil, {
×
496
      [==[
×
497
  \def{frac}{\mfrac{#1}{#2}}
498
  \def{sqrt}{\msqrt{#1}}
499
  \def{bi}{\mi[mathvariant=bold-italic]{#1}}
500
  \def{dsi}{\mi[mathvariant=double-struck]{#1}}
501

502
  \def{lim}{\mo[atom=big]{lim}}
503

504
  % From amsmath:
505
  \def{to}{\mo[atom=bin]{→}}
506
  \def{gcd}{\mo[atom=big]{gcd}}
507
  \def{sup}{\mo[atom=big]{sup}}
508
  \def{inf}{\mo[atom=big]{inf}}
509
  \def{max}{\mo[atom=big]{max}}
510
  \def{min}{\mo[atom=big]{min}}
511
  % Those use U+202F NARROW NO-BREAK SPACE in their names
512
  \def{limsup}{\mo[atom=big]{lim sup}}
513
  \def{liminf}{\mo[atom=big]{lim inf}}
514
  \def{projlim}{\mo[atom=big]{proj lim}}
515
  \def{injlim}{\mo[atom=big]{inj lim}}
516

517
  % Standard spaces gleaned from plain TeX
518
  \def{thinspace}{\mspace[width=thin]}
519
  \def{negthinspace}{\mspace[width=-thin]}
520
  \def{,}{\thinspace}
521
  \def{!}{\negthinspace}
522
  \def{medspace}{\mspace[width=med]}
523
  \def{negmedspace}{\mspace[width=-med]}
524
  \def{>}{\medspace}
525
  \def{thickspace}{\mspace[width=thick]}
526
  \def{negthickspace}{\mspace[width=-thick]}
527
  \def{;}{\thickspace}
528
  \def{enspace}{\mspace[width=1en]}
529
  \def{enskip}{\enspace}
530
  \def{quad}{\mspace[width=1em]}
531
  \def{qquad}{\mspace[width=2em]}
532

533
  % Modulus operator forms
534
  \def{bmod}{\mo{mod}}
535
  \def{pmod}{\quad(\mo{mod} #1)}
536
]==],
537
   })
538
)
539

UNCOV
540
return { convertTexlike, compileToMathML }
×
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