Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clear user defined TeX macros from global state #348

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from

Conversation

rianmcguire
Copy link

MathJax supports TeX macros, and once defined, they persist forever in the global state of MathJax-node.

This PR changes the behaviour so any definitions are cleared before typeset, but still allows them to be persisted across multiple typesets using the state option.

@rianmcguire rianmcguire changed the base branch from master to develop August 3, 2017 23:25
@rianmcguire
Copy link
Author

Sorry - looks like I should have forked off develop, not master. Fixing now.

lib/main.js Outdated
@@ -741,6 +741,19 @@ function GetState(state) {
MML.SUPER.ID = ID = 0;
MathJax.OutputJax.CommonHTML.ID = 0;
}

// Clear any existing user defined macros, then load macros from state
Object.keys(TEX.Definitions.macros).forEach(function(macroName){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you use TeX.Definitions.macros a number of times, here, perhaps

var MACROS = TeX.Definitions.macros;

would be in order.

(Also, there should be a space between the ) and the following {.)

lib/main.js Outdated
}
});
if (state && state.macros) {
for (var macroName in state.macros) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use if (state.macros.hasOwnProperty(macroName) {...} inside this loop, so that you will be sure to only use the names that are actually in the object. For example, if someone augments the Object.prototype object (and we have encountered that before), then you will loop through this additions as well.

lib/main.js Outdated
if (state && state.macros) {
for (var macroName in state.macros) {
TEX.Definitions.macros[macroName] = state.macros[macroName];
TEX.Definitions.macros[macroName].isUser = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not need to set this, as it is already set in the objects that are pointed to in state.macros.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured if someone were to serialize the state to JSON, the isUser property would be lost because the macro is an array.

If that's not something you want to support, I'm happy to remove it. It's not important for our use case at the moment.

lib/main.js Outdated
@@ -773,6 +787,13 @@ function ReturnResult(result) {
state.defs = GLYPH.defs;
state.n = GLYPH.n;
state.ID = ID;
state.macros = {};
for (var macroName in TEX.Definitions.macros) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, if (TeX.Definitions.macros.hasOwnProperty(maroName)) {...} should be used inside this loop.

@dpvc
Copy link
Member

dpvc commented Aug 4, 2017

I like the idea of this, but I've made some review comments above. A couple of additional thoughts:

  • You might consider adding user-defined environments to the state as well (those created via \newenvironment).

  • You should note that this does not remove macros loaded from extensions, either autoloader or explicitly loaded. So someone typesetting \require{color} {\color{red} x}y followed by \color{red}{x} y would get a red y in the second one. I don't think there is anything to be done about that, but just wanted to point out that this is not a total reset.

  • The loop through all the defined macros (to remove user-defined ones) is being done every time an expression is typeset. That seems a bit inefficient. Perhaps you could record the number of macros before the typesetting occurs, and only do the loop if that number changes. Probably not a big deal, but every bit helps.

@dpvc
Copy link
Member

dpvc commented Aug 4, 2017

In terms of my last bullet point above, there are methods setDef and setEnv that are used to create any user-defined macro and environment definitions. These could be overridden to record the newly created macros and environments, so you know exactly which ones have been created, rather than having to do any looping through the definitions at all. These are used by the begingroup extension, but could be used here, too, with a little care. Just a thought.

@rianmcguire
Copy link
Author

Thanks for the feedback. I liked your setDef/setEnv idea, so I've re-implemented it by adding overrides for those.

@dpvc
Copy link
Member

dpvc commented Aug 9, 2017

I appreciate your continued work on this, and like the direction this is going. I do have two issues that still concern me a bit.

First, if the begingroup extension is loaded (say via \require{begingroup}), then your setDef and setEnv will be overridden. So you probably should do something similar to what you have done for newcommand in order to tie into them again (I think a direct copy of what you have but with the extension name changed would do it).

Second, if someone redefines an existing macro (e.g., \def\phi{\varphi}), then when you remove the user-defined macros, there will be no definition for \phi any more. So you might want your setDef and setEnv replacements to be a little more sophisticated, and check if there is a (non-user-defined) version of the macro already, and save it for restoration when the user-defined macros are removed.

There is still one hitch even with that: extensions that define macros will not override user-defined macros (i.e., they act as though the extension had been loaded initially, even if they are auto-loaded later). That means that if typesetting causes macro to be defined with the same name as one in an extension and then loads the extension, you will remove the user-defined one, but not restore the extension-loaded version that should be there in the absence of user-defined macros.

This behavior is controlled by the TeX.Definitions.Add() method. You could override that to check if a definition is not being applied due to a user-defined one already existing, and save the new definition for restoration later.

Of course, this is a pretty low-likelihood situation, but it is one of those places where it could cause confusion about macros not being defined that should be.

I suppose an alternative would be to just end up using the begingroup extension all the time, as it properly handles user-supplied macros, and add a "reset" method that simply clears all the user data and local definition groups. Someone proposed such a reset (as a macro) at one point — I may be able to dig that up somewhere.

@pkra
Copy link
Contributor

pkra commented Aug 15, 2017

@rianmcguire any chance you'll get back to this some time soon? It would make a nice addition to 1.2.0 but it can easily wait for a later release.

@pkra pkra added this to the v1.2.0 milestone Aug 15, 2017
@rianmcguire
Copy link
Author

@pkra I'm not going to have time to get back to it for at least 2 weeks.

@pkra
Copy link
Contributor

pkra commented Aug 17, 2017

@rianmcguire thanks for the update!

@pkra pkra removed this from the v1.2.0 milestone Aug 23, 2017
@pkra
Copy link
Contributor

pkra commented Nov 2, 2017

@rianmcguire ping.

ambar added a commit to ambar/MathJax-node that referenced this pull request Dec 13, 2017
@Menci
Copy link

Menci commented Mar 21, 2019

So, any progress here?

@dpvc
Copy link
Member

dpvc commented Mar 25, 2019

So, any progress here?

I've been thinking about a different approach that uses the begingroup extension and a custom reset macro that will remove any user-defined macros (including those made with \global or \gdef) when the reset command is given. So you can process the math on a page without resetting, and have the definitions affect later expressions, and then reset if you want to start a new page fresh.

Here's how to do it right now. Make a directory named extensions and create the file extensions/reset.js containing

MathJax.Hub.Register.StartupHook("TeX begingroup Ready", function () {
  var TEX = MathJax.InputJax.TeX, NSSTACK = TEX.nsStack, NSFRAME = NSSTACK.nsFrame;
  var rootStack = TEX.rootStack, FIND = rootStack.Find.bind(rootStack);
  rootStack.globalFrame = rootStack.stack[0];      // save the original global definitions
  rootStack.stack[0] = NSFRAME();                  // a new (temporary) global definition holder
  rootStack.Find = function (name, type) {
    if (name === 'ReSeT' && type === "macros") return "ReSeT";       // don't let this be redefined
    return FIND(name, type) || this.globalFrame.Find(name, type);    // look up the name
  }
  TEX.Definitions.macros.ReSeT = 'ReSeT';        // define the reset macro to clear the stack
  TEX.Parse.Augment({ReSeT: function (name) {rootStack.Clear()}});
});

MathJax.Ajax.loadComplete("[extensions]/reset.js");

Then in your application for MathJax, configure the path to the extensions directory, load [extensions]/reset.js, TeX/begingroup.js, and TeX/newcommand.js, and process \ReSeT whenever you want to reset the macros to their initial state.

For example, this driver file (tex-reset.js)

#! /usr/bin/env node

const path = require('path');
const mjAPI = require("./lib/main.js");

mjAPI.config({
  extensions: '[extensions]/reset.js,TeX/begingroup.js,TeX/newcommand.js',
  paths: {extensions: path.join(__dirname,'extensions/')}
});
mjAPI.start();

mjAPI.typeset({
  math: process.argv[2],
  format: "inline-TeX",
  mml:true
}, (data) => !data.errors && console.log(data.mml));

mjAPI.typeset({
  math: '\\ReSeT',
  format: 'inline-TeX'
}, () => {});

mjAPI.typeset({
  math: process.argv[3],
  format: "inline-TeX",
  mml:true
}, (data) => !data.errors && console.log(data.mml));

takes two arguments (TeX expressions), and processes them with a \ReSeT in between. So you can test if this works by, for example,

node tex-reset '\def\sin{\cos} \sin(x)' '\sin(x)'

The output of the first should be a cosine, and the second a sine. You can also check that \ReSeT can't be redefined

node tex-reset '\def\ReSeT{Boom!} \def\sin{\cos} \sin(x)' '\sin(x)'

This is based on an idea of Imari Karonen on the Math StackExchange site.

This does not make the definitions available in the data object, as this PR does, but I'm not sure that is necessary. Also, neither this alternative nor the original PR prevents macros defined by loading extensions, so if an expression uses \require{color}, for example, then the \color macro will remain the LaTeX-compatible one rather than the non-standard MathJax-specific one.

To avoid this, you could disable the \require command, or use the Safe extension to limit the extensions that are allowed to be loaded (to disallow color). Or you could track the use of \require{} and when you want to reset the definitions, if \require{} has been used you could restart MathJax via mjAPI.start() rather than typesetting \ReSeT. That is more expensive, of course, but it is the only way to remove the fact that an extension has been loaded.

Mathjax just updated to Version 3. This totally broke the encoding service mathjax conversion from math to text.
@dpvc dpvc mentioned this pull request Sep 5, 2019
@dpvc
Copy link
Member

dpvc commented Sep 5, 2019

The last commit should not be included, here. As mentioned in #451, the change is not needed. Also, it is unrelated to the changes for TeX macros that are the subject of this PR, so should not be part of this PR. Had #451 been merged, there would be no need for this commit anyway.

One way to roll it back would be:

git checkout master
git reset --hard HEAD~1
git push --force origin master

If you are using this branch for your production work, and have included it for that, then you should not be using that branch for a pull request (it is best to make a separate branch for the PR, rather than using your own master branch). Because there is already a lot of important comments on this PR with this branch, it would be hard to change it now. But you could branch a new "production" branch from your master branch and use that for your additional commits like this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants