When a multi-character \text{...} block appears inside a \left...\right pair, its characters are duplicated in the rendered output.
The cause is a combination of two things:
MTTypesetter.makeLeftRight typesets the inner math list twice: once to measure the height the delimiters need, and again to produce the display that's actually rendered. Both passes go through an internal createLineForMathList call that passes the list straight through, without the .finalized copy the public entry points apply.
- The atom-fusion step in
preprocessMathList calls MTMathAtom.fuse(with:), which mutates the receiver in place (self.nucleus += atom.nucleus).
When combined, the first pass permanently fuses the atoms of the original list, and the second pass re-fuses the already-fused nuclei, duplicating the text.
Repro steps:
\left( \text{clip}(x) \right)
Expected: renders as (clip(x))
Actual: renders as (cliplip(x))
Any nesting also compounds the problem. Each nested \left...\right level doubles the fused run, so at nesting depth d a run like clip picks up 2^d − 1 extra “lip” fragments. E.g. with \text{clip} two levels deep, it renders as “cliplipliplip”.
Other symptoms of this problem:
The double typeset exposes other in-place mutations besides fusion: the large-operator limits handling consumes the operator’s sub/superscripts while building the limits display, so e.g. the limits of \sum_{i=1}^{n} inside \left...\right are dropped. The measurement pass strips them out, so we end up with an operator without displayed limits.
When a multi-character
\text{...}block appears inside a\left...\rightpair, its characters are duplicated in the rendered output.The cause is a combination of two things:
MTTypesetter.makeLeftRighttypesets the inner math list twice: once to measure the height the delimiters need, and again to produce the display that's actually rendered. Both passes go through an internalcreateLineForMathListcall that passes the list straight through, without the.finalizedcopy the public entry points apply.preprocessMathListcallsMTMathAtom.fuse(with:), which mutates the receiver in place (self.nucleus += atom.nucleus).When combined, the first pass permanently fuses the atoms of the original list, and the second pass re-fuses the already-fused nuclei, duplicating the text.
Repro steps:
Expected: renders as
(clip(x))Actual: renders as
(cliplip(x))Any nesting also compounds the problem. Each nested
\left...\rightlevel doubles the fused run, so at nesting depth d a run likeclippicks up 2^d − 1 extra “lip” fragments. E.g. with\text{clip}two levels deep, it renders as “cliplipliplip”.Other symptoms of this problem:
The double typeset exposes other in-place mutations besides fusion: the large-operator limits handling consumes the operator’s sub/superscripts while building the limits display, so e.g. the limits of
\sum_{i=1}^{n}inside\left...\rightare dropped. The measurement pass strips them out, so we end up with an operator without displayed limits.