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

sile-typesetter / sile / 9302844829

30 May 2024 12:51PM UTC coverage: 74.707% (+1.7%) from 73.034%
9302844829

push

github

web-flow
Merge pull request #1931 from alerque/stylua

156 of 166 new or added lines in 4 files covered. (93.98%)

1 existing line in 1 file now uncovered.

11939 of 15981 relevant lines covered (74.71%)

7260.67 hits per line

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

79.64
/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✔
NEW
37
         local ret = ""
×
NEW
38
         local t = {...}
×
NEW
39
         for _,b in ipairs(t) do
×
NEW
40
         ret = ret .. b
×
41
         end
NEW
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✔
NEW
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✔
NEW
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✔
140
          SU.error("Wrong number of arguments (" .. #body ..
×
141
            ") for command " .. body.command .. " (should be " ..
×
142
            #cmdArgTypes .. ")")
×
143
        else
144
          for i = 1, #cmdArgTypes do
182✔
145
            accumulator = inferArgTypes_aux(accumulator, cmdArgTypes[i], body[i])
110✔
146
          end
147
        end
148
        return accumulator
127✔
149
      elseif body.command == "mi" or body.command == "mo" or body.command == "mn" then
121✔
150
        if #body ~= 1 then
×
151
          SU.error("Wrong number of arguments ("..#body..") for command "..
×
152
            body.command.." (should be 1)")
×
153
        end
154
        accumulator = inferArgTypes_aux(accumulator, objType.str, body[1])
×
155
        return accumulator
×
156
      else
157
        -- Not a macro, recurse on children assuming tree type for all
158
        -- arguments
159
        for _, child in ipairs(body) do
147✔
160
          accumulator = inferArgTypes_aux(accumulator, objType.tree, child)
52✔
161
        end
162
        return accumulator
121✔
163
      end
164
    elseif body.id == "atom" then
422✔
165
      return accumulator
104✔
166
    else
167
      -- Simply recurse on children
168
      for _, child in ipairs(body) do
738✔
169
        accumulator = inferArgTypes_aux(accumulator, typeRequired, child)
840✔
170
      end
171
      return accumulator
318✔
172
    end
173
  else SU.error("invalid argument to inferArgTypes_aux") end
×
174
end
175

176
local inferArgTypes = function (body)
177
  return inferArgTypes_aux({}, objType.tree, body)
235✔
178
end
179

180
local function registerCommand (name, argTypes, func)
181
  commands[name] = { argTypes, func }
283✔
182
end
183

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

198
local function forall (pred, list)
199
  for _,x in ipairs(list) do
57✔
200
    if not pred(x) then return false end
90✔
201
  end
202
  return true
12✔
203
end
204

205
local compileToStr = function (argEnv, mathlist)
206
  if #mathlist == 1 and mathlist.id == "atom" then
114✔
207
    -- List is a single atom
208
    return mathlist[1]
×
209
  elseif #mathlist == 1 and mathlist[1].id == "argument" then
114✔
210
    return argEnv[mathlist[1].index]
3✔
211
  elseif mathlist.id == "argument" then
111✔
212
    return argEnv[mathlist.index]
×
213
  else
214
    local ret = ""
111✔
215
    for _,elt in ipairs(mathlist) do
443✔
216
      if elt.id == "atom" then
332✔
217
        ret = ret .. elt[1]
332✔
218
      elseif elt.id == "command" and symbols[elt.command] then
×
219
        ret = ret .. symbols[elt.command]
×
220
      else
221
        SU.error("Encountered non-character token in command that takes a string")
×
222
      end
223
    end
224
    return ret
111✔
225
  end
226
end
227

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

374
local function printMathML (tree)
375
  if type(tree) == "string" then
×
376
    return tree
×
377
  end
378
  local result = "\\" .. tree.command
×
379
  if tree.options then
×
380
    local options = {}
×
381
    for k,v in pairs(tree.options) do
×
382
      table.insert(options, k .. "=" .. v)
×
383
    end
384
    if #options > 0 then
×
385
      result = result .. "[" .. table.concat(options, ", ") .. "]"
×
386
    end
387
  end
388
  if #tree > 0 then
×
389
    result = result .. "{"
×
390
    for _, child in ipairs(tree) do
×
391
      result = result .. printMathML(child)
×
392
    end
393
    result = result .. "}"
×
394
  end
395
  return result
×
396
end
397

398
local function compileToMathML (_, arg_env, tree)
399
  local result = compileToMathML_aux(_, arg_env, tree)
57✔
400
  SU.debug("texmath", function ()
114✔
401
    return "Resulting MathML: " .. printMathML(result)
×
402
  end)
403
  return result
57✔
404
end
405

406
local function convertTexlike (_, content)
407
  local ret = epnf.parsestring(mathParser, content[1])
57✔
408
  SU.debug("texmath", function ()
114✔
409
    return "Parsed TeX math: " .. pl.pretty.write(ret)
×
410
  end)
411
  return ret
57✔
412
end
413

414
registerCommand("%", {}, function ()
24✔
415
  return { "%", command = "mo", options = {} }
1✔
416
end)
417
registerCommand("mi", { [1] = objType.str }, function (x) return x end)
103✔
418
registerCommand("mo", { [1] = objType.str }, function (x) return x end)
31✔
419
registerCommand("mn", { [1] = objType.str }, function (x) return x end)
13✔
420

421
compileToMathML(nil, {}, convertTexlike(nil, {[==[
24✔
422
  \def{frac}{\mfrac{#1}{#2}}
423
  \def{bi}{\mi[mathvariant=bold-italic]{#1}}
424
  \def{dsi}{\mi[mathvariant=double-struck]{#1}}
425

426
  % Standard spaces gleaned from plain TeX
427
  \def{thinspace}{\mspace[width=thin]}
428
  \def{negthinspace}{\mspace[width=-thin]}
429
  \def{,}{\thinspace}
430
  \def{!}{\negthinspace}
431
  \def{medspace}{\mspace[width=med]}
432
  \def{negmedspace}{\mspace[width=-med]}
433
  \def{>}{\medspace}
434
  \def{thickspace}{\mspace[width=thick]}
435
  \def{negthickspace}{\mspace[width=-thick]}
436
  \def{;}{\thickspace}
437
  \def{enspace}{\mspace[width=1en]}
438
  \def{enskip}{\enspace}
439
  \def{quad}{\mspace[width=1em]}
440
  \def{qquad}{\mspace[width=2em]}
441

442
  % Modulus operator forms
443
  \def{bmod}{\mo{mod}}
444
  \def{pmod}{\quad(\mo{mod} #1)}
445
]==]}))
446

447
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