mathjax / MathJax

Beautiful and accessible math in all browsers
http://www.mathjax.org/
Apache License 2.0
10.18k stars 1.16k forks source link

svg fraction scaling with exponents not correct #1186

Open corinnaSchultz opened 9 years ago

corinnaSchultz commented 9 years ago

Here is a jsfiddle which demonstrates this issue: http://jsfiddle.net/PGhkg/7/

When I inspect the svg, I see that the scaling for x^2 is as expected: the 2 is smaller than the x. But the scaling for x^(x^2) is not what I expect. The x and the 2 are the same size. I would expect the 2 to be smaller, limited by the value of scriptminsize.

The scaling for x^(x^2) when it isn't in a fraction shows that the 2 is smaller, which is what I expect.

dpvc commented 9 years ago

MathJax uses TeX's approach to layout, in general, and in TeX there are basically three sizes: text-size, script-size, and script-script-size. A fraction in text-size will have its numerator and denominator in script-size, and a power in script-size will have its superscript in script-script-size. If that superscript is itself a power, then its superscript will still be in script-script-size. That is what is happening in your case.

Currently, this is by design. I'm marking this as a feature request for that reason.

corinnaSchultz commented 9 years ago

Is this sizing behavior documented anywhere? I looked and couldn't find anything which is why I thought it might be a bug.

dpvc commented 9 years ago

I don't think so. But the initial implementation of the HTML-CSS and SVG output processors were designed to reproduce TeX output as closely as possible, so it follows from that. The spacing rules for most layout are also taken from TeX; there is a configuration parameter that controls whether MathML spacing or TeX spacing is used for the space between characters, and I suppose that could also be used to control this situation as well (though it doesn't currently).

corinnaSchultz commented 9 years ago

OK, good to know that Tex is a de facto standard. Thanks.

corinnaSchultz commented 8 years ago

Is there any way to make the size of a fraction different from the size of an exponent? I tried playing with CSS classes, and addMMLclasses=true, but that doesn't seem to have an effect, probably because font-size doesn't make any sense with path elements.

It appears that the scaling is controlled by scriptsizemultiplier, and that number applies to anything that's not "baseline" font size, such as superscript, exponents, and fractions. Is this true? There is no finer-grained control of sizing?

pkra commented 8 years ago

Is there any way to make the size of a fraction different from the size of an exponent?

I don't quite understand what you might be after. Some input examples and desired renderings (i.e., as images) might be useful to add.

I tried playing with CSS classes,

I'm not sure what that refers to.

and addMMLclasses=true,

That only affects the markup structure of the SVG (de-optimizing it to expose more of its underlying MathML structure) but has no effect on the rendering itself.

There is no finer-grained control of sizing?

There are various ways. The spec might be a helpful starting point, https://www.w3.org/Math/draft-spec/mathml.html#chapter3_presm.scriptlevel.

pkra commented 8 years ago

@dpvc is this an upstream issue?

dpvc commented 8 years ago

is this an upstream issue?

@pkra, I'm not sure what is upstream from MathJax itself. Do you mean an issue with the MathML spec?

dpvc commented 8 years ago

@corinnaSchultz, you will not be able to use CSS to control the font sizing (at least not effectively), as MathJax needs to know the size of all the elements in order to lay them out carefully, and if you change the size using CSS, MathJax won't know about that and you will not get the results that you want. You can use the mathsize attribute to change the size of a specific (token) element, but it would have to be included explicitly on each element that you want to adjust. There are no defaults that you can chance to adjust the size of one type of node (e.g., mfrac) separately from other types.

For example:

<math>
  <mi>x</mi>
  <mo>+</mo>
  <mstyle mathsize="3">
    <mfrac>
      <mi>x</mi>
      <mrow>
        <mn>1</mn>
        <mo>-</mo>
        <mi>x</mi>
      </mrow>
    </mfrac>
  </mstyle>
</math>

would make the fraction 3 times larger than normal.

pkra commented 8 years ago

@pkra, I'm not sure what is upstream from MathJax itself. Do you mean an issue with the MathML spec?

Oops. I thought I was on mathjax-node. Sorry.

corinnaSchultz commented 8 years ago

What I was being asked to do was scale exponents to .715 and fractions to .921, relative to the base font size. For example, for "3^2", the 2 would be 71.5% the size of the 3. For "3 1/4", the 1 and the 4 would be 92.1% the size of the 3.

If I set the value of scriptsizemultiplier to .715, that scale factor also affects the fraction size, because the exponent and the fraction both are at the same script level.

With the CSS, by turning on the class names, I tried defining font-size styles but that had no effect-- I presume, because the svg was using path elements and (afaik) CSS doesn't have a way of affecting those. And for dpvc's comments about calculating layouts, yes I tried altering the transform attribute and adding a scale (thinking I could achieve what I wanted via jquery) and discovered that the overall proportions don't allow for that kind of hacking.

I was unaware of mathsize, perhaps that will work for me, since we want this scale factor for all mfracs. I'll play with that.

Thank you for the link to the mathML spec. I'm still fairly new at working with mathML and mathJax and mathjax-node, and I'm trying to satisfy the (somewhat specific) desires of content creators while learning all this stuff.

pkra commented 8 years ago

With the CSS, by turning on the class names, I tried defining font-size styles but that had no effect-- I presume, because the svg was using path elements and (afaik) CSS doesn't have a way of affecting those. And for dpvc's comments about calculating layouts, yes I tried altering the transform attribute and adding a scale (thinking I could achieve what I wanted via jquery) and discovered that the overall proportions don't allow for that kind of hacking.

General advice would be never to try to hack MathJax's output.

I'm still fairly new at working with mathML and mathJax and mathjax-node, and I'm trying to satisfy the (somewhat specific) desires of content creators while learning all this stuff.

Please understand that we cannot provide free technical support for all things MathML (or even all things MathJax) on the issue tracker. The MathJax User Group is more suitable for this since community members can support each other. You can also email me directly if you need additional suggestions.

dpvc commented 8 years ago

I had already nearly worked out a possible solution for your problem before Peter indicated that we weren't going to do it, so I'm sending it anyway. There are some comments to indicate what is happening, but the basic idea is use a post-filter on the MathML input jax to modify the results to add the mathsize attributes when needed. To make it easier, we set the scriptsizemultiplier to 1 so that the fractions and scripts don't change size unless we tell them to and then look through the expression for fractions and scripts, and set the mathsize directly as needed. There are a couple of technical issues to take care of, but it does give an example of how to modify the MathJax internal MathML in a somewhat sophisticated way. Note, however, that this does mean that MathJax will be doing extra work for every expression, so it might be better to do this in the original MathML. You can use this to experiment with different settings quickly, though, which is nice.

Try adding the following just before the script that loads MathJax.js:

<script type="text/x-mathjax-config">
MathJax.Hub.Register.StartupHook("MathML Jax Ready",function () {
  var MathML = MathJax.InputJax.MathML;
  var MML = MathJax.ElementJax.mml;

  //
  //  Use a post-filter hook on the MathML input jax to mark the math as
  //  having scriptsizemultiplier = 1.  This means that scripts and
  //  fractions will not get smaller unless we explicitly tell them to.
  //  Then we look through the MathML for mfrac, msup, etc., and scale the
  //  fractions to .921 while the scripts are scaled to .715.  We keep
  //  track of the current scale so that we can reduce the size as we go.
  //  We also have to keep track of the inherit property (so that when we
  //  insert mstyle elements, their children will refer to them properly).
  //  The inherit property is a pointer to the closest container that the
  //  element inherits from (i.e., the mstyle or math element that is its
  //  closest direct container).
  //

  MathML.postfilterHooks.Add(function (data) {
    var math = data.math.root;
    if (math.attr.scriptsizemultiplier == null) {
      math.scriptsizemultiplier = math.attr.scriptsizemultiplier = 1;
      math.attrNames.push("scriptsizemultiplier");
    }
    FILTER(math,1,null,null);
  });
  //
  //  Here we look recursively through the nodes for fractions and scripts,
  //  and scale the appropriate children.  We also update the inherit
  //  property, if needed.
  //
  var FILTER = function (node,scale,oldinherit,inherit) {
    if (!node) return;
    switch (node.type) {
     case 'mfrac':
      SCALE(node,0,scale*.921,oldinherit,inherit);
      SCALE(node,1,scale*.921,oldinherit,inherit);
      break;
     case 'munder':
     case 'mover':
     case 'munderover':
     case 'msub':
     case 'msup':
     case 'msubsup':
      FILTER(node.data[0],scale,oldinherit,inherit);
      SCALE(node,1,scale*.715,oldinherit,inherit);
      SCALE(node,2,scale*.715,oldinherit,inherit);
      break;
     case 'chars':
     case 'entity':
      return;
     default:
      for (var i = 0, m = node.data.length; i < m; i++) {
        FILTER(node.data[i],scale,oldinherit,inherit);
      }
    }
    if (node.inherit === oldinherit) node.inherit = inherit;
  };
  //
  //  Scale a node (if the scriptlevel is 1 or 2) and continue filtering
  //  the children.  Add mathsize directly to token elements, otherwise,
  //  enclose them in an mstyle node.
  //
  var SCALE = function (node,i,scale,oldinherit,inherit) {
    var child = node.data[i];
    if (!child) return;
    var level = child.Get("scriptlevel");
    if (level > 0 && level < 3) {
      if (child.isToken) {
        if (child.inherit === oldinherit) child.inherit = inherit;
        if (child.attr.mathsize == null) {
          child.mathsize = child.attr.mathsize = scale.toFixed(3);
          child.attrNames.push("mathsize");
        }
      } else {
        var mstyle = MML.mstyle(child).With({mathsize: scale.toFixed(3)});
        node.SetData(i,mstyle);
        FILTER(child,scale,node.inherit,mstyle);
      }
    } else {
      FILTER(child,scale,oldinherit,inherit);
    }
  };
});
</script>
corinnaSchultz commented 8 years ago

Thank you for going the extra mile! I hadn't considered using the post-filter hook feature of mathjax. This gives me some ideas :)