/*************************************************************
 *
 *  Copyright (c) 2017-2026 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.
 */

/**
 * @file The Basic Parse methods.
 *
 * @author v.sorge@mathjax.org (Volker Sorge)
 */

import { HandlerType } from '../HandlerTypes.js';
import * as sitem from './BaseItems.js';
import { StackItem, EnvList } from '../StackItem.js';
import { Macro } from '../Token.js';
import { ParseResult, ParseMethod } from '../Types.js';
import NodeUtil from '../NodeUtil.js';
import TexError from '../TexError.js';
import TexParser from '../TexParser.js';
import { TexConstant } from '../TexConstants.js';
import { ParseUtil } from '../ParseUtil.js';
import { UnitUtil } from '../UnitUtil.js';
import { PropertyList } from '../../../core/Tree/Node.js';
import { MmlNode, TEXCLASS } from '../../../core/MmlTree/MmlNode.js';
import { MmlMo } from '../../../core/MmlTree/MmlNodes/mo.js';
import { MmlMsubsup } from '../../../core/MmlTree/MmlNodes/msubsup.js';
import { MmlMunderover } from '../../../core/MmlTree/MmlNodes/munderover.js';
import { Label } from '../Tags.js';
import { em } from '../../../util/lengths.js';
import { entities } from '../../../util/Entities.js';
import { lookup } from '../../../util/Options.js';
import { ColumnState } from '../ColumnParser.js';
import { replaceUnicode } from '../../../util/string.js';

const P_HEIGHT = 1.2 / 0.85; // cmex10 height plus depth over .85
const MmlTokenAllow: { [key: string]: number } = {
  fontfamily: 1,
  fontsize: 1,
  fontweight: 1,
  fontstyle: 1,
  color: 1,
  background: 1,
  id: 1,
  class: 1,
  href: 1,
  style: 1,
};

/**
 * @param {string} align   The vertical aligment to use (t, b, m or c)
 * @param {number} n       The number of expected alignment characters
 * @returns {string} String with space separated alignment characters
 */
export function splitAlignArray(align: string, n: number = Infinity): string {
  const list = align
    .replace(/\s+/g, '')
    .split('')
    .map((s: string) => {
      const name = { t: 'top', b: 'bottom', m: 'middle', c: 'center' }[s];
      if (!name) {
        throw new TexError(
          'BadBreakAlign',
          'Invalid alignment character: %1',
          s
        );
      }
      return name;
    });
  if (list.length > n) {
    throw new TexError(
      'TooManyAligns',
      'Too many alignment characters: %1',
      align
    );
  }
  return n === 1 ? list[0] : list.join(' ');
}

/**
 * Parse a general root.
 *
 * @param {TexParser} parser The calling parser.
 * @param {string} n The index of the root.
 * @returns {MmlNode} The node with the parsed root.
 */
function parseRoot(parser: TexParser, n: string): MmlNode {
  // @test General Root, Explicit Root
  const env = parser.stack.env;
  const inRoot = env['inRoot'];
  env['inRoot'] = true;
  const newParser = new TexParser(n, env, parser.configuration);
  let node = newParser.mml();
  const global = newParser.stack.global;
  if (global['leftRoot'] || global['upRoot']) {
    // @test Tweaked Root
    const def: EnvList = {};
    if (global['leftRoot']) {
      def['width'] = global['leftRoot'];
    }
    if (global['upRoot']) {
      def['voffset'] = global['upRoot'];
      def['height'] = global['upRoot'];
    }
    node = parser.create('node', 'mpadded', [node], def);
  }
  env['inRoot'] = inRoot;
  return node;
}

/**
 * Handle LaTeX tokens.
 */
const BaseMethods: { [key: string]: ParseMethod } = {
  /**
   * Handle {
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _c The parsed character.
   */
  Open(parser: TexParser, _c: string) {
    // @test Identifier Font, Prime, Prime with subscript
    parser.Push(parser.itemFactory.create('open'));
  },

  /**
   * Handle }
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _c The parsed character.
   */
  Close(parser: TexParser, _c: string) {
    // @test Identifier Font, Prime, Prime with subscript
    parser.Push(parser.itemFactory.create('close'));
  },

  /**
   * Handle |
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} c The parsed character.
   */
  Bar(parser: TexParser, c: string) {
    const mo = parser.create(
      'token',
      'mo',
      { stretchy: false, texClass: TEXCLASS.ORD },
      c
    );
    mo.setProperty('keep-attrs', 'stretchy');
    parser.Push(mo);
  },

  /**
   * Handle tilde and spaces.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _c The parsed character.
   */
  Tilde(parser: TexParser, _c: string) {
    // @test Tilde, Tilde2
    parser.Push(parser.create('token', 'mtext', {}, entities.nbsp));
  },

  /**
   * Handling space, by doing nothing.
   *
   * @param {TexParser} _parser The calling parser.
   * @param {string} _c The parsed character.
   */
  Space(_parser: TexParser, _c: string) {},

  /**
   * Handle ^
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _c The parsed character.
   */
  Superscript(parser: TexParser, _c: string) {
    if (parser.GetNext().match(/\d/)) {
      // don't treat numbers as a unit
      parser.string =
        parser.string.substring(0, parser.i + 1) +
        ' ' +
        parser.string.substring(parser.i + 1);
    }
    let primes: MmlNode;
    let base: MmlNode | void;
    const top = parser.stack.Top();
    if (top.isKind('prime')) {
      // @test Prime on Prime
      [base, primes] = top.Peek(2);
      parser.stack.Pop();
    } else {
      // @test Empty base2, Square, Cube
      base = parser.stack.Prev();
      if (!base) {
        // @test Empty base
        base = parser.create('token', 'mi', {}, '');
      }
    }
    const movesupsub = NodeUtil.getProperty(base, 'movesupsub');
    let position = NodeUtil.isType(base, 'msubsup')
      ? (base as MmlMsubsup).sup
      : (base as MmlMunderover).over;
    if (
      (NodeUtil.isType(base, 'msubsup') &&
        !NodeUtil.isType(base, 'msup') &&
        NodeUtil.getChildAt(base, (base as MmlMsubsup).sup)) ||
      (NodeUtil.isType(base, 'munderover') &&
        !NodeUtil.isType(base, 'mover') &&
        NodeUtil.getChildAt(base, (base as MmlMunderover).over) &&
        !NodeUtil.getProperty(base, 'subsupOK'))
    ) {
      // @test Double-super-error, Double-over-error
      throw new TexError(
        'DoubleExponent',
        'Double exponent: use braces to clarify'
      );
    }
    if (!NodeUtil.isType(base, 'msubsup') || NodeUtil.isType(base, 'msup')) {
      if (movesupsub) {
        // @test Move Superscript, Large Operator
        if (
          !NodeUtil.isType(base, 'munderover') ||
          NodeUtil.isType(base, 'mover') ||
          NodeUtil.getChildAt(base, (base as MmlMunderover).over)
        ) {
          // @test Large Operator
          base = parser.create('node', 'munderover', [base], {
            movesupsub: true,
          });
        }
        position = (base as MmlMunderover).over;
      } else {
        // @test Empty base, Empty base2, Square, Cube
        base = parser.create('node', 'msubsup', [base]);
        position = (base as MmlMsubsup).sup;
      }
    }
    parser.Push(
      parser.itemFactory.create('subsup', base).setProperties({
        position: position,
        primes: primes,
        movesupsub: movesupsub,
      })
    );
  },

  /**
   * Handle _
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _c The parsed character.
   */
  Subscript(parser: TexParser, _c: string) {
    if (parser.GetNext().match(/\d/)) {
      // don't treat numbers as a unit
      parser.string =
        parser.string.substring(0, parser.i + 1) +
        ' ' +
        parser.string.substring(parser.i + 1);
    }
    let primes, base;
    const top = parser.stack.Top();
    if (top.isKind('prime')) {
      // @test Prime on Sub
      [base, primes] = top.Peek(2);
      parser.stack.Pop();
    } else {
      base = parser.stack.Prev();
      if (!base) {
        // @test Empty Base Index
        base = parser.create('token', 'mi', {}, '');
      }
    }
    const movesupsub = NodeUtil.getProperty(base, 'movesupsub');
    let position = NodeUtil.isType(base, 'msubsup')
      ? (base as MmlMsubsup).sub
      : (base as MmlMunderover).under;
    if (
      (NodeUtil.isType(base, 'msubsup') &&
        !NodeUtil.isType(base, 'msup') &&
        NodeUtil.getChildAt(base, (base as MmlMsubsup).sub)) ||
      (NodeUtil.isType(base, 'munderover') &&
        !NodeUtil.isType(base, 'mover') &&
        NodeUtil.getChildAt(base, (base as MmlMunderover).under) &&
        !NodeUtil.getProperty(base, 'subsupOK'))
    ) {
      // @test Double-sub-error, Double-under-error
      throw new TexError(
        'DoubleSubscripts',
        'Double subscripts: use braces to clarify'
      );
    }
    if (!NodeUtil.isType(base, 'msubsup') || NodeUtil.isType(base, 'msup')) {
      if (movesupsub) {
        // @test Large Operator, Move Superscript
        if (
          !NodeUtil.isType(base, 'munderover') ||
          NodeUtil.isType(base, 'mover') ||
          NodeUtil.getChildAt(base, (base as MmlMunderover).under)
        ) {
          // @test Move Superscript
          base = parser.create('node', 'munderover', [base], {
            movesupsub: true,
          });
        }
        position = (base as MmlMunderover).under;
      } else {
        // @test Empty Base Index, Empty Base Index2, Index
        base = parser.create('node', 'msubsup', [base]);
        position = (base as MmlMsubsup).sub;
      }
    }
    parser.Push(
      parser.itemFactory.create('subsup', base).setProperties({
        position: position,
        primes: primes,
        movesupsub: movesupsub,
      })
    );
  },

  /**
   * Handle '
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} c The parsed character.
   */
  Prime(parser: TexParser, c: string) {
    // @test Prime
    let base = parser.stack.Prev();
    if (!base) {
      // @test PrimeSup, PrePrime, Prime on Sup
      base = parser.create('token', 'mi');
    }
    if (
      (NodeUtil.isType(base, 'msubsup') &&
        !NodeUtil.isType(base, 'msup') &&
        NodeUtil.getChildAt(base, (base as MmlMsubsup).sup)) ||
      (NodeUtil.isType(base, 'munderover') &&
        !NodeUtil.isType(base, 'mover') &&
        NodeUtil.getChildAt(base, (base as MmlMunderover).over) &&
        !NodeUtil.getProperty(base, 'subsupOK'))
    ) {
      // @test Double Prime Error
      throw new TexError(
        'DoubleExponentPrime',
        'Prime causes double exponent: use braces to clarify'
      );
    }
    let sup = '';
    parser.i--;
    do {
      // @test Prime, PrimeSup, Double Prime, PrePrime
      sup += entities.prime;
      parser.i++;
      c = parser.GetNext();
    } while (c === "'" || c === entities.rsquo);
    sup = ['', '\u2032', '\u2033', '\u2034', '\u2057'][sup.length] || sup;
    const node = parser.create('token', 'mo', { variantForm: true }, sup);
    parser.Push(parser.itemFactory.create('prime', base, node));
  },

  /**
   * Handle comments
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _c The parsed character.
   */
  Comment(parser: TexParser, _c: string) {
    while (
      parser.i < parser.string.length &&
      parser.string.charAt(parser.i) !== '\n'
    ) {
      parser.i++;
    }
  },

  /**
   * Handle hash marks outside of definitions
   *
   * @param {TexParser} _parser The calling parser.
   * @param {string} _c The parsed character.
   */
  Hash(_parser: TexParser, _c: string) {
    // @test Hash Error
    throw new TexError(
      'CantUseHash1',
      "You can't use 'macro parameter character #' in math mode"
    );
  },

  /**
   * Handle \mathrm, \mathbf, etc, allowing for multi-letter runs to be one <mi>.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {string} variant The font variant.
   * @param {string} italic Optionally, the italic font name.
   */
  MathFont(
    parser: TexParser,
    name: string,
    variant: string,
    italic: string = ''
  ) {
    const text = parser.GetArgument(name);
    const mml = new TexParser(
      text,
      {
        multiLetterIdentifiers: parser.options.identifierPattern,
        ...parser.stack.env,
        font: variant,
        italicFont: italic,
        noAutoOP: true,
      },
      parser.configuration
    ).mml();
    parser.Push(parser.create('node', 'TeXAtom', [mml]));
  },

  /**
   * Setting font, e.g., via \\rm, \\bf etc.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   * @param {string} font The font name.
   */
  SetFont(parser: TexParser, _name: string, font: string) {
    parser.stack.env['font'] = font;
    parser.Push(parser.itemFactory.create('null'));
  },

  /**
   * Setting style, e.g., via \\displaystyle, \\textstyle, etc.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   * @param {string} texStyle The tex style name: D, T, S, SS
   * @param {boolean} style True if we are in displaystyle.
   * @param {string} level The nesting level for scripts.
   */
  SetStyle(
    parser: TexParser,
    _name: string,
    texStyle: string,
    style: boolean,
    level: string
  ) {
    parser.stack.env['style'] = texStyle;
    parser.stack.env['level'] = level;
    parser.Push(
      parser.itemFactory
        .create('style')
        .setProperty('styles', { displaystyle: style, scriptlevel: level })
    );
  },

  /**
   * Setting size of an expression, e.g., \\small, \\huge.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   * @param {number} size The size value.
   */
  SetSize(parser: TexParser, _name: string, size: number) {
    parser.stack.env['size'] = size;
    parser.Push(
      parser.itemFactory
        .create('style')
        .setProperty('styles', { mathsize: em(size) })
    );
  },

  /**
   * Setting explicit spaces, e.g., via commata or colons.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   * @param {string} space The space value.
   */
  Spacer(parser: TexParser, _name: string, space: number) {
    // @test Positive Spacing, Negative Spacing
    const node = parser.create('node', 'mspace', [], { width: em(space) });
    const style = parser.create('node', 'mstyle', [node], { scriptlevel: 0 });
    parser.Push(style);
  },

  /**
   * Create a discretionary times operator.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   */
  DiscretionaryTimes(parser: TexParser, _name: string) {
    parser.Push(
      parser.create('token', 'mo', { linebreakmultchar: '\u00D7' }, '\u2062')
    );
  },

  /**
   * Create a discretionary breakpoint.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   */
  AllowBreak(parser: TexParser, _name: string) {
    parser.Push(parser.create('token', 'mspace'));
  },

  /**
   * Create a forced breakpoint.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   */
  Break(parser: TexParser, _name: string) {
    parser.Push(
      parser.create('token', 'mspace', {
        linebreak: TexConstant.LineBreak.NEWLINE,
      })
    );
  },

  /**
   * Set surrounding mo linebreak attributes
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   * @param {string} linebreak The linebreak attribute to use.
   */
  Linebreak(parser: TexParser, _name: string, linebreak: string) {
    let insert = true;
    const prev = parser.stack.Prev(true);
    if (prev && prev.isKind('mo')) {
      const style = NodeUtil.getMoAttribute(prev, 'linebreakstyle');
      if (style !== TexConstant.LineBreakStyle.BEFORE) {
        prev.attributes.set('linebreak', linebreak);
        insert = false;
      }
    }
    parser.Push(parser.itemFactory.create('break', linebreak, insert));
  },

  /**
   * Parses left/right fenced expressions.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  LeftRight(parser: TexParser, name: string) {
    // @test Fenced, Fenced3
    const first = name.substring(1);
    parser.Push(
      parser.itemFactory.create(
        first,
        parser.GetDelimiter(name),
        parser.stack.env.color
      )
    );
  },

  /**
   * Handle a named math function, e.g., \\sin, \\cos
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {string} id Alternative string representation of the function.
   */
  NamedFn(parser: TexParser, name: string, id: string) {
    // @test Named Function
    if (!id) {
      id = name.substring(1);
    }
    const mml = parser.create('token', 'mi', { texClass: TEXCLASS.OP }, id);
    parser.Push(parser.itemFactory.create('fn', mml));
  },

  /**
   * Handle a named math operator, e.g., \\min, \\lim
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {string} id Alternative string representation of the operator.
   */
  NamedOp(parser: TexParser, name: string, id: string) {
    // @test Limit
    if (!id) {
      id = name.substring(1);
    }
    id = id.replace(/&thinsp;/, '\u2006');
    const mml = parser.create(
      'token',
      'mo',
      {
        movablelimits: true,
        movesupsub: true,
        form: TexConstant.Form.PREFIX,
        texClass: TEXCLASS.OP,
      },
      id
    );
    parser.Push(mml);
  },

  /**
   * Handle a limits command for math operators.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   * @param {boolean} limits True for \limits, false for \nolimits.
   */
  Limits(parser: TexParser, _name: string, limits: boolean) {
    // @test Limits
    let op = parser.stack.Prev(true);
    // Get the texclass for the core operator.
    if (
      !op ||
      (NodeUtil.getTexClass(NodeUtil.getCoreMO(op)) !== TEXCLASS.OP &&
        NodeUtil.getProperty(op, 'movesupsub') == null)
    ) {
      // @test Limits Error
      throw new TexError(
        'MisplacedLimits',
        '%1 is allowed only on operators',
        parser.currentCS
      );
    }
    const top = parser.stack.Top();
    let node;
    if (NodeUtil.isType(op, 'munderover') && !limits) {
      // @test Limits UnderOver
      node = parser.create('node', 'msubsup');
      NodeUtil.copyChildren(op, node);
      op = top.Last = node;
    } else if (NodeUtil.isType(op, 'msubsup') && limits) {
      // @test Limits SubSup
      // node = parser.create('node', 'munderover', NodeUtil.getChildren(op), {});
      // Needs to be copied, otherwise we get an error in MmlNode.appendChild!
      node = parser.create('node', 'munderover');
      NodeUtil.copyChildren(op, node);
      op = top.Last = node;
    }
    NodeUtil.setProperty(op, 'movesupsub', limits ? true : false);
    NodeUtil.setProperties(NodeUtil.getCoreMO(op), { movablelimits: false });
    if (
      (NodeUtil.isType(op, 'mo')
        ? NodeUtil.getMoAttribute(op, 'movableLimits')
        : NodeUtil.getAttribute(op, 'movablelimits')) ||
      NodeUtil.getProperty(op, 'movablelimits')
    ) {
      NodeUtil.setProperties(op, { movablelimits: false });
    }
  },

  /**
   * Handle over commands.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {string} open The open delimiter in case of a "withdelim" version.
   * @param {string} close The close delimiter.
   */
  Over(parser: TexParser, name: string, open: string, close: string) {
    // @test Over
    const mml = parser.itemFactory
      .create('over')
      .setProperty('name', parser.currentCS);
    if (open || close) {
      // @test Choose
      mml.setProperty('ldelim', open);
      mml.setProperty('rdelim', close);
    } else if (name.match(/withdelims$/)) {
      // @test Over With Delims, Above With Delims
      mml.setProperty('ldelim', parser.GetDelimiter(name));
      mml.setProperty('rdelim', parser.GetDelimiter(name));
    }
    if (name.match(/^\\above/)) {
      // @test Above, Above With Delims
      mml.setProperty('thickness', parser.GetDimen(name));
    } else if (name.match(/^\\atop/) || open || close) {
      // @test Choose
      mml.setProperty('thickness', 0);
    }
    parser.Push(mml);
  },

  /**
   * Parses a fraction.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  Frac(parser: TexParser, name: string) {
    // @test Frac
    const num = parser.ParseArg(name);
    const den = parser.ParseArg(name);
    const node = parser.create('node', 'mfrac', [num, den]);
    parser.Push(node);
  },

  /**
   * Parses a square root element.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  Sqrt(parser: TexParser, name: string) {
    const n = parser.GetBrackets(name);
    let arg = parser.GetArgument(name);
    if (arg === '\\frac') {
      arg +=
        '{' + parser.GetArgument(arg) + '}{' + parser.GetArgument(arg) + '}';
    }
    let mml = new TexParser(arg, parser.stack.env, parser.configuration).mml();
    if (!n) {
      // @test Square Root
      mml = parser.create('node', 'msqrt', [mml]);
    } else {
      // @test General Root
      mml = parser.create('node', 'mroot', [mml, parseRoot(parser, n)]);
    }
    parser.Push(mml);
  },

  /**
   * Parse a general root.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  Root(parser: TexParser, name: string) {
    const n = parser.GetUpTo(name, '\\of');
    const arg = parser.ParseArg(name);
    const node = parser.create('node', 'mroot', [arg, parseRoot(parser, n)]);
    parser.Push(node);
  },

  /**
   * Parses a movable index element in a root, e.g. \\uproot, \\leftroot
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {string} id Argument which should be a string representation of an integer.
   */
  MoveRoot(parser: TexParser, name: string, id: string) {
    // @test Tweaked Root
    if (!parser.stack.env['inRoot']) {
      // @test Misplaced Move Root
      throw new TexError(
        'MisplacedMoveRoot',
        '%1 can appear only within a root',
        parser.currentCS
      );
    }
    if (parser.stack.global[id]) {
      // @test Multiple Move Root
      throw new TexError(
        'MultipleMoveRoot',
        'Multiple use of %1',
        parser.currentCS
      );
    }
    let n = parser.GetArgument(name);
    if (!n.match(/-?[0-9]+/)) {
      // @test Incorrect Move Root
      throw new TexError(
        'IntegerArg',
        'The argument to %1 must be an integer',
        parser.currentCS
      );
    }
    n = parseInt(n, 10) / 15 + 'em';
    if (n.substring(0, 1) !== '-') {
      n = '+' + n;
    }
    parser.stack.global[id] = n;
  },

  /**
   * Handle accents.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {string} accent The accent.
   * @param {boolean} stretchy True if accent is stretchy, false if mathaccent should be false.
   */
  Accent(parser: TexParser, name: string, accent: string, stretchy: boolean) {
    // @test Vector
    const c = parser.ParseArg(name);
    // @test Vector Font
    const def = {
      ...ParseUtil.getFontDef(parser),
      accent: true,
      mathaccent: stretchy === undefined ? true : stretchy,
    };
    const entity = NodeUtil.createEntity(accent);
    const mml = parser.create('token', 'mo', def, entity);
    NodeUtil.setAttribute(mml, 'stretchy', stretchy ? true : false);
    // @test Vector Op, Vector
    const mo = NodeUtil.isEmbellished(c) ? NodeUtil.getCoreMO(c) : c;
    if (
      NodeUtil.isType(mo, 'mo') ||
      NodeUtil.getProperty(mo, 'movablelimits')
    ) {
      // @test Vector Op
      NodeUtil.setProperties(mo, { movablelimits: false });
    }
    const muoNode = parser.create('node', 'munderover');
    // This is necessary to get the empty element into the children.
    NodeUtil.setChild(muoNode, 0, c);
    NodeUtil.setChild(muoNode, 1, null);
    NodeUtil.setChild(muoNode, 2, mml);
    const texAtom = parser.create('node', 'TeXAtom', [muoNode]);
    parser.Push(texAtom);
  },

  /**
   * Handles stacked elements.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {string} c Character to stack.
   * @param {boolean} stack True if stacked operator.
   */
  UnderOver(parser: TexParser, name: string, c: string, stack: boolean) {
    const entity = NodeUtil.createEntity(c);
    const mo = parser.create(
      'token',
      'mo',
      { stretchy: true, accent: true },
      entity
    );
    if (entity.match(MmlMo.mathaccentsWithWidth)) {
      mo.setProperty('mathaccent', false);
    }
    const pos = name.charAt(1) === 'o' ? 'over' : 'under';
    const base = parser.ParseArg(name);
    parser.Push(ParseUtil.underOver(parser, base, mo, pos, stack));
  },

  /**
   * Handles overset.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  Overset(parser: TexParser, name: string) {
    // @test Overset
    const top = parser.ParseArg(name);
    const base = parser.ParseArg(name);
    const topMo = top.coreMO();
    const accent =
      topMo.isKind('mo') && NodeUtil.getMoAttribute(topMo, 'accent') === true;
    ParseUtil.checkMovableLimits(base);
    const node = parser.create('node', 'mover', [base, top], { accent });
    parser.Push(node);
  },

  /**
   * Handles underset.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  Underset(parser: TexParser, name: string) {
    // @test Underset
    const bot = parser.ParseArg(name);
    const base = parser.ParseArg(name);
    const botMo = bot.coreMO();
    const accentunder =
      botMo.isKind('mo') && NodeUtil.getMoAttribute(botMo, 'accent') === true;
    ParseUtil.checkMovableLimits(base);
    const node = parser.create('node', 'munder', [base, bot], { accentunder });
    parser.Push(node);
  },

  /**
   * Handles overunderset.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  Overunderset(parser: TexParser, name: string) {
    const top = parser.ParseArg(name);
    const bot = parser.ParseArg(name);
    const base = parser.ParseArg(name);
    const topMo = top.coreMO();
    const botMo = bot.coreMO();
    const accent =
      topMo.isKind('mo') && NodeUtil.getMoAttribute(topMo, 'accent') === true;
    const accentunder =
      botMo.isKind('mo') && NodeUtil.getMoAttribute(botMo, 'accent') === true;
    ParseUtil.checkMovableLimits(base);
    const node = parser.create('node', 'munderover', [base, bot, top], {
      accent,
      accentunder,
    });
    parser.Push(node);
  },

  /**
   * Creates TeXAtom, when class of element is changed explicitly.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {number} mclass The new TeX class.
   */
  TeXAtom(parser: TexParser, name: string, mclass: number) {
    const def: EnvList = { texClass: mclass };
    let mml: StackItem | MmlNode;
    let node: MmlNode;
    if (mclass === TEXCLASS.OP) {
      def['movesupsub'] = def['movablelimits'] = true;
      const arg = parser.GetArgument(name);
      const match = arg.match(/^\s*\\rm\s+([a-zA-Z0-9 ]+)$/);
      if (match) {
        // @test Mathop
        def['mathvariant'] = TexConstant.Variant.NORMAL;
        node = parser.create('token', 'mi', def, match[1]);
      } else {
        // @test Mathop Cal
        const parsed = new TexParser(
          arg,
          parser.stack.env,
          parser.configuration
        ).mml();
        node = parser.create('node', 'TeXAtom', [parsed], def);
      }
      mml = parser.itemFactory.create('fn', node);
    } else {
      // @test Mathrel
      mml = parser.create('node', 'TeXAtom', [parser.ParseArg(name)], def);
    }
    parser.Push(mml);
  },

  /**
   * Creates vboxes with various vertical alignments
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {string} align The alignment for the box.
   */
  VBox(parser: TexParser, name: string, align: string) {
    const arg = new TexParser(
      parser.GetArgument(name),
      parser.stack.env,
      parser.configuration
    );
    const def = {
      'data-vertical-align': align,
      texClass: TEXCLASS.ORD,
    } as EnvList;
    if (arg.stack.env.hsize) {
      def.width = arg.stack.env.hsize;
      def['data-overflow'] = 'linebreak';
    }
    const mml = parser.create('node', 'mpadded', [arg.mml()], def);
    mml.setProperty('vbox', align);
    parser.Push(mml);
  },

  /**
   * Sets hsize for \vbox, \vtop, \vcenter boxes
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  Hsize(parser: TexParser, name: string) {
    if (parser.GetNext() === '=') {
      parser.i++;
    }
    parser.stack.env.hsize = parser.GetDimen(name);
    parser.Push(parser.itemFactory.create('null'));
  },

  /**
   * Handle \parbox[align]{width}{text}
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  ParBox(parser: TexParser, name: string) {
    const c = parser.GetBrackets(name, 'c');
    const width = parser.GetDimen(name);
    const text = ParseUtil.internalMath(parser, parser.GetArgument(name));
    const align = splitAlignArray(c, 1);
    const mml = parser.create('node', 'mpadded', text, {
      width: width,
      'data-overflow': 'linebreak',
      'data-vertical-align': align,
    });
    mml.setProperty('vbox', align);
    parser.Push(mml);
  },

  /**
   * Handle \breakAlign{type}{align} for type = c, r, or t
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  BreakAlign(parser: TexParser, name: string) {
    const top = parser.stack.Top() as sitem.ArrayItem;
    if (!(top instanceof sitem.ArrayItem)) {
      throw new TexError(
        'BreakNotInArray',
        '%1 must be used in an alignment environment',
        parser.currentCS
      );
    }
    const type = parser.GetArgument(name).trim();
    switch (type) {
      case 'c':
        if (top.First) {
          throw new TexError(
            'BreakFirstInEntry',
            '%1 must be at the beginning of an alignment entry',
            parser.currentCS + '{c}'
          );
        }
        top.breakAlign.cell = splitAlignArray(parser.GetArgument(name), 1);
        break;
      case 'r':
        if (top.row.length || top.First) {
          throw new TexError(
            'BreakFirstInRow',
            '%1 must be at the beginning of an alignment row',
            parser.currentCS + '{r}'
          );
        }
        top.breakAlign.row = splitAlignArray(parser.GetArgument(name));
        break;
      case 't':
        if (top.table.length || top.row.length || top.First) {
          throw new TexError(
            'BreakFirstInTable',
            '%1 must be at the beginning of an alignment',
            parser.currentCS + '{t}'
          );
        }
        top.breakAlign.table = splitAlignArray(parser.GetArgument(name));
        break;
      default:
        throw new TexError(
          'BreakType',
          'First argument to %1 must be one of c, r, or t',
          parser.currentCS
        );
    }
  },

  /**
   * Creates mmltoken elements. Used in Macro substitutions.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  MmlToken(parser: TexParser, name: string) {
    // @test Modulo
    const kind = parser.GetArgument(name);
    let attr = parser.GetBrackets(name, '').replace(/^\s+/, '');
    const text = parser.GetArgument(name);
    const def: EnvList = {};
    const keep: string[] = [];
    let node: MmlNode;
    try {
      node = parser.create('node', kind);
    } catch (_e) {
      node = null;
    }
    if (!node || !node.isToken) {
      // @test Token Illegal Type, Token Wrong Type
      throw new TexError('NotMathMLToken', '%1 is not a token element', kind);
    }
    while (attr !== '') {
      const match = attr.match(
        /^([a-z]+)\s*=\s*('[^'\n]*'|"[^"\n]*"|[^ ,\n]*)[\s\n]*,?[\s\n]*/i
      );
      if (!match) {
        // @test Token Invalid Attribute
        throw new TexError(
          'InvalidMathMLAttr',
          'Invalid MathML attribute: %1',
          attr.split(/[\s\n=]/)[0]
        );
      }
      if (!node.attributes.hasDefault(match[1]) && !MmlTokenAllow[match[1]]) {
        // @test Token Unknown Attribute, Token Wrong Attribute
        throw new TexError(
          'UnknownAttrForElement',
          '%1 is not a recognized attribute for %2',
          match[1],
          kind
        );
      }
      let value: string | boolean = ParseUtil.mmlFilterAttribute(
        parser,
        match[1],
        match[2].replace(/^(['"])(.*)\1$/, '$2')
      );
      if (value) {
        if (value.toLowerCase() === 'true') {
          value = true;
        } else if (value.toLowerCase() === 'false') {
          value = false;
        }
        def[match[1]] = value;
        keep.push(match[1]);
      }
      attr = attr.substring(match[0].length);
    }
    if (keep.length) {
      node.setProperty('keep-attrs', keep.join(' '));
    }
    const textNode = parser.create('text', replaceUnicode(text));
    node.appendChild(textNode);
    NodeUtil.setProperties(node, def);
    parser.Push(node);
  },

  /**
   * Handle strut.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   */
  Strut(parser: TexParser, _name: string) {
    // @test Strut
    const row = parser.create('node', 'mrow');
    const padded = parser.create('node', 'mpadded', [row], {
      height: '8.6pt',
      depth: '3pt',
      width: 0,
    });
    parser.Push(padded);
  },

  /**
   * Handle phantom commands.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {string} v Vertical size.
   * @param {string} h Horizontal size.
   */
  Phantom(parser: TexParser, name: string, v: string, h: string) {
    // @test Phantom
    let box = parser.create('node', 'mphantom', [parser.ParseArg(name)]);
    if (v || h) {
      // TEMP: Changes here
      box = parser.create('node', 'mpadded', [box]);
      if (h) {
        // @test Horizontal Phantom
        NodeUtil.setAttribute(box, 'height', 0);
        NodeUtil.setAttribute(box, 'depth', 0);
      }
      if (v) {
        // @test Vertical Phantom
        NodeUtil.setAttribute(box, 'width', 0);
      }
    }
    const atom = parser.create('node', 'TeXAtom', [box]);
    parser.Push(atom);
  },

  /**
   * Handle smash.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  Smash(parser: TexParser, name: string) {
    // @test Smash, Smash Top, Smash Bottom
    const bt = UnitUtil.trimSpaces(parser.GetBrackets(name, ''));
    const smash = parser.create('node', 'mpadded', [parser.ParseArg(name)]);
    // TEMP: Changes here:
    switch (bt) {
      case 'b':
        NodeUtil.setAttribute(smash, 'depth', 0);
        break;
      case 't':
        NodeUtil.setAttribute(smash, 'height', 0);
        break;
      default:
        NodeUtil.setAttribute(smash, 'height', 0);
        NodeUtil.setAttribute(smash, 'depth', 0);
    }
    const atom = parser.create('node', 'TeXAtom', [smash]);
    parser.Push(atom);
  },

  /**
   * Handle rlap and llap commands.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  Lap(parser: TexParser, name: string) {
    // @test Llap, Rlap
    const mml = parser.create('node', 'mpadded', [parser.ParseArg(name)], {
      width: 0,
    });
    if (name === '\\llap') {
      // @test Llap
      NodeUtil.setAttribute(mml, 'lspace', '-1width');
    }
    const atom = parser.create('node', 'TeXAtom', [mml]);
    parser.Push(atom);
  },

  /**
   * Handle raise and lower commands.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  RaiseLower(parser: TexParser, name: string) {
    // @test Raise, Lower, Raise Negative, Lower Negative
    let h = parser.GetDimen(name);
    const item = parser.itemFactory
      .create('position')
      .setProperties({ name: parser.currentCS, move: 'vertical' });
    // TEMP: Changes here:
    if (h.charAt(0) === '-') {
      // @test Raise Negative, Lower Negative
      h = h.slice(1);
      name = name.substring(1) === 'raise' ? '\\lower' : '\\raise';
    }
    if (name === '\\lower') {
      // @test Raise, Raise Negative
      item.setProperty('dh', '-' + h);
      item.setProperty('dd', '+' + h);
    } else {
      // @test Lower, Lower Negative
      item.setProperty('dh', '+' + h);
      item.setProperty('dd', '-' + h);
    }
    parser.Push(item);
  },

  /**
   * Handle moveleft, moveright commands
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  MoveLeftRight(parser: TexParser, name: string) {
    // @test Move Left, Move Right, Move Left Negative, Move Right Negative
    let h = parser.GetDimen(name);
    let nh = h.charAt(0) === '-' ? h.slice(1) : '-' + h;
    if (name === '\\moveleft') {
      const tmp = h;
      h = nh;
      nh = tmp;
    }
    parser.Push(
      parser.itemFactory.create('position').setProperties({
        name: parser.currentCS,
        move: 'horizontal',
        left: parser.create('node', 'mspace', [], { width: h }),
        right: parser.create('node', 'mspace', [], { width: nh }),
      })
    );
  },

  /**
   * Handle horizontal spacing commands.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {boolean} nobreak Flag indicating if it is a linebreaking command.
   */
  Hskip(parser: TexParser, name: string, nobreak: boolean = false) {
    // @test Modulo
    const node = parser.create('node', 'mspace', [], {
      width: parser.GetDimen(name),
    });
    if (nobreak) {
      NodeUtil.setAttribute(node, 'linebreak', 'nobreak');
    }
    parser.Push(node);
  },

  /**
   * Handle removal of spaces in script modes
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   */
  Nonscript(parser: TexParser, _name: string) {
    parser.Push(parser.itemFactory.create('nonscript'));
  },

  /**
   * Handle Rule and Space command
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {string} style The style of the rule spacer.
   */
  Rule(parser: TexParser, name: string, style: string) {
    // @test Rule 3D, Space 3D
    const w = parser.GetDimen(name),
      h = parser.GetDimen(name),
      d = parser.GetDimen(name);
    const def: EnvList = { width: w, height: h, depth: d };
    if (style !== 'blank') {
      def['mathbackground'] = parser.stack.env['color'] || 'black';
    }
    const node = parser.create('node', 'mspace', [], def);
    parser.Push(node);
  },

  /**
   * Handle rule command.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  rule(parser: TexParser, name: string) {
    // @test Rule 2D
    const v = parser.GetBrackets(name),
      w = parser.GetDimen(name),
      h = parser.GetDimen(name);
    let mml = parser.create('node', 'mspace', [], {
      width: w,
      height: h,
      mathbackground: parser.stack.env['color'] || 'black',
    });
    if (v) {
      mml = parser.create('node', 'mpadded', [mml], { voffset: v });
      if (v.match(/^-/)) {
        NodeUtil.setAttribute(mml, 'height', v);
        NodeUtil.setAttribute(mml, 'depth', '+' + v.substring(1));
      } else {
        NodeUtil.setAttribute(mml, 'height', '+' + v);
      }
    }
    parser.Push(mml);
  },

  /**
   * Handle big command sequences, e.g., \\big, \\Bigg.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {number} mclass The TeX class of the element.
   * @param {number} size The em size.
   */
  MakeBig(parser: TexParser, name: string, mclass: number, size: number) {
    // @test Choose, Over With Delims, Above With Delims
    size *= P_HEIGHT;
    const sizeStr = String(size).replace(/(\.\d\d\d).+/, '$1') + 'em';
    const delim = parser.GetDelimiter(name, true);
    const mo = parser.create(
      'token',
      'mo',
      {
        minsize: sizeStr,
        maxsize: sizeStr,
        fence: true,
        stretchy: true,
        symmetric: true,
      },
      delim
    );
    const node = parser.create('node', 'TeXAtom', [mo], { texClass: mclass });
    parser.Push(node);
  },

  /**
   * Handle buildrel command.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  BuildRel(parser: TexParser, name: string) {
    // @test BuildRel, BuildRel Expression
    const top = parser.ParseUpTo(name, '\\over');
    const bot = parser.ParseArg(name);
    const node = parser.create('node', 'munderover');
    // This is necessary to get the empty element into the children.
    NodeUtil.setChild(node, 0, bot);
    NodeUtil.setChild(node, 1, null);
    NodeUtil.setChild(node, 2, top);
    const atom = parser.create('node', 'TeXAtom', [node], {
      texClass: TEXCLASS.REL,
    });
    parser.Push(atom);
  },

  /**
   * Handle horizontal boxes.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {string} style Box style.
   * @param {string} font The mathvariant to use
   */
  HBox(parser: TexParser, name: string, style: string, font?: string) {
    // @test Hbox
    parser.PushAll(
      ParseUtil.internalMath(parser, parser.GetArgument(name), style, font)
    );
  },

  /**
   * Handle framed boxes.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  FBox(parser: TexParser, name: string) {
    // @test Fbox
    const internal = ParseUtil.internalMath(parser, parser.GetArgument(name));
    const node = parser.create('node', 'menclose', internal, {
      notation: 'box',
    });
    parser.Push(node);
  },

  /**
   * Handle framed boxes with options.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  FrameBox(parser: TexParser, name: string) {
    const width = parser.GetBrackets(name);
    const pos = parser.GetBrackets(name) || 'c';
    let mml = ParseUtil.internalMath(parser, parser.GetArgument(name));
    if (width) {
      mml = [
        parser.create('node', 'mpadded', mml, {
          width,
          'data-align': lookup(pos, { l: 'left', r: 'right' }, 'center'),
        }),
      ];
    }
    const node = parser.create(
      'node',
      'TeXAtom',
      [parser.create('node', 'menclose', mml, { notation: 'box' })],
      { texClass: TEXCLASS.ORD }
    );
    parser.Push(node);
  },

  /**
   * Implements \makebox.
   *
   * @param {TexParser} parser   The calling parser.
   * @param {string} name        The macro name.
   */
  MakeBox(parser: TexParser, name: string) {
    const width = parser.GetBrackets(name);
    const pos = parser.GetBrackets(name, 'c');
    const mml = parser.create(
      'node',
      'mpadded',
      ParseUtil.internalMath(parser, parser.GetArgument(name))
    );
    if (width) {
      NodeUtil.setAttribute(mml, 'width', width);
    }
    const align = lookup(pos.toLowerCase(), { c: 'center', r: 'right' }, '');
    if (align) {
      NodeUtil.setAttribute(mml, 'data-align', align);
    }
    if (pos.toLowerCase() !== pos) {
      NodeUtil.setAttribute(mml, 'data-overflow', 'linebreak');
    }
    parser.Push(mml);
  },

  /**
   * Handle \\not.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   */
  Not(parser: TexParser, _name: string) {
    // @test Negation Simple, Negation Complex, Negation Explicit,
    //       Negation Large
    parser.Push(parser.itemFactory.create('not'));
  },

  /**
   * Handle dots.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   */
  Dots(parser: TexParser, _name: string) {
    // @test Operator Dots
    const ldotsEntity = NodeUtil.createEntity('2026');
    const cdotsEntity = NodeUtil.createEntity('22EF');
    const ldots = parser.create(
      'token',
      'mo',
      { stretchy: false },
      ldotsEntity
    );
    const cdots = parser.create(
      'token',
      'mo',
      { stretchy: false },
      cdotsEntity
    );
    parser.Push(
      parser.itemFactory.create('dots').setProperties({
        ldots: ldots,
        cdots: cdots,
      })
    );
  },

  /**
   * Handle small matrix environments.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   * @param {string} open Opening fence.
   * @param {string} close Closing fence.
   * @param {string} align Column alignment.
   * @param {string} spacing Column spacing.
   * @param {string} vspacing Row spacing.
   * @param {string} style Display or text style.
   * @param {boolean} cases Is it a cases environment.
   * @param {boolean} numbered Is it a numbered environment.
   */
  Matrix(
    parser: TexParser,
    _name: string,
    open: string,
    close: string,
    align: string,
    spacing: string,
    vspacing: string,
    style: string,
    cases: boolean,
    numbered: boolean
  ) {
    const c = parser.GetNext();
    if (c === '') {
      // @test Matrix Error
      throw new TexError(
        'MissingArgFor',
        'Missing argument for %1',
        parser.currentCS
      );
    }
    if (c === '{') {
      // @test Matrix Braces, Matrix Columns, Matrix Rows.
      parser.i++;
    } else {
      // @test Matrix Arg
      parser.string = c + '}' + parser.string.slice(parser.i + 1);
      parser.i = 0;
    }
    // @test Matrix Braces, Matrix Columns, Matrix Rows.
    const array = parser.itemFactory
      .create('array')
      .setProperty('requireClose', true) as sitem.ArrayItem;
    if (open || !align) {
      array.setProperty('arrayPadding', '.2em .125em');
    }
    array.arraydef = {
      rowspacing: vspacing || '4pt',
      columnspacing: spacing || '1em',
    };
    if (cases) {
      // @test Matrix Cases
      array.setProperty('isCases', true);
    }
    if (numbered) {
      // @test Matrix Numbered
      array.setProperty('isNumbered', true);
      array.arraydef.side = numbered;
    }
    if (open || close) {
      // @test Matrix Parens, Matrix Parens Subscript, Matrix Cases
      array.setProperty('open', open);
      array.setProperty('close', close);
    }
    if (style === 'D') {
      // @test Matrix Numbered
      array.arraydef.displaystyle = true;
    }
    if (align != null) {
      // @test Matrix Cases, Matrix Numbered
      array.arraydef.columnalign = align;
    }
    parser.Push(array);
  },

  /**
   * Handle array entry.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  Entry(parser: TexParser, name: string) {
    // @test Label, Array, Cross Product Formula
    parser.Push(
      parser.itemFactory
        .create('cell')
        .setProperties({ isEntry: true, name: name })
    );
    const top = parser.stack.Top();
    const env = top.getProperty('casesEnv') as string;
    const cases = top.getProperty('isCases');
    if (!cases && !env) return;
    //
    //  Make second column be in \text{...} (unless it is already
    //  in a \text{...}, for backward compatibility).
    //
    const str = parser.string;
    let braces = 0;
    let close = -1;
    let i = parser.i;
    let m = str.length;
    const end = env
      ? new RegExp(`^\\\\end\\s*\\{${env.replace(/\*/, '\\*')}\\}`)
      : null;
    //
    //  Look through the string character by character...
    //
    while (i < m) {
      const c = str.charAt(i);
      if (c === '{') {
        //
        //  Increase the nested brace count and go on
        //
        braces++;
        i++;
      } else if (c === '}') {
        //
        //  If there are too many close braces, just end (we will get an
        //    error message later when the rest of the string is parsed)
        //  Otherwise
        //    decrease the nested brace count,
        //    if it is now zero and we haven't already marked the end of the
        //      first brace group, record the position (use to check for \text{} later)
        //    go on to the next character.
        //
        if (braces === 0) {
          m = 0;
        } else {
          braces--;
          if (braces === 0 && close < 0) {
            close = i - parser.i;
          }
          i++;
        }
      } else if (c === '&' && braces === 0) {
        //
        //  Extra alignment tabs are not allowed in cases
        //
        // @test ExtraAlignTab
        throw new TexError(
          'ExtraAlignTab',
          'Extra alignment tab in \\cases text'
        );
      } else if (c === '\\') {
        //
        //  If the macro is \cr or \\, end the search, otherwise skip the macro
        //  (multi-letter names don't matter, as we will skip the rest of the
        //   characters in the main loop)
        //
        const rest = str.substring(i);
        if (rest.match(/^((\\cr)[^a-zA-Z]|\\\\)/) || (end && rest.match(end))) {
          m = 0;
        } else {
          i += 2;
        }
      } else {
        //
        //  Go on to the next character
        //
        i++;
      }
    }
    //
    //  Check if the second column text is already in \text{};
    //  If not, process the second column as text and continue parsing from there,
    //    (otherwise process the second column as normal, since it is in \text{}
    //
    // i >= parser.i
    //
    const text = str.substring(parser.i, i);
    if (
      !text.match(/^\s*\\text[^a-zA-Z]/) ||
      close !== text.replace(/\s+$/, '').length - 1
    ) {
      const internal = ParseUtil.internalMath(
        parser,
        UnitUtil.trimSpaces(text),
        0
      );
      parser.PushAll(internal);
      parser.i = i;
    }
  },

  /**
   * Handle newline in array.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  Cr(parser: TexParser, name: string) {
    // @test Cr Linebreak, Misplaced Cr
    parser.Push(
      parser.itemFactory
        .create('cell')
        .setProperties({ isCR: true, name: name })
    );
  },

  /**
   * Handle newline outside array.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {boolean} nobrackets Flag indicating if newline is followed by
   *     brackets.
   */
  CrLaTeX(parser: TexParser, name: string, nobrackets: boolean = false) {
    let n: string;
    if (!nobrackets) {
      // TODO: spaces before * and [ are not allowed in AMS environments like align, but
      //       should be allowed in array and eqnarray.  This distinction should be honored here.
      if (parser.string.charAt(parser.i) === '*') {
        // The * controls page breaking, so ignore it
        parser.i++;
      }
      if (parser.string.charAt(parser.i) === '[') {
        const dim = parser.GetBrackets(name, '');
        const [value, unit] = UnitUtil.matchDimen(dim);
        // @test Custom Linebreak
        if (dim && !value) {
          // @test Dimension Error
          throw new TexError(
            'BracketMustBeDimension',
            'Bracket argument to %1 must be a dimension',
            parser.currentCS
          );
        }
        n = value + unit;
      }
    }
    parser.Push(
      parser.itemFactory
        .create('cell')
        .setProperties({ isCR: true, name: name, linebreak: true })
    );
    const top = parser.stack.Top();
    let node: MmlNode;
    if (top instanceof sitem.ArrayItem) {
      // @test Array
      if (n) {
        top.addRowSpacing(n);
      }
    } else {
      // @test Linebreak
      node = parser.create('node', 'mspace', [], {
        linebreak: TexConstant.LineBreak.NEWLINE,
      });
      // @test Custom Linebreak
      if (n) {
        NodeUtil.setAttribute(node, 'data-lineleading', n);
      }
      parser.Push(node);
    }
  },

  /**
   * Handle horizontal lines in arrays.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   * @param {string} style Style of the line. E.g., dashed.
   */
  HLine(parser: TexParser, _name: string, style: string) {
    if (style == null) {
      style = 'solid';
    }
    const top = parser.stack.Top();
    if (!(top instanceof sitem.ArrayItem) || top.Size()) {
      // @test Misplaced hline
      throw new TexError('Misplaced', 'Misplaced %1', parser.currentCS);
    }
    if (!top.table.length) {
      // @test Enclosed top, Enclosed top bottom
      top.frame.push(['top', style]);
    } else {
      // @test Enclosed bottom, Enclosed top bottom
      const lines = top.arraydef['rowlines']
        ? (top.arraydef['rowlines'] as string).split(/ /)
        : [];
      while (lines.length < top.table.length) {
        lines.push('none');
      }
      lines[top.table.length - 1] = style;
      top.arraydef['rowlines'] = lines.join(' ');
    }
  },

  /**
   * Handle hfill commands.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   */
  HFill(parser: TexParser, _name: string) {
    const top = parser.stack.Top();
    if (top instanceof sitem.ArrayItem) {
      // @test Hfill
      top.hfill.push(top.Size());
    } else {
      // @test UnsupportedHFill
      throw new TexError(
        'UnsupportedHFill',
        'Unsupported use of %1',
        parser.currentCS
      );
    }
  },

  /**
   * Create new column declarations
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  NewColumnType(parser: TexParser, name: string) {
    const c = parser.GetArgument(name);
    const n = parser.GetBrackets(name, '0');
    const macro = parser.GetArgument(name);
    if (c.length !== 1) {
      throw new TexError(
        'BadColumnName',
        'Column specifier must be exactly one character: %1',
        c
      );
    }
    if (!n.match(/^\d+$/)) {
      throw new TexError(
        'PositiveIntegerArg',
        'Argument to %1 must be a positive integer',
        n
      );
    }
    const cparser = parser.configuration.columnParser;
    cparser.columnHandler[c] = (state: ColumnState) =>
      cparser.macroColumn(state, macro, parseInt(n));
    parser.Push(parser.itemFactory.create('null'));
  },

  /**
   *   LaTeX environments
   */

  /**
   * Handle begin and end environments. This is a macro method.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  BeginEnd(parser: TexParser, name: string) {
    // @test Array1, Array2, Array Test
    const env = parser.GetArgument(name);
    if (env.match(/\\/)) {
      // @test InvalidEnv
      throw new TexError('InvalidEnv', "Invalid environment name '%1'", env);
    }
    const macro = parser.configuration.handlers
      .get(HandlerType.ENVIRONMENT)
      .lookup(env) as Macro;
    if (macro && name === '\\end') {
      // If the first argument is true, we have some sort of user defined
      // environment. Otherwise we have a standard LaTeX environment that is
      // handled with begin and end items.
      if (!macro.args[0]) {
        const mml = parser.itemFactory.create('end').setProperty('name', env);
        parser.Push(mml);
        return;
      }
      // Remember the user defined environment we are closing.
      parser.stack.env['closing'] = env;
    }
    ParseUtil.checkMaxMacros(parser, false);
    parser.parse(HandlerType.ENVIRONMENT, [parser, env]);
  },

  /**
   * Handle array environment.
   *
   * @param {TexParser} parser The calling parser.
   * @param {StackItem} begin The opening stackitem.
   * @param {string} open Opening fence.
   * @param {string} close Closing fence.
   * @param {string} align Column alignment.
   * @param {string} spacing Column spacing.
   * @param {string} vspacing Row spacing.
   * @param {string} style Display or text style.
   * @param {boolean} raggedHeight Does the height need to be adjusted?
   * @returns {ParseResult} The array stack item.
   */
  Array(
    parser: TexParser,
    begin: StackItem,
    open: string,
    close: string,
    align: string,
    spacing: string,
    vspacing: string,
    style: string,
    raggedHeight: boolean
  ): ParseResult {
    if (!align) {
      // @test Array Single
      align = parser.GetArgument('\\begin{' + begin.getName() + '}');
    }
    const array = parser.itemFactory.create('array') as sitem.ArrayItem;
    if (begin.getName() === 'array') {
      array.setProperty('arrayPadding', '.5em .125em');
    }
    array.parser = parser;
    array.arraydef = {
      columnspacing: spacing || '1em',
      rowspacing: vspacing || '4pt',
    };
    parser.configuration.columnParser.process(parser, align, array);
    if (open) {
      // @test Cross Product
      array.setProperty('open', parser.convertDelimiter(open));
    }
    if (close) {
      // @test Cross Product
      array.setProperty('close', parser.convertDelimiter(close));
    }
    if ((style || '').charAt(1) === "'") {
      array.arraydef['data-cramped'] = true;
      style = style.charAt(0);
    }
    if (style === 'D') {
      // TODO: This case never seems to occur! No test.
      array.arraydef['displaystyle'] = true;
    } else if (style) {
      // @test Subarray, Small Matrix
      array.arraydef['displaystyle'] = false;
    }
    // @test Subarray, Small Matrix
    array.arraydef['scriptlevel'] = style === 'S' ? 1 : 0;
    if (raggedHeight) {
      // @test Subarray, Small Matrix
      array.arraydef['useHeight'] = false;
    }
    parser.Push(begin);
    array.StartEntry();
    return array;
  },

  /**
   * Handle aligned arrays.
   *
   * @param {TexParser} parser The calling parser.
   * @param {StackItem} begin The opening stackitem.
   * @param {string=} style The display style to use
   * @returns {ParseResult} The array stack item.
   */
  AlignedArray(
    parser: TexParser,
    begin: StackItem,
    style: string = ''
  ): ParseResult {
    // @test Array1, Array2, Array Test
    const align = parser.GetBrackets('\\begin{' + begin.getName() + '}');
    const item = BaseMethods.Array(
      parser,
      begin,
      null,
      null,
      null,
      null,
      null,
      style
    );
    return ParseUtil.setArrayAlign(item as sitem.ArrayItem, align);
  },

  /**
   * Handle indentalign environment
   *
   * @param {TexParser} parser The calling parser.
   * @param {StackItem} begin The opening stackitem.
   */
  IndentAlign(parser: TexParser, begin: StackItem) {
    const name = `\\begin{${begin.getName()}}`;
    //
    // Get the indentshift values, if any
    //
    const first = parser.GetBrackets(name, '');
    const shift = parser.GetBrackets(name, '');
    const last = parser.GetBrackets(name, '');
    if (
      (first && !UnitUtil.matchDimen(first)[0]) ||
      (shift && !UnitUtil.matchDimen(shift)[0]) ||
      (last && !UnitUtil.matchDimen(last)[0])
    ) {
      throw new TexError(
        'BracketMustBeDimension',
        'Bracket argument to %1 must be a dimension',
        name
      );
    }
    //
    // Get the indentalign values, if any
    //
    const lcr = parser.GetArgument(name);
    if (lcr && !lcr.match(/^([lcr]{1,3})?$/)) {
      throw new TexError(
        'BadAlignment',
        'Alignment must be one to three copies of l, c, or r'
      );
    }
    const align = [...lcr].map(
      (c) => ({ l: 'left', c: 'center', r: 'right' })[c]
    );
    if (align.length === 1) {
      align.push(align[0]);
    }
    //
    // Set the properties for the mstyle
    //
    const attr: PropertyList = {};
    for (const [name, value] of [
      ['indentshiftfirst', first],
      ['indentshift', shift || first],
      ['indentshiftlast', last],
      ['indentalignfirst', align[0]],
      ['indentalign', align[1]],
      ['indentalignlast', align[2]],
    ]) {
      if (value) {
        attr[name] = value;
      }
    }
    //
    // Push the indentalign item on the stack
    //
    parser.Push(parser.itemFactory.create('mstyle', attr, begin.getName()));
  },

  /**
   * Handle equation environment.
   *
   * @param {TexParser} parser The calling parser.
   * @param {StackItem} begin The opening stackitem.
   * @param {boolean} numbered True if environment is numbered.
   * @param {boolean} display True if equation is in display mode
   *
   * @returns {ParseResult} The constructed equation stackitem.
   */
  Equation(
    parser: TexParser,
    begin: StackItem,
    numbered: boolean,
    display: boolean = true
  ): ParseResult {
    parser.configuration.mathItem.display = display;
    parser.stack.env.display = display;
    ParseUtil.checkEqnEnv(parser);
    parser.Push(begin);
    return parser.itemFactory
      .create('equation', numbered)
      .setProperty('name', begin.getName());
  },

  /**
   * Handle eqnarray.
   *
   * @param {TexParser} parser The calling parser.
   * @param {StackItem} begin The opening stackitem.
   * @param {boolean} numbered True if environment is numbered.
   * @param {boolean} taggable True if taggable.
   * @param {string} align Alignment string.
   * @param {string} balign Vertical alignment string.
   * @param {string} spacing Spacing between columns.
   *
   * @returns {ParseResult} The constructed array stackitem.
   */
  EqnArray(
    parser: TexParser,
    begin: StackItem,
    numbered: boolean,
    taggable: boolean,
    align: string,
    balign: string,
    spacing: string
  ): ParseResult {
    // @test The Lorenz Equations, Maxwell's Equations, Cubic Binomial
    const name = begin.getName();
    const isGather = name === 'gather' || name === 'gather*';
    if (taggable) {
      ParseUtil.checkEqnEnv(parser, !isGather);
    }
    parser.Push(begin);
    align = align
      .replace(/[^clr]/g, '')
      .split('')
      .join(' ');
    align = align
      .replace(/l/g, 'left')
      .replace(/r/g, 'right')
      .replace(/c/g, 'center');
    balign = splitAlignArray(balign);
    const newItem = parser.itemFactory.create(
      'eqnarray',
      name,
      numbered,
      taggable,
      parser.stack.global
    ) as sitem.ArrayItem;
    newItem.arraydef = {
      displaystyle: true,
      columnalign: align,
      columnspacing: spacing || '1em',
      rowspacing: '3pt',
      'data-break-align': balign,
      side: parser.options['tagSide'],
      minlabelspacing: parser.options['tagIndent'],
    };
    if (isGather) {
      newItem.setProperty('nestable', true);
    }
    return newItem;
  },

  /**
   * Handles no tag commands.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} _name The macro name.
   */
  HandleNoTag(parser: TexParser, _name: string) {
    parser.tags.notag();
  },

  /**
   * Record a label name for a tag
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  HandleLabel(parser: TexParser, name: string) {
    // @test Label, Label Empty
    const label = parser.GetArgument(name);
    if (label === '') {
      // @test Label Empty
      return;
    }
    if (!parser.tags.refUpdate) {
      // @test Label, Ref, Ref Unknown
      if (parser.tags.label) {
        // @test Double Label Error
        throw new TexError('MultipleCommand', 'Multiple %1', parser.currentCS);
      }
      parser.tags.label = label;
      if (
        (parser.tags.allLabels[label] || parser.tags.labels[label]) &&
        !parser.options['ignoreDuplicateLabels']
      ) {
        // @ Duplicate Label Error
        throw new TexError(
          'MultipleLabel',
          "Label '%1' multiply defined",
          label
        );
      }
      // TODO: This should be set in the tags structure!
      parser.tags.labels[label] = new Label(); // will be replaced by tag value later
    }
  },

  /**
   * Handle a label reference.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {boolean} eqref True if formatted as eqref.
   */
  HandleRef(parser: TexParser, name: string, eqref: boolean) {
    // @test Ref, Ref Unknown, Eqref, Ref Default, Ref Named
    const label = parser.GetArgument(name);
    let ref = parser.tags.allLabels[label] || parser.tags.labels[label];
    if (!ref) {
      // @test Ref Unknown
      if (!parser.tags.refUpdate) {
        parser.tags.redo = true;
      }
      ref = new Label();
    }
    let tag: string | string[] = ref.tag;
    if (eqref) {
      // @test Eqref
      tag = parser.tags.formatRef(tag);
    }
    const node = parser.create(
      'node',
      'mrow',
      ParseUtil.internalMath(parser, Array.isArray(tag) ? tag.join('') : tag),
      {
        href: parser.tags.formatUrl(ref.id, parser.options.baseURL),
        class: 'MathJax_ref',
      }
    );
    parser.Push(node);
  },

  /**
   * Basic macro replacement.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   * @param {string} macro The macro string.
   * @param {number} argcount The number of arguments of the macro.
   * @param {string} def Additional definitions.
   */
  Macro(
    parser: TexParser,
    name: string,
    macro: string,
    argcount: number,
    def?: string
  ) {
    if (argcount) {
      const args: string[] = [];
      if (def != null) {
        const optional = parser.GetBrackets(name);
        args.push(optional == null ? def : optional);
      }
      for (let i = args.length; i < argcount; i++) {
        args.push(parser.GetArgument(name));
      }
      macro = ParseUtil.substituteArgs(parser, args, macro);
    }
    parser.string = ParseUtil.addArgs(
      parser,
      macro,
      parser.string.slice(parser.i)
    );
    parser.i = 0;
    ParseUtil.checkMaxMacros(parser);
  },

  /**
   * Handle MathChoice for elements whose exact size/style properties can only be
   * determined after the expression has been parsed.
   *
   * @param {TexParser} parser The calling parser.
   * @param {string} name The macro name.
   */
  MathChoice(parser: TexParser, name: string) {
    const D = parser.ParseArg(name);
    const T = parser.ParseArg(name);
    const S = parser.ParseArg(name);
    const SS = parser.ParseArg(name);
    parser.Push(parser.create('node', 'MathChoice', [D, T, S, SS]));
  },
};

export default BaseMethods;
