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

sile-typesetter / sile / 12179437494

05 Dec 2024 12:08PM CUT coverage: 43.501% (-17.2%) from 60.747%
12179437494

push

github

web-flow
Merge 6dc7e6d83 into 154f441e0

6 of 9 new or added lines in 3 files covered. (66.67%)

3214 existing lines in 31 files now uncovered.

8832 of 20303 relevant lines covered (43.5%)

2991.63 hits per line

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

0.0
/packages/math/typesetter.lua
1
-- Interpret a MathML or TeX-like AST, typeset it and add it to the output.
UNCOV
2
local lpeg = require("lpeg")
×
UNCOV
3
local atoms = require("packages.math.atoms")
×
UNCOV
4
local b = require("packages.math.base-elements")
×
UNCOV
5
local syms = require("packages.math.unicode-symbols")
×
UNCOV
6
local mathvariants = require("packages.math.unicode-mathvariants")
×
UNCOV
7
local mathVariantToScriptType, scriptType = mathvariants.mathVariantToScriptType, mathvariants.scriptType
×
8

9
local ConvertMathML
10

11
-- See MathML Core "Algorithm for determining the form of an embellished operator"
UNCOV
12
local scriptedElements = {
×
13
   mmultiscripts = true,
14
   mover = true,
15
   msub = true,
16
   msubsup = true,
17
   msup = true,
18
   munder = true,
19
   munderover = true,
20
}
UNCOV
21
local groupingElements = {
×
22
   maction = true,
23
   math = true,
24
   merror = true,
25
   mphantom = true,
26
   mprescripts = true,
27
   mrow = true,
28
   mstyle = true,
29
   semantics = true,
30
}
UNCOV
31
local spaceLikeElements = {
×
32
   mtext = true,
33
   mspace = true,
34
}
35

36
-- Space like elements are:
37
-- an mtext or mspace;
38
-- or a grouping element or mpadded all of whose in-flow children are space-like.
39
local function isSpaceLike (tree)
UNCOV
40
   if spaceLikeElements[tree.command] then
×
UNCOV
41
      return true
×
42
   end
UNCOV
43
   if groupingElements[tree.command] or tree.command == "mpadded" then
×
UNCOV
44
      for _, n in ipairs(tree) do
×
UNCOV
45
         if not isSpaceLike(n) then
×
UNCOV
46
            return false
×
47
         end
48
      end
UNCOV
49
      return true
×
50
   end
51
end
52
-- Grouping-like elements in this operator embellishing context are:
53
-- a grouping element
54
-- or an mpadded or msqrt element.
55
local isGroupingLike = function (tree)
UNCOV
56
   return groupingElements[tree.command] or tree.command == "mpadded" or tree.command == "msqrt"
×
57
end
58

59
local function embellishOperatorInPlace (tree)
60
   local lastChild
61
   local lastMo
UNCOV
62
   local groupLike = isGroupingLike(tree)
×
UNCOV
63
   local scripLike = scriptedElements[tree.command]
×
UNCOV
64
   for _, n in ipairs(tree) do
×
65
      -- FIXME Maybe ncomplete vs. "core form" (of other elements) in MathML Core
66
      -- This specification would make anyone's eyes bleed :D
UNCOV
67
      if n.command == "mo" then
×
UNCOV
68
         lastMo = n
×
UNCOV
69
         n.options.form = n.options.form
×
UNCOV
70
            or groupLike and not lastChild and "prefix" -- first-in-flow child
×
UNCOV
71
            or scripLike and lastChild and "postfix" -- last-in-flow child of a scripted element other than the first
×
UNCOV
72
            or nil
×
73
      end
UNCOV
74
      if n.command and not isSpaceLike(n) then
×
UNCOV
75
         lastChild = n
×
76
      end
77
   end
UNCOV
78
   if lastMo then
×
UNCOV
79
      lastMo.options.form = lastMo.options.form
×
UNCOV
80
         or groupLike and lastMo == lastChild and "postfix" -- last-in-flow child
×
UNCOV
81
         or nil
×
82
   end
83
end
84

85
local function convertChildren (tree)
UNCOV
86
   local mboxes = {}
×
UNCOV
87
   embellishOperatorInPlace(tree)
×
UNCOV
88
   for _, n in ipairs(tree) do
×
UNCOV
89
      local box = ConvertMathML(nil, n)
×
UNCOV
90
      if box then
×
UNCOV
91
         table.insert(mboxes, box)
×
92
      end
93
   end
UNCOV
94
   return mboxes
×
95
end
96

97
local function convertFirstChild (tree)
98
   -- We need to loop until the first non-nil box is found, because
99
   -- we may have blank lines in the tree.
100
   for _, n in ipairs(tree) do
×
101
      local box = ConvertMathML(nil, n)
×
102
      if box then
×
103
         return box
×
104
      end
105
   end
106
end
107

108
-- convert MathML into mbox
UNCOV
109
function ConvertMathML (_, content)
×
UNCOV
110
   if content == nil or content.command == nil then
×
UNCOV
111
      return nil
×
112
   end
UNCOV
113
   if content.command == "math" or content.command == "mathml" then -- toplevel
×
UNCOV
114
      return b.stackbox("H", convertChildren(content))
×
UNCOV
115
   elseif content.command == "mrow" then
×
UNCOV
116
      local ret = b.stackbox("H", convertChildren(content))
×
117
      -- Internal property to keep tracks or paired open/close in TeX-like syntax
UNCOV
118
      ret.is_paired = content.is_paired
×
UNCOV
119
      return ret
×
UNCOV
120
   elseif content.command == "mphantom" then
×
121
      local special = content.options.special
×
122
      return b.phantom(convertChildren(content), special)
×
UNCOV
123
   elseif content.command == "mi" then
×
UNCOV
124
      local script = content.options.mathvariant and mathVariantToScriptType(content.options.mathvariant)
×
UNCOV
125
      local text = content[1]
×
UNCOV
126
      if type(text) ~= "string" then
×
127
         SU.error("mi command contains content which is not text")
×
128
      end
UNCOV
129
      script = script or (luautf8.len(text) == 1 and scriptType.italic or scriptType.upright)
×
UNCOV
130
      return b.text("identifier", {}, script, text)
×
UNCOV
131
   elseif content.command == "mo" then
×
UNCOV
132
      content.options.form = content.options.form or "infix"
×
UNCOV
133
      local script = content.options.mathvariant and mathVariantToScriptType(content.options.mathvariant)
×
UNCOV
134
         or scriptType.upright
×
UNCOV
135
      local text = content[1]
×
UNCOV
136
      local attributes = {}
×
137
      -- Attributes from the (default) operator table
UNCOV
138
      if syms.operatorDict[text] then
×
UNCOV
139
         attributes.atom = syms.operatorDict[text].atom
×
UNCOV
140
         local forms = syms.operatorDict[text].forms
×
UNCOV
141
         local defaultOps = forms and (forms[content.options.form] or forms.infix or forms.prefix or forms.postfix)
×
UNCOV
142
         if defaultOps then
×
UNCOV
143
            for attribute, value in pairs(defaultOps) do
×
UNCOV
144
               attributes[attribute] = value
×
145
            end
146
         end
147
      end
148
      -- Overwrite with attributes from the element
UNCOV
149
      for attribute, value in pairs(content.options) do
×
UNCOV
150
         attributes[attribute] = value
×
151
      end
UNCOV
152
      if content.options.atom then
×
UNCOV
153
         if not atoms.types[content.options.atom] then
×
154
            SU.error("Unknown atom type " .. content.options.atom)
×
155
         else
UNCOV
156
            attributes.atom = atoms.types[content.options.atom]
×
157
         end
158
      end
UNCOV
159
      if type(text) ~= "string" then
×
160
         SU.error("mo command contains content which is not text")
×
161
      end
UNCOV
162
      local cp = text and luautf8.len(text) == 1 and luautf8.codepoint(text, 1)
×
UNCOV
163
      if cp and cp >= 0x2061 and cp <= 0x2064 then
×
164
         -- "Invisible operators"
165
         --  - Several test cases in Joe Javawaski's Browser Test and the
166
         --  - The MathML Test Suite use these too, with ad hoc spacing attributes.
167
         -- MathML Core doesn't mention anything special about these.
168
         -- MathML4 §8.3: "They are especially important new additions to the UCS
169
         -- because they provide textual clues which can increase the quality of
170
         -- print rendering (...)" (Note the absence of indication on how "print"
171
         -- rendering is supposed to be improved.)
172
         -- MathML4 §3.1.1: "they usually render invisibly (...) but may influence
173
         -- visual spacing." (Note the ill-defined "usually" and "may" in this
174
         -- specification.)
175
         -- The best we can do is to suppress these operators, but handle any
176
         -- explicitspacing (despite not handling rsup/rspace attributes on other
177
         -- operators in our TeX-based spacing logic).
178
         -- stylua: ignore start
179
         local number = lpeg.R("09")^0  * (lpeg.P(".")^-1 * lpeg.R("09")^1)^0 / tonumber
×
180
         -- stylua: ignore end
181
         -- 0 something is 0 in whatever unit (ex. "0", "0mu", "0em" etc.)
182
         local rspace, lspace = number:match(attributes.rspace), number:match(attributes.lspace)
×
183
         if rspace == 0 and lspace == 0 then
×
184
            return nil -- Just skip the invisible operator.
×
185
         end
186
         -- Skip it but honor the non-zero spacing.
187
         if rspace == 0 then
×
188
            return b.space(attributes.lspace, 0, 0)
×
189
         end
190
         if lspace == 0 then
×
191
            return b.space(attributes.rspace, 0, 0)
×
192
         end
193
         -- I haven't found examples of invisible operators with both rspace and lspace set,
194
         -- but it may happen, whatever spaces around something invisible mean.
195
         -- We'll just stack the spaces in this case (as we can only return one box).
196
         return b.stackbox("H", { b.space(attributes.lspace, 0, 0), b.space(attributes.rspace, 0, 0) })
×
197
      end
UNCOV
198
      return b.text("operator", attributes, script, text)
×
UNCOV
199
   elseif content.command == "mn" then
×
UNCOV
200
      local script = content.options.mathvariant and mathVariantToScriptType(content.options.mathvariant)
×
UNCOV
201
         or scriptType.upright
×
UNCOV
202
      local text = content[1]
×
UNCOV
203
      if type(text) ~= "string" then
×
204
         SU.error("mn command contains content which is not text")
×
205
      end
UNCOV
206
      if string.sub(text, 1, 1) == "-" then
×
UNCOV
207
         text = "−" .. string.sub(text, 2)
×
208
      end
UNCOV
209
      return b.text("number", {}, script, text)
×
UNCOV
210
   elseif content.command == "mspace" then
×
UNCOV
211
      return b.space(content.options.width, content.options.height, content.options.depth)
×
UNCOV
212
   elseif content.command == "msub" then
×
UNCOV
213
      local children = convertChildren(content)
×
UNCOV
214
      if #children ~= 2 then
×
215
         SU.error("Wrong number of children in msub")
×
216
      end
UNCOV
217
      return b.newSubscript({ base = children[1], sub = children[2] })
×
UNCOV
218
   elseif content.command == "msup" then
×
UNCOV
219
      local children = convertChildren(content)
×
UNCOV
220
      if #children ~= 2 then
×
221
         SU.error("Wrong number of children in msup")
×
222
      end
UNCOV
223
      return b.newSubscript({ base = children[1], sup = children[2] })
×
UNCOV
224
   elseif content.command == "msubsup" then
×
UNCOV
225
      local children = convertChildren(content)
×
UNCOV
226
      if #children ~= 3 then
×
227
         SU.error("Wrong number of children in msubsup")
×
228
      end
UNCOV
229
      return b.newSubscript({ base = children[1], sub = children[2], sup = children[3] })
×
UNCOV
230
   elseif content.command == "munder" then
×
UNCOV
231
      local children = convertChildren(content)
×
UNCOV
232
      if #children ~= 2 then
×
233
         SU.error("Wrong number of children in munder")
×
234
      end
UNCOV
235
      return b.newUnderOver({ base = children[1], sub = children[2] })
×
UNCOV
236
   elseif content.command == "mover" then
×
UNCOV
237
      local children = convertChildren(content)
×
UNCOV
238
      if #children ~= 2 then
×
239
         SU.error("Wrong number of children in mover")
×
240
      end
UNCOV
241
      return b.newUnderOver({ base = children[1], sup = children[2] })
×
UNCOV
242
   elseif content.command == "munderover" then
×
UNCOV
243
      local children = convertChildren(content)
×
UNCOV
244
      if #children ~= 3 then
×
245
         SU.error("Wrong number of children in munderover")
×
246
      end
UNCOV
247
      return b.newUnderOver({ base = children[1], sub = children[2], sup = children[3] })
×
UNCOV
248
   elseif content.command == "mfrac" then
×
UNCOV
249
      local children = convertChildren(content)
×
UNCOV
250
      if #children ~= 2 then
×
251
         SU.error("Wrong number of children in mfrac: " .. #children)
×
252
      end
UNCOV
253
      return SU.boolean(content.options.bevelled, false)
×
UNCOV
254
            and b.bevelledFraction(content.options, children[1], children[2])
×
UNCOV
255
         or b.fraction(content.options, children[1], children[2])
×
UNCOV
256
   elseif content.command == "msqrt" then
×
257
      local children = convertChildren(content)
×
258
      -- "The <msqrt> element generates an anonymous <mrow> box called the msqrt base
259
      return b.sqrt(b.stackbox("H", children))
×
UNCOV
260
   elseif content.command == "mroot" then
×
261
      local children = convertChildren(content)
×
262
      return b.sqrt(children[1], children[2])
×
UNCOV
263
   elseif content.command == "mtable" or content.command == "table" then
×
UNCOV
264
      local children = convertChildren(content)
×
UNCOV
265
      return b.table(children, content.options)
×
UNCOV
266
   elseif content.command == "mtr" then
×
UNCOV
267
      return b.mtr(convertChildren(content))
×
UNCOV
268
   elseif content.command == "mtd" then
×
UNCOV
269
      return b.stackbox("H", convertChildren(content))
×
UNCOV
270
   elseif content.command == "mtext" or content.command == "ms" then
×
UNCOV
271
      if #content > 1 then
×
272
         SU.error("Wrong number of children in " .. content.command .. ": " .. #content)
×
273
      end
UNCOV
274
      local text = content[1] or "" -- empty mtext is allowed, and found in examples...
×
UNCOV
275
      if type(text) ~= "string" then
×
276
         SU.error(content.command .. " command contains content which is not text")
×
277
      end
278
      -- MathML Core 3.2.1.1 Layout of <mtext> has some wording about forced line breaks
279
      -- and soft wrap opportunities: ignored here.
280
      -- There's also some explanations about CSS, italic correction etc. which we ignore too.
UNCOV
281
      text = text:gsub("[\n\r]", " ")
×
UNCOV
282
      return b.text("string", {}, scriptType.upright, text:gsub("%s+", " "))
×
283
   elseif content.command == "maction" then
×
284
      -- MathML Core 3.6: display as mrow, ignoring all but the first child
285
      return b.stackbox("H", { convertFirstChild(content) })
×
286
   elseif content.command == "mstyle" then
×
287
      -- It's an mrow, but with some style attributes that we ignore.
288
      SU.warn("MathML mstyle is not fully supported yet")
×
289
      return b.stackbox("H", convertChildren(content))
×
290
   elseif content.command == "mpadded" then
×
291
      -- MathML Core 3.3.6.1: The <mpadded> element generates an anonymous <mrow> box
292
      -- called the "impadded inner box"
293
      return b.padded(content.options, b.stackbox("H", convertChildren(content)))
×
294
   else
295
      SU.error("Unknown math command " .. content.command)
×
296
   end
297
end
298

299
local function handleMath (_, mbox, options)
UNCOV
300
   local mode = options and options.mode or "text"
×
UNCOV
301
   local counter = SU.boolean(options.numbered, false) and "equation"
×
UNCOV
302
   counter = options.counter or counter -- overrides the default "equation" counter
×
303

UNCOV
304
   if mode == "display" then
×
UNCOV
305
      mbox.mode = b.mathMode.display
×
UNCOV
306
   elseif mode == "text" then
×
UNCOV
307
      mbox.mode = b.mathMode.textCramped
×
308
   else
309
      SU.error("Unknown math mode " .. mode)
×
310
   end
311

UNCOV
312
   SU.debug("math", function ()
×
313
      return "Resulting mbox: " .. tostring(mbox)
×
314
   end)
UNCOV
315
   mbox:styleDescendants()
×
UNCOV
316
   mbox:shapeTree()
×
317

UNCOV
318
   if mode == "display" then
×
319
      -- See https://github.com/sile-typesetter/sile/issues/2160
320
      --    We are not exactly doing the right things here with respect to
321
      --    paragraphing expectations.
322
      -- The vertical penalty will flush the previous paragraph, if any.
UNCOV
323
      SILE.call("penalty", { penalty = SILE.settings:get("math.predisplaypenalty"), vertical = true })
×
UNCOV
324
      SILE.typesetter:pushExplicitVglue(SILE.settings:get("math.displayskip"))
×
325
      -- Repeating the penalty after the skip does not hurt but should not be
326
      -- necessary if our page builder did its stuff correctly.
UNCOV
327
      SILE.call("penalty", { penalty = SILE.settings:get("math.predisplaypenalty"), vertical = true })
×
UNCOV
328
      SILE.settings:temporarily(function ()
×
329
         -- Center the equation in the space available up to the counter (if any),
330
         -- respecting the fixed part of the left and right skips.
UNCOV
331
         local lskip = SILE.settings:get("document.lskip") or SILE.types.node.glue()
×
UNCOV
332
         local rskip = SILE.settings:get("document.rskip") or SILE.types.node.glue()
×
UNCOV
333
         SILE.settings:set("document.parindent", SILE.types.node.glue())
×
UNCOV
334
         SILE.settings:set("current.parindent", SILE.types.node.glue())
×
UNCOV
335
         SILE.settings:set("document.lskip", SILE.types.node.hfillglue(lskip.width.length))
×
UNCOV
336
         SILE.settings:set("document.rskip", SILE.types.node.glue(rskip.width.length))
×
UNCOV
337
         SILE.settings:set("typesetter.parfillskip", SILE.types.node.glue())
×
UNCOV
338
         SILE.settings:set("document.spaceskip", SILE.types.length("1spc", 0, 0))
×
UNCOV
339
         SILE.typesetter:pushHorizontal(mbox)
×
UNCOV
340
         SILE.typesetter:pushExplicitGlue(SILE.types.node.hfillglue())
×
UNCOV
341
         if counter then
×
342
            options.counter = counter
×
343
            SILE.call("increment-counter", { id = counter })
×
344
            SILE.call("math:numberingstyle", options)
×
UNCOV
345
         elseif options.number then
×
346
            SILE.call("math:numberingstyle", options)
×
347
         end
348
         -- The vertical penalty will flush the equation.
349
         -- It must be done in the temporary settings block, because these have
350
         -- to apply as line boxes are being built.
UNCOV
351
         SILE.call("penalty", { penalty = SILE.settings:get("math.postdisplaypenalty"), vertical = true })
×
352
      end)
UNCOV
353
      SILE.typesetter:pushExplicitVglue(SILE.settings:get("math.displayskip"))
×
354
      -- Repeating: Same remark as for the predisplay penalty above.
UNCOV
355
      SILE.call("penalty", { penalty = SILE.settings:get("math.postdisplaypenalty"), vertical = true })
×
356
   else
UNCOV
357
      SILE.typesetter:pushHorizontal(mbox)
×
358
   end
359
end
360

UNCOV
361
return { ConvertMathML, handleMath }
×
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

© 2024 Coveralls, Inc