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

sile-typesetter / sile / 11534409649

26 Oct 2024 07:27PM UTC coverage: 33.196% (-28.7%) from 61.897%
11534409649

push

github

alerque
chore(tooling): Update editor-config key for stylua as accepted upstream

Our setting addition is still not in a tagged release, but the PR was
accepted into the default branch of stylua. This means you no longer
need to run my fork of Stylua to get this project's style, you just nead
any build from the main development branch. However the config key was
renamed as part of the acceptance, so this is the relevant adjustment.

5810 of 17502 relevant lines covered (33.2%)

1300.57 hits per line

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

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

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

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

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

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

134
local mathParser = epnf.define(mathGrammar)
×
135

136
local commands = {}
×
137

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

143
local objType = {
×
144
   tree = 1,
145
   str = 2,
146
}
147

148
local function inferArgTypes_aux (accumulator, typeRequired, body)
149
   if type(body) == "table" then
×
150
      if body.id == "argument" then
×
151
         local ret = accumulator
×
152
         table.insert(ret, body.index, typeRequired)
×
153
         return ret
×
154
      elseif body.id == "command" then
×
155
         if commands[body.command] then
×
156
            local cmdArgTypes = commands[body.command][1]
×
157
            if #cmdArgTypes ~= #body then
×
158
               SU.error(
×
159
                  "Wrong number of arguments ("
160
                     .. #body
×
161
                     .. ") for command "
×
162
                     .. body.command
×
163
                     .. " (should be "
×
164
                     .. #cmdArgTypes
×
165
                     .. ")"
×
166
               )
167
            else
168
               for i = 1, #cmdArgTypes do
×
169
                  accumulator = inferArgTypes_aux(accumulator, cmdArgTypes[i], body[i])
×
170
               end
171
            end
172
            return accumulator
×
173
         elseif body.command == "mi" or body.command == "mo" or body.command == "mn" then
×
174
            if #body ~= 1 then
×
175
               SU.error("Wrong number of arguments (" .. #body .. ") for command " .. body.command .. " (should be 1)")
×
176
            end
177
            accumulator = inferArgTypes_aux(accumulator, objType.str, body[1])
×
178
            return accumulator
×
179
         else
180
            -- Not a macro, recurse on children assuming tree type for all
181
            -- arguments
182
            for _, child in ipairs(body) do
×
183
               accumulator = inferArgTypes_aux(accumulator, objType.tree, child)
×
184
            end
185
            return accumulator
×
186
         end
187
      elseif body.id == "atom" then
×
188
         return accumulator
×
189
      else
190
         -- Simply recurse on children
191
         for _, child in ipairs(body) do
×
192
            accumulator = inferArgTypes_aux(accumulator, typeRequired, child)
×
193
         end
194
         return accumulator
×
195
      end
196
   else
197
      SU.error("invalid argument to inferArgTypes_aux")
×
198
   end
199
end
200

201
local inferArgTypes = function (body)
202
   return inferArgTypes_aux({}, objType.tree, body)
×
203
end
204

205
local function registerCommand (name, argTypes, func)
206
   commands[name] = { argTypes, func }
×
207
end
208

209
-- Computes func(func(... func(init, k1, v1), k2, v2)..., k_n, v_n), i.e. applies
210
-- func on every key-value pair in the table. Keys with numeric indices are
211
-- processed in order. This is an important property for MathML compilation below.
212
local function fold_pairs (func, table)
213
   local accumulator = {}
×
214
   for k, v in pl.utils.kpairs(table) do
×
215
      accumulator = func(v, k, accumulator)
×
216
   end
217
   for i, v in ipairs(table) do
×
218
      accumulator = func(v, i, accumulator)
×
219
   end
220
   return accumulator
×
221
end
222

223
local function forall (pred, list)
224
   for _, x in ipairs(list) do
×
225
      if not pred(x) then
×
226
         return false
×
227
      end
228
   end
229
   return true
×
230
end
231

232
local compileToStr = function (argEnv, mathlist)
233
   if #mathlist == 1 and mathlist.id == "atom" then
×
234
      -- List is a single atom
235
      return mathlist[1]
×
236
   elseif #mathlist == 1 and mathlist[1].id == "argument" then
×
237
      return argEnv[mathlist[1].index]
×
238
   elseif mathlist.id == "argument" then
×
239
      return argEnv[mathlist.index]
×
240
   else
241
      local ret = ""
×
242
      for _, elt in ipairs(mathlist) do
×
243
         if elt.id == "atom" then
×
244
            ret = ret .. elt[1]
×
245
         elseif elt.id == "command" and symbols[elt.command] then
×
246
            ret = ret .. symbols[elt.command]
×
247
         else
248
            SU.error("Encountered non-character token in command that takes a string")
×
249
         end
250
      end
251
      return ret
×
252
   end
253
end
254

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

420
local function printMathML (tree)
421
   if type(tree) == "string" then
×
422
      return tree
×
423
   end
424
   local result = "\\" .. tree.command
×
425
   if tree.options then
×
426
      local options = {}
×
427
      for k, v in pairs(tree.options) do
×
428
         table.insert(options, k .. "=" .. v)
×
429
      end
430
      if #options > 0 then
×
431
         result = result .. "[" .. table.concat(options, ", ") .. "]"
×
432
      end
433
   end
434
   if #tree > 0 then
×
435
      result = result .. "{"
×
436
      for _, child in ipairs(tree) do
×
437
         result = result .. printMathML(child)
×
438
      end
439
      result = result .. "}"
×
440
   end
441
   return result
×
442
end
443

444
local function compileToMathML (_, arg_env, tree)
445
   local result = compileToMathML_aux(_, arg_env, tree)
×
446
   SU.debug("texmath", function ()
×
447
      return "Resulting MathML: " .. printMathML(result)
×
448
   end)
449
   return result
×
450
end
451

452
local function convertTexlike (_, content)
453
   local ret = epnf.parsestring(mathParser, content[1])
×
454
   SU.debug("texmath", function ()
×
455
      return "Parsed TeX math: " .. pl.pretty.write(ret)
×
456
   end)
457
   return ret
×
458
end
459

460
registerCommand("%", {}, function ()
×
461
   return { "%", command = "mo", options = {} }
×
462
end)
463
registerCommand("mi", { [1] = objType.str }, function (x)
×
464
   return x
×
465
end)
466
registerCommand("mo", { [1] = objType.str }, function (x)
×
467
   return x
×
468
end)
469
registerCommand("mn", { [1] = objType.str }, function (x)
×
470
   return x
×
471
end)
472

473
compileToMathML(
×
474
   nil,
475
   {},
476
   convertTexlike(nil, {
×
477
      [==[
×
478
  \def{frac}{\mfrac{#1}{#2}}
479
  \def{sqrt}{\msqrt{#1}}
480
  \def{bi}{\mi[mathvariant=bold-italic]{#1}}
481
  \def{dsi}{\mi[mathvariant=double-struck]{#1}}
482

483
  % Standard spaces gleaned from plain TeX
484
  \def{thinspace}{\mspace[width=thin]}
485
  \def{negthinspace}{\mspace[width=-thin]}
486
  \def{,}{\thinspace}
487
  \def{!}{\negthinspace}
488
  \def{medspace}{\mspace[width=med]}
489
  \def{negmedspace}{\mspace[width=-med]}
490
  \def{>}{\medspace}
491
  \def{thickspace}{\mspace[width=thick]}
492
  \def{negthickspace}{\mspace[width=-thick]}
493
  \def{;}{\thickspace}
494
  \def{enspace}{\mspace[width=1en]}
495
  \def{enskip}{\enspace}
496
  \def{quad}{\mspace[width=1em]}
497
  \def{qquad}{\mspace[width=2em]}
498

499
  % Modulus operator forms
500
  \def{bmod}{\mo{mod}}
501
  \def{pmod}{\quad(\mo{mod} #1)}
502
]==],
503
   })
504
)
505

506
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