Skip to content

Commit 81a1be5

Browse files
Omikhleiaalerque
authored andcommitted
fix(math): Spacing rules must distinguish binary and unary operators
1 parent 3dd25e9 commit 81a1be5

1 file changed

Lines changed: 84 additions & 3 deletions

File tree

packages/math/base-elements.lua

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,41 +297,84 @@ local spaceKind = {
297297
thick = "thick",
298298
}
299299

300-
-- Indexed by left atom
300+
-- Spacing table indexed by left atom, as in TeXbook p. 170.
301+
-- Notes
302+
-- - the "notScript" key is used to prevent spaces in script and scriptscript modes
303+
-- (= parenthesized non-zero value in The TeXbook's table).
304+
-- - Cases commented are as expected, just listed for clarity and completeness.
305+
-- (= no space i.e. 0 in in The TeXbook's table)
306+
-- - Cases marked as impossible are not expected to happen (= stars in the TeXbook):
307+
-- "... such cases never arise, because binary atoms must be preceded and followed
308+
-- by atoms compatible with the nature of binary operations."
309+
-- This must be understood with the context explained onp. 133:
310+
-- "... binary operations are treated as ordinary symbols if they don’t occur
311+
-- between two quantities that they can operate on." (a rule which notably helps
312+
-- addressing binary atoms used as unary operators.)
301313
local spacingRules = {
302314
[atomType.ordinary] = {
315+
-- [atomType.ordinary] = nil
303316
[atomType.bigOperator] = { spaceKind.thin },
304317
[atomType.binaryOperator] = { spaceKind.med, notScript = true },
305318
[atomType.relationalOperator] = { spaceKind.thick, notScript = true },
319+
-- [atomType.openingSymbol] = nil
320+
-- [atomType.closeSymbol] = nil
321+
-- [atomType.punctuationSymbol] = nil
306322
[atomType.inner] = { spaceKind.thin, notScript = true },
307323
},
308324
[atomType.bigOperator] = {
309325
[atomType.ordinary] = { spaceKind.thin },
310326
[atomType.bigOperator] = { spaceKind.thin },
327+
[atomType.binaryOperator] = { impossible = true },
311328
[atomType.relationalOperator] = { spaceKind.thick, notScript = true },
329+
-- [atomType.openingSymbol] = nil
330+
-- [atomType.closeSymbol] = nil
331+
-- [atomType.punctuationSymbol] = nil
312332
[atomType.inner] = { spaceKind.thin, notScript = true },
313333
},
314334
[atomType.binaryOperator] = {
315335
[atomType.ordinary] = { spaceKind.med, notScript = true },
316336
[atomType.bigOperator] = { spaceKind.med, notScript = true },
337+
[atomType.binaryOperator] = { impossible = true },
338+
[atomType.relationalOperator] = { impossible = true },
317339
[atomType.openingSymbol] = { spaceKind.med, notScript = true },
340+
[atomType.closeSymbol] = { impossible = true },
341+
[atomType.punctuationSymbol] = { impossible = true },
318342
[atomType.inner] = { spaceKind.med, notScript = true },
319343
},
320344
[atomType.relationalOperator] = {
321345
[atomType.ordinary] = { spaceKind.thick, notScript = true },
322346
[atomType.bigOperator] = { spaceKind.thick, notScript = true },
347+
[atomType.binaryOperator] = { impossible = true },
348+
-- [atomType.relationalOperator] = nil
323349
[atomType.openingSymbol] = { spaceKind.thick, notScript = true },
350+
-- [atomType.closeSymbol] = nil
351+
-- [atomType.punctuationSymbol] = nil
324352
[atomType.inner] = { spaceKind.thick, notScript = true },
325353
},
354+
[atomType.openingSymbol] = {
355+
-- [atomType.ordinary] = nil
356+
-- [atomType.bigOperator] = nil
357+
[atomType.binaryOperator] = { impossible = true },
358+
-- [atomType.relationalOperator] = nil
359+
-- [atomType.openingSymbol] = nil
360+
-- [atomType.closeSymbol] = nil
361+
-- [atomType.punctuationSymbol] = nil
362+
-- [atomType.inner] = nil
363+
},
326364
[atomType.closeSymbol] = {
365+
-- [atomType.ordinary] = nil
327366
[atomType.bigOperator] = { spaceKind.thin },
328367
[atomType.binaryOperator] = { spaceKind.med, notScript = true },
329368
[atomType.relationalOperator] = { spaceKind.thick, notScript = true },
369+
-- [atomType.openingSymbol] = nil
370+
-- [atomType.closeSymbol] = nil
371+
-- [atomType.punctuationSymbol] = nil
330372
[atomType.inner] = { spaceKind.thin, notScript = true },
331373
},
332374
[atomType.punctuationSymbol] = {
333375
[atomType.ordinary] = { spaceKind.thin, notScript = true },
334376
[atomType.bigOperator] = { spaceKind.thin, notScript = true },
377+
[atomType.binaryOperator] = { impossible = true },
335378
[atomType.relationalOperator] = { spaceKind.thin, notScript = true },
336379
[atomType.openingSymbol] = { spaceKind.thin, notScript = true },
337380
[atomType.closeSymbol] = { spaceKind.thin, notScript = true },
@@ -345,6 +388,7 @@ local spacingRules = {
345388
[atomType.relationalOperator] = { spaceKind.thick, notScript = true },
346389
[atomType.openingSymbol] = { spaceKind.thin, notScript = true },
347390
[atomType.punctuationSymbol] = { spaceKind.thin, notScript = true },
391+
-- [atomType.closeSymbol] = nil
348392
[atomType.inner] = { spaceKind.thin, notScript = true },
349393
},
350394
}
@@ -377,14 +421,51 @@ function elements.stackbox:styleChildren ()
377421
end
378422
if self.direction == "H" then
379423
-- Insert spaces according to the atom type, following Knuth's guidelines
380-
-- in the TeXbook
424+
-- in The TeXbook, p. 170 (amended with p. 133 for binary operators)
425+
-- FIXME: This implementation is not using the atom form and the MathML logic (lspace/rspace).
426+
-- (This is notably unsatisfactory for <mphantom> elements)
381427
local spaces = {}
428+
if #self.children >= 1 then
429+
-- An interpretation of the TeXbook p. 133 for binary operator exceptions:
430+
-- A binary operator at the beginning of the expression is treated as an ordinary atom
431+
-- (so as to be considered as a unary operator, without more context).
432+
local v = self.children[1]
433+
if v.atom == atomType.binaryOperator then
434+
v.atom = atomType.ordinary
435+
end
436+
end
382437
for i = 1, #self.children - 1 do
383438
local v = self.children[i]
384439
local v2 = self.children[i + 1]
385440
if spacingRules[v.atom] and spacingRules[v.atom][v2.atom] then
386441
local rule = spacingRules[v.atom][v2.atom]
387-
if not (rule.notScript and (isScriptMode(self.mode) or isScriptScriptMode(self.mode))) then
442+
if rule.impossible then
443+
-- Another interpretation of the TeXbook p. 133 for binary operator exceptions:
444+
if v2.atom == atomType.binaryOperator then
445+
-- If a binary atom follows an atom that is not compatible with it, make it an ordinary.
446+
-- (so as to be conidered as a unary operator).
447+
-- Typical case: "a = -b" (ord rel bin ord), "a + -b" (ord bin bin ord)
448+
v2.atom = atomType.ordinary
449+
else
450+
-- If a binary atom precedes an atom that is not compatible with it, make it an ordinary.
451+
-- Quite unusual case (bin, rel/close/punct) unlikely to happen in practice.
452+
-- (Not seen in 80+ test formulas)
453+
-- We might address it a bit late here, the preceding atom has already based its spacing
454+
-- on the binary atom... but this might not be a big deal.
455+
-- (i.e. rather than add an extra look-ahead just for this case).
456+
-- Artificial example: "a + = b" (ord bin rel ord)
457+
v.atom = atomType.ordinary
458+
end
459+
rule = spacingRules[v.atom][v2.atom]
460+
if rule and rule.impossible then
461+
-- Should not occur if we did our table based on the TeXbook correctly?
462+
-- We can still handle it by ignoring the rule: no spacing sounds logical.
463+
-- But let's have a warning so it might be investigated further.
464+
SU.warn("Impossible spacing rule for (" .. v.atom .. ", " .. v2.atom .. "), please report this issue")
465+
rule = nil
466+
end
467+
end
468+
if rule and not (rule.notScript and (isScriptMode(self.mode) or isScriptScriptMode(self.mode))) then
388469
spaces[i + 1] = rule[1]
389470
end
390471
end

0 commit comments

Comments
 (0)