5

I have a specific need regarding mathematical expression rendering in a web application, and while I've so far been mostly looking at MathJax, I'm certainly not wedded to it.

What I need is the ability to render a mathematical expression where one or more of the terms can be, essentially, an arbitrary HTML box element, like so: Example output

...It would be preferable if the layout responded to the size of the HTML box (e.g. parenthesis would auto-size to match its height, like any other "normal" LaTeX/MathJax element), but if I need to specify an exact size in pixels or something like that, that'd be OK too. Would even be OK if I had to e.g. insert a "placeholder" element in the actual visualization, but could know exactly where it wound up in the output so I could overlay the HTML element precisely on top of it.

In other words, I'm open to outside-the-box or even hackish solutions; just need something that works.

Also possibly relevant: I'm really only going to be using pretty basic LaTeX elements: parentheses, fractions, normal operators. I don't even really need to be able to support the summation in the above example, if that's a complicating factor.

DanM
  • 7,037
  • 11
  • 51
  • 86
  • @DavideCervone Sorry for the late response -- was focused on another project and hadn't had time to really digest your answer before this morning. I think this will work quite perfectly! Thanks for the answer. – DanM Nov 25 '19 at 16:04
  • thanks for awarding the bounty. I hope one of these methods work out for you. – Davide Cervone Nov 26 '19 at 16:26

1 Answers1

5

There are a couple of ways to go about this. Your suggestion of inserting a placeholder and overlaying it is one, which can be done as follows:

<script type="text/x-mathjax-config">
MathJax.Hub.Queue(function () {
  var PX = function (x) {return x.toFixed(2) + 'px'};
  var space = document.getElementById('space');
  var sbox = space.getBoundingClientRect();
  var math = MathJax.Hub.getJaxFor(space).SourceElement().previousSibling;
  var mbox = math.getBoundingClientRect();
  var x = sbox.left - mbox.left;
  var y = sbox.top - mbox.top;
  var html = MathJax.HTML.Element('div', {id:'html', style: {
    position:'absolute', top:PX(y), left:PX(x), 
    width:PX(sbox.width), height:PX(sbox.height), 'line-height':'normal'
  }}, [[
    'table',{style:{height:'100%', width:'100%', background:'red', border:'5px solid green'}}, [[
      'tr',{},[[
        'td',{},['abc']
      ]]
    ]]
  ]]);
  math.style.position = 'relative';
  math.appendChild(html);
});
</script>
<script id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js?config=TeX-AMS_CHTML" defer></script>

<div style="xfont-size: 150%">
$$x + \left(\,\cssId{space}{\Space{100px}{55px}{45px}}\,\right) + y$$
</div>

Here we use \Space to insert a blank area of the desired width, height, and depth, and \cssId to mark the space so we can retrieve it later. We use MathJax.Hub.Queue() to queue a function to run after MathJax typesets the math. This function looks up the space-holder element and finds the container element that MathJax uses to hold the math. It gets the bounding box information for each, and computes the position of the overlay in relation to the math. Then it created the overlay (the content is hardcoded in this instance), with the proper size and potion to lay over top of the space-holder.

This works, but is a bit awkward, and for displayed equations, if the window size changes, the overlay may not stay in the correct position. Also, it is only set up to work for CommonHTML output.


An alternative is the following, which uses the a <semantics> element to include HTML into the internal MathML structure used by MathJax. It adds a new macro \insertHTML{} that does it.

<script type="text/x-mathjax-config">
MathJax.Hub.Config({
  CommonHTML: {
    styles: {
      //
      // remove CSS for '.mjx-math *'
      //
      '.mjx-math *': {
        display: null,
        '-webkit-box-sizing': null,
        '-moz-box-sizing': null,
        'box-sizing': null,
        'tex-align': null
      },
      //
      // add CSS for .mjx-math span instead
      //
      '.mjx-math span': {
        display: 'inline-block',
        '-webkit-box-sizing': 'context-box !important',
        '-moz-box-sizing': 'context-box !important',
        'box-sizing': 'context-box !important',
        'tex-align': 'left'
      },
      //
      // override display for .mjx-char spans
      //
      'span.mjx-char': {
        display: 'block'
      }
    }
  }
});
MathJax.Hub.Register.StartupHook("TeX Jax Ready", function () {
  var MML = MathJax.ElementJax.mml;
  var TEX = MathJax.InputJax.TeX;
  TEX.Definitions.macros.insertHTML = 'InsertHTML';
  TEX.Parse.Augment({
    InsertHTML: function (name) {
      var html = this.GetArgument(name).replace(/^\s*<!--\s*/,'').replace(/\s*-->\s*$/,'');
      var span = MathJax.HTML.Element('mjx-reset', {style: {display:'inline-block'}});
      span.innerHTML = html;  // serious security risk if users can enter math
      span.setAttribute("xmlns","http://www.w3.org/1999/xhtml");
      var mml = MML["annotation-xml"](MML.xml(span)).With({encoding:"application/xhtml+xml",isToken:true});
      this.Push(MML.semantics(mml));
    }
  });
});
</script>
<script id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@2/MathJax.js?config=TeX-AMS_CHTML" defer></script>

<div style="xfont-size: 150%">
$$x + \left(\,\insertHTML{<!--
<table width="100" height="100"
  style="display:inline-table; vertical-align:-.25em; background:red; border:5px solid green;
  box-sizing:border-box !important">
<tr><td style="text-align:center">abc</td></tr>
</table>
-->}\,\right) + y$$
</div>


$$x+\left(\insertHTML{<!--
<i>this</i> is <b>html</b>
-->}\right)+y$$

Because MathJax won't process math that contains HTML tag directly, you have to put the HTML in a comment, which you see in the two example expressions above. This version works on all output formats (even MathML output), but it does require a bit of configuration to override some of the CommonHTML CSS that would apply to the content due to it being contained internally within the MathJax layout.

The HTML in this case will stay properly positioned no matter what happens to the window, and it has the advantage of allowing the HTML to be given in the expression itself. Note, however, that this presents a security risk if your site allows users to enter mathematics, since it means that they can enter arbitrary HTML into your page. In this case, you would want to sanitize the HTML prior to setting the innerHTML in the InsertHTML function.

Davide Cervone
  • 11,211
  • 1
  • 28
  • 48
  • Question regarding the 2nd method: Using Chrome on Mac, the vertical sizing is off on the first example -- specifically, the parentheses are about 40% taller than the red HTML element. It sizes properly on Safari; haven't checked other browsers. Any idea what might cause that? Thanks! – DanM Nov 27 '19 at 14:55
  • ...Having done some more testing, it looks like it sizes properly when the HTML element is the height of a single line of text, but then (as far as I can tell by eyeballing it) it looks like for every pixel of additional height beyond that one line of text, the parentheses grow 2 pixels in height, with the HTML element aligned with the bottom of the parentheses. – DanM Nov 27 '19 at 15:24
  • @DanM, Chrome seems not to implement the `revert` CSS that I was using to undo some CSS used by the CommonHTML output renderer. This was causing the table elements to not have the proper `display` properties for a table, and so the cell containing the `abc` was not being properly centered. That made most of the height of the table become depth of line containing the table. Because the parentheses are symmetric and must cover that large depth, they also became extra tall (that is correct handling of the parentheses for the table as displayed... – Davide Cervone Nov 27 '19 at 17:25
  • 1
    ... So the CSS reset wasn't working, and has to be handled in a different way. I will modify the example above to alter the CSS instead of resetting it, which should take care of the problem in Chrome. It is better that way, anyway. – Davide Cervone Nov 27 '19 at 17:26