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

sile-typesetter / sile / 6934957716

20 Nov 2023 07:35PM UTC coverage: 57.468% (-3.2%) from 60.703%
6934957716

push

github

web-flow
Merge c91d9a7d4 into 34e2e5335

60 of 79 new or added lines in 1 file covered. (75.95%)

717 existing lines in 27 files now uncovered.

8957 of 15586 relevant lines covered (57.47%)

5715.38 hits per line

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

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

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

7
local atomType = syms.atomType
6✔
8
local symbolDefaults = syms.symbolDefaults
6✔
9
local symbols = syms.symbols
6✔
10

11
-- Grammar to parse TeX-like math
12
-- luacheck: push ignore
13
---@diagnostic disable: undefined-global, unused-local, lowercase-global
14
local mathGrammar = function (_ENV)
15
  local _ = WS^0
6✔
16
  local eol = S"\r\n"
6✔
17
  local digit = R("09")
6✔
18
  local natural = digit^1 / tostring
6✔
19
  local pos_natural = R("19") * digit^0 / tonumber
6✔
20
  local ctrl_word = R("AZ", "az")^1
6✔
21
  local ctrl_symbol = P(1) - S"{}\\"
6✔
22
  local ctrl_sequence_name = C(ctrl_word + ctrl_symbol) / 1
6✔
23
  local comment = (
24
      P"%" *
6✔
25
      P(1-eol)^0 *
6✔
26
      eol^-1
6✔
27
    )
28
  local utf8cont = R("\128\191")
6✔
29
  local utf8code = lpeg.R("\0\127")
6✔
30
    + lpeg.R("\194\223") * utf8cont
6✔
31
    + lpeg.R("\224\239") * utf8cont * utf8cont
6✔
32
    + lpeg.R("\240\244") * utf8cont * utf8cont * utf8cont
6✔
33
  -- Identifiers inside \mo and \mi tags
34
  local sileID = C(bits.identifier + P(1)) / 1
6✔
35
  local mathMLID = (utf8code - S"\\{}%")^1 / function (...)
6✔
36
      local ret = ""
×
37
      local t = {...}
×
38
      for _,b in ipairs(t) do
×
39
        ret = ret .. b
×
40
      end
41
      return ret
×
42
    end
43
  local group = P"{" * V"mathlist" * (P"}" + E("`}` expected"))
12✔
44
  local element_no_infix =
45
    V"def" +
6✔
46
    V"command" +
6✔
47
    group +
6✔
48
    V"argument" +
6✔
49
    V"atom"
6✔
50
  local element =
51
    V"supsub" +
6✔
52
    V"subsup" +
6✔
53
    V"sup" +
6✔
54
    V"sub" +
6✔
55
    element_no_infix
6✔
56
  local sep = S",;" * _
6✔
57
  local quotedString = (P'"' * C((1-P'"')^1) * P'"')
6✔
58
  local value = ( quotedString + (1-S",;]")^1 )
6✔
59
  local pair = Cg(sileID * _ * "=" * _ * C(value)) * sep^-1 / function (...)
6✔
60
      local t = {...}; return t[1], t[#t]
84✔
61
    end
62
  local list = Cf(Ct"" * pair^0, rawset)
6✔
63
  local parameters = (
64
      P"[" *
6✔
65
      list *
6✔
66
      P"]"
6✔
67
    )^-1 / function (a)
6✔
68
        return type(a)=="table" and a or {}
198✔
69
      end
70

71
  local dim2_arg_inner = Ct(V"mathlist" * (P"&" * V"mathlist")^0) /
6✔
72
    function (t)
UNCOV
73
      t.id = "mathlist"
×
UNCOV
74
      return t
×
75
    end
76
  local dim2_arg =
77
    Cg(P"{" *
12✔
78
       dim2_arg_inner *
6✔
79
       (P"\\\\" * dim2_arg_inner)^1 *
6✔
80
       (P"}" + E("`}` expected"))
12✔
81
      ) / function (...)
×
UNCOV
82
        local t = {...}
×
83
        -- Remove the last mathlist if empty. This way,
84
        -- `inner1 \\ inner2 \\` is the same as `inner1 \\ inner2`.
UNCOV
85
        if not t[#t][1] or not t[#t][1][1] then table.remove(t) end
×
UNCOV
86
        return table.unpack(t)
×
87
      end
88

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

115
local mathParser = epnf.define(mathGrammar)
6✔
116

117
local commands = {}
6✔
118

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

124
local objType = {
6✔
125
  tree = 1,
126
  str = 2
×
127
}
128

129
local function inferArgTypes_aux (accumulator, typeRequired, body)
130
  if type(body) == "table" then
348✔
131
    if body.id == "argument" then
348✔
132
      local ret = accumulator
30✔
133
      table.insert(ret, body.index, typeRequired)
30✔
134
      return ret
30✔
135
    elseif body.id == "command" then
318✔
136
      if commands[body.command] then
120✔
137
        local cmdArgTypes = commands[body.command][1]
60✔
138
        if #cmdArgTypes ~= #body then
60✔
139
          SU.error("Wrong number of arguments (" .. #body ..
×
140
            ") for command " .. body.command .. " (should be " ..
×
141
            #cmdArgTypes .. ")")
×
142
        else
143
          for i = 1, #cmdArgTypes do
84✔
144
            accumulator = inferArgTypes_aux(accumulator, cmdArgTypes[i], body[i])
48✔
145
          end
146
        end
147
        return accumulator
60✔
148
      elseif body.command == "mi" or body.command == "mo" or body.command == "mn" then
60✔
149
        if #body ~= 1 then
×
150
          SU.error("Wrong number of arguments ("..#body..") for command "..
×
151
            body.command.." (should be 1)")
×
152
        end
153
        accumulator = inferArgTypes_aux(accumulator, objType.str, body[1])
×
154
        return accumulator
×
155
      else
156
        -- Not a macro, recurse on children assuming tree type for all
157
        -- arguments
158
        for _, child in ipairs(body) do
72✔
159
          accumulator = inferArgTypes_aux(accumulator, objType.tree, child)
24✔
160
        end
161
        return accumulator
60✔
162
      end
163
    elseif body.id == "atom" then
198✔
164
      return accumulator
48✔
165
    else
166
      -- Simply recurse on children
167
      for _, child in ipairs(body) do
348✔
168
        accumulator = inferArgTypes_aux(accumulator, typeRequired, child)
396✔
169
      end
170
      return accumulator
150✔
171
    end
172
  else SU.error("invalid argument to inferArgTypes_aux") end
×
173
end
174

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

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

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

197
local function forall (pred, list)
198
  for _,x in ipairs(list) do
19✔
199
    if not pred(x) then return false end
26✔
200
  end
201
  return true
6✔
202
end
203

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

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

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

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

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

413
registerCommand("%", {}, function ()
12✔
414
  return { "%", command = "mo", options = {} }
×
415
end)
416
registerCommand("mi", { [1] = objType.str }, function (x) return x end)
22✔
417
registerCommand("mo", { [1] = objType.str }, function (x) return x end)
10✔
418
registerCommand("mn", { [1] = objType.str }, function (x) return x end)
6✔
419

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

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

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

446
return { convertTexlike, compileToMathML }
6✔
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