MathJax icon indicating copy to clipboard operation
MathJax copied to clipboard

Dynamic line breaking

Open sebsciarra opened this issue 2 years ago • 10 comments

Although line breaking works in Mathjax 4.0.0, the equations do not break themselves as the window size decreases and I have to refresh the website with in the smaller screen size so that the equations break properly. Is there any way to have the equations dynamically break so that the content does not go outside the window margins?

Here is my MathJax setup:

window.MathJax = {
  section: {
    n: -1,
    useLetters: false,
    letters: "AABCDEFGHIJKLMNOPQRSTUVWXYZ"
  },

  loader: {load: ['[tex]/tagformat', '[tex]/mathtools', 'output/chtml']},
  tex: {
    inlineMath: [['$', '$'], ['\\(', '\\)']], //allow inline math
    displayMath: [['$$','$$']],
    tagSide: 'right', //location of equation numbers
    tags: 'all',
    packages: {'[+]': ['tagformat', 'sections', 'autoload-all', 'mathtools']},
    tagformat: {
      number: (n) => {
        const section = MathJax.config.section;
        return (section.useLetters ? section.letters[section.n] : section.n) + '.' + n;
      }
    }},

  chtml: {
   mtextInheritFont: true,         // font to use for mtext, if not inheriting (empty means use MathJax fonts)
   displayOverflow: 'linebreak'
  },

  linebreaks: {                  // options for when overflow is linebreak
      inline: true,                   // true for browser-based breaking of inline equations
      width: '100%',                  // a fixed size or a percentage of the container width
      lineleading: 2,                // the default lineleading in em units
      LinebreakVisitor: null,         // The LinebreakVisitor to use
  },



  startup: {
    ready() {
                  const {CommonWrapper} = MathJax._.output.common.Wrapper;
      const {LineBBox} = MathJax._.output.common.LineBBox;

      const Configuration = MathJax._.input.tex.Configuration.Configuration;
      const CommandMap = MathJax._.input.tex.SymbolMap.CommandMap;
      new CommandMap('sections', {
        nextSection: 'NextSection',
        setSection: 'SetSection',
      }, {
        NextSection(parser, name) {
          MathJax.config.section.n++;
          parser.tags.counter = parser.tags.allCounter = 0;
        },
        SetSection(parser, name) {
          const section = MathJax.config.section;
          const c = parser.GetArgument(name);
          const n = section.letters.indexOf(c);
          if (n >= 0) {
            section.n = n;
            section.useLetters = true;
          } else {
            section.n = parseInt(c);
            section.useLetters = false;
          }
        },
      });
      Object.assign(CommonWrapper.prototype, {
        invalidateBBox(bubble = true) {
          if (this.bboxComputed || this._breakCount >= 0) {
            this.bboxComputed = false;
            this.lineBBox = [];
            this._breakCount = -1;
            if (this.parent && bubble) {
              this.parent.invalidateBBox();
            }
          }
        },
        _getLineBBox: CommonWrapper.prototype.getLineBBox,
        getLineBBox(i) {
          if (!this.lineBBox[i] && !this.breakCount) {
            const obox = this.getOuterBBox();
            this.lineBBox[i] = LineBBox.from(obox, this.linebreakOptions.lineleading);
          }
          return this._getLineBBox(i);
        }
      });

      Configuration.create(
        'sections', {handler: {macro: ['sections']}}
      );
      MathJax.startup.defaultReady();
    }
  }
};

sebsciarra avatar Mar 14 '23 17:03 sebsciarra

MathJax wraps displayed equations to the initial size of the window, and does not reflow equations when the window changes (as you have found). Breaking displayed equations is an expensive process, and MathJax does not currently have a process for triggering equations to be re-rendered when their container size changes. It would be possible to write an extension that does that, and that would make a nice contribution to the project, if you or anyone is interested in doing so. I can make suggestions about how to do so if anyone does want to try it.

dpvc avatar Mar 15 '23 16:03 dpvc

Sounds good. If the project is not too demanding, I would be interested.

sebsciarra avatar Mar 15 '23 17:03 sebsciarra

Well, the main idea would be to use a ResizeObserver and register all the displayed equations with it, then when a resize event occurs loop through the nodes that are changed (these are passed to the observer) and set the state for the associated math items back to before the metrics action (e.g., mathitem.state(STATE.METRICS - 1)).

You can use a renderAction that follows the update action (e.g., STATE.INSERTED + 1) and run through the document's math list for new displayed equations (mathitem.display is true for displayed equations), and add any new ones to the resize observer. You can use the mathitem.outputData to store a flag that indicates whether the math item is already in the resize observer (e.g., set mathitem.outputData.resizeObserved = true when you add to the resize observer). The mathitem.typesetRoot should be a pointer to the mjx-container DOM element, which should have display: block for a displayed equation, and so should be able to generate the needed resize events. You will want to keep a Map() object that ties the DOM node back to the MathItem object so that when the resize event occurs, you can recover the MathItem and set its state back to STATE.METRICS - 1, as indicated above.

Then remove the DOM node from the ResizeObserver (since when the math is re-typeset, it will get a new mix-container, and your renderAction will add that to the resize observer again.

Once you have looped through all the math items whose sizes have changed, you can ask the document to typeset again, and it should remeasure and re-typeset the math items for which you have set the state back.

That's the main idea. There are details to be worked out, and eventually some additional structure to make it an actual extension, but you can start by just working with a configuration that sets up the renderActions and come globally defined functions for now. There are examples of renderActions in various issues here, so search for renderActions and you should get several examples. A render action has a name and an array that consists of a number (giving its order in the list), a function that has one argument (the MathDocument being used) that is called when the document is typeset, and a function with two arguments (a MathItem and the MathDocument it is in) that is called when an individual equation is re-rendered (e.g., when an maction item changes the equation).

If you want to give it a try, see how far you can get, and I can give you a hand once you have a start at it.

dpvc avatar Mar 16 '23 02:03 dpvc

Ok sounds good. I will set aside some time next week to try this out. Quick comment: My equation numbers still seem to go outside of the margins on small screens (320px wide) even when using the above MathJax setup. Here is one such example:

\begin{align}
\log\big(L(\theta|h,n)\big) &= \log {n \choose h}\ + h\log(\theta) + (n-h)\log(1-\theta)
\label{eq:binom-log-likelihood}
\end{align}

sebsciarra avatar Mar 17 '23 21:03 sebsciarra

My equation numbers still seem to go outside of the margins

Yes, that is a separate issue, which I corrected in mathjax/MathJax-src#926, but didn't give you a patch for. You can add

      const {ChtmlMtable} = MathJax._.output.chtml.Wrappers.mtable;
      Object.assign(ChtmlMtable.prototype, {
        adjustWideTable() {
          const attributes = this.node.attributes;
          if (attributes.get('width') !== 'auto') return;
          const [pad, align] = this.getPadAlignShift(attributes.get('side'));
          const W = Math.max(this.containerWidth / 10, this.containerWidth - pad - (align === 'center' ? pad : 0));
          this.naturalWidth() > W && this.adjustColumnWidths(W);
        }
      });

to your startup.ready function and that should take care of it.

dpvc avatar Mar 17 '23 23:03 dpvc

There is also a PR that improves the results for breaking in tables, but it is too big to make a patch for it. But see mathjax/Mathjax-src#927 for details. Note that you can use \vbox{} (or \mathmakebox{} from the mathtools extension) around the left to prevent it from breaking.

dpvc avatar Mar 17 '23 23:03 dpvc

Okay perfect! Thanks again for the very fast responses! The equation numbers are now within the margin of the post.

sebsciarra avatar Mar 18 '23 00:03 sebsciarra

Great! Thanks for the confirmation.

dpvc avatar Mar 18 '23 00:03 dpvc

Inline equations do automatically adjust linebreaks when the window is resized, presumably because it's left to the browser. Is there anything stopping display equations from acting in this way (or having an option for that to happen) - i.e. splitting the equation into multiple parts which can then be broken accordingly by the browser?

pufferfish101007 avatar Sep 02 '24 09:09 pufferfish101007

Is there anything stopping display equations from acting in this way

Yes. The breaking for displayed equations is much more sophisticated than for in-line expressions, as it takes into account the nesting depth, good- and bad-break indicators, balancing the width of lines, and lots of other factors, not just how much material can fit on one line. The browser's line breaking is based solely on maximizing the content per line, and is not appropriate for breaking displayed equations.

Handling changing window sizes would involve an extension like the one I describe above that would re-break wide expressions when needed. It's not something that the browser will be able to handle itself.

dpvc avatar Sep 03 '24 14:09 dpvc