How to render MathML in browser using Angular and MathJax 3 along with lazy loading?
If you have some MathML expressions that you are trying to render beautifully in browser using angular, then you are at the right place!
In this article, we will see how we can render mathml expressions in a browser using Angular 9 and MathJax 3.
Note: I will be using images to explain the concepts step by step. You can always refer to the complete code from my git repository here
Code Time!
1. Define the module structure
Let’s start by creating a module in our angular application, which will contain a directive and service. I will call this a MathModule. The folder structure of our module will look like this
The initial content of module, service and directive will look like below
2. Setting up a strategy to load MathJax script.
We will consider the below steps/ options given below:
- We will load the script from a CDN asynchronously.
- Sub Resource Integrity consideration for security and fallback strategy.
- We will notify our directive once the script has finished loading.
Loading the script from CDN
Let’s copy the details of mml-chtml.js from this CDN including SRI (Sub Resoure Integrity)
Add the below interface for Mathjax config in math.service itself.
Let’s update our math.service in order to support loading the script from CDN asynchronously and to send a signal after loading. After performing the above-mentioned steps, it will look like the following code section:
Whats going on here?
- registerMathjaxAsync() → This method registers the mathjax script based on provided config.
- It returns a promise based on the result.
- We have setup a signal in this service. Once the script registration is successful, we will emit the signal by invoking next().
- ready() → This method simply exposes our signal.
- We will add a fallback strategy when the script registration fails. We will come back and implement this later.
3. Handling global MathJax in typescript
Let’s add the below interface in our math service. Since MathJax will be present in global window object, this is to setup a little intellisense around what we are interested in.
4. Method to update DOM
Let’s add a render() method which will take care of updating the DOM and typesetting (A way of converting string containing mathematics into another form).
- In this method we take initial typesetting which MathJax performs into account by using MathJax.startup.promise
- MathJax.typesetPromise() → This method is used to typeset dynamic pages asynchronously.
- You can find very detailed information about typesetting concepts here.
5. Directive implementation
Now, we need to perform the following steps:
- We need to connect our directive and service in such a way that it listens to the signal from math service and invokes render() method.
- We will take the MathMl expression as a string input via directive.
- Also we will include ngOnChanges() lifecycle hook to handle change detection.
Considering all these, Let’s update our directive as shown in the image below:
That’s all the changes we need w.r.t our math module. Now, it’s time to hook up our directive with a component and see how it works.
6. Using math module
Import math module in app module
We will use the AppComponent for demonstrating our math module. Let’s update our app component shown here:
Please consider the following points after performing the above-mentioned steps:
- We are using our appMath directive in the html
- We have initialised mathml variable to empty string. And we are purposefully using ngIf along with appMath directive in this demo, to show that mathjax script is not loaded from cdn when the page loads.
- It gets loaded lazily when the directive is rendered. That is, when you enter a mathjax expression in text area.
- Please use the network tab in your developer tool to check this lazy loading feature.
7. Running the application
On running the application and putting a mathml expression in text area will result into a beautiful rendering of math.
For example, we can put the below mathml code in text area to see the output.
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
<msup>
<mrow data-mjx-texclass="INNER">
<mo data-mjx-texclass="OPEN">(</mo>
<munderover>
<mo data-mjx-texclass="OP">∑</mo>
<mrow>
<mi>k</mi>
<mo>=</mo>
<mn>1</mn>
</mrow>
<mi>n</mi>
</munderover>
<msub>
<mi>a</mi>
<mi>k</mi>
</msub>
<msub>
<mi>b</mi>
<mi>k</mi>
</msub>
<mo data-mjx-texclass="CLOSE">)</mo>
</mrow>
<mrow>
<mstyle scriptlevel="0">
<mspace width="negativethinmathspace"></mspace>
</mstyle>
<mstyle scriptlevel="0">
<mspace width="negativethinmathspace"></mspace>
</mstyle>
<mn>2</mn>
</mrow>
</msup>
<mo>≤</mo>
<mrow data-mjx-texclass="INNER">
<mo data-mjx-texclass="OPEN">(</mo>
<munderover>
<mo data-mjx-texclass="OP">∑</mo>
<mrow>
<mi>k</mi>
<mo>=</mo>
<mn>1</mn>
</mrow>
<mi>n</mi>
</munderover>
<msubsup>
<mi>a</mi>
<mi>k</mi>
<mn>2</mn>
</msubsup>
<mo data-mjx-texclass="CLOSE">)</mo>
</mrow>
<mrow data-mjx-texclass="INNER">
<mo data-mjx-texclass="OPEN">(</mo>
<munderover>
<mo data-mjx-texclass="OP">∑</mo>
<mrow>
<mi>k</mi>
<mo>=</mo>
<mn>1</mn>
</mrow>
<mi>n</mi>
</munderover>
<msubsup>
<mi>b</mi>
<mi>k</mi>
<mn>2</mn>
</msubsup>
<mo data-mjx-texclass="CLOSE">)</mo>
</mrow>
</math>
The output of parsing this expression will look something like this:
This should be good enough for us to render MathML in browser!
8. Sub Resource Integrity and Implementing a fallback strategy
Imagine a scenario where the CDN from which you are loading the script has been compromised and the attacker has managed to update the script with some malicious code. How can we prevent loading an altered script? This is exactly where SRI (Sub Resource Integrity) comes into the picture.
What is Sub Resource Integrity?
It is a security feature that allows our browser to find out if the file being loaded from an external source has been compromised. This is why the integrity hash is of utmost importance while loading scripts from any CDN.
Note: The integrity key is generated by hashing the content of a file. So if this file is altered by an attacker, the hash will not match and the browser will prevent the file from loading.
How to implement a fallback strategy for our math module?
- We will maintain a local copy of the same file (Under assets)
- When an error occurs while loading script from CDN, the we will load the script from our assets.
Let’s copy paste the script file from CDN into a local file under assets/mathjax.
Note: Do not update the content of the copied file
After this, let’s create a fallback config for Mathjax in math.service. In this configuration, we will point to the javascript file in our local. However; the integrity key will be the same.
Since we have a fallback configuration now, let’s update the code within the constructor to implement fallback mechanism. As you can see in the below snippet, when an error occurs while registering the mathjax script from CDN, we will try to load the fallback script from our server.
In this way, we can prevent loading the script from CDN when there is a SRI failure and continue using the script from our server.
You can verify this scenario by changing the integrity key of the CDN mathjax configuration. If we change the key, then the integrity check will fail and we can see the below error.
However; you can still see that the application will render math properly due to our fallback strategy implementation.
With this, we are able to successfully render MathMl in browser beautifully with the help of MathJax!
Summary
Now, you will see that the module we have built has the following features:
- MathMl rendering capability
- Lazy loadable
- Sub Resource Integrity Consideration for Security
- Fallback Strategy when there is an issue in loading script from CDN
I prefer to put out blogs on use cases which I find absolutely interesting. And this was one such use case that I found very interesting to work with. I have tried my best to demonstrate things in-depth here. Please feel free to provide your feedback and suggestions.
You can see the demo of this module here.