/*************************************************************
 *
 *  MathJax/extensions/TeX/text-macros.js
 *  
 *  Implements the processing of some text-mode macros inside
 *  \text{} and other text boxes.
 *
 *  ---------------------------------------------------------------------
 *  
 *  Copyright (c) 2018-2020 The MathJax Consortium
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

MathJax.Extension["TeX/text-macros"] = {
  version: "2.7.9"
};

MathJax.Hub.Register.StartupHook("TeX Jax Ready", function () {
  var MML = MathJax.ElementJax.mml;
  var TEX = MathJax.InputJax.TeX;
  var TEXDEF = TEX.Definitions;

  TEX.Parse.Augment({
    //
    // Replace InternalMath with parser that handles some text macros
    //
    InternalMath: function(text,level) {
      var mml = TextParser(text, {}).Parse();
      if (level != null) {
        mml = [MML.mstyle.apply(MML,mml).With({displaystyle:false, scriptlevel:level})];
      } else if (mml.length > 1) {
        mml = [MML.mrow.apply(MML,mml)];
      }
      return mml;
    },

    //
    //  Correctly skip newline as well as comment
    //
    Comment: function (c) {
      while (this.i < this.string.length && this.string.charAt(this.i) != "\n") {this.i++}
      this.i++;
    },

    //
    // Correctly skip trailing space as well
    //
    GetCS: function () {
      var CS = this.string.slice(this.i).match(/^([a-z]+|.) ?/i);
      if (CS) {this.i += CS[0].length; return CS[1]} else {this.i++; return " "}
    }

  });

  //
  //  The text parser is a subclass of the math parser, so we can use
  //  some of the existing methods (like GetArgument()), and some of the
  //  control sequence implementations (like Macro, Spacer, etc.)
  //
  var TextParser = TEX.TextParser = TEX.Parse.Subclass({

    Init: function (text, env) {
      this.env = MathJax.Hub.Insert({},env);
      this.stack = {env: this.env};
      this.string = text;
      this.i = 0;
      this.mml = [];  // the accumulated MathML elements
      this.text = ''; // the accumulated text so far
    },

    //
    //  These are the special characters in text mode
    //
    textSpecial: {
      '\\': 'ControlSequence',
      '$': 'Math',
      '%': 'Comment',
      '^': 'MathModeOnly',
      '_': 'MathModeOnly',
      '&': 'Misplaced',
      '#': 'Misplaced',
      '~': 'Tilde',
      ' ': 'Space',
      '\t': 'Space',
      '\r': 'Space',
      '\n': 'Space',
      '\u00A0': 'Tilde',
      '{': 'OpenBrace',
      '}': 'CloseBrace',
      '`': 'OpenQuote',
      "'": 'CloseQuote'
    },

    //
    //  These are text-mode macros we support
    //
    textMacros: {
      '(': 'Math',

      '$': 'SelfQuote',
      '_': 'SelfQuote',
      '%': 'SelfQuote',
      '{': 'SelfQuote',
      '}': 'SelfQuote',
      ' ': 'SelfQuote',
      '&': 'SelfQuote',
      '#': 'SelfQuote',
      '\\': 'SelfQuote',

      "'": ['Accent', '\u00B4'],
      '`': ['Accent', '\u0060'],
      '^': ['Accent', '^'],
      '"': ['Accent', '\u00A8'],
      '~': ['Accent', '~'],
      '=': ['Accent', '\u00AF'],
      '.': ['Accent', '\u02D9'],
      'u': ['Accent', '\u02D8'],
      'v': ['Accent', '\u02C7'],

      emph:      'Emph',
      rm:       ['SetFont',MML.VARIANT.NORMAL],
      mit:      ['SetFont',MML.VARIANT.ITALIC],
      oldstyle: ['SetFont',MML.VARIANT.OLDSTYLE],
      cal:      ['SetFont',MML.VARIANT.CALIGRAPHIC],
      it:       ['SetFont','-tex-mathit'], // needs special handling
      bf:       ['SetFont',MML.VARIANT.BOLD],
      bbFont:   ['SetFont',MML.VARIANT.DOUBLESTRUCK],
      scr:      ['SetFont',MML.VARIANT.SCRIPT],
      frak:     ['SetFont',MML.VARIANT.FRAKTUR],
      sf:       ['SetFont',MML.VARIANT.SANSSERIF],
      tt:       ['SetFont',MML.VARIANT.MONOSPACE],

      tiny:        ['SetSize',0.5],
      Tiny:        ['SetSize',0.6],  // non-standard
      scriptsize:  ['SetSize',0.7],
      small:       ['SetSize',0.85],
      normalsize:  ['SetSize',1.0],
      large:       ['SetSize',1.2],
      Large:       ['SetSize',1.44],
      LARGE:       ['SetSize',1.73],
      huge:        ['SetSize',2.07],
      Huge:        ['SetSize',2.49],

      mathcal:  'MathModeOnly',
      mathscr:  'MathModeOnly',
      mathrm:   'MathModeOnly',
      mathbf:   'MathModeOnly',
      mathbb:   'MathModeOnly',
      mathit:   'MathModeOnly',
      mathfrak: 'MathModeOnly',
      mathsf:   'MathModeOnly',
      mathtt:   'MathModeOnly',
      Bbb:     ['Macro','{\\bbFont #1}',1],
      textrm:  ['Macro','{\\rm #1}',1],
      textit:  ['Macro','{\\it #1}',1],
      textbf:  ['Macro','{\\bf #1}',1],
      textsf:  ['Macro','{\\sf #1}',1],
      texttt:  ['Macro','{\\tt #1}',1],

      dagger:   ['Insert', '\u2020'],
      ddagger:  ['Insert', '\u2021'],
      S:        ['Insert', '\u00A7']
    },

    //
    //  These are the original macros that are allowed in text mode
    //
    useMathMacros: {
      ',':          true,
      ':':          true,
      '>':          true,
      ';':          true,
      '!':          true,
      enspace:      true,
      quad:         true,
      qquad:        true,
      thinspace:    true,
      negthinspace: true,
    
      hskip:        true,
      hspace:       true,
      kern:         true,
      mskip:        true,
      mspace:       true,
      mkern:        true,
      rule:         true,
      Rule:         true,
      Space:        true,

      color:        true,
      href:         true,
      unicode:      true,

      ref:          true,
      eqref:        true
    },

    //
    //  Look through the text for special characters and process them.
    //  Save any accumulated text aat the end and return the MathML
    //   elements produced.
    //
    Parse: function () {
      var c;
      while ((c = this.string.charAt(this.i++))) {
        if (this.textSpecial.hasOwnProperty(c)) {
          this[this.textSpecial[c]](c);
        } else {
          this.text += c;
        }
      }
      this.SaveText();
      return this.mml;
    },

    //
    //  Handle a control sequence name
    //    If it is a text-mode macro, use it.
    //    Otherwise look for it in the math-mode lists
    //      Report an error if it is not there
    //      Otherwise check if it is a macro or one of the allowed control sequences
    //    Run the macro (with arguments if given)
    //
    ControlSequence: function (c) {
      var cs = this.GetCS(), name = c + cs, cmd;
      if (this.textMacros.hasOwnProperty(cs)) {
        cmd = this.textMacros[cs];
      } else {
        cmd = this.LookupCS(cs);
        if (!cmd) {
          this.Error(["UndefinedControlSequence","Undefined control sequence %1",name]);
        }
        if ((!(cmd instanceof Array) || cmd[0] !== 'Macro') &&
             !this.useMathMacros.hasOwnProperty(cs)) {
          this.Error(["MathMacro","'%1' is only supported in math mode",name]);
        }
      }
      if (cmd instanceof Array) {
        if (!this.hasOwnProperty[cmd[0]]) this.SaveText();
        this[cmd[0]].apply(this,[name].concat(cmd.slice(1)));
      } else {
        if (!this.hasOwnProperty[cmd]) this.SaveText();
        this[cmd].call(this,name);
      }
    },

    //
    //  Lookup the CS as a math-mode macro
    //
    LookupCS: function(cs) {
      if (TEXDEF.macros.hasOwnProperty(cs)) return TEXDEF.macros[cs];
      if (TEXDEF.mathchar0mi.hasOwnProperty(cs)) return TEXDEF.mathchar0mi[cs];
      if (TEXDEF.mathchar0mo.hasOwnProperty(cs)) return TEXDEF.mathchar0mo[cs];
      if (TEXDEF.mathchar7.hasOwnProperty(cs)) return TEXDEF.mathchar7[cs];
      if (TEXDEF.delimiter.hasOwnProperty('\\'+cs)) return TEXDEF.delimiter['\\'+cs];
      return null;
    },

    //
    //  Handle internal math mode
    //    Look for the close delimiter and process the contents
    //
    Math: function (open) {
      this.SaveText();
      var i = this.i, j;
      var braces = 0, c;
      while ((c = this.GetNext())) {
        j = this.i++;
        switch(c) {
          case '\\':
            var cs = this.GetCS();
            if (cs === ')') c = '\\(';
          case '$':
            if (braces === 0 && open === c) {
              this.Push(TEX.Parse(this.string.substr(i, j-i),this.env).mml());
              return;
            }
            break;

          case '{':
            braces++;
            break;

          case '}':
            if (braces == 0) {
              this.Error(["ExtraCloseMissingOpen","Extra close brace or missing open brace"]);
            }
            braces--;
            break;
        }
      }
      this.Error(["MathNotTerminated","Math not terminated in text box"]);
    },

    //
    //  Character can only be used in math mode
    //
    MathModeOnly: function (c) {
      this.Error(["MathModeOnly","'%1' allowed only in math mode",c]);
    },

    //
    //  Character is being used out of place
    //
    Misplaced: function (c) {
      this.Error(["Misplaced","'%1' can not be used here",c]);
    },

    //
    //  Braces start new environments
    //
    OpenBrace: function (c) {
      var env = this.env;
      this.env = MathJax.Hub.Insert({}, env);
      this.env.oldEnv = env;
    },
    CloseBrace: function (c) {
      if (this.env.oldEnv) {
        this.SaveText();
        this.env = this.env.oldEnv;
      } else {
        this.Error(["ExtraCloseMissingOpen","Extra close brace or missing open brace"]);
      }
    },

    //
    //  Handle open and close quotes
    //
    OpenQuote: function (c) {
      if (this.string.charAt(this.i) === c) {
        this.text += "\u201C";
        this.i++;
      } else {
        this.text += "\u2018"
      }
    },
    CloseQuote: function (c) {
      if (this.string.charAt(this.i) === c) {
        this.text += "\u201D";
        this.i++;
      } else {
        this.text += "\u2019"
      }
    },

    //
    //  Handle non-breaking and regular spaces
    //
    Tilde: function (c) {
      this.text += '\u00A0';
    },
    Space: function (c) {
      this.text += ' ';
      while (this.GetNext().match(/\s/)) this.i++;
    },

    //
    //  Insert the escaped characer
    //
    SelfQuote: function (name) {
      this.text += name.substr(1);
    },

    //
    //  Insert a given character
    //
    Insert: function (name, c) {
      this.text += c;
    },

    //
    //  Create an accented character using mover
    //
    Accent: function (name, c) {
      this.SaveText();
      var base = this.ParseArg(name);
      var accent = MML.mo(MML.chars(c));
      if (this.env.mathvariant) accent.mathvariant = this.env.mathvariant;
      this.Push(MML.mover(base,accent));
    },

    //
    //  Switch to/from italics
    //
    Emph: function (name) {
      this.UseFont(name, this.env.mathvariant === '-tex-mathit' ? 'normal' : '-tex-mathit');
    },

    //
    //  Use a given font on its argument
    //
    UseFont: function (name, variant) {
      this.SaveText();
      this.Push(this.ParseTextArg(name,{mathvariant: variant}));
    },

    //
    //  Set a font for the rest of the text
    //
    SetFont: function (name, variant) {
      this.SaveText();
      this.env.mathvariant = variant;
    },

    //
    //  Set the size to use
    //
    SetSize: function (name, size) {
      this.SaveText();
      this.env.mathsize = size;
    },

    //
    //  Process the argument as text with the given environment settings
    //
    ParseTextArg: function (name, env) {
      var text = this.GetArgument(name);
      env = MathJax.Hub.Insert(MathJax.Hub.Insert({}, this.env), env);
      delete env.oldEnv;
      return TextParser(text, env).Parse();
    },

    //
    //  Process an argument as text (overrides the math-mode version)
    //
    ParseArg: function (name) {
      var mml = TextParser(this.GetArgument(name), this.env).Parse();
      if (mml.length === 0) return mml[0];
      return MML.mrow.apply(MML.mrow, mml);
    },

    //
    //  Create an mtext element with the accumulated text, if any
    //    and set it variant
    //
    SaveText: function () {
      if (this.text) {
        var text = MML.mtext(MML.chars(this.text));
        if (this.env.mathvariant) text.mathvariant = this.env.mathvariant;
        this.Push(text);
      }
      this.text = "";
    },

    //
    //  Save a MathML element or array, setting its size and color, if any
    //
    Push: function (mml) {
      if (mml instanceof Array) {
        if (this.env.mathsize || this.env.mathcolor) {
          mml = MML.mstyle.apply(MML,mml);
          if (this.env.mathsize) mml.mathsize = this.env.mathsize;
          if (this.env.mathcolor) mml.mathcolor = this.env.mathcolor;
        }
        this.mml.push.apply(this.mml,mml);
      } else {
        if (this.env.mathsize && !mml.mathsize) mml.mathsize = this.env.mathsize;
        if (this.env.mathcolor && !mml.mathcolor) mml.mathcolor = this.env.mathcolor;
        this.mml.push(mml);
      }
    },

    //
    //  Throw an error
    //
    Error: function (message) {
      TEX.Error(message);
    }

  });
  
  MathJax.Hub.Startup.signal.Post('TeX text-macros Ready');

});

MathJax.Ajax.loadComplete('[MathJax]/extensions/TeX/text-macros.js');
