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

/**
 * @fileoverview  Implements the interface and abstract class for MathDocument objects
 *
 * @author dpvc@mathjax.org (Davide Cervone)
 */

import {userOptions, defaultOptions, OptionList, expandable} from '../util/Options.js';
import {InputJax, AbstractInputJax} from './InputJax.js';
import {OutputJax, AbstractOutputJax} from './OutputJax.js';
import {MathList, AbstractMathList} from './MathList.js';
import {MathItem, AbstractMathItem, STATE} from './MathItem.js';
import {MmlNode, TextNode} from './MmlTree/MmlNode.js';
import {MmlFactory} from '../core/MmlTree/MmlFactory.js';
import {DOMAdaptor} from '../core/DOMAdaptor.js';
import {BitField, BitFieldClass} from '../util/BitField.js';

import {PrioritizedList} from '../util/PrioritizedList.js';

/*****************************************************************/

/**
 * A function to call while rendering a document (usually calls a MathDocument method)
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export type RenderDoc<N, T, D> = (document: MathDocument<N, T, D>) => boolean;

/**
 * A function to call while rendering a MathItem (usually calls one of its methods)
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export type RenderMath<N, T, D> = (math: MathItem<N, T, D>, document: MathDocument<N, T, D>) => boolean;

/**
 * The data for an action to perform during rendering or conversion
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export type RenderData<N, T, D> = {
  id: string,                           //  The name for the action
  renderDoc: RenderDoc<N, T, D>,        //  The action to take during a render() call
  renderMath: RenderMath<N, T, D>,      //  The action to take during a rerender() or convert() call
  convert: boolean                      //  Whether the action is to be used during convert()
};

/**
 * The data used to define a render action in configurations and options objects
 *   (the key is used as the id, the number in the data below is the priority, and
 *    the remainind data is as described below; if no boolean is given, convert = true
 *    by default)
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export type RenderAction<N, T, D> =
  [number] |                                                     // id (i.e., key) is method name to use
  [number, string] |                                             // string is method to call
  [number, string, string] |                                     // the strings are methods names for doc and math
  [number, RenderDoc<N, T, D>, RenderMath<N, T, D>] |            // explicit functions for doc and math
  [number, boolean] |                                            // same as first above, with boolean for convert
  [number, string, boolean] |                                    // same as second above, with boolean for convert
  [number, string, string, boolean] |                            // same as third above, with boolean for convert
  [number, RenderDoc<N, T, D>, RenderMath<N, T, D>, boolean];    // same as forth above, with boolean for convert

/**
 * An object representing a collection of rendering actions (id's tied to priority-and-method data)
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export type RenderActions<N, T, D> = {[id: string]: RenderAction<N, T, D>};

/**
 * Implements a prioritized list of render actions.  Extensions can add actions to the list
 *   to make it easy to extend the normal typesetting and conversion operations.
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export class RenderList<N, T, D> extends PrioritizedList<RenderData<N, T, D>> {

  /**
   * Creates a new RenderList from an initial list of rendering actions
   *
   * @param {RenderActions} actions The list of actions to take during render(), rerender(), and convert() calls
   * @returns {RenderList}    The newly created prioritied list
   */
  public static create<N, T, D>(actions: RenderActions<N, T, D>): RenderList<N, T, D> {
    const list = new this<N, T, D>();
    for (const id of Object.keys(actions)) {
      const [action, priority] = this.action<N, T, D>(id, actions[id]);
      if (priority) {
        list.add(action, priority);
      }
    }
    return list;
  }

  /**
   * Parses a RenderAction to produce the correspinding RenderData item
   *  (e.g., turn method names into actual functions that call the method)
   *
   * @param {string} id               The id of the action
   * @param {RenderAction} action     The RenderAction defining the action
   * @returns {[RenderData,number]}   The corresponding RenderData definition for the action and its priority
   */
  public static action<N, T, D>(id: string, action: RenderAction<N, T, D>): [RenderData<N, T, D>, number] {
    let renderDoc, renderMath;
    let convert = true;
    let priority = action[0];
    if (action.length === 1 || typeof action[1] === 'boolean') {
      action.length === 2 && (convert = action[1] as boolean);
      [renderDoc, renderMath] = this.methodActions(id);
    } else if (typeof action[1] === 'string') {
      if (typeof action[2] === 'string') {
        action.length === 4 && (convert = action[3] as boolean);
        const [method1, method2] = action.slice(1) as [string, string];
        [renderDoc, renderMath] = this.methodActions(method1, method2);
      } else {
        action.length === 3 && (convert = action[2] as boolean);
        [renderDoc, renderMath] = this.methodActions(action[1] as string);
      }
    } else {
      action.length === 4 && (convert = action[3] as boolean);
      [renderDoc, renderMath] = action.slice(1) as [RenderDoc<N, T, D>, RenderMath<N, T, D>];
    }
    return [{id, renderDoc, renderMath, convert} as RenderData<N, T, D>, priority];
  }

  /**
   * Produces the doc and math actions for the given method name(s)
   *   (a blank name is a no-op)
   *
   * @param {string} method1    The method to use for the render() call
   * @param {string} method1    The method to use for the rerender() and convert() calls
   */
  protected static methodActions(method1: string, method2: string = method1) {
    return [
      (document: any) => {method1 && document[method1](); return false; },
      (math: any, document: any) => {method2 && math[method2](document); return false; }
    ];
  }

  /**
   * Perform the document-level rendering functions
   *
   * @param {MathDocument} document   The MathDocument whose methods are to be called
   * @param {number=} start           The state at which to start rendering (default is UNPROCESSED)
   */
  public renderDoc(document: MathDocument<N, T, D>, start: number = STATE.UNPROCESSED) {
    for (const item of this.items) {
      if (item.priority >= start) {
        if (item.item.renderDoc(document)) return;
      }
    }
  }

  /**
   * Perform the MathItem-level rendering functions
   *
   * @param {MathItem} math           The MathItem whose methods are to be called
   * @param {MathDocument} document   The MathDocument to pass to the MathItem methods
   * @param {number=} start           The state at which to start rendering (default is UNPROCESSED)
   */
  public renderMath(math: MathItem<N, T, D>, document: MathDocument<N, T, D>, start: number = STATE.UNPROCESSED) {
    for (const item of this.items) {
      if (item.priority >= start) {
        if (item.item.renderMath(math, document)) return;
      }
    }
  }

  /**
   * Perform the MathItem-level conversion functions
   *
   * @param {MathItem} math           The MathItem whose methods are to be called
   * @param {MathDocument} document   The MathDocument to pass to the MathItem methods
   * @param {number=} end             The state at which to end rendering (default is LAST)
   */
  public renderConvert(math: MathItem<N, T, D>, document: MathDocument<N, T, D>, end: number = STATE.LAST) {
    for (const item of this.items) {
      if (item.priority > end) return;
      if (item.item.convert) {
        if (item.item.renderMath(math, document)) return;
      }
    }
  }

  /**
   * Find an entry in the list with a given ID
   *
   * @param {string} id            The id to search for
   * @returns {RenderData|null}   The data for the given id, if found, or null
   */
  public findID(id: string): RenderData<N, T, D> | null {
    for (const item of this.items) {
      if (item.item.id === id) {
        return item.item;
      }
    }
    return null;
  }

}

/*****************************************************************/

/**
 * The ways of specifying a container (a selector string, an actual node,
 * or an array of those (e.g., the result of document.getElementsByTagName())
 *
 * @template N  The HTMLElement node class
 */
export type ContainerList<N> = string | N | (string | N | N[])[];

/**
 * The options allowed for the reset() method
 */
export type ResetList = {
  all?: boolean,
  processed?: boolean,
  inputJax?: any[],
  outputJax?: any[]
};

/**
 * The default option list for the reset() method
 */
export const resetOptions: ResetList = {
  all: false,
  processed: false,
  inputJax: null,
  outputJax: null
};

/**
 * The option list for when all options are to be reset
 */
export const resetAllOptions: ResetList = {
  all: true,
  processed: true,
  inputJax: [],
  outputJax: []
};

/*****************************************************************/
/**
 *  The MathDocument interface
 *
 *  The MathDocument is created by MathJax.Document() and holds the
 *  document, the math found in it, and so on.  The methods of the
 *  MathDocument all return the MathDocument itself, so you can
 *  chain the method calls.  E.g.,
 *
 *    const html = MathJax.Document('<html>...</html>');
 *    html.findMath()
 *        .compile()
 *        .getMetrics()
 *        .typeset()
 *        .updateDocument();
 *
 *  The MathDocument is the main interface for page authors to
 *  interact with MathJax.
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export interface MathDocument<N, T, D> {
  /**
   * The document being processed (e.g., DOM document, or Markdown string)
   */
  document: D;

  /**
   * The kind of MathDocument (e.g., "HTML")
   */
  kind: string;

  /**
   * The options for the document
   */
  options: OptionList;

  /**
   * The list of MathItems found in this page
   */
  math: MathList<N, T, D>;

  /**
   * The list of actions to take during a render() or convert() call
   */
  renderActions: RenderList<N, T, D>;

  /**
   * This object tracks what operations have been performed, so that (when
   *  asynchronous operations are used), the ones that have already been
   *  completed won't be performed again.
   */
  processed: BitField;

  /**
   * An array of input jax to run on the document
   */
  inputJax: InputJax<N, T, D>[];

  /**
   * The output jax to use for the document
   */
  outputJax: OutputJax<N, T, D>;

  /**
   * The DOM adaptor to use for input and output
   */
  adaptor: DOMAdaptor<N, T, D>;

  /**
   * The MmlFactory to be used for input jax and error processing
   */
  mmlFactory: MmlFactory;

  /**
   * @param {string} id      The id of the action to add
   * @param {any[]} action   The RenderAction to take
   */
  addRenderAction(id: string, ...action: any[]): void;

  /**
   * @param {string} id   The id of the action to remove
   */
  removeRenderAction(id: string): void;

  /**
   * Perform the renderActions on the document
   */
  render(): MathDocument<N, T, D>;

  /**
   * Rerender the MathItems on the page
   *
   * @param {number=} start    The state to start rerendering at
   * @return {MathDocument}    The math document instance
   */
  rerender(start?: number): MathDocument<N, T, D>;

  /**
   * Convert a math string to the document's output format
   *
   * @param {string} math           The math string to convert
   * @params {OptionList} options   The options for the conversion (e.g., format, ex, em, etc.)
   * @return {MmlNode|N}            The MmlNode or N node for the converted content
   */
  convert(math: string, options?: OptionList): MmlNode | N;

  /**
   * Locates the math in the document and constructs the MathList
   *  for the document.
   *
   * @param {OptionList} options  The options for locating the math
   * @return {MathDocument}       The math document instance
   */
  findMath(options?: OptionList): MathDocument<N, T, D>;

  /**
   * Calls the input jax to process the MathItems in the MathList
   *
   * @return {MathDocument}  The math document instance
   */
  compile(): MathDocument<N, T, D>;

  /**
   * Gets the metric information for the MathItems
   *
   * @return {MathDocument}  The math document instance
   */
  getMetrics(): MathDocument<N, T, D>;

  /**
   * Calls the output jax to process the compiled math in the MathList
   *
   * @return {MathDocument}  The math document instance
   */
  typeset(): MathDocument<N, T, D>;

  /**
   * Updates the document to include the typeset math
   *
   * @return {MathDocument}  The math document instance
   */
  updateDocument(): MathDocument<N, T, D>;

  /**
   * Removes the typeset math from the document
   *
   * @param {boolean} restore  True if the original math should be put
   *                            back into the document as well
   * @return {MathDocument}    The math document instance
   */
  removeFromDocument(restore?: boolean): MathDocument<N, T, D>;

  /**
   * Set the state of the document (allowing you to roll back
   *  the state to a previous one, if needed).
   *
   * @param {number} state     The new state of the document
   * @param {boolean} restore  True if the original math should be put
   *                            back into the document during the rollback
   * @return {MathDocument}    The math document instance
   */
  state(state: number, restore?: boolean): MathDocument<N, T, D>;

  /**
   * Clear the processed values so that the document can be reprocessed
   *
   * @param {ResetList} options   The things to be reset
   * @return {MathDocument}       The math document instance
   */
  reset(options?: ResetList): MathDocument<N, T, D>;

  /**
   * Reset the processed values and clear the MathList (so that new math
   * can be processed in the document).
   *
   * @return {MathDocument}  The math document instance
   */
  clear(): MathDocument<N, T, D>;

  /**
   * Merges a MathList into the list for this document.
   *
   * @param {MathList} list   The MathList to be merged into this document's list
   * @return {MathDocument}   The math document instance
   */
  concat(list: MathList<N, T, D>): MathDocument<N, T, D>;

  /**
   * Clear the typeset MathItems that are within the given container
   *   from the document's MathList.  (E.g., when the content of the
   *   container has been updated and you want to remove the
   *   associated MathItems)
   *
   * @param {ContainerList<N>} elements   The container DOM elements whose math items are to be removed
   * @return {MathItem<N,T,D>[]}          The removed MathItems
   */
  clearMathItemsWithin(containers: ContainerList<N>): MathItem<N, T, D>[];

  /**
   * Get the typeset MathItems that are within a given container.
   *
   * @param {ContainerList<N>} elements   The container DOM elements whose math items are to be found
   * @return {MathItem<N,T,D>[]}          The list of MathItems within that container
   */
  getMathItemsWithin(elements: ContainerList<N>): MathItem<N, T, D>[];

}

/*****************************************************************/

/**
 * Defaults used when input jax isn't specified
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
class DefaultInputJax<N, T, D> extends AbstractInputJax<N, T, D> {
  /**
   * @override
   */
  public compile(_math: MathItem<N, T, D>) {
    return null as MmlNode;
  }
}

/**
 * Defaults used when ouput jax isn't specified
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
class DefaultOutputJax<N, T, D> extends AbstractOutputJax<N, T, D> {
  /**
   * @override
   */
  public typeset(_math: MathItem<N, T, D>, _document: MathDocument<N, T, D> = null) {
    return null as N;
  }
  /**
   * @override
   */
  public escaped(_math: MathItem<N, T, D>, _document?: MathDocument<N, T, D>) {
    return null as N;
  }
}

/**
 * Default for the MathList when one isn't specified
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
class DefaultMathList<N, T, D> extends AbstractMathList<N, T, D> {}

/**
 * Default for the Mathitem when one isn't specified
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
class DefaultMathItem<N, T, D> extends AbstractMathItem<N, T, D> {}

/*****************************************************************/
/**
 *  Implements the abstract MathDocument class
 *
 * @template N  The HTMLElement node class
 * @template T  The Text node class
 * @template D  The Document class
 */
export abstract class AbstractMathDocument<N, T, D> implements MathDocument<N, T, D> {

  /**
   * The type of MathDocument
   */
  public static KIND: string = 'MathDocument';

  /**
   * The default options for the document
   */
  public static OPTIONS: OptionList = {
    OutputJax: null,           // instance of an OutputJax for the document
    InputJax: null,            // instance of an InputJax or an array of them
    MmlFactory: null,          // instance of a MmlFactory for this document
    MathList: DefaultMathList, // constructor for a MathList to use for the document
    MathItem: DefaultMathItem, // constructor for a MathItem to use for the MathList
    compileError: (doc: AbstractMathDocument<any, any, any>, math: MathItem<any, any, any>, err: Error) => {
      doc.compileError(math, err);
    },
    typesetError: (doc: AbstractMathDocument<any, any, any>, math: MathItem<any, any, any>, err: Error) => {
      doc.typesetError(math, err);
    },
    renderActions: expandable({
      find:    [STATE.FINDMATH, 'findMath', '', false],
      compile: [STATE.COMPILED],
      metrics: [STATE.METRICS, 'getMetrics', '', false],
      typeset: [STATE.TYPESET],
      update:  [STATE.INSERTED, 'updateDocument', false]
    }) as RenderActions<any, any, any>
  };

  /**
   * A bit-field for the actions that have been processed
   */
  public static ProcessBits = BitFieldClass('findMath', 'compile', 'getMetrics', 'typeset', 'updateDocument');

  /**
   * The document managed by this MathDocument
   */
  public document: D;
  /**
   * The actual options for this document (with user-supplied ones merged in)
   */
  public options: OptionList;

  /**
   * The list of MathItems for this document
   */
  public math: MathList<N, T, D>;

  /**
   * The list of render actions
   */
  public renderActions: RenderList<N, T, D>;

  /**
   * The bit-field used to tell what steps have been taken on the document (for retries)
   */
  public processed: BitField;

  /**
   * The list of input jax for the document
   */
  public inputJax: InputJax<N, T, D>[];

  /**
   * The output jax for the document
   */
  public outputJax: OutputJax<N, T, D>;

  /**
   * The DOM adaptor for the document
   */
  public adaptor: DOMAdaptor<N, T, D>;

  /**
   * The MathML node factory for the internal MathML representation
   */
  public mmlFactory: MmlFactory;


  /**
   * @param {any} document           The document (HTML string, parsed DOM, etc.) to be processed
   * @param {DOMAdaptor} adaptor     The DOM adaptor for this document
   * @param {OptionList} options     The options for this document
   * @constructor
   */
  constructor (document: D, adaptor: DOMAdaptor<N, T, D>, options: OptionList) {
    let CLASS = this.constructor as typeof AbstractMathDocument;
    this.document = document;
    this.options = userOptions(defaultOptions({}, CLASS.OPTIONS), options);
    this.math = new (this.options['MathList'] || DefaultMathList)();
    this.renderActions = RenderList.create<N, T, D>(this.options['renderActions']);
    this.processed = new AbstractMathDocument.ProcessBits();
    this.outputJax = this.options['OutputJax'] || new DefaultOutputJax<N, T, D>();
    let inputJax = this.options['InputJax'] || [new DefaultInputJax<N, T, D>()];
    if (!Array.isArray(inputJax)) {
      inputJax = [inputJax];
    }
    this.inputJax = inputJax;
    //
    // Pass the DOM adaptor to the jax
    //
    this.adaptor = adaptor;
    this.outputJax.setAdaptor(adaptor);
    this.inputJax.map(jax => jax.setAdaptor(adaptor));
    //
    // Pass the MmlFactory to the jax
    //
    this.mmlFactory = this.options['MmlFactory'] || new MmlFactory();
    this.inputJax.map(jax => jax.setMmlFactory(this.mmlFactory));
    //
    // Do any initialization that requires adaptors or factories
    //
    this.outputJax.initialize();
    this.inputJax.map(jax => jax.initialize());
  }

  /**
   * @return {string}  The kind of document
   */
  public get kind(): string {
    return (this.constructor as typeof AbstractMathDocument).KIND;
  }

  /**
   * @override
   */
  public addRenderAction(id: string, ...action: any[]) {
    const [fn, p] = RenderList.action<N, T, D>(id, action as RenderAction<N, T, D>);
    this.renderActions.add(fn, p);
  }

  /**
   * @override
   */
  public removeRenderAction(id: string) {
    const action = this.renderActions.findID(id);
    if (action) {
      this.renderActions.remove(action);
    }
  }

  /**
   * @override
   */
  public render() {
    this.renderActions.renderDoc(this);
    return this;
  }

  /**
   * @override
   */
  public rerender(start: number = STATE.RERENDER) {
    this.state(start - 1);
    this.render();
    return this;
  }

  /**
   * @override
   */
  public convert(math: string, options: OptionList = {}) {
    let {format, display, end, ex, em, containerWidth, lineWidth, scale, family} = userOptions({
      format: this.inputJax[0].name, display: true, end: STATE.LAST,
      em: 16, ex: 8, containerWidth: null, lineWidth: 1000000, scale: 1, family: ''
    }, options);
    if (containerWidth === null) {
      containerWidth = 80 * ex;
    }
    const jax = this.inputJax.reduce((jax, ijax) => (ijax.name === format ? ijax : jax), null);
    const mitem = new this.options.MathItem(math, jax, display);
    mitem.start.node = this.adaptor.body(this.document);
    mitem.setMetrics(em, ex, containerWidth, lineWidth, scale);
    if (this.outputJax.options.mtextInheritFont) {
      mitem.outputData.mtextFamily = family;
    }
    if (this.outputJax.options.merrorInheritFont) {
      mitem.outputData.merrorFamily = family;
    }
    mitem.convert(this, end);
    return (mitem.typesetRoot || mitem.root);
  }

  /**
   * @override
   */
  public findMath(_options: OptionList = null) {
    this.processed.set('findMath');
    return this;
  }

  /**
   * @override
   */
  public compile() {
    if (!this.processed.isSet('compile')) {
      //
      //  Compile all the math in the list
      //
      const recompile = [];
      for (const math of this.math) {
        this.compileMath(math);
        if (math.inputData.recompile !== undefined) {
          recompile.push(math);
        }
      }
      //
      //  If any were added to the recompile list,
      //    compile them again
      //
      for (const math of recompile) {
        const data = math.inputData.recompile;
        math.state(data.state);
        math.inputData.recompile = data;
        this.compileMath(math);
      }
      this.processed.set('compile');
    }
    return this;
  }

  /**
   * @param {MathItem} math   The item to compile
   */
  protected compileMath(math: MathItem<N, T, D>) {
    try {
      math.compile(this);
    } catch (err) {
      if (err.retry || err.restart) {
        throw err;
      }
      this.options['compileError'](this, math, err);
      math.inputData['error'] = err;
    }
  }

  /**
   * Produce an error using MmlNodes
   *
   * @param {MathItem} math  The MathItem producing the error
   * @param {Error} err      The Error object for the error
   */
  public compileError(math: MathItem<N, T, D>, err: Error) {
    math.root = this.mmlFactory.create('math', null, [
      this.mmlFactory.create('merror', {'data-mjx-error': err.message, title: err.message}, [
        this.mmlFactory.create('mtext', null, [
          (this.mmlFactory.create('text') as TextNode).setText('Math input error')
        ])
      ])
    ]);
    if (math.display) {
      math.root.attributes.set('display', 'block');
    }
    math.inputData.error = err.message;
  }

  /**
   * @override
   */
  public typeset() {
    if (!this.processed.isSet('typeset')) {
      for (const math of this.math) {
        try {
          math.typeset(this);
        } catch (err) {
          if (err.retry || err.restart) {
            throw err;
          }
          this.options['typesetError'](this, math, err);
          math.outputData['error'] = err;
        }
      }
      this.processed.set('typeset');
    }
    return this;
  }

  /**
   * Produce an error using HTML
   *
   * @param {MathItem} math  The MathItem producing the error
   * @param {Error} err      The Error object for the error
   */
  public typesetError(math: MathItem<N, T, D>, err: Error) {
    math.typesetRoot = this.adaptor.node('mjx-container', {
      class: 'MathJax mjx-output-error',
      jax: this.outputJax.name,
    }, [
      this.adaptor.node('span', {
        'data-mjx-error': err.message,
        title: err.message,
        style: {
          color: 'red',
          'background-color': 'yellow',
          'line-height': 'normal'
        }
      }, [
        this.adaptor.text('Math output error')
      ])
    ]);
    if (math.display) {
      this.adaptor.setAttributes(math.typesetRoot, {
        style: {
          display: 'block',
          margin: '1em 0',
          'text-align': 'center'
        }
      });
    }
    math.outputData.error = err.message;
  }

  /**
   * @override
   */
  public getMetrics() {
    if (!this.processed.isSet('getMetrics')) {
      this.outputJax.getMetrics(this);
      this.processed.set('getMetrics');
    }
    return this;
  }

  /**
   * @override
   */
  public updateDocument() {
    if (!this.processed.isSet('updateDocument')) {
      for (const math of this.math.reversed()) {
        math.updateDocument(this);
      }
      this.processed.set('updateDocument');
    }
    return this;
  }

  /**
   * @override
   */
  public removeFromDocument(_restore: boolean = false) {
    return this;
  }

  /**
   * @override
   */
  public state(state: number, restore: boolean = false) {
    for (const math of this.math) {
      math.state(state, restore);
    }
    if (state < STATE.INSERTED) {
      this.processed.clear('updateDocument');
    }
    if (state < STATE.TYPESET) {
      this.processed.clear('typeset');
      this.processed.clear('getMetrics');
    }
    if (state < STATE.COMPILED) {
      this.processed.clear('compile');
    }
    return this;
  }

  /**
   * @override
   */
  public reset(options: ResetList = {processed: true}) {
    options = userOptions(Object.assign({}, resetOptions), options);
    options.all && Object.assign(options, resetAllOptions);
    options.processed && this.processed.reset();
    options.inputJax && this.inputJax.forEach(jax => jax.reset(...options.inputJax));
    options.outputJax && this.outputJax.reset(...options.outputJax);
    return this;
  }

  /**
   * @override
   */
  public clear() {
    this.reset();
    this.math.clear();
    return this;
  }

  /**
   * @override
   */
  public concat(list: MathList<N, T, D>) {
    this.math.merge(list);
    return this;
  }

  /**
   * @override
   */
  public clearMathItemsWithin(containers: ContainerList<N>) {
    const items = this.getMathItemsWithin(containers);
    this.math.remove(...items);
    return items;
  }

  /**
   * @override
   */
  public getMathItemsWithin(elements: ContainerList<N>) {
    if (!Array.isArray(elements)) {
      elements = [elements];
    }
    const adaptor = this.adaptor;
    const items = [] as MathItem<N, T, D>[];
    const containers = adaptor.getElements(elements, this.document);
    ITEMS:
    for (const item of this.math) {
      for (const container of containers) {
        if (item.start.node && adaptor.contains(container, item.start.node)) {
          items.push(item);
          continue ITEMS;
        }
      }
    }
    return items;
  }

}

/**
 * The constructor type for a MathDocument
 *
 * @template D    The MathDocument type this constructor is for
 */
export interface MathDocumentConstructor<D extends MathDocument<any, any, any>> {
  KIND: string;
  OPTIONS: OptionList;
  ProcessBits: typeof BitField;
  new (...args: any[]): D;
}