1.5.0 — \nolimits, amsmath display style, and a parser that stops eating your formula
Correctness release: \nolimits, amsmath display style in aligned blocks, a parser that no longer eats your formula, and three accessibility bugs.
Minor bump rather than a patch: MathNode gains a case (.noLimitsOperator), which is source-breaking for any client that switches exhaustively over it. Most hosts never do — it's a layout-model type — and if you do, it's a transparent wrapper: delegate to its base.
Added
\nolimitsworks. (#20)
In TeX it forces an operator's scripts to the side even in display style (\sum\nolimits_{i=1}^n) — the standard way to keep an operator compact inside a display equation. Vinculum recognized the command, consumed it, and threw the modifier away, so a display\sumstacked regardless.toLaTeX()dropped it too, so a round-trip silently re-stacked.\displaylimitsremains a no-op, which is correct: it restores the current style's default, which an unmodified operator already uses.
Fixed
-
aligned/align/gather/splitcells are display style, per amsmath. (#21)
Every grid environment's cells were laid out at a hardcoded text style, so a\sumor\intinside analignedblock rendered text-size with side-set limits where real amsmath stacks display-size ones.matrix/array/casescorrectly stay text style regardless of surroundings — that half was already right, and the report's "cells should inherit the surrounding style" framing would have broken it. -
A missing
}or]no longer swallows the rest of the formula. (#8)
Every argument reader scanned to its closer with an unbounded loop and no end-of-stream fallback, so one missing delimiter let a reader consume the whole remaining stream as a single argument — which is then discarded, since it's a colour/thickness name.\textcolor{red a+bproduced colourreda+band an empty body, and reportedisFullySupported == true, so the host's fallback never fired anda+bvanished behind a successful-looking blank render. Braces are now balance-checked up front (escape-aware, so\left\{ x \right\}is untouched); brackets are checked per reader, since[0,1)is ordinary math. -
iOS: an equation first rendered off-main lost its VoiceOver text forever. (#6)
The render entry points arenonisolatedprecisely so hosts can pre-render off the main thread, but the accessibility stamp was gated behindThread.isMainThread— and cache entries are immutable, so every later host reused the unlabeled bitmap. In theMathTextdocument path the image's own label is the only per-equation carrier, so VoiceOver read "image". The guard's premise was wrong: UIImage's accessibility setters are not MainActor-isolated. -
VinculumLabelno longer reads raw LaTeX to VoiceOver. (#7)
On unsupported input withdisplayErrorInlineoff, the view drew nothing and reported zero size — yet stayed a live accessibility element labelled with the raw source, so VoiceOver landed on an invisible node spelling out "backslash n o t a command". Accessibility now follows the visible outcome. -
MathViewannounces math as static text, not "image". (#26)
It kept SwiftUI's default.isImagetrait while the AppKit/UIKit sibling declared.staticText— the same equation announced two different ways depending on which view a host picked. -
A truncated size-variant ladder is rejected instead of kept partial. (#36)
On a short read the parser kept the partial ladder rather than discarding the record like every sibling parser does. Variant records are ordered smallest-first, so a truncated font silently lost a glyph's largest sizes — a\left(that stops growing and falls back to scaling. Only affects already-malformed fonts.
Under the hood
- The rasterizers are now fuzzed (#12) — the fuzz corpus only ever drove a mock measurer, so the FreeType/Cairo/CoreText interop, the code most likely to hold a memory-safety bug, only saw clean equations. Both renderers gate on
isFullySupported, so random garbage never reaches them; the new corpus is valid-by-construction but adversarial, and each test asserts it actually reached the rasterizer. - The render/measure caches are stress-tested concurrently (#46) — their comments made specific multi-thread claims that nothing exercised.
- ARCHITECTURE's "how to add a new command" checklist was wrong and is now measured: five compiler-enforced exhaustive switches, not seven, and
glyphTypographyhas adefault:that swallows a missing case silently.
Performance was re-verified in release after two parser changes rather than assumed — parse 23.8 µs, warm cache hit 0.46 µs, headless layout 7.8 µs, ~77% of a cold render is still CoreText. The published numbers hold. See docs/PERFORMANCE.md.
Install
.package(url: "https://github.com/2389-research/Vinculum.git", from: "1.5.0")Full detail: CHANGELOG.md · Docs & gallery