Note: While the introduction provides a high-level overview to the changes in Wolfram Cloud 1.49, the bulk of this post is technical.
In any application dealing with mathematics, typesetting is incredibly essential. Great formatting can go a long way in elevating your work, whether your focus is a scientific paper or a school assignment. Luckily, the Wolfram Language provides an incredibly robust toolkit to accomplish this. If you’re unfamiliar with the Mathematical Typesetting features of Wolfram Language, I highly encourage you to learn more about it here.
One of the great things about Wolfram Language is its portability across devices, so its a given that Mathematical Typesetting should work great everywhere. We’ve done quite a lot to accomplish this goal, but on Cloud it can still be a little rough.
Our latest release, Wolfram Cloud 1.49, fixes all of these issues. We included a major overhaul of the typesetting for spanning characters, available today however you use Wolfram Cloud. These include characters such as “curly” brackets, parenthesis, radical symbols, and square roots.
In Wolfram Cloud, we use React components for driving the UI, along with an internal library called Reback. We also use modern tooling, which includes Babel for ES6 features, Flow for strong typing, and Jest for unit tests.
Specific to spanning characters, the old implementation lived almost entirely in a file called
spanningCharacters.js . At first glance, it seemed pretty straightforward. It’s mostly just a React component, rendering a couple elements that are stitched together using their
style properties. These elements had our Mathematica Sans font.
The stitching also occurred in pretty predictable ways:
Example A, really tall parenthesis: The rendered character here was stitched vertically. It includes an element for the top glyph, an element for the bottom glyph, and however many elements it takes to fill the height with side glyphs.
Example B, square root symbol: Here the rendered character was stitched horizontally. It includes an element for the left glyph (the radical), and as many elements are needed to fill the width with side glyphs (the vinculum).
Example C, really tall square root symbol: In this case, it would stitch twice, horizontally and vertically. The left glyph (the radical) is made up of different glyphs, stitched vertically like in Example A. After that, the new left glyph is stitched again like in Example B.
But for all its simplicity, it carried a few assumptions. Good React code usually tries to minimize directly accessing the DOM. To do this with spanning characters, we need to measure glyphs outside of the DOM using the opentype.js library. And as shown in Exhibit B, DOM stitching doesn’t always work correctly and can vary from browser to browser.
If we already measure the glyphs in a DOM-independent way, why not rely even less on the DOM?
The file also hadn’t been updated to use many of our tools like Flow typing and unit testing. While we’re at it, let’s add those too.
This is where my work started.
How a Font becomes a Path
When I say we want to draw the spanning characters ourselves, early on we decided to use the path element available in Scalable Vector Graphics (SVG). We already use SVG extensively in other parts of the codebase and have a good working knowledge of how it works across browsers.
To use SVG paths, first, we need to convert the glyphs into a form that resembles its drawing commands. After experimenting, I found that the opentype.js library already supported this use-case through the
getPath function here, which returns an instance of
OpenType.Path as shown below.
View my JSFiddle here.
As shown above,
OpenType.Path has not only the drawing commands we need, but also a bounding box. This provides all the information we need to replace our previous measurements! The opentype.js library will draw paths at the font’s baseline. Since the SVG coordinate system increases in the bottom-right direction, this means the ascent is equal to
-y1 , and the descent is equal to
Below is a graphic that shows what the different font parameters represent. We also used these measurements to re-do the spacing for square root symbols, mirroring Wolfram Player for iOS. I was happy to find how well these concepts translated from CoreText to here.
At this stage I could apply transforms to multiple
path elements and stitch them together like our old implementation. However, we would open to many of the same layout issues that we discussed above.
Why don’t we stitch them together into a single
Connect them … where?
If we want to stitch glyphs into a single
path , we’ll need to find lines where we can connect them. In our list of drawing commands, these lines are denoted by a special marker I call a breakpoint.
Above is the middle glyph for curly brackets. To a human (and likely a machine learning algorithm), it’s pretty easy to spot where the breakpoints should be. The top and bottom ends of the character, right? But, should the wedge in the middle be a breakpoint too? What rules could we have to solve this algorithmically?
To start, what do all three of these potential breakpoints have in common?
All three lines are line butts, meaning we’ll have to consider the adjacent lines as well. Since the line drawing commands have a specific order & direction, a simple test for a butt is to check the directions of the adjacent lines.
For the breakpoint lines themselves, we need them to be completely horizontal or vertical.
These are where the first rule came from.
Rule 1: (a) The adjacent lines can’t point in the same direction. (b) The breakpoint line must be exactly horizontal or vertical.
Second, imagine a square bracket. The long vertical line that makes up the bracket is straight, and the adjacent lines point in different directions. By our ruleset right now, this would count as a wide breakpoint. That’s not what we want.
What if Rule 2 was, then, “The length of the breakpoint line must be shorter than both adjacent lines?” Consider the following.
If you look at this glyph instead, we can identify the ledge in the top-right corner to be a breakpoint. Rule 1 succeeds without issue, however, Rule 2 would fail because the line under the breakpoint is shorter than itself.
What should I do then? If we make Rule 2, “The length of the breakpoint line must be shorter than one of the adjacent lines,” it would be pretty easy to find a counterexample.
Here’s what I came up with (it’s a little messy):
Rule 2: Make sure the breakpoint line is …(a) shorter than one of the adjacent lines (b) make sure the other adjacent line multiplied by 1.5 is larger than the breakpoint line.
With my original proposition, one question is left: “Should the wedge in the middle be a breakpoint too?”
Right now, it passes both of our tests! But seeing as these wedges weren’t useful for stitching, I decided to detect and remove them.
Before when talking about how each breakpoint is a line butt, I failed to clarify that there were two types.
- The first, called line caps, are the butt of a single line. Line caps include the top and bottom of this glyph.
- The second is called a line join, and these connect two different lines. Line joins include the wedges I’ve talked about.
Since I only wanted line caps, I just needed a test that checked for single lines. This observation makes up the third rule.
Rule 3: The breakpoint line and its adjacent lines should “always” make a parallelogram.
Just how is this test implemented? According to Wolfram Mathworld, adding two adjacent angles in a parallelogram should equal 180 degrees. Given that the glyphs wouldn’t be perfect, I gave this comparison +/- 5 degrees of error. Using the breakpoint line and its adjacent lines, you can use the Law of Cosines to find these angles.
a dot b = ||a|| * ||b|| * cos(angle)
These three rules encompass what I implemented to detect breakpoints.
“Stitching” it all together
Before we continue, I also stored these breakpoints in a separate array for fast access. For my sanity, I combined this with the glyph commands in a structure I call a “split path.”
The next step is stitching them together. In our old implementation stitching only happened one direction at a time. We can create something similar here. I chose to implement a reducer function. The function takes an array of split paths and extra parameters (most importantly an direction), and returns a single split path.
Since the function’s output is a split path too, it can be passed as an input when stitching in the opposite direction (like for Example #3 above).
Using SVG removes many restrictions, and we can now create polygons that connect breakpoints (vs. positioning them next to each other). We no longer have to tessellate the side glyphs! For curly brackets, this now means we can adequately typeset tall expressions as desktop Mathematica would.
This is all diagrammed below, which shows how our function (which is partially recursive) works. The numbers correspond to the drawing order of the path, and the bright blue lines are added by the function to connect glyphs.
Note that this works because opentype.js, as far as I know, is consistent with polygon winding. If one glyph was drawn clockwise and another counter-clockwise, as-is our function would produce strange results.
After reducing the split paths as many times as needed, rendering the resulting path in React is incredibly easy.
We re-implemented part of our mathematical typesetting, what we call spanning characters, to be less reliant on CSS-positioned nodes and more reliant on SVG.
This improved both the user and developer experience in Cloud. On the user side, visual artifacts are drastically reduced, and typesetting in general looks better.
On the developer side, we were able to clean up our code and have better control over the rendering process. For this, I took inspiration from React’s Design Principles. This also allowed better test coverage, adding 18 unit tests where there were previously 0.
I learned a lot throughout the project, and hope that my findings may help others as well. I believe it is a good example of how web codebases can think about legacy code. It’s also a study of how much web development has changed, even in the past five years.
I currently work as an Intern at Wolfram, working on Wolfram Cloud. If you found this blog post interesting, we’re hiring! Check out our Careers page.