baja/obj/Format.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Gareth Johnson
 */

/**
 * Defines {@link baja.Format}.
 * @module baja/obj/Format
 */
define([
  "bajaScript/sys",
  "bajaScript/baja/obj/Simple",
  "bajaScript/baja/obj/objUtil",
  "bajaPromises",
  "lex!" ], function (
  baja,
  Simple,
  objUtil,
  Promise,
  lexjs) {

  "use strict";

  const { callSuper, def: bajaDef, hasType: bajaHasType, objectify, strictArg, subclass } = baja;
  const { capitalizeFirstLetter } = objUtil;

  const FUNCTION_PARAMS_REGEX = /^\s*(\w+)\s*\((.*)\)/;
  const LEXICON_REGEX = /^lexicon\(([a-zA-Z0-9]+):([a-zA-Z0-9.\-_]+)((?::[a-zA-Z0-9$_($?:\s)*]+)*)\)$/;

  // region baja.Format

  /**
   * `Format` is used to format `Object`s into `String`s using
   * a standardized formatting pattern language.  The format String is normal
   * text with embedded scripts denoted by the percent (%) character.  Use
   * "%%" to escape a real %.
   *
   * A script is one or more calls chained together using the dot (.) operator.
   * Calls are mapped to methods using the order given below.
   *
   * If a script cannot be processed successfully, an error will be returned.
   *
   * To define an alternate output to use if an error is encountered, include a
   * ? followed by another script within the same % pair.  More than one fallback
   * can be defined by delimiting the fallbacks with a ?.
   *
   * Given the call "foo" the order of attempted resolutions is:
   * <ol>
   *   <li>special call (see below)</li>
   *   <li>getFoo(Context)</li>
   *   <li>foo(Context)</li>
   *   <li>get("foo")</li>
   * </ol>
   *
   * The following special functions are available to use in a script:
   * <ol>
   *   <li>lexicon(module:key:<escaped string args separated by ':'>)</li>
   *   <li>time() returns the current time as an AbsTime</li>
   *   <li>user() returns gets the current user's name</li>
   *   <li>decodeFromString(<module>:<type>:<escaped string encoding>) returns the toString of the
   *   encoded value for specified escaped string encoding for the specified type in
   *   the given module.
   *   </li>
   *   <li>escape() returns the escaped text value of the given objects toString()</li>
   *   <li>unescape() returns the unescaped text value of the given objects toString()</li>
   *   <li>substring() returns a substring value of a given objects toString()</li>
   * </ol>
   *
   * This Constructor shouldn't be invoked directly. Please use the `make()`
   * methods to create an instance of a `Format`.
   *
   * Examples of formats:
   * <pre>
   * "hello world"
   * "my name is %displayName%"
   * "my parent's name is %parent.displayName%"
   * "%parent.value?lexicon(bajaui:dialog.error)%"
   * "%out.value?out.status?lexicon(bajaui:dialog.error)%"
   * %lexicon(bajaui:fileSearch.scanningFiles:5:10)% // Scanning files (found 5 of 10)...
   * "The escaped value %out.value.escape%"
   * "The unescaped valued %out.value.unescape%"
   * "The first two characters %out.value.substring(2)%"
   * "The first five characters %out.value.substring(5)%"
   * "The first five characters %out.value.substring(0, 5)%"
   * "The last five characters %out.value.substring(-5)%"
   * "The toString of a decoded baja:AbsTime from %decodeFromString(baja:AbsTime:$32016$2d04$2d10T13$3a37$3a00$2e000$2d04$3a00)%"
   * </pre>
   *
   * @example <caption>Formats can use getter functions from an object as well
   * as function names</caption>
   * var obj = {
   *   getFoo: function () {
   *     return {
   *       getBar: function () {
   *         return {
   *           value: function () {
   *             return 3.1415;
   *          }
   *        }
   *       }
   *     }
   *   }
   * }
   *
   * var fmt = baja.Format.make("%foo.bar.value%");
   * return fmt.format( { object: obj } )
   *   .then(function (value) {
   *     // prints 3.1415
   *     console.log(value);
   *   });
   *
   * @class
   * @alias baja.Format
   * @extends baja.Simple
   */
  const Format = function Format(pattern) {
    callSuper(Format, this, arguments);
    this.$pattern = strictArg(pattern, String);
  };

  subclass(Format, Simple);

  /**
   * Default Format instance.
   * @type {baja.Format}
   */
  Format.DEFAULT = new Format("");

  /**
   * Make a `Format`.
   *
   * @param {String} [pattern] the `Format` Pattern `String`.
   * @returns {baja.Format}
   */
  Format.make = function (pattern) {
    pattern = pattern || "";

    if (pattern === "") {
      return Format.DEFAULT;
    }

    strictArg(pattern, String);

    return new Format(pattern);
  };

  /**
   * Make a `Format`.
   *
   * @param {String} [pattern] the `Format` Pattern `String`.
   * @returns {baja.Format}
   */
  Format.prototype.make = function (pattern) {
    return Format.make.apply(Format, arguments);
  };

  /**
   * Decode a `String` to a `Format`.
   *
   * @param {String} str
   * @returns {baja.Format}
   */
  Format.prototype.decodeFromString = function (str) {
    return Format.make(str);
  };

  /**
   * Encode `Format` to a `String`.
   *
   * @returns {String}
   */
  Format.prototype.encodeToString = function () {
    return this.$pattern;
  };

  /**
   * Return a `String` representation of the `Format`.
   *
   * @returns {String}
   */
  Format.prototype.toString = function () {
    return this.$pattern;
  };

  /**
   * Return the inner value of the `Format`.
   *
   * @returns {String}
   */
  Format.prototype.valueOf = Format.prototype.toString;

  /**
   * Format the specified object using the format pattern.
   *
   * This method can take an object literal or a single pattern `String`
   * argument.
   *
   * @param {Object} obj
   * @param {String} obj.pattern the format pattern to process.
   * @param {Object|baja.Component} [obj.object] JavaScript Object or baja:Component
   * referenced by the format scripts.
   * @param {Boolean} [obj.display] if true, the display string of a Property value is used.
   *                                If false, the `toString` version of a Property value is used.
   *                                By default, this value is true (in BajaScript, most of the time
   *                                we're dealing with mounted Components in a Proxy Component Space).
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback
   * called when the Format string has been processed. The resultant String
   * will be passed to this function as an argument
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback
   * called if there's a fatal error processing the format.
   * @param {Object} [obj.cx] the designated context to be passed down to the toString method. Defaults to
   * an empty object.
   * @returns {Promise.<string>}
   */
  Format.format = function (obj) {

    const { ok, fail } = obj;

    // TODO: Currently format processing doesn't work in exactly the same way as Niagara.
    // Certainly it can never be 100% accurate. However, we can try to cover most common use cases
    // that we want to support.

    const formatContext = makeFormatContext(obj);
    const cb = new baja.comm.Callback(ok, fail || baja.fail);

    return processAllScripts(formatContext)
      .then((result) => {
        cb.ok(result);
        return cb.promise();
      })
      .catch((err) => {
        baja.error("Could not format object: " + err);
        cb.fail(err);
      });
  };

  /**
   * Format the specified object using the format pattern.
   *
   * @see baja.Format.format
   * 
   * @param {Object} [obj]
   * @param {Function} [obj.ok] (Deprecated: use Promise) the ok callback called
   * when the Format string has been processed. The resultant String will be
   * passed to this function as an argument
   * @param {Function} [obj.fail] (Deprecated: use Promise) the fail callback
   * called if there's a fatal error processing the format.
   * @returns {Promise.<String>}
   */
  Format.prototype.format = function (obj) {
    obj = objectify(obj);
    obj.pattern = this.$pattern;
    return Format.format(obj);
  };

  // endregion baja.Format

  //region Formatters

  // %% -> %
  /** @implements baja.Format~Formatter */
  class EscapedPercentSignFormatter {
    canFormat(script) {
      return script === '';
    }
    doFormat() {
      return '%';
    }
  }

  // %.% -> identity string
  /** @implements baja.Format~Formatter */
  class IdentityFormatter {
    canFormat(script) {
      return script === '.';
    }
    doFormat(script, obj) {
      const { target, display } = obj;
      let { cx, object } = obj;

      if (target) {
        const { container, propertyPath } = target;
        cx = Object.assign(target.getFacets().toObject(), cx);

        if (container) {
          if (propertyPath && propertyPath.length) {
            let parent = container;
            let val = parent;
            let slot;
            for (var i = 0; i < propertyPath.length; ++i) {
              slot = propertyPath[i];
              parent = val;
              val = val.get(slot);
            }

            cx = prepareContext(parent.get(slot), cx);
            return display ? parent.getDisplay(slot, cx) : parent.get(slot).toString(cx);
          } else {
            return display ? container.getDisplay(target.slot) : container.get(target.slot).toString(cx);
          }
        } else {
          object = target.getObject();
          return display && isComplex(object) ? object.getDisplay(undefined, cx) : object.toString(cx);
        }
      } else if (object !== undefined && object !== null) {
        cx = prepareContext(object, cx);
        return display && isComplex(object) ? object.getDisplay(undefined, cx) : object.toString(cx);
      }
    }
  }

  // current user name
  /** @implements baja.Format~Formatter */
  class UserFormatter {
    canFormat(script) {
      return script === 'user()';
    }
    doFormat() {
      return baja.getUserName();
    }
  }

  // decodeFromString()
  /** @implements baja.Format~Formatter */
  class DecodeFromStringFormatter {
    canFormat(script) {
      return script.split('(')[0] === 'decodeFromString';
    }
    doFormat(script, formatContext) {
      const { cx } = formatContext;
      const [ , , params ] = FUNCTION_PARAMS_REGEX.exec(script);
      const [ moduleName, typeName, ...args ] = params.split(':');
      const stringEncoding = baja.SlotPath.unescape(args.join(':'));
      const decodedValue = baja.$(moduleName + ":" + typeName).decodeFromString(stringEncoding);
      return Promise.resolve(decodedValue.toString(cx));
    }
  }

  // lexicon(lexArgs)
  /** @implements baja.Format~Formatter */
  class LexiconFormatter {
    canFormat(script) {
      return script.match(LEXICON_REGEX);
    }
    doFormat(script) {
      const [ , moduleName, key, argsString ] = LEXICON_REGEX.exec(script);
      LEXICON_REGEX.lastIndex = 0;

      // Asynchronously request the lexicon value
      return lexjs.module(moduleName)
        .then((lex) => {
          if (argsString) {
            const args = argsString.substring(1).split(':').map(baja.SlotPath.unescape);
            return lex.get({ key, args });
          } else {
            return lex.get(key);
          }
        }, (err) => {
          return 'error: ' + err + ' + ' + script;
        });
    }
  }

  // escape()/unescape()
  /** @implements baja.Format~Formatter */
  class EscapeUnescapeFormatter {
    canFormat(script) {
      const [ functionName ] = script.split('(');
      return functionName === 'escape' || functionName === 'unescape';
    }
    doFormat(script, formatContext) {
      const { object, cx } = formatContext;

      function getMethodOperationsFromText(operationString) {
        const [ , operation, nextOperation ] = FUNCTION_PARAMS_REGEX.exec(operationString);

        if (nextOperation === '') { // base case.. we're done here.
          return [ operation ];
        }

        return getMethodOperationsFromText(nextOperation).concat(operation);
      }

      const operations = getMethodOperationsFromText(script);

      return Promise.resolve(object.toString(cx))
        .then((objectText) => {
          return operations.reduce((currentText, currentOperation) => {
            if (currentText === null) {
              return null;
            }

            if (currentOperation === 'escape') {
              return baja.SlotPath.escape(currentText);
            }

            if (currentOperation === 'unescape') {
              return baja.SlotPath.unescape(currentText);
            }

            return null;
          }, objectText);
        });
    }
  }

  /** @implements baja.Format~Formatter */
  class ReflectCallFormatter {
    canFormat() {
      return true; // always last in the list
    }
    doFormat(script, formatContext, suggestError) {
      const { display, object } = formatContext;
      const pieces = script.split(/\./g);

      let cx = extend(formatContext.cx);
      let mostRecentLeftValueText = "";
      let parent = object;
      let val = parent;
      let slot = null;

      function getErrorMessage(leftObject, leftText, rightText) {
        const type = baja.hasType(leftObject) ? leftObject.getType().toString() : "";
        return  "%err:" + type + ":" + rightText + "%";
      }


      // leftValue.rightValue that was split over the period
      function evaluateLeftToRight(leftValue, rightValue) {
        if (rightValue === "time()") {
          return baja.AbsTime.now();
        }

        if (rightValue === "user()") {
          return baja.getUserName();
        }

        const valueIsComplex = isComplex(leftValue);

        if (leftValue === null || leftValue === undefined) {
          return null;
        }

        if (valueIsComplex) {
          if (!canRead(leftValue, null)) {
            suggestError(getErrorMessage(leftValue, mostRecentLeftValueText, rightValue));
            return null;
          }
        }
        return Promise.resolve()
          .then(() => {
            // First try looking for the Slot
            if (valueIsComplex && leftValue.has(rightValue)) {
              slot = leftValue.getSlot(rightValue);
              if (!canRead(leftValue, slot)) {
                suggestError(getErrorMessage(leftValue, mostRecentLeftValueText, rightValue));
                return null;
              }
              parent = leftValue;
              Object.assign(cx, leftValue.getFacets(rightValue).toObject());
              return leftValue.get(slot);
            }

            // If there's no Slot then see if a function exists
            // Nullify this since at this point we're no longer looking up a Slot chain
            slot = null;
            parent = null;

            //If pattern starts with . returns the object itself for first empty split
            if (rightValue === "") {
              return object;
            } else if (isFunctionWithParameters(rightValue, "substring")) {
              return resolveSubstringReplace(leftValue, rightValue, formatContext);
            } else if (isFunctionWithParameters(rightValue, "escape")) {
              return resolveStringEscape(leftValue, rightValue, formatContext);
            } else if (isFunctionWithParameters(rightValue, "unescape")) {
              return resolveStringUnescape(leftValue, rightValue, formatContext);
            } else if (typeof leftValue["get" + capitalizeFirstLetter(rightValue)] === "function") {
              return reflectCall(leftValue, "get" + capitalizeFirstLetter(rightValue), cx);
            } else if (typeof leftValue[rightValue] === "function") {
              return reflectCall(leftValue, rightValue, cx);
            } else if (canCallGet(leftValue)) {
              return baja.def(leftValue.get(rightValue), null);
            } else {
              suggestError(getErrorMessage(leftValue, mostRecentLeftValueText, rightValue));
              return null;
            }
          })
          .then((result) => {
            return Promise.resolve(isComplex(result) && result.isAncestorOf(leftValue) && result.loadSlots())
              .then(() => result);
          });
      }

      return pieces.reduce((prom, rightValueText) => {
        return prom
          .then((leftValue) => evaluateLeftToRight(leftValue, rightValueText))
          .then((evaluatedValue) => {
            mostRecentLeftValueText = rightValueText;
            return evaluatedValue;
          });
      }, Promise.resolve(val))
        .then((val) => {
          cx = prepareContext(val, cx);

          if (val !== null && val !== undefined && slot && parent) {
            return display ? parent.getDisplay(slot, cx) : parent.get(slot).toString(cx);
          }

          if (isComplex(val)) {
            parent = val.getParent();
            if (parent) {
              slot = val.getPropertyInParent();
              Object.assign(cx, parent.getFacets(slot).toObject());
              return display ? parent.getDisplay(slot, cx) : parent.get(slot).toString(cx);
            }
          }
          // As a last resort, just call toString
          if (val !== null && val !== undefined) {
            return val.toString(cx);
          }
          return val;
        });
    }
  }


  /** @type baja.Format~Formatter[] */
  const FORMATTERS = [
    new EscapedPercentSignFormatter(),
    new IdentityFormatter(),
    new UserFormatter(),
    new DecodeFromStringFormatter(),
    new LexiconFormatter(),
    new EscapeUnescapeFormatter(),
    new ReflectCallFormatter()
  ];

  // endregion Formatters

  // region helper functions

  /**
   * This method will take the given object and attempt to format it by applying
   * the different format matchers. It also handles conditional formats
   * that have multiple fallback values.
   *
   * @param {string[]} scriptCandidates all potential fallbacks in a piece of a
   * format string. This is the text inside of percent signs. Length will be
   * more than 1 if conditional formatting is used; e.g. if the piece is
   * `displayName?typeDisplayName` then this will be
   * `[ 'displayName', 'typeDisplayName' ]`.
   * @param {baja.Format~FormatContext} formatContext
   * @param {string} [currentError]
   * @returns {Promise<string>}
   */
  function processScript(scriptCandidates, formatContext, currentError) {
    if (!scriptCandidates.length) {
      throw new Error(currentError || '');
    }
    const [ script, ...rest ] = scriptCandidates;
    let suggestedErrorForPiece = currentError;

    return FORMATTERS.reduce((formatProm, formatter) => {
      return formatProm
        .then((formattedString) => {
          if (formattedString !== null) {
            return formattedString;
          }

          if (!formatter.canFormat(script)) {
            return null;
          }

          return formatter.doFormat(script, formatContext, (suggestedError) => {
            suggestedErrorForPiece = suggestedError;
          });
        })
        .catch((ignore) => null);
    }, Promise.resolve(null))
      .then((formattedString) => {
        if (formattedString === null) {
          return processScript(rest, formatContext, suggestedErrorForPiece);
        } else {
          return formattedString;
        }
      });
  }

  /**
   * @param {baja.Format~FormatContext} formatContext
   * @returns {Promise.<string>} the format string, with all scripts processed and replaced with
   * their computed string values
   */
  function processAllScripts(formatContext) {
    const { pattern } = formatContext;
    const scripts = findScripts(formatContext);

    return Promise.all(scripts.map((script) => {
      const scriptCandidates = script.split('?');
      return processScript(scriptCandidates, formatContext)
        .catch((err) => err.message || 'error: ' + script);
    })).then((replacedTexts) => {
      if (replacedTexts.indexOf(undefined) >= 0) {
        return null;
      } else {
        let index = 0;
        return pattern.replace(findScriptsRegex(), () => replacedTexts[index++]);
      }
    });
  }

  /**
   * @param {baja.Format~FormatContext} formatContext
   * @returns {string[]} all scripts found within the format pattern string (the stuff between the
   * matching "%" pairs)
   */
  function findScripts(formatContext) {
    const { pattern } = formatContext;
    const regex = findScriptsRegex();
    const scripts = [];

    for (;;) {
      const res = regex.exec(pattern);
      if (res) {
        // Add data (remove start and end % characters)
        scripts.push(trimPercents(res[0]));
      } else {
        break;
      }
    }

    return scripts;
  }

  function trimPercents(script) {
    return script.substring(1, script.length - 1);
  }
  /**
   * @param {*} leftValue
   * @param {string} rightValue
   * @param {baja.Format~FormatContext} formatContext
   * @returns {Promise.<string>}
   */
  function resolveSubstringReplace(leftValue, rightValue, formatContext) {
    return Promise.resolve(leftValue.toString(formatContext.cx))
      .then((stringValue) => {
        const [ , , paramsString ] = FUNCTION_PARAMS_REGEX.exec(rightValue);
        const params = paramsString.split(',');
        if (params.length === 1) {
          const param1 = parseInt(params[0].trim());
          if (param1 >= 0) {
            return stringValue.substring(param1);
          }
          return stringValue.substring(
            stringValue.length + param1,
            stringValue.length
          );
        } else {
          return stringValue.substring(
            parseInt(params[0].trim()),
            parseInt(params[1].trim())
          );
        }
      });
  }

  /**
   * @param {*} leftValue
   * @param {string} rightValue
   * @param {baja.Format~FormatContext} formatContext
   * @returns {Promise.<string>} escaped string
   */
  function resolveStringEscape(leftValue, rightValue, formatContext) {
    return Promise.resolve(leftValue.toString(formatContext.cx))
      .then((stringValue) => {
        return baja.SlotPath.escape(stringValue);
      });
  }

  /**
   * @param {*} leftValue
   * @param {string} rightValue
   * @param {baja.Format~FormatContext} formatContext
   * @returns {Promise.<string>} unescaped string
   */
  function resolveStringUnescape(leftValue, rightValue, formatContext) {
    return Promise.resolve(leftValue.toString(formatContext.cx))
      .then((stringValue) => {
        return baja.SlotPath.unescape(stringValue);
      });
  }

  function isFunctionWithParameters(textToTestForFunctionName, functionNameToCheckFor) {
    const match = FUNCTION_PARAMS_REGEX.exec(textToTestForFunctionName);
    if (!match || match.length < 2) {
      return false;
    }

    const extractedFunctionName = match[1];
    return extractedFunctionName === functionNameToCheckFor;
  }

  /**
   * @param {baja.Complex} complex component or struct to be checked permission for
   * @param {baja.Slot} slot slot to be checked permission for
   * @returns {boolean} true if the component/slot is readable
   */
  function canRead(complex, slot) {
    if (complex.getType().isComponent()) {
      return complex.$canRead(slot);
    }
    return true;
  }

  function prepareContext(value, cx) {
    if (baja.hasType(value, "baja:Number") && cx &&
      cx.precision === undefined &&
      cx.trimTrailingZeros === undefined) {
      return extend(cx, { trimTrailingZeros: true });
    }
    return cx;
  }

  /**
   * @param {baja.Value} obj
   * @param {string} functionName
   * @param {object} [cx]
   * @returns {Promise}
   */
  function reflectCall(obj, functionName, cx) {
    const contextForCall = getContextForCall(obj, functionName);
    if (contextForCall) {
      cx = extend(cx, contextForCall);
      return obj[functionName](prepareContext(obj, cx));
    } else {
      return obj[functionName]();
    }
  }

  /**
   * @param {baja.Value} obj
   * @param {string} functionName
   * @returns {object|undefined} a default context to use for this function
   * call - e.g. slot facets when calling get(Slot)Display, or an empty context
   * for toString. If we don't know we need a context for this function call,
   * return undefined.
   */
  function getContextForCall(obj, functionName) {
    if (isComplex(obj)) {
      const match = functionName.match(/^get(.*)Display$/);
      const slotName = match && toSlotName(match[1]);
      if (slotName && obj.has(slotName)) {
        return obj.getFacets(slotName).toObject();
      }
    }

    switch (functionName) {
      case 'getTypeDisplayName':
      case 'getValueWithFacets':
      case 'toString':
      case 'valueToString':
        return {};
    }
  }

  function toSlotName(getterString) {
    return getterString[0].toLowerCase() + getterString.substring(1);
  }

  function canCallGet(obj) {
    if (!obj || typeof obj.get !== 'function') { return false; }
    return !bajaHasType(obj, 'baja:Ord');
  }

  function extend(...objects) { return Object.assign({}, ...objects); }

  function isComplex(obj) { return bajaHasType(obj, 'baja:Complex'); }

  function findScriptsRegex() { return /%[^%]*%/g; }

  /**
   * @param {object|string} obj arguments to Format.format()
   * @returns {baja.Format~FormatContext}
   */
  function makeFormatContext(obj) {
    const formatContext = extend(objectify(obj, "pattern"));
    formatContext.display = bajaDef(formatContext.display, true);
    formatContext.cx = formatContext.cx || {};
    return formatContext;
  }

  // endregion helper functions

  // region typedefs

  /**
   * A context object used for performing one Format replacement of a string.
   *
   * @private
   * @typedef baja.Format~FormatContext
   * @property {string} pattern the String pattern being Format-ted
   * @property {boolean} display true if `getDisplay` should be used to format slots of a Complex -
   * otherwise values will just be toString()ed.
   * @property {baja.Value|*} object the object the Format string is being evaluated against
   * (commonly a Component).
   * @property {boolean} [loadSlots] not yet publicly documented - set to true to cause a format
   * to call loadSlots on any components it tries to do a reflect call on.
   * @property {object} cx context object used for formatting strings (can contain
   * trueText/falseText etc).
   */

  /**
   * An object that knows how to replace a bit of inline script in a Format string, with an
   * evaluated string. The Java analog to these are the Call subclasses in BFormat.java.
   *
   * @private
   * @interface baja.Format~Formatter
   */

  /**
   * @function
   * @name baja.Format~Formatter#canFormat
   * @param {string} script the current bit of script being evaluated.
   * @returns {boolean} true if this matcher can perform a Format replacement on the given string
   */

  /**
   * @function
   * @name baja.Format~Formatter#doFormat
   * @param {string} script the current bit of script being evaluated. This is the `out.value` in
   * `%out.value%`. Will be called multiple times for formats with multiple scripts. BFormat.java
   * confusingly calls this "id".
   * @param {baja.Format~FormatContext} formatContext
   * @param {function} suggestError if this formatter is not capable of correctly formatting the
   * script, it can call this given function with an error to be inserted into the formatted string
   * explaining why.
   * @returns {string|Promise.<string>}
   */

  // endregion typedefs

  return Format;
});