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

sile-typesetter / sile / 9304060604

30 May 2024 02:07PM UTC coverage: 74.124% (-0.6%) from 74.707%
9304060604

push

github

alerque
style: Reformat Lua with stylua

8104 of 11995 new or added lines in 184 files covered. (67.56%)

15 existing lines in 11 files now uncovered.

12444 of 16788 relevant lines covered (74.12%)

7175.1 hits per line

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

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

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

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

89
   START "texlike_math"
12✔
90
   texlike_math = V"mathlist" * EOF"Unexpected character at end of math code"
24✔
91
   mathlist = (comment + (WS * _) + element)^0
12✔
92
   supsub = element_no_infix * _ * P"^" * _ * element_no_infix * _ *
12✔
93
      P"_" * _ * element_no_infix
12✔
94
   subsup = element_no_infix * _ * P"_" * _ * element_no_infix * _ *
12✔
95
      P"^" * _ * element_no_infix
12✔
96
   sup = element_no_infix * _ * P"^" * _ * element_no_infix
12✔
97
   sub = element_no_infix * _ * P"_" * _ * element_no_infix
12✔
98
   atom = natural + C(utf8code - S"\\{}%^_&") +
12✔
99
      (P"\\{" + P"\\}") / function (s) return string.sub(s, -1) end
12✔
100
   command = (
×
101
         P"\\" *
12✔
102
         Cg(ctrl_sequence_name, "command") *
12✔
103
         Cg(parameters, "options") *
12✔
104
         (dim2_arg + group^0)
12✔
105
      )
12✔
106
   def = P"\\def" * _ * P"{" *
12✔
107
      Cg(ctrl_sequence_name, "command-name") * P"}" * _ *
12✔
108
      --P"[" * Cg(digit^1, "arity") * P"]" * _ *
109
      P"{" * V"mathlist" * P"}"
12✔
110
   argument = P"#" * Cg(pos_natural, "index")
12✔
111
end
112
-- luacheck: pop
113
-- stylua: ignore end
114
---@diagnostic enable: undefined-global, unused-local, lowercase-global
115

116
local mathParser = epnf.define(mathGrammar)
12✔
117

118
local commands = {}
12✔
119

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

125
local objType = {
12✔
126
   tree = 1,
127
   str = 2,
128
}
129

130
local function inferArgTypes_aux (accumulator, typeRequired, body)
131
   if type(body) == "table" then
736✔
132
      if body.id == "argument" then
736✔
133
         local ret = accumulator
66✔
134
         table.insert(ret, body.index, typeRequired)
66✔
135
         return ret
66✔
136
      elseif body.id == "command" then
670✔
137
         if commands[body.command] then
248✔
138
            local cmdArgTypes = commands[body.command][1]
127✔
139
            if #cmdArgTypes ~= #body then
127✔
NEW
140
               SU.error(
×
141
                  "Wrong number of arguments ("
NEW
142
                     .. #body
×
NEW
143
                     .. ") for command "
×
NEW
144
                     .. body.command
×
NEW
145
                     .. " (should be "
×
NEW
146
                     .. #cmdArgTypes
×
NEW
147
                     .. ")"
×
148
               )
149
            else
150
               for i = 1, #cmdArgTypes do
182✔
151
                  accumulator = inferArgTypes_aux(accumulator, cmdArgTypes[i], body[i])
110✔
152
               end
153
            end
154
            return accumulator
127✔
155
         elseif body.command == "mi" or body.command == "mo" or body.command == "mn" then
121✔
NEW
156
            if #body ~= 1 then
×
NEW
157
               SU.error("Wrong number of arguments (" .. #body .. ") for command " .. body.command .. " (should be 1)")
×
158
            end
NEW
159
            accumulator = inferArgTypes_aux(accumulator, objType.str, body[1])
×
NEW
160
            return accumulator
×
161
         else
162
            -- Not a macro, recurse on children assuming tree type for all
163
            -- arguments
164
            for _, child in ipairs(body) do
147✔
165
               accumulator = inferArgTypes_aux(accumulator, objType.tree, child)
52✔
166
            end
167
            return accumulator
121✔
168
         end
169
      elseif body.id == "atom" then
422✔
170
         return accumulator
104✔
171
      else
172
         -- Simply recurse on children
173
         for _, child in ipairs(body) do
738✔
174
            accumulator = inferArgTypes_aux(accumulator, typeRequired, child)
840✔
175
         end
176
         return accumulator
318✔
177
      end
178
   else
NEW
179
      SU.error("invalid argument to inferArgTypes_aux")
×
180
   end
181
end
182

183
local inferArgTypes = function (body)
184
   return inferArgTypes_aux({}, objType.tree, body)
235✔
185
end
186

187
local function registerCommand (name, argTypes, func)
188
   commands[name] = { argTypes, func }
283✔
189
end
190

191
-- Computes func(func(... func(init, k1, v1), k2, v2)..., k_n, v_n), i.e. applies
192
-- func on every key-value pair in the table. Keys with numeric indices are
193
-- processed in order. This is an important property for MathML compilation below.
194
local function fold_pairs (func, table)
195
   local accumulator = {}
1,439✔
196
   for k, v in pl.utils.kpairs(table) do
11,827✔
197
      accumulator = func(v, k, accumulator)
7,510✔
198
   end
199
   for i, v in ipairs(table) do
3,435✔
200
      accumulator = func(v, i, accumulator)
3,992✔
201
   end
202
   return accumulator
1,439✔
203
end
204

205
local function forall (pred, list)
206
   for _, x in ipairs(list) do
57✔
207
      if not pred(x) then
90✔
208
         return false
45✔
209
      end
210
   end
211
   return true
12✔
212
end
213

214
local compileToStr = function (argEnv, mathlist)
215
   if #mathlist == 1 and mathlist.id == "atom" then
114✔
216
      -- List is a single atom
NEW
217
      return mathlist[1]
×
218
   elseif #mathlist == 1 and mathlist[1].id == "argument" then
114✔
219
      return argEnv[mathlist[1].index]
3✔
220
   elseif mathlist.id == "argument" then
111✔
NEW
221
      return argEnv[mathlist.index]
×
222
   else
223
      local ret = ""
111✔
224
      for _, elt in ipairs(mathlist) do
443✔
225
         if elt.id == "atom" then
332✔
226
            ret = ret .. elt[1]
332✔
NEW
227
         elseif elt.id == "command" and symbols[elt.command] then
×
NEW
228
            ret = ret .. symbols[elt.command]
×
229
         else
NEW
230
            SU.error("Encountered non-character token in command that takes a string")
×
231
         end
232
      end
233
      return ret
111✔
234
   end
235
end
236

237
local function compileToMathML_aux (_, arg_env, tree)
238
   if type(tree) == "string" then
1,847✔
239
      return tree
408✔
240
   end
241
   local function compile_and_insert (child, key, accumulator)
242
      if type(key) ~= "number" then
5,751✔
243
         accumulator[key] = child
3,755✔
244
         return accumulator
3,755✔
245
      -- Compile all children, except if this node is a macro definition (no
246
      -- evaluation "under lambda") or the application of a registered macro
247
      -- (since evaluating the nodes depends on the macro's signature, it is more
248
      -- complex and done below)..
249
      elseif tree.id == "def" or (tree.id == "command" and commands[tree.command]) then
1,996✔
250
         -- Conserve unevaluated child
251
         table.insert(accumulator, child)
386✔
252
      else
253
         -- Compile next child
254
         local comp = compileToMathML_aux(nil, arg_env, child)
1,610✔
255
         if comp then
1,610✔
256
            if comp.id == "wrapper" then
1,375✔
257
               -- Insert all children of the wrapper node
258
               for _, inner_child in ipairs(comp) do
184✔
259
                  table.insert(accumulator, inner_child)
94✔
260
               end
261
            else
262
               table.insert(accumulator, comp)
1,285✔
263
            end
264
         end
265
      end
266
      return accumulator
1,996✔
267
   end
268
   tree = fold_pairs(compile_and_insert, tree)
2,878✔
269
   if tree.id == "texlike_math" then
1,439✔
270
      tree.command = "math"
57✔
271
      -- If the outermost `mrow` contains only other `mrow`s, remove it
272
      -- (allowing vertical stacking).
273
      if forall(function (c)
114✔
274
         return c.command == "mrow"
45✔
275
      end, tree[1]) then
114✔
276
         tree[1].command = "math"
12✔
277
         return tree[1]
12✔
278
      end
279
   elseif tree.id == "mathlist" then
1,382✔
280
      -- Turn mathlist into `mrow` except if it has exactly one `mtr` or `mtd`
281
      -- child.
282
      -- Note that `def`s have already been compiled away at this point.
283
      if #tree == 1 and (tree[1].command == "mtr" or tree[1].command == "mtd") then
306✔
NEW
284
         return tree[1]
×
285
      else
286
         tree.command = "mrow"
306✔
287
      end
288
      tree.command = "mrow"
306✔
289
   elseif tree.id == "atom" then
1,076✔
290
      local codepoints = {}
408✔
291
      for _, cp in luautf8.codes(tree[1]) do
828✔
292
         table.insert(codepoints, cp)
420✔
293
      end
294
      local cp = codepoints[1]
408✔
295
      if
296
         #codepoints == 1
408✔
NEW
297
         and ( -- If length of UTF-8 string is 1
×
298
            cp >= SU.codepoint("A") and cp <= SU.codepoint("Z")
963✔
299
            or cp >= SU.codepoint("a") and cp <= SU.codepoint("z")
945✔
300
            or cp >= SU.codepoint("Α") and cp <= SU.codepoint("Ω")
630✔
301
            or cp >= SU.codepoint("α") and cp <= SU.codepoint("ω")
624✔
302
         )
303
      then
304
         tree.command = "mi"
131✔
305
      elseif lpeg.match(lpeg.R("09") ^ 1, tree[1]) then
277✔
306
         tree.command = "mn"
108✔
307
      else
308
         tree.command = "mo"
169✔
309
      end
310
      tree.options = {}
408✔
311
   -- Translate TeX-like sub/superscripts to `munderover` or `msubsup`,
312
   -- depending on whether the base is a big operator
313
   elseif tree.id == "sup" and tree[1].command == "mo" and tree[1].atom == atomType.bigOperator then
668✔
NEW
314
      tree.command = "mover"
×
315
   elseif tree.id == "sub" and tree[1].command == "mo" and symbolDefaults[tree[1][1]].atom == atomType.bigOperator then
668✔
316
      tree.command = "munder"
1✔
NEW
317
   elseif
×
318
      tree.id == "subsup"
667✔
319
      and tree[1].command == "mo"
14✔
320
      and symbolDefaults[tree[1][1]].atom == atomType.bigOperator
10✔
321
   then
322
      tree.command = "munderover"
8✔
NEW
323
   elseif
×
324
      tree.id == "supsub"
659✔
NEW
325
      and tree[1].command == "mo"
×
NEW
326
      and symbolDefaults[tree[1][1]].atom == atomType.bigOperator
×
327
   then
NEW
328
      tree.command = "munderover"
×
NEW
329
      local tmp = tree[2]
×
NEW
330
      tree[2] = tree[3]
×
NEW
331
      tree[3] = tmp
×
332
   elseif tree.id == "sup" then
659✔
333
      tree.command = "msup"
14✔
334
   elseif tree.id == "sub" then
645✔
335
      tree.command = "msub"
32✔
336
   elseif tree.id == "subsup" then
613✔
337
      tree.command = "msubsup"
6✔
338
   elseif tree.id == "supsub" then
607✔
NEW
339
      tree.command = "msubsup"
×
NEW
340
      local tmp = tree[2]
×
NEW
341
      tree[2] = tree[3]
×
NEW
342
      tree[3] = tmp
×
343
   elseif tree.id == "def" then
607✔
344
      local commandName = tree["command-name"]
235✔
345
      local argTypes = inferArgTypes(tree[1])
235✔
346
      registerCommand(commandName, argTypes, function (compiledArgs)
470✔
347
         return compileToMathML_aux(nil, compiledArgs, tree[1])
90✔
348
      end)
349
      return nil
235✔
350
   elseif tree.id == "command" and commands[tree.command] then
372✔
351
      local argTypes = commands[tree.command][1]
202✔
352
      local cmdFun = commands[tree.command][2]
202✔
353
      local applicationTree = tree
202✔
354
      local cmdName = tree.command
202✔
355
      if #applicationTree ~= #argTypes then
202✔
NEW
356
         SU.error(
×
357
            "Wrong number of arguments ("
NEW
358
               .. #applicationTree
×
NEW
359
               .. ") for command "
×
NEW
360
               .. cmdName
×
NEW
361
               .. " (should be "
×
NEW
362
               .. #argTypes
×
NEW
363
               .. ")"
×
364
         )
365
      end
366
      -- Compile every argument
367
      local compiledArgs = {}
202✔
368
      for i, arg in pairs(applicationTree) do
1,161✔
369
         if type(i) == "number" then
959✔
370
            if argTypes[i] == objType.tree then
151✔
371
               table.insert(compiledArgs, compileToMathML_aux(nil, arg_env, arg))
74✔
372
            else
373
               local x = compileToStr(arg_env, arg)
114✔
374
               table.insert(compiledArgs, x)
114✔
375
            end
376
         else
377
            -- Not an argument but an attribute. Add it to the compiled
378
            -- argument tree as-is
379
            compiledArgs[i] = applicationTree[i]
808✔
380
         end
381
      end
382
      local res = cmdFun(compiledArgs)
202✔
383
      if res.command == "mrow" then
202✔
384
         -- Mark the outer mrow to be unwrapped in the parent
385
         res.id = "wrapper"
90✔
386
      end
387
      return res
202✔
388
   elseif tree.id == "command" and symbols[tree.command] then
170✔
389
      local atom = { id = "atom", [1] = symbols[tree.command] }
53✔
390
      tree = compileToMathML_aux(nil, arg_env, atom)
106✔
391
   elseif tree.id == "argument" then
117✔
392
      if arg_env[tree.index] then
37✔
393
         return arg_env[tree.index]
37✔
394
      else
NEW
395
         SU.error("Argument #" .. tree.index .. " has escaped its scope (probably not fully applied command).")
×
396
      end
397
   end
398
   tree.id = nil
953✔
399
   return tree
953✔
400
end
401

402
local function printMathML (tree)
NEW
403
   if type(tree) == "string" then
×
NEW
404
      return tree
×
405
   end
NEW
406
   local result = "\\" .. tree.command
×
NEW
407
   if tree.options then
×
NEW
408
      local options = {}
×
NEW
409
      for k, v in pairs(tree.options) do
×
NEW
410
         table.insert(options, k .. "=" .. v)
×
411
      end
NEW
412
      if #options > 0 then
×
NEW
413
         result = result .. "[" .. table.concat(options, ", ") .. "]"
×
414
      end
415
   end
NEW
416
   if #tree > 0 then
×
NEW
417
      result = result .. "{"
×
NEW
418
      for _, child in ipairs(tree) do
×
NEW
419
         result = result .. printMathML(child)
×
420
      end
NEW
421
      result = result .. "}"
×
422
   end
NEW
423
   return result
×
424
end
425

426
local function compileToMathML (_, arg_env, tree)
427
   local result = compileToMathML_aux(_, arg_env, tree)
57✔
428
   SU.debug("texmath", function ()
114✔
NEW
429
      return "Resulting MathML: " .. printMathML(result)
×
430
   end)
431
   return result
57✔
432
end
433

434
local function convertTexlike (_, content)
435
   local ret = epnf.parsestring(mathParser, content[1])
57✔
436
   SU.debug("texmath", function ()
114✔
NEW
437
      return "Parsed TeX math: " .. pl.pretty.write(ret)
×
438
   end)
439
   return ret
57✔
440
end
441

442
registerCommand("%", {}, function ()
24✔
443
   return { "%", command = "mo", options = {} }
1✔
444
end)
445
registerCommand("mi", { [1] = objType.str }, function (x)
24✔
446
   return x
91✔
447
end)
448
registerCommand("mo", { [1] = objType.str }, function (x)
24✔
449
   return x
19✔
450
end)
451
registerCommand("mn", { [1] = objType.str }, function (x)
24✔
452
   return x
1✔
453
end)
454

455
compileToMathML(
24✔
456
   nil,
12✔
457
   {},
458
   convertTexlike(nil, {
12✔
NEW
459
      [==[
×
460
  \def{frac}{\mfrac{#1}{#2}}
461
  \def{bi}{\mi[mathvariant=bold-italic]{#1}}
462
  \def{dsi}{\mi[mathvariant=double-struck]{#1}}
463

464
  % Standard spaces gleaned from plain TeX
465
  \def{thinspace}{\mspace[width=thin]}
466
  \def{negthinspace}{\mspace[width=-thin]}
467
  \def{,}{\thinspace}
468
  \def{!}{\negthinspace}
469
  \def{medspace}{\mspace[width=med]}
470
  \def{negmedspace}{\mspace[width=-med]}
471
  \def{>}{\medspace}
472
  \def{thickspace}{\mspace[width=thick]}
473
  \def{negthickspace}{\mspace[width=-thick]}
474
  \def{;}{\thickspace}
475
  \def{enspace}{\mspace[width=1en]}
476
  \def{enskip}{\enspace}
477
  \def{quad}{\mspace[width=1em]}
478
  \def{qquad}{\mspace[width=2em]}
479

480
  % Modulus operator forms
481
  \def{bmod}{\mo{mod}}
482
  \def{pmod}{\quad(\mo{mod} #1)}
483
]==],
484
   })
485
)
486

487
return { convertTexlike, compileToMathML }
12✔
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