(function (exports) {
  'use strict';

  /**
   * @typedef {import("../types.mjs").ColorSource} ColorSource
   */

  /**
   * A representation of a color in hexadecimal format.
   * This class provides methods for transformations and manipulations of colors.
   */
  let Color$1 = class Color extends Number {

    /**
     * Is this a valid color?
     * @type {boolean}
     */
    get valid() {
      const v = this.valueOf();
      return Number.isInteger(v) && v >= 0 && v <= 0xFFFFFF;
    }

    /* ------------------------------------------ */

    /**
     * A CSS-compatible color string.
     * If this color is not valid, the empty string is returned.
     * An alias for Color#toString.
     * @type {string}
     */
    get css() {
      return this.toString(16);
    }

    /* ------------------------------------------ */

    /**
     * The color represented as an RGB array.
     * @type {[number, number, number]}
     */
    get rgb() {
      return [((this >> 16) & 0xFF) / 255, ((this >> 8) & 0xFF) / 255, (this & 0xFF) / 255];
    }

    /* ------------------------------------------ */

    /**
     * The numeric value of the red channel between [0, 1].
     * @type {number}
     */
    get r() {
      return ((this >> 16) & 0xFF) / 255;
    }

    /* ------------------------------------------ */

    /**
     * The numeric value of the green channel between [0, 1].
     * @type {number}
     */
    get g() {
      return ((this >> 8) & 0xFF) / 255;
    }

    /* ------------------------------------------ */

    /**
     * The numeric value of the blue channel between [0, 1].
     * @type {number}
     */
    get b() {
      return (this & 0xFF) / 255;
    }

    /* ------------------------------------------ */

    /**
     * The maximum value of all channels.
     * @type {number}
     */
    get maximum() {
      return Math.max(...this);
    }

    /* ------------------------------------------ */

    /**
     * The minimum value of all channels.
     * @type {number}
     */
    get minimum() {
      return Math.min(...this);
    }

    /* ------------------------------------------ */

    /**
     * Get the value of this color in little endian format.
     * @type {number}
     */
    get littleEndian() {
      return ((this >> 16) & 0xFF) + (this & 0x00FF00) + ((this & 0xFF) << 16);
    }

    /* ------------------------------------------ */

    /**
     * The color represented as an HSV array.
     * Conversion formula adapted from http://en.wikipedia.org/wiki/HSV_color_space.
     * Assumes r, g, and b are contained in the set [0, 1] and returns h, s, and v in the set [0, 1].
     * @type {[number, number, number]}
     */
    get hsv() {
      const [r, g, b] = this.rgb;
      const max = Math.max(r, g, b);
      const min = Math.min(r, g, b);
      const d = max - min;

      let h;
      const s = max === 0 ? 0 : d / max;
      const v = max;

      // Achromatic colors
      if (max === min) return [0, s, v];

      // Normal colors
      switch (max) {
        case r: h = (g - b) / d + (g < b ? 6 : 0); break;
        case g: h = (b - r) / d + 2; break;
        case b: h = (r - g) / d + 4; break;
      }
      h /= 6;
      return [h, s, v];
    }

    /* ------------------------------------------ */

    /**
     * The color represented as an HSL array.
     * Assumes r, g, and b are contained in the set [0, 1] and returns h, s, and l in the set [0, 1].
     * @type {[number, number, number]}
     */
    get hsl() {
      const [r, g, b] = this.rgb;

      // Compute luminosity, saturation and hue
      const l = Math.max(r, g, b);
      const s = l - Math.min(r, g, b);
      let h = 0;
      if ( s > 0 ) {
        if ( l === r ) {
          h = (g - b) / s;
        } else if ( l === g ) {
          h = 2 + (b - r) / s;
        } else {
          h = 4 + (r - g) / s;
        }
      }
      const finalHue = (60 * h < 0 ? 60 * h + 360 : 60 * h) / 360;
      const finalSaturation = s ? (l <= 0.5 ? s / (2 * l - s) : s / (2 - (2 * l - s))) : 0;
      const finalLuminance = (2 * l - s) / 2;
      return [finalHue, finalSaturation, finalLuminance];
    }

    /* ------------------------------------------ */

    /**
     * The color represented as a linear RGB array.
     * Assumes r, g, and b are contained in the set [0, 1] and returns linear r, g, and b in the set [0, 1].
     * @link https://en.wikipedia.org/wiki/SRGB#Transformation
     * @type {Color}
     */
    get linear() {
      const toLinear = c => (c > 0.04045) ? Math.pow((c + 0.055) / 1.055, 2.4) : (c / 12.92);
      return this.constructor.fromRGB([toLinear(this.r), toLinear(this.g), toLinear(this.b)]);
    }

    /* ------------------------------------------ */
    /*  Color Manipulation Methods                */
    /* ------------------------------------------ */

    /** @override */
    toString(radix) {
      if ( !this.valid ) return "";
      return `#${super.toString(16).padStart(6, "0")}`;
    }

    /* ------------------------------------------ */

    /**
     * Serialize the Color.
     * @returns {string}    The color as a CSS string
     */
    toJSON() {
      return this.css;
    }

    /* ------------------------------------------ */

    /**
     * Returns the color as a CSS string.
     * @returns {string}    The color as a CSS string
     */
    toHTML() {
      return this.css;
    }

    /* ------------------------------------------ */

    /**
     * Test whether this color equals some other color
     * @param {Color|number} other  Some other color or hex number
     * @returns {boolean}           Are the colors equal?
     */
    equals(other) {
      return this.valueOf() === other.valueOf();
    }

    /* ------------------------------------------ */

    /**
     * Get a CSS-compatible RGBA color string.
     * @param {number} alpha      The desired alpha in the range [0, 1]
     * @returns {string}          A CSS-compatible RGBA string
     */
    toRGBA(alpha) {
      const rgba = [(this >> 16) & 0xFF, (this >> 8) & 0xFF, this & 0xFF, alpha];
      return `rgba(${rgba.join(", ")})`;
    }

    /* ------------------------------------------ */

    /**
     * Mix this Color with some other Color using a provided interpolation weight.
     * @param {Color} other       Some other Color to mix with
     * @param {number} weight     The mixing weight placed on this color where weight is placed on the other color
     * @returns {Color}           The resulting mixed Color
     */
    mix(other, weight) {
      return new Color(Color.mix(this, other, weight));
    }

    /* ------------------------------------------ */

    /**
     * Multiply this Color by another Color or a static scalar.
     * @param {Color|number} other  Some other Color or a static scalar.
     * @returns {Color}             The resulting Color.
     */
    multiply(other) {
      if ( other instanceof Color ) return new Color(Color.multiply(this, other));
      return new Color(Color.multiplyScalar(this, other));
    }

    /* ------------------------------------------ */

    /**
     * Add this Color by another Color or a static scalar.
     * @param {Color|number} other  Some other Color or a static scalar.
     * @returns {Color}             The resulting Color.
     */
    add(other) {
      if ( other instanceof Color ) return new Color(Color.add(this, other));
      return new Color(Color.addScalar(this, other));
    }

    /* ------------------------------------------ */

    /**
     * Subtract this Color by another Color or a static scalar.
     * @param {Color|number} other  Some other Color or a static scalar.
     * @returns {Color}             The resulting Color.
     */
    subtract(other) {
      if ( other instanceof Color ) return new Color(Color.subtract(this, other));
      return new Color(Color.subtractScalar(this, other));
    }

    /* ------------------------------------------ */

    /**
     * Max this color by another Color or a static scalar.
     * @param {Color|number} other  Some other Color or a static scalar.
     * @returns {Color}             The resulting Color.
     */
    maximize(other) {
      if ( other instanceof Color ) return new Color(Color.maximize(this, other));
      return new Color(Color.maximizeScalar(this, other));
    }

    /* ------------------------------------------ */

    /**
     * Min this color by another Color or a static scalar.
     * @param {Color|number} other  Some other Color or a static scalar.
     * @returns {Color}             The resulting Color.
     */
    minimize(other) {
      if ( other instanceof Color ) return new Color(Color.minimize(this, other));
      return new Color(Color.minimizeScalar(this, other));
    }

    /* ------------------------------------------ */
    /*  Iterator                                  */
    /* ------------------------------------------ */

    /**
     * Iterating over a Color is equivalent to iterating over its [r,g,b] color channels.
     * @returns {Generator<number>}
     */
    *[Symbol.iterator]() {
      yield this.r;
      yield this.g;
      yield this.b;
    }

    /* ------------------------------------------------------------------------------------------- */
    /*                      Real-time performance Methods and Properties                           */
    /*  Important Note:                                                                            */
    /*  These methods are not a replacement, but a tool when real-time performance is needed.      */
    /*  They do not have the flexibility of the "classic" methods and come with some limitations.  */
    /*  Unless you have to deal with real-time performance, you should use the "classic" methods.  */
    /* ------------------------------------------------------------------------------------------- */

    /**
     * Set an rgb array with the rgb values contained in this Color class.
     * @param {number[]} vec3  Receive the result. Must be an array with at least a length of 3.
     */
    applyRGB(vec3) {
      vec3[0] = ((this >> 16) & 0xFF) / 255;
      vec3[1] = ((this >> 8) & 0xFF) / 255;
      vec3[2] = (this & 0xFF) / 255;
    }

    /* ------------------------------------------ */

    /**
     * Apply a linear interpolation between two colors, according to the weight.
     * @param {number}        color1       The first color to mix.
     * @param {number}        color2       The second color to mix.
     * @param {number}        weight       Weight of the linear interpolation.
     * @returns {number}                   The resulting mixed color
     */
    static mix(color1, color2, weight) {
      return (((((color1 >> 16) & 0xFF) * (1 - weight) + ((color2 >> 16) & 0xFF) * weight) << 16) & 0xFF0000)
        | (((((color1 >> 8) & 0xFF) * (1 - weight) + ((color2 >> 8) & 0xFF) * weight) << 8) & 0x00FF00)
        | (((color1 & 0xFF) * (1 - weight) + (color2 & 0xFF) * weight) & 0x0000FF);
    }

    /* ------------------------------------------ */

    /**
     * Multiply two colors.
     * @param {number}        color1       The first color to multiply.
     * @param {number}        color2       The second color to multiply.
     * @returns {number}                   The result.
     */
    static multiply(color1, color2) {
      return ((((color1 >> 16) & 0xFF) / 255 * ((color2 >> 16) & 0xFF) / 255) * 255 << 16)
        | ((((color1 >> 8) & 0xFF) / 255 * ((color2 >> 8) & 0xFF) / 255) * 255 << 8)
        | (((color1 & 0xFF) / 255 * ((color2 & 0xFF) / 255)) * 255);
    }

    /* ------------------------------------------ */

    /**
     * Multiply a color by a scalar
     * @param {number} color        The color to multiply.
     * @param {number} scalar       A static scalar to multiply with.
     * @returns {number}            The resulting color as a number.
     */
    static multiplyScalar(color, scalar) {
      return (Math.clamp(((color >> 16) & 0xFF) / 255 * scalar, 0, 1) * 255 << 16)
        | (Math.clamp(((color >> 8) & 0xFF) / 255 * scalar, 0, 1) * 255 << 8)
        | (Math.clamp((color & 0xFF) / 255 * scalar, 0, 1) * 255);
    }

    /* ------------------------------------------ */

    /**
     * Maximize two colors.
     * @param {number}        color1       The first color.
     * @param {number}        color2       The second color.
     * @returns {number}                   The result.
     */
    static maximize(color1, color2) {
      return (Math.clamp(Math.max((color1 >> 16) & 0xFF, (color2 >> 16) & 0xFF), 0, 0xFF) << 16)
        | (Math.clamp(Math.max((color1 >> 8) & 0xFF, (color2 >> 8) & 0xFF), 0, 0xFF) << 8)
        | Math.clamp(Math.max(color1 & 0xFF, color2 & 0xFF), 0, 0xFF);
    }

    /* ------------------------------------------ */

    /**
     * Maximize a color by a static scalar.
     * @param {number} color         The color to maximize.
     * @param {number} scalar        Scalar to maximize with (normalized).
     * @returns {number}             The resulting color as a number.
     */
    static maximizeScalar(color, scalar) {
      return (Math.clamp(Math.max((color >> 16) & 0xFF, scalar * 255), 0, 0xFF) << 16)
        | (Math.clamp(Math.max((color >> 8) & 0xFF, scalar * 255), 0, 0xFF) << 8)
        | Math.clamp(Math.max(color & 0xFF, scalar * 255), 0, 0xFF);
    }

    /* ------------------------------------------ */

    /**
     * Add two colors.
     * @param {number}        color1       The first color.
     * @param {number}        color2       The second color.
     * @returns {number}                   The resulting color as a number.
     */
    static add(color1, color2) {
      return (Math.clamp((((color1 >> 16) & 0xFF) + ((color2 >> 16) & 0xFF)), 0, 0xFF) << 16)
        | (Math.clamp((((color1 >> 8) & 0xFF) + ((color2 >> 8) & 0xFF)), 0, 0xFF) << 8)
        | Math.clamp(((color1 & 0xFF) + (color2 & 0xFF)), 0, 0xFF);
    }

    /* ------------------------------------------ */

    /**
     * Add a static scalar to a color.
     * @param {number} color         The color.
     * @param {number} scalar        Scalar to add with (normalized).
     * @returns {number}             The resulting color as a number.
     */
    static addScalar(color, scalar) {
      return (Math.clamp((((color >> 16) & 0xFF) + scalar * 255), 0, 0xFF) << 16)
        | (Math.clamp((((color >> 8) & 0xFF) + scalar * 255), 0, 0xFF) << 8)
        | Math.clamp(((color & 0xFF) + scalar * 255), 0, 0xFF);
    }

    /* ------------------------------------------ */

    /**
     * Subtract two colors.
     * @param {number}        color1       The first color.
     * @param {number}        color2       The second color.
     */
    static subtract(color1, color2) {
      return (Math.clamp((((color1 >> 16) & 0xFF) - ((color2 >> 16) & 0xFF)), 0, 0xFF) << 16)
        | (Math.clamp((((color1 >> 8) & 0xFF) - ((color2 >> 8) & 0xFF)), 0, 0xFF) << 8)
        | Math.clamp(((color1 & 0xFF) - (color2 & 0xFF)), 0, 0xFF);
    }

    /* ------------------------------------------ */

    /**
     * Subtract a color by a static scalar.
     * @param {number} color         The color.
     * @param {number} scalar        Scalar to subtract with (normalized).
     * @returns {number}             The resulting color as a number.
     */
    static subtractScalar(color, scalar) {
      return (Math.clamp((((color >> 16) & 0xFF) - scalar * 255), 0, 0xFF) << 16)
        | (Math.clamp((((color >> 8) & 0xFF) - scalar * 255), 0, 0xFF) << 8)
        | Math.clamp(((color & 0xFF) - scalar * 255), 0, 0xFF);
    }

    /* ------------------------------------------ */

    /**
     * Minimize two colors.
     * @param {number}        color1       The first color.
     * @param {number}        color2       The second color.
     */
    static minimize(color1, color2) {
      return (Math.clamp(Math.min((color1 >> 16) & 0xFF, (color2 >> 16) & 0xFF), 0, 0xFF) << 16)
        | (Math.clamp(Math.min((color1 >> 8) & 0xFF, (color2 >> 8) & 0xFF), 0, 0xFF) << 8)
        | Math.clamp(Math.min(color1 & 0xFF, color2 & 0xFF), 0, 0xFF);
    }

    /* ------------------------------------------ */

    /**
     * Minimize a color by a static scalar.
     * @param {number} color         The color.
     * @param {number} scalar        Scalar to minimize with (normalized).
     */
    static minimizeScalar(color, scalar) {
      return (Math.clamp(Math.min((color >> 16) & 0xFF, scalar * 255), 0, 0xFF) << 16)
        | (Math.clamp(Math.min((color >> 8) & 0xFF, scalar * 255), 0, 0xFF) << 8)
        | Math.clamp(Math.min(color & 0xFF, scalar * 255), 0, 0xFF);
    }

    /* ------------------------------------------ */

    /**
     * Convert a color to RGB and assign values to a passed array.
     * @param {number} color   The color to convert to RGB values.
     * @param {number[]} vec3  Receive the result. Must be an array with at least a length of 3.
     */
    static applyRGB(color, vec3) {
      vec3[0] = ((color >> 16) & 0xFF) / 255;
      vec3[1] = ((color >> 8) & 0xFF) / 255;
      vec3[2] = (color & 0xFF) / 255;
    }

    /* ------------------------------------------ */
    /*  Factory Methods                           */
    /* ------------------------------------------ */

    /**
     * Create a Color instance from an RGB array.
     * @param {ColorSource} color     A color input
     * @returns {Color}               The hex color instance or NaN
     */
    static from(color) {
      if ( (color === null) || (color === undefined) ) return new this(NaN);
      if ( typeof color === "string" ) return this.fromString(color);
      if ( typeof color === "number" ) return new this(color);
      if ( (color instanceof Array) && (color.length === 3) ) return this.fromRGB(color);
      if ( color instanceof Color ) return color;
      return new this(color);
    }

    /* ------------------------------------------ */

    /**
     * Create a Color instance from a color string which either includes or does not include a leading #.
     * @param {string} color                      A color string
     * @returns {Color}                           The hex color instance
     */
    static fromString(color) {
      return new this(parseInt(color.startsWith("#") ? color.substring(1) : color, 16));
    }

    /* ------------------------------------------ */

    /**
     * Create a Color instance from an RGB array.
     * @param {[number, number, number]} rgb      An RGB tuple
     * @returns {Color}                           The hex color instance
     */
    static fromRGB(rgb) {
      return new this(((rgb[0] * 255) << 16) + ((rgb[1] * 255) << 8) + (rgb[2] * 255 | 0));
    }

    /* ------------------------------------------ */

    /**
     * Create a Color instance from an RGB normalized values.
     * @param {number} r                          The red value
     * @param {number} g                          The green value
     * @param {number} b                          The blue value
     * @returns {Color}                           The hex color instance
     */
    static fromRGBvalues(r, g, b) {
      return new this(((r * 255) << 16) + ((g * 255) << 8) + (b * 255 | 0));
    }

    /* ------------------------------------------ */

    /**
     * Create a Color instance from an HSV array.
     * Conversion formula adapted from http://en.wikipedia.org/wiki/HSV_color_space.
     * Assumes h, s, and v are contained in the set [0, 1].
     * @param {[number, number, number]} hsv      An HSV tuple
     * @returns {Color}                           The hex color instance
     */
    static fromHSV(hsv) {
      const [h, s, v] = hsv;
      const i = Math.floor(h * 6);
      const f = (h * 6) - i;
      const p = v * (1 - s);
      const q = v * (1 - f * s);
      const t = v * (1 - (1 - f) * s);
      let rgb;
      switch (i % 6) {
        case 0: rgb = [v, t, p]; break;
        case 1: rgb = [q, v, p]; break;
        case 2: rgb = [p, v, t]; break;
        case 3: rgb = [p, q, v]; break;
        case 4: rgb = [t, p, v]; break;
        case 5: rgb = [v, p, q]; break;
      }
      return this.fromRGB(rgb);
    }

    /* ------------------------------------------ */

    /**
     * Create a Color instance from an HSL array.
     * Assumes h, s, and l are contained in the set [0, 1].
     * @param {[number, number, number]} hsl      An HSL tuple
     * @returns {Color}                           The hex color instance
     */
    static fromHSL(hsl) {
      const [h, s, l] = hsl;

      // Calculate intermediate values for the RGB components
      const chroma = (1 - Math.abs(2 * l - 1)) * s;
      const hue = h * 6;
      const x = chroma * (1 - Math.abs(hue % 2 - 1));
      const m = l - chroma / 2;

      let r, g, b;
      switch (Math.floor(hue)) {
        case 0: [r, g, b] = [chroma, x, 0]; break;
        case 1: [r, g, b] = [x, chroma, 0]; break;
        case 2: [r, g, b] = [0, chroma, x]; break;
        case 3: [r, g, b] = [0, x, chroma]; break;
        case 4: [r, g, b] = [x, 0, chroma]; break;
        case 5:
        case 6:[r, g, b] = [chroma, 0, x]; break;
        default: [r, g, b] = [0, 0, 0]; break;
      }

      // Adjust for luminance
      r += m;
      g += m;
      b += m;
      return this.fromRGB([r, g, b]);
    }

    /* ------------------------------------------ */

    /**
     * Create a Color instance (sRGB) from a linear rgb array.
     * Assumes r, g, and b are contained in the set [0, 1].
     * @link https://en.wikipedia.org/wiki/SRGB#Transformation
     * @param {[number, number, number]} linear   The linear rgb array
     * @returns {Color}                           The hex color instance
     */
    static fromLinearRGB(linear) {
      const [r, g, b] = linear;
      const tosrgb = c => (c <= 0.0031308) ? (12.92 * c) : (1.055 * Math.pow(c, 1 / 2.4) - 0.055);
      return this.fromRGB([tosrgb(r), tosrgb(g), tosrgb(b)]);
    }
  };

  /** @module constants */

  /**
   * The shortened software name
   * @type {string}
   */
  const vtt$1 = "Foundry VTT";

  /**
   * The full software name
   * @type {string}
   */
  const VTT = "Foundry Virtual Tabletop";

  /**
   * The software website URL
   * @type {string}
   */
  const WEBSITE_URL = "https://foundryvtt.com";

  /**
   * The serverless API URL
   */
  const WEBSITE_API_URL = "https://api.foundryvtt.com";

  /**
   * An ASCII greeting displayed to the client
   * @type {string}
   */
  const ASCII = `_______________________________________________________________
 _____ ___  _   _ _   _ ____  ______   __ __     _______ _____ 
|  ___/ _ \\| | | | \\ | |  _ \\|  _ \\ \\ / / \\ \\   / |_   _|_   _|
| |_ | | | | | | |  \\| | | | | |_) \\ V /   \\ \\ / /  | |   | |  
|  _|| |_| | |_| | |\\  | |_| |  _ < | |     \\ V /   | |   | |  
|_|   \\___/ \\___/|_| \\_|____/|_| \\_\\|_|      \\_/    |_|   |_|  
===============================================================`;

  /**
   * Define the allowed ActiveEffect application modes.
   * @remarks
   * Other arbitrary mode numbers can be used by systems and modules to identify special behaviors and are ignored
   * @enum {number}
   */
  const ACTIVE_EFFECT_MODES = {
    /**
     * Used to denote that the handling of the effect is programmatically provided by a system or module.
     */
    CUSTOM: 0,

    /**
     * Multiplies a numeric base value by the numeric effect value
     * @example
     * 2 (base value) * 3 (effect value) = 6 (derived value)
     */
    MULTIPLY: 1,

    /**
     * Adds a numeric base value to a numeric effect value, or concatenates strings
     * @example
     * 2 (base value) + 3 (effect value) = 5 (derived value)
     * @example
     * "Hello" (base value) + " World" (effect value) = "Hello World"
     */
    ADD: 2,

    /**
     * Keeps the lower value of the base value and the effect value
     * @example
     * 2 (base value), 0 (effect value) = 0 (derived value)
     * @example
     * 2 (base value), 3 (effect value) = 2 (derived value)
     */
    DOWNGRADE: 3,

    /**
     * Keeps the greater value of the base value and the effect value
     * @example
     * 2 (base value), 4 (effect value) = 4 (derived value)
     * @example
     * 2 (base value), 1 (effect value) = 2 (derived value)
     */
    UPGRADE: 4,

    /**
     * Directly replaces the base value with the effect value
     * @example
     * 2 (base value), 4 (effect value) = 4 (derived value)
     */
    OVERRIDE: 5
  };

  /**
   * Define the string name used for the base document type when specific sub-types are not defined by the system
   * @type {string}
   */
  const BASE_DOCUMENT_TYPE = "base";

  /**
   * Define the methods by which a Card can be drawn from a Cards stack
   * @enum {number}
   */
  const CARD_DRAW_MODES = {
    /**
     * Draw the first card from the stack
     * Synonymous with {@link CARD_DRAW_MODES.TOP}
     */
    FIRST: 0,

    /**
     * Draw the top card from the stack
     * Synonymous with {@link CARD_DRAW_MODES.FIRST}
     */
    TOP: 0,

    /**
     * Draw the last card from the stack
     * Synonymous with {@link CARD_DRAW_MODES.BOTTOM}
     */
    LAST: 1,

    /**
     * Draw the bottom card from the stack
     * Synonymous with {@link CARD_DRAW_MODES.LAST}
     */
    BOTTOM: 1,

    /**
     * Draw a random card from the stack
     */
    RANDOM: 2
  };

  /**
   * An enumeration of canvas performance modes.
   * @enum {number}
   */
  const CANVAS_PERFORMANCE_MODES = {
    LOW: 0,
    MED: 1,
    HIGH: 2,
    MAX: 3
  };

  /**
   * Valid Chat Message styles which affect how the message is presented in the chat log.
   * @enum {number}
   */
  const CHAT_MESSAGE_STYLES = {
    /**
     * An uncategorized chat message
     */
    OTHER: 0,

    /**
     * The message is spoken out of character (OOC).
     * OOC messages will be outlined by the player's color to make them more easily recognizable.
     */
    OOC: 1,

    /**
     * The message is spoken by an associated character.
     */
    IC: 2,

    /**
     * The message is an emote performed by the selected character.
     * Entering "/emote waves his hand." while controlling a character named Simon will send the message, "Simon waves his hand."
     */
    EMOTE: 3,
  };


  /**
   * Define the set of languages which have built-in support in the core software
   * @type {string[]}
   */
  const CORE_SUPPORTED_LANGUAGES = ["en"];

  /**
   * Configure the severity of compatibility warnings.
   * @enum {number}
   */
  const COMPATIBILITY_MODES = {
    /**
     * Nothing will be logged
     */
    SILENT: 0,

    /**
     * A message will be logged at the "warn" level
     */
    WARNING: 1,

    /**
     * A message will be logged at the "error" level
     */
    ERROR: 2,

    /**
     * An Error will be thrown
     */
    FAILURE: 3
  };

  /**
   * The lighting illumination levels which are supported.
   * @enum {number}
   */
  const LIGHTING_LEVELS = {
    DARKNESS: -2,
    HALFDARK: -1,
    UNLIT: 0,
    DIM: 1,
    BRIGHT: 2,
    BRIGHTEST: 3
  };

  /**
   * The CSS themes which are currently supported for the V11 Setup menu.
   * @enum {{id: string, label: string}}
   */
  const CSS_THEMES = Object.freeze({
    foundry: "THEME.foundry",
    fantasy: "THEME.fantasy",
    scifi: "THEME.scifi"
  });

  /**
   * The default artwork used for Token images if none is provided
   * @type {string}
   */
  const DEFAULT_TOKEN = 'icons/svg/mystery-man.svg';

  /**
   * The primary Document types.
   * @type {string[]}
   */
  const PRIMARY_DOCUMENT_TYPES = [
    "Actor",
    "Adventure",
    "Cards",
    "ChatMessage",
    "Combat",
    "FogExploration",
    "Folder",
    "Item",
    "JournalEntry",
    "Macro",
    "Playlist",
    "RollTable",
    "Scene",
    "Setting",
    "User"
  ];

  /**
   * The embedded Document types.
   * @type {Readonly<string[]>}
   */
  const EMBEDDED_DOCUMENT_TYPES = [
    "ActiveEffect",
    "ActorDelta",
    "AmbientLight",
    "AmbientSound",
    "Card",
    "Combatant",
    "Drawing",
    "Item",
    "JournalEntryPage",
    "MeasuredTemplate",
    "Note",
    "PlaylistSound",
    "Region",
    "RegionBehavior",
    "TableResult",
    "Tile",
    "Token",
    "Wall"
  ];

  /**
   * A listing of all valid Document types, both primary and embedded.
   * @type {Readonly<string[]>}
   */
  const ALL_DOCUMENT_TYPES = Array.from(new Set([
    ...PRIMARY_DOCUMENT_TYPES,
    ...EMBEDDED_DOCUMENT_TYPES
  ])).sort();

  /**
   * The allowed primary Document types which may exist within a World.
   * @type {string[]}
   */
  const WORLD_DOCUMENT_TYPES = [
    "Actor",
    "Cards",
    "ChatMessage",
    "Combat",
    "FogExploration",
    "Folder",
    "Item",
    "JournalEntry",
    "Macro",
    "Playlist",
    "RollTable",
    "Scene",
    "Setting",
    "User"
  ];

  /**
   * The allowed primary Document types which may exist within a Compendium pack.
   * @type {string[]}
   */
  const COMPENDIUM_DOCUMENT_TYPES = [
    "Actor",
    "Adventure",
    "Cards",
    "Item",
    "JournalEntry",
    "Macro",
    "Playlist",
    "RollTable",
    "Scene"
  ];

  /**
   * Define the allowed ownership levels for a Document.
   * Each level is assigned a value in ascending order.
   * Higher levels grant more permissions.
   * @enum {number}
   * @see https://foundryvtt.com/article/users/
   */
  const DOCUMENT_OWNERSHIP_LEVELS = {
    /**
     * The User inherits permissions from the parent Folder.
     */
    INHERIT: -1,

    /**
     * Restricts the associated Document so that it may not be seen by this User.
     */
    NONE: 0,

    /**
     * Allows the User to interact with the Document in basic ways, allowing them to see it in sidebars and see only limited aspects of its contents. The limits of this interaction are defined by the game system being used.
     */
    LIMITED: 1,

    /**
     * Allows the User to view this Document as if they were owner, but prevents them from making any changes to it.
     */
    OBSERVER: 2,

    /**
     * Allows the User to view and make changes to the Document as its owner. Owned documents cannot be deleted by anyone other than a gamemaster level User.
     */
    OWNER: 3
  };
  Object.freeze(DOCUMENT_OWNERSHIP_LEVELS);

  /**
   * Meta ownership levels that are used in the UI but never stored.
   * @enum {number}
   */
  const DOCUMENT_META_OWNERSHIP_LEVELS = {
    DEFAULT: -20,
    NOCHANGE: -10
  };
  Object.freeze(DOCUMENT_META_OWNERSHIP_LEVELS);

  /**
   * Define the allowed Document types which may be dynamically linked in chat
   * @type {string[]}
   */
  const DOCUMENT_LINK_TYPES = ["Actor", "Cards", "Item", "Scene", "JournalEntry", "Macro", "RollTable", "PlaylistSound"];

  /**
   * The supported dice roll visibility modes
   * @enum {string}
   * @see https://foundryvtt.com/article/dice/
   */
  const DICE_ROLL_MODES = {
    /**
     * This roll is visible to all players.
     */
    PUBLIC: "publicroll",

    /**
     * Rolls of this type are only visible to the player that rolled and any Game Master users.
     */
    PRIVATE: "gmroll",

    /**
     * A private dice roll only visible to Game Master users. The rolling player will not see the result of their own roll.
     */
    BLIND: "blindroll",

    /**
     * A private dice roll which is only visible to the user who rolled it.
     */
    SELF: "selfroll"
  };

  /**
   * The allowed fill types which a Drawing object may display
   * @enum {number}
   * @see https://foundryvtt.com/article/drawings/
   */
  const DRAWING_FILL_TYPES = {
    /**
     * The drawing is not filled
     */
    NONE: 0,

    /**
     * The drawing is filled with a solid color
     */
    SOLID: 1,

    /**
     * The drawing is filled with a tiled image pattern
     */
    PATTERN: 2
  };

  /**
   * Define the allowed Document types which Folders may contain
   * @type {string[]}
   */
  const FOLDER_DOCUMENT_TYPES = ["Actor", "Adventure", "Item", "Scene", "JournalEntry", "Playlist", "RollTable", "Cards", "Macro", "Compendium"];

  /**
   * The maximum allowed level of depth for Folder nesting
   * @type {number}
   */
  const FOLDER_MAX_DEPTH = 4;

  /**
   * A list of allowed game URL names
   * @type {string[]}
   */
  const GAME_VIEWS = ["game", "stream"];

  /**
   * The directions of movement.
   * @enum {number}
   */
  const MOVEMENT_DIRECTIONS = {
    UP: 0x1,
    DOWN: 0x2,
    LEFT: 0x4,
    RIGHT: 0x8,
    UP_LEFT: 0x1 | 0x4,
    UP_RIGHT: 0x1 | 0x8,
    DOWN_LEFT: 0x2 | 0x4,
    DOWN_RIGHT: 0x2 | 0x8
  };

  /**
   * The minimum allowed grid size which is supported by the software
   * @type {number}
   */
  const GRID_MIN_SIZE = 20;

  /**
   * The allowed Grid types which are supported by the software
   * @enum {number}
   * @see https://foundryvtt.com/article/scenes/
   */
  const GRID_TYPES = {
    /**
     * No fixed grid is used on this Scene allowing free-form point-to-point measurement without grid lines.
     */
    GRIDLESS: 0,

    /**
     * A square grid is used with width and height of each grid space equal to the chosen grid size.
     */
    SQUARE: 1,

    /**
     * A row-wise hexagon grid (pointy-topped) where odd-numbered rows are offset.
     */
    HEXODDR: 2,

    /**
     * A row-wise hexagon grid (pointy-topped) where even-numbered rows are offset.
     */
    HEXEVENR: 3,

    /**
     * A column-wise hexagon grid (flat-topped) where odd-numbered columns are offset.
     */
    HEXODDQ: 4,

    /**
     * A column-wise hexagon grid (flat-topped) where even-numbered columns are offset.
     */
    HEXEVENQ: 5
  };

  /**
   * The different rules to define and measure diagonal distance/cost in a square grid.
   * The description of each option refers to the distance/cost of moving diagonally relative to the distance/cost of a horizontal or vertical move.
   * @enum {number}
   */
  const GRID_DIAGONALS = {
    /**
     * The diagonal distance is 1. Diagonal movement costs the same as horizontal/vertical movement.
     */
    EQUIDISTANT: 0,

    /**
     * The diagonal distance is √2. Diagonal movement costs √2 times as much as horizontal/vertical movement.
     */
    EXACT: 1,

    /**
     * The diagonal distance is 1.5. Diagonal movement costs 1.5 times as much as horizontal/vertical movement.
     */
    APPROXIMATE: 2,

    /**
     * The diagonal distance is 2. Diagonal movement costs 2 times as much as horizontal/vertical movement.
     */
    RECTILINEAR: 3,

    /**
     * The diagonal distance alternates between 1 and 2 starting at 1.
     * The first diagonal movement costs the same as horizontal/vertical movement
     * The second diagonal movement costs 2 times as much as horizontal/vertical movement.
     * And so on...
     */
    ALTERNATING_1: 4,

    /**
     * The diagonal distance alternates between 2 and 1 starting at 2.
     * The first diagonal movement costs 2 times as much as horizontal/vertical movement.
     * The second diagonal movement costs the same as horizontal/vertical movement.
     * And so on...
     */
    ALTERNATING_2: 5,

    /**
     * The diagonal distance is ∞. Diagonal movement is not allowed/possible.
     */
    ILLEGAL: 6,
  };

  /**
   * The grid snapping modes.
   * @enum {number}
   */
  const GRID_SNAPPING_MODES = {
    /**
     * Nearest center point.
     */
    CENTER: 0x1,

    /**
     * Nearest edge midpoint.
     */
    EDGE_MIDPOINT: 0x2,

    /**
     * Nearest top-left vertex.
     */
    TOP_LEFT_VERTEX: 0x10,

    /**
     * Nearest top-right vertex.
     */
    TOP_RIGHT_VERTEX: 0x20,

    /**
     * Nearest bottom-left vertex.
     */
    BOTTOM_LEFT_VERTEX: 0x40,

    /**
     * Nearest bottom-right vertex.
     */
    BOTTOM_RIGHT_VERTEX: 0x80,

    /**
     * Nearest vertex.
     * Alias for `TOP_LEFT_VERTEX | TOP_RIGHT_VERTEX | BOTTOM_LEFT_VERTEX | BOTTOM_RIGHT_VERTEX`.
     */
    VERTEX: 0xF0,

    /**
     * Nearest top-left corner.
     */
    TOP_LEFT_CORNER: 0x100,

    /**
     * Nearest top-right corner.
     */
    TOP_RIGHT_CORNER: 0x200,

    /**
     * Nearest bottom-left corner.
     */
    BOTTOM_LEFT_CORNER: 0x400,

    /**
     * Nearest bottom-right corner.
     */
    BOTTOM_RIGHT_CORNER: 0x800,

    /**
     * Nearest corner.
     * Alias for `TOP_LEFT_CORNER | TOP_RIGHT_CORNER | BOTTOM_LEFT_CORNER | BOTTOM_RIGHT_CORNER`.
     */
    CORNER: 0xF00,

    /**
     * Nearest top side midpoint.
     */
    TOP_SIDE_MIDPOINT: 0x1000,

    /**
     * Nearest bottom side midpoint.
     */
    BOTTOM_SIDE_MIDPOINT: 0x2000,

    /**
     * Nearest left side midpoint.
     */
    LEFT_SIDE_MIDPOINT: 0x4000,

    /**
     * Nearest right side midpoint.
     */
    RIGHT_SIDE_MIDPOINT: 0x8000,

    /**
     * Nearest side midpoint.
     * Alias for `TOP_SIDE_MIDPOINT | BOTTOM_SIDE_MIDPOINT | LEFT_SIDE_MIDPOINT | RIGHT_SIDE_MIDPOINT`.
     */
    SIDE_MIDPOINT: 0xF000,
  };

  /**
   * A list of supported setup URL names
   * @type {string[]}
   */
  const SETUP_VIEWS = ["auth", "license", "setup", "players", "join", "update"];

  /**
   * An Array of valid MacroAction scope values
   * @type {string[]}
   */
  const MACRO_SCOPES = ["global", "actors", "actor"];

  /**
   * An enumeration of valid Macro types
   * @enum {string}
   * @see https://foundryvtt.com/article/macros/
   */
  const MACRO_TYPES = {
    /**
     * Complex and powerful macros which leverage the FVTT API through plain JavaScript to perform functions as simple or as advanced as you can imagine.
     */
    SCRIPT: "script",

    /**
     * Simple and easy to use, chat macros post pre-defined chat messages to the chat log when executed. All users can execute chat macros by default.
     */
    CHAT: "chat"
  };

  /**
   * The allowed channels for audio playback.
   * @enum {string}
   */
  const AUDIO_CHANNELS = {
    music: "AUDIO.CHANNELS.MUSIC.label",
    environment: "AUDIO.CHANNELS.ENVIRONMENT.label",
    interface: "AUDIO.CHANNELS.INTERFACE.label",
  };

  /**
   * The allowed playback modes for an audio Playlist
   * @enum {number}
   * @see https://foundryvtt.com/article/playlists/
   */
  const PLAYLIST_MODES = {
    /**
     * The playlist does not play on its own, only individual Sound tracks played as a soundboard.
     */
    DISABLED: -1,

    /**
     * The playlist plays sounds one at a time in sequence.
     */
    SEQUENTIAL: 0,

    /**
     * The playlist plays sounds one at a time in randomized order.
     */
    SHUFFLE: 1,

    /**
     * The playlist plays all contained sounds at the same time.
     */
    SIMULTANEOUS: 2
  };

  /**
   * The available sort modes for an audio Playlist.
   * @enum {string}
   * @see https://foundryvtt.com/article/playlists/
   */
  const PLAYLIST_SORT_MODES = {
    /**
     * Sort sounds alphabetically.
     * @defaultValue
     */
    ALPHABETICAL: "a",

    /**
     * Sort sounds by manual drag-and-drop.
     */
    MANUAL: "m"
  };

  /**
   * The available modes for searching within a DirectoryCollection
   * @type {{FULL: string, NAME: string}}
   */
  const DIRECTORY_SEARCH_MODES = {
    FULL: "full",
    NAME: "name"
  };

  /**
   * The allowed package types
   * @type {string[]}
   */
  const PACKAGE_TYPES = ["world", "system", "module"];

  /**
   * Encode the reasons why a package may be available or unavailable for use
   * @enum {number}
   */
  const PACKAGE_AVAILABILITY_CODES = {
    /**
     * Package availability could not be determined
     */
    UNKNOWN: 0,

    /**
     * The Package is verified to be compatible with the current core software build
     */
    VERIFIED: 1,

    /**
     * Package is available for use, but not verified for the current core software build
     */
    UNVERIFIED_BUILD: 2,

    /**
     * One or more installed system is incompatible with the Package.
     */
    UNVERIFIED_SYSTEM: 3,

    /**
     * Package is available for use, but not verified for the current core software generation
     */
    UNVERIFIED_GENERATION: 4,

    /**
     * The System that the Package relies on is not available
     */
    MISSING_SYSTEM: 5,

    /**
     * A dependency of the Package is not available
     */
    MISSING_DEPENDENCY: 6,

    /**
     * The Package is compatible with an older version of Foundry than the currently installed version
     */
    REQUIRES_CORE_DOWNGRADE: 7,

    /**
     * The Package is compatible with a newer version of Foundry than the currently installed version, and that version is Stable
     */
    REQUIRES_CORE_UPGRADE_STABLE: 8,

    /**
     * The Package is compatible with a newer version of Foundry than the currently installed version, and that version is not yet Stable
     */
    REQUIRES_CORE_UPGRADE_UNSTABLE: 9,

    /**
     * A required dependency is not compatible with the current version of Foundry
     */
    REQUIRES_DEPENDENCY_UPDATE: 10
  };

  /**
   * A safe password string which can be displayed
   * @type {string}
   */
  const PASSWORD_SAFE_STRING = "•".repeat(16);

  /**
   * The allowed software update channels
   * @enum {string}
   */
  const SOFTWARE_UPDATE_CHANNELS = {
    /**
     * The Stable release channel
     */
    stable: "SETUP.UpdateStable",

    /**
     * The User Testing release channel
     */
    testing: "SETUP.UpdateTesting",

    /**
     * The Development release channel
     */
    development: "SETUP.UpdateDevelopment",

    /**
     * The Prototype release channel
     */
    prototype: "SETUP.UpdatePrototype"
  };

  /**
   * The default sorting density for manually ordering child objects within a parent
   * @type {number}
   */
  const SORT_INTEGER_DENSITY = 100000;

  /**
   * The allowed types of a TableResult document
   * @enum {string}
   * @see https://foundryvtt.com/article/roll-tables/
   */
  const TABLE_RESULT_TYPES = {
    /**
     *  Plain text or HTML scripted entries which will be output to Chat.
     */
    TEXT: "text",

    /**
     * An in-World Document reference which will be linked to in the chat message.
     */
    DOCUMENT: "document",

    /**
     * A Compendium Pack reference which will be linked to in the chat message.
     */
    COMPENDIUM: "pack"
  };

  /**
   * The allowed formats of a Journal Entry Page.
   * @enum {number}
   * @see https://foundryvtt.com/article/journal/
   */
  const JOURNAL_ENTRY_PAGE_FORMATS = {
    /**
     * The page is formatted as HTML.
     */
    HTML: 1,

    /**
     * The page is formatted as Markdown.
     */
    MARKDOWN: 2,
  };

  /**
   * Define the valid anchor locations for a Tooltip displayed on a Placeable Object
   * @enum {number}
   * @see TooltipManager
   */
  const TEXT_ANCHOR_POINTS = {
    /**
     * Anchor the tooltip to the center of the element.
     */
    CENTER: 0,

    /**
     * Anchor the tooltip to the bottom of the element.
     */
    BOTTOM: 1,

    /**
     * Anchor the tooltip to the top of the element.
     */
    TOP: 2,

    /**
     * Anchor the tooltip to the left of the element.
     */
    LEFT: 3,

    /**
     * Anchor the tooltip to the right of the element.
     */
    RIGHT: 4
  };

  /**
   * Define the valid occlusion modes which a tile can use
   * @enum {number}
   * @see https://foundryvtt.com/article/tiles/
   */
  const OCCLUSION_MODES = {
    /**
     * Turns off occlusion, making the tile never fade while tokens are under it.
     */
    NONE: 0,

    /**
     * Causes the whole tile to fade when an actor token moves under it.
     * @defaultValue
     */
    FADE: 1,

    // ROOF: 2,  This mode is no longer supported so we don't use 2 for any other mode

    /**
     * Causes the tile to reveal the background in the vicinity of an actor token under it. The radius is determined by the token's size.
     */
    RADIAL: 3,

    /**
     * Causes the tile to be partially revealed based on the vision of the actor, which does not need to be under the tile to see what's beneath it.
     *
     * @remarks
     * This is useful for rooves on buildings where players could see through a window or door, viewing only a portion of what is obscured by the roof itself.
     */
    VISION: 4
  };

  /**
   * Alias for old tile occlusion modes definition
   */
  const TILE_OCCLUSION_MODES = OCCLUSION_MODES;

  /**
   * The occlusion modes that define the set of tokens that trigger occlusion.
   * @enum {number}
   */
  const TOKEN_OCCLUSION_MODES = {

    /**
     * Owned tokens that aren't hidden.
     */
    OWNED: 0x1,

    /**
     * Controlled tokens.
     */
    CONTROLLED: 0x2,

    /**
     * Hovered tokens that are visible.
     */
    HOVERED: 0x4,

    /**
     * Highlighted tokens that are visible.
     */
    HIGHLIGHTED: 0x8,

    /**
     * All visible tokens.
     */
    VISIBLE: 0x10
  };

  /**
   * Describe the various thresholds of token control upon which to show certain pieces of information
   * @enum {number}
   * @see https://foundryvtt.com/article/tokens/
   */
  const TOKEN_DISPLAY_MODES = {
    /**
     * No information is displayed.
     */
    NONE: 0,

    /**
     * Displayed when the token is controlled.
     */
    CONTROL: 10,

    /**
     * Displayed when hovered by a GM or a user who owns the actor.
     */
    OWNER_HOVER: 20,

    /**
     * Displayed when hovered by any user.
     */
    HOVER: 30,

    /**
     * Always displayed for a GM or for a user who owns the actor.
     */
    OWNER: 40,

    /**
     * Always displayed for everyone.
     */
    ALWAYS: 50
  };

  /**
   * The allowed Token disposition types
   * @enum {number}
   * @see https://foundryvtt.com/article/tokens/
   */
  const TOKEN_DISPOSITIONS = {
    /**
     * Displayed with a purple borders for owners and with no borders for others (and no pointer change).
     */
    SECRET: -2,

    /**
     * Displayed as an enemy with a red border.
     */
    HOSTILE: -1,

    /**
     * Displayed as neutral with a yellow border.
     */
    NEUTRAL: 0,

    /**
     * Displayed as an ally with a cyan border.
     */
    FRIENDLY: 1
  };

  /**
   * The possible shapes of Tokens in hexagonal grids.
   * @enum {number}
   */
  const TOKEN_HEXAGONAL_SHAPES = {

    /**
     * Ellipse (Variant 1)
     */
    ELLIPSE_1: 0,

    /**
     * Ellipse (Variant 2)
     */
    ELLIPSE_2: 1,

    /**
     * Trapezoid (Variant 1)
     */
    TRAPEZOID_1: 2,

    /**
     * Trapezoid (Variant 2)
     */
    TRAPEZOID_2: 3,

    /**
     * Rectangle (Variant 1)
     */
    RECTANGLE_1: 4,

    /**
     * Rectangle (Variant 2)
     */
    RECTANGLE_2: 5,
  };

  /**
   * Define the allowed User permission levels.
   * Each level is assigned a value in ascending order. Higher levels grant more permissions.
   * @enum {number}
   * @see https://foundryvtt.com/article/users/
   */
  const USER_ROLES = {
    /**
     * The User is blocked from taking actions in Foundry Virtual Tabletop.
     * You can use this role to temporarily or permanently ban a user from joining the game.
     */
    NONE: 0,

    /**
     * The User is able to join the game with permissions available to a standard player.
     * They cannot take some more advanced actions which require Trusted permissions, but they have the basic functionalities needed to operate in the virtual tabletop.
     */
    PLAYER: 1,

    /**
     * Similar to the Player role, except a Trusted User has the ability to perform some more advanced actions like create drawings, measured templates, or even to (optionally) upload media files to the server.
     */
    TRUSTED: 2,

    /**
     * A special User who has many of the same in-game controls as a Game Master User, but does not have the ability to perform administrative actions like changing User roles or modifying World-level settings.
     */
    ASSISTANT: 3,

    /**
     *  A special User who has administrative control over this specific World.
     *  Game Masters behave quite differently than Players in that they have the ability to see all Documents and Objects within the world as well as the capability to configure World settings.
     */
    GAMEMASTER: 4
  };

  /**
   * Invert the User Role mapping to recover role names from a role integer
   * @enum {string}
   * @see USER_ROLES
   */
  const USER_ROLE_NAMES = Object.entries(USER_ROLES).reduce((obj, r) => {
    obj[r[1]] = r[0];
    return obj;
  }, {});

  /**
   * An enumeration of the allowed types for a MeasuredTemplate embedded document
   * @enum {string}
   * @see https://foundryvtt.com/article/measurement/
   */
  const MEASURED_TEMPLATE_TYPES = {
    /**
     * Circular templates create a radius around the starting point.
     */
    CIRCLE: "circle",

    /**
     * Cones create an effect in the shape of a triangle or pizza slice from the starting point.
     */
    CONE: "cone",

    /**
     * A rectangle uses the origin point as one of the corners, treating the origin as being inside of the rectangle's area.
     */
    RECTANGLE: "rect",

    /**
     * A ray creates a single line that is one square in width and as long as you want it to be.
     */
    RAY: "ray"
  };

  /**
   * @typedef {Object} UserPermission
   * @property {string} label
   * @property {string} hint
   * @property {boolean} disableGM
   * @property {number} defaultRole
   */

  /**
   * Define the recognized User capabilities which individual Users or role levels may be permitted to perform
   * @type {Record<string, UserPermission>}
   */
  const USER_PERMISSIONS = {
    ACTOR_CREATE: {
      label: "PERMISSION.ActorCreate",
      hint: "PERMISSION.ActorCreateHint",
      disableGM: false,
      defaultRole: USER_ROLES.ASSISTANT
    },
    BROADCAST_AUDIO: {
      label: "PERMISSION.BroadcastAudio",
      hint: "PERMISSION.BroadcastAudioHint",
      disableGM: true,
      defaultRole: USER_ROLES.TRUSTED
    },
    BROADCAST_VIDEO: {
      label: "PERMISSION.BroadcastVideo",
      hint: "PERMISSION.BroadcastVideoHint",
      disableGM: true,
      defaultRole: USER_ROLES.TRUSTED
    },
    CARDS_CREATE: {
      label: "PERMISSION.CardsCreate",
      hint: "PERMISSION.CardsCreateHint",
      disableGM: false,
      defaultRole: USER_ROLES.ASSISTANT
    },
    DRAWING_CREATE: {
      label: "PERMISSION.DrawingCreate",
      hint: "PERMISSION.DrawingCreateHint",
      disableGM: false,
      defaultRole: USER_ROLES.TRUSTED
    },
    ITEM_CREATE: {
      label: "PERMISSION.ItemCreate",
      hint: "PERMISSION.ItemCreateHint",
      disableGM: false,
      defaultRole: USER_ROLES.ASSISTANT
    },
    FILES_BROWSE: {
      label: "PERMISSION.FilesBrowse",
      hint: "PERMISSION.FilesBrowseHint",
      disableGM: false,
      defaultRole: USER_ROLES.TRUSTED
    },
    FILES_UPLOAD: {
      label: "PERMISSION.FilesUpload",
      hint: "PERMISSION.FilesUploadHint",
      disableGM: false,
      defaultRole: USER_ROLES.ASSISTANT
    },
    JOURNAL_CREATE: {
      label: "PERMISSION.JournalCreate",
      hint: "PERMISSION.JournalCreateHint",
      disableGM: false,
      defaultRole: USER_ROLES.TRUSTED
    },
    MACRO_SCRIPT: {
      label: "PERMISSION.MacroScript",
      hint: "PERMISSION.MacroScriptHint",
      disableGM: false,
      defaultRole: USER_ROLES.PLAYER
    },
    MANUAL_ROLLS: {
      label: "PERMISSION.ManualRolls",
      hint: "PERMISSION.ManualRollsHint",
      disableGM: true,
      defaultRole: USER_ROLES.TRUSTED
    },
    MESSAGE_WHISPER: {
      label: "PERMISSION.MessageWhisper",
      hint: "PERMISSION.MessageWhisperHint",
      disableGM: false,
      defaultRole: USER_ROLES.PLAYER
    },
    NOTE_CREATE: {
      label: "PERMISSION.NoteCreate",
      hint: "PERMISSION.NoteCreateHint",
      disableGM: false,
      defaultRole: USER_ROLES.TRUSTED
    },
    PING_CANVAS: {
      label: "PERMISSION.PingCanvas",
      hint: "PERMISSION.PingCanvasHint",
      disableGM: true,
      defaultRole: USER_ROLES.PLAYER
    },
    PLAYLIST_CREATE: {
      label: "PERMISSION.PlaylistCreate",
      hint: "PERMISSION.PlaylistCreateHint",
      disableGM: false,
      defaultRole: USER_ROLES.ASSISTANT
    },
    SETTINGS_MODIFY: {
      label: "PERMISSION.SettingsModify",
      hint: "PERMISSION.SettingsModifyHint",
      disableGM: false,
      defaultRole: USER_ROLES.ASSISTANT
    },
    SHOW_CURSOR: {
      label: "PERMISSION.ShowCursor",
      hint: "PERMISSION.ShowCursorHint",
      disableGM: true,
      defaultRole: USER_ROLES.PLAYER
    },
    SHOW_RULER: {
      label: "PERMISSION.ShowRuler",
      hint: "PERMISSION.ShowRulerHint",
      disableGM: true,
      defaultRole: USER_ROLES.PLAYER
    },
    TEMPLATE_CREATE: {
      label: "PERMISSION.TemplateCreate",
      hint: "PERMISSION.TemplateCreateHint",
      disableGM: false,
      defaultRole: USER_ROLES.PLAYER
    },
    TOKEN_CREATE: {
      label: "PERMISSION.TokenCreate",
      hint: "PERMISSION.TokenCreateHint",
      disableGM: false,
      defaultRole: USER_ROLES.ASSISTANT
    },
    TOKEN_DELETE: {
      label: "PERMISSION.TokenDelete",
      hint: "PERMISSION.TokenDeleteHint",
      disableGM: false,
      defaultRole: USER_ROLES.ASSISTANT
    },
    TOKEN_CONFIGURE: {
      label: "PERMISSION.TokenConfigure",
      hint: "PERMISSION.TokenConfigureHint",
      disableGM: false,
      defaultRole: USER_ROLES.TRUSTED
    },
    WALL_DOORS: {
      label: "PERMISSION.WallDoors",
      hint: "PERMISSION.WallDoorsHint",
      disableGM: false,
      defaultRole: USER_ROLES.PLAYER
    }
  };

  /**
   * The allowed directions of effect that a Wall can have
   * @enum {number}
   * @see https://foundryvtt.com/article/walls/
   */
  const WALL_DIRECTIONS = {
    /**
     * The wall collides from both directions.
     */
    BOTH: 0,

    /**
     * The wall collides only when a ray strikes its left side.
     */
    LEFT: 1,

    /**
     * The wall collides only when a ray strikes its right side.
     */
    RIGHT: 2
  };

  /**
   * The allowed door types which a Wall may contain
   * @enum {number}
   * @see https://foundryvtt.com/article/walls/
   */
  const WALL_DOOR_TYPES = {
    /**
     * The wall does not contain a door.
     */
    NONE: 0,

    /**
     *  The wall contains a regular door.
     */
    DOOR: 1,

    /**
     * The wall contains a secret door.
     */
    SECRET: 2
  };

  /**
   * The allowed door states which may describe a Wall that contains a door
   * @enum {number}
   * @see https://foundryvtt.com/article/walls/
   */
  const WALL_DOOR_STATES = {
    /**
     * The door is closed.
     */
    CLOSED: 0,

    /**
     * The door is open.
     */
    OPEN: 1,

    /**
     * The door is closed and locked.
     */
    LOCKED: 2
  };

  /**
   * The possible ways to interact with a door
   * @enum {string[]}
   */
  const WALL_DOOR_INTERACTIONS = ["open", "close", "lock", "unlock", "test"];

  /**
   * The wall properties which restrict the way interaction occurs with a specific wall
   * @type {string[]}
   */
  const WALL_RESTRICTION_TYPES = ["light", "sight", "sound", "move"];

  /**
   * The types of sensory collision which a Wall may impose
   * @enum {number}
   * @see https://foundryvtt.com/article/walls/
   */
  const WALL_SENSE_TYPES = {
    /**
     * Senses do not collide with this wall.
     */
    NONE: 0,

    /**
     * Senses collide with this wall.
     */
    LIMITED: 10,

    /**
     * Senses collide with the second intersection, bypassing the first.
     */
    NORMAL: 20,

    /**
     * Senses bypass the wall within a certain proximity threshold.
     */
    PROXIMITY: 30,

    /**
     * Senses bypass the wall outside a certain proximity threshold.
     */
    DISTANCE: 40
  };

  /**
   * The types of movement collision which a Wall may impose
   * @enum {number}
   * @see https://foundryvtt.com/article/walls/
   */
  const WALL_MOVEMENT_TYPES = {
    /**
     * Movement does not collide with this wall.
     */
    NONE: WALL_SENSE_TYPES.NONE,

    /**
     * Movement collides with this wall.
     */
    NORMAL: WALL_SENSE_TYPES.NORMAL
  };

  /**
   * The possible precedence values a Keybinding might run in
   * @enum {number}
   * @see https://foundryvtt.com/article/keybinds/
   */
  const KEYBINDING_PRECEDENCE = {
    /**
     * Runs in the first group along with other PRIORITY keybindings.
     */
    PRIORITY: 0,

    /**
     * Runs after the PRIORITY group along with other NORMAL keybindings.
     */
    NORMAL: 1,

    /**
     * Runs in the last group along with other DEFERRED keybindings.
     */
    DEFERRED: 2
  };

  /**
   * The allowed set of HTML template extensions
   * @type {string[]}
   */
  const HTML_FILE_EXTENSIONS = ["html", "handlebars", "hbs"];

  /**
   * The supported file extensions for image-type files, and their corresponding mime types.
   * @type {Record<string, string>}
   */
  const IMAGE_FILE_EXTENSIONS = {
    apng: "image/apng",
    avif: "image/avif",
    bmp: "image/bmp",
    gif: "image/gif",
    jpeg: "image/jpeg",
    jpg: "image/jpeg",
    png: "image/png",
    svg: "image/svg+xml",
    tiff: "image/tiff",
    webp: "image/webp"
  };

  /**
   * The supported file extensions for video-type files, and their corresponding mime types.
   * @type {Record<string, string>}
   */
  const VIDEO_FILE_EXTENSIONS = {
    m4v: "video/mp4",
    mp4: "video/mp4",
    ogv: "video/ogg",
    webm: "video/webm"
  };

  /**
   * The supported file extensions for audio-type files, and their corresponding mime types.
   * @type {Record<string, string>}
   */
  const AUDIO_FILE_EXTENSIONS = {
    aac: "audio/aac",
    flac: "audio/flac",
    m4a: "audio/mp4",
    mid: "audio/midi",
    mp3: "audio/mpeg",
    ogg: "audio/ogg",
    opus: "audio/opus",
    wav: "audio/wav",
    webm: "audio/webm"
  };

  /**
   * The supported file extensions for text files, and their corresponding mime types.
   * @type {Record<string, string>}
   */
  const TEXT_FILE_EXTENSIONS = {
    csv: "text/csv",
    json: "application/json",
    md: "text/markdown",
    pdf: "application/pdf",
    tsv: "text/tab-separated-values",
    txt: "text/plain",
    xml: "application/xml",
    yml: "application/yaml",
    yaml: "application/yaml"
  };

  /**
   * Supported file extensions for font files, and their corresponding mime types.
   * @type {Record<string, string>}
   */
  const FONT_FILE_EXTENSIONS = {
    ttf: "font/ttf",
    otf: "font/otf",
    woff: "font/woff",
    woff2: "font/woff2"
  };

  /**
   * Supported file extensions for 3D files, and their corresponding mime types.
   * @type {Record<string, string>}
   */
  const GRAPHICS_FILE_EXTENSIONS = {
    fbx: "application/octet-stream",
    glb: "model/gltf-binary",
    gltf: "model/gltf+json",
    mtl: "model/mtl",
    obj: "model/obj",
    stl: "model/stl",
    usdz: "model/vnd.usdz+zip"
  };

  /**
   * A consolidated mapping of all extensions permitted for upload.
   * @type {Record<string, string>}
   */
  const UPLOADABLE_FILE_EXTENSIONS = {
    ...IMAGE_FILE_EXTENSIONS,
    ...VIDEO_FILE_EXTENSIONS,
    ...AUDIO_FILE_EXTENSIONS,
    ...TEXT_FILE_EXTENSIONS,
    ...FONT_FILE_EXTENSIONS,
    ...GRAPHICS_FILE_EXTENSIONS
  };

  /**
   * A list of MIME types which are treated as uploaded "media", which are allowed to overwrite existing files.
   * Any non-media MIME type is not allowed to replace an existing file.
   * @type {string[]}
   */
  const MEDIA_MIME_TYPES = Object.values(UPLOADABLE_FILE_EXTENSIONS);

  /**
   * An enumeration of file type categories which can be selected
   * @enum {Record<string, string>}
   */
  const FILE_CATEGORIES = {
    HTML: HTML_FILE_EXTENSIONS,
    IMAGE: IMAGE_FILE_EXTENSIONS,
    VIDEO: VIDEO_FILE_EXTENSIONS,
    AUDIO: AUDIO_FILE_EXTENSIONS,
    TEXT: TEXT_FILE_EXTENSIONS,
    FONT: FONT_FILE_EXTENSIONS,
    GRAPHICS: GRAPHICS_FILE_EXTENSIONS,
    MEDIA: MEDIA_MIME_TYPES,
  };

  /**
   * A font weight to name mapping.
   * @enum {number}
   */
  const FONT_WEIGHTS = {
    Thin: 100,
    ExtraLight: 200,
    Light: 300,
    Regular: 400,
    Medium: 500,
    SemiBold: 600,
    Bold: 700,
    ExtraBold: 800,
    Black: 900
  };

  /**
   * Stores shared commonly used timeouts, measured in MS
   * @enum {number}
   */
  const TIMEOUTS = {
    /**
     * The default timeout for interacting with the foundryvtt.com API.
     */
    FOUNDRY_WEBSITE: 10000,

    /**
     * The specific timeout for loading the list of packages from the foundryvtt.com API.
     */
    PACKAGE_REPOSITORY: 5000,

    /**
     * The specific timeout for the IP address lookup service.
     */
    IP_DISCOVERY: 5000
  };

  /**
   * A subset of Compendium types which require a specific system to be designated
   * @type {string[]}
   */
  const SYSTEM_SPECIFIC_COMPENDIUM_TYPES = ["Actor", "Item"];

  /**
   * The configured showdown bi-directional HTML <-> Markdown converter options.
   * @type {Record<string, boolean>}
   */
  const SHOWDOWN_OPTIONS = {
    disableForced4SpacesIndentedSublists: true,
    noHeaderId: true,
    parseImgDimensions: true,
    strikethrough: true,
    tables: true,
    tablesHeaderId: true
  };

  /**
   * The list of allowed attributes in HTML elements.
   * @type {Record<string, string[]>}
   */
  const ALLOWED_HTML_ATTRIBUTES = Object.freeze({
    "*": Object.freeze([
      "class", "data-*", "id", "title", "style", "draggable", "aria-*", "tabindex", "dir", "hidden", "inert", "role",
      "is", "lang", "popover"
    ]),
    a: Object.freeze(["href", "name", "target", "rel"]),
    area: Object.freeze(["alt", "coords", "href", "rel", "shape", "target"]),
    audio: Object.freeze(["controls", "loop", "muted", "src", "autoplay"]),
    blockquote: Object.freeze(["cite"]),
    button: Object.freeze(["disabled", "name", "type", "value"]),
    col: Object.freeze(["span"]),
    colgroup: Object.freeze(["span"]),
    details: Object.freeze(["open"]),
    fieldset: Object.freeze(["disabled"]),
    form: Object.freeze(["name"]),
    iframe: Object.freeze(["src", "srcdoc", "name", "height", "width", "loading", "sandbox"]),
    img: Object.freeze(["height", "src", "width", "usemap", "sizes", "srcset", "alt"]),
    input: Object.freeze([
      "checked", "disabled", "name", "value", "placeholder", "type", "alt", "height", "list",
      "max", "min", "placeholder", "readonly", "size", "src", "step", "width"
    ]),
    label: Object.freeze(["for"]),
    li: Object.freeze(["value"]),
    map: Object.freeze(["name"]),
    meter: Object.freeze(["value", "min", "max", "low", "high", "optimum"]),
    ol: Object.freeze(["reversed", "start", "type"]),
    optgroup: Object.freeze(["disabled", "label"]),
    option: Object.freeze(["disabled", "selected", "label", "value"]),
    progress: Object.freeze(["max", "value"]),
    select: Object.freeze(["name", "disabled", "multiple", "size"]),
    source: Object.freeze(["media", "sizes", "src", "srcset", "type"]),
    table: Object.freeze(["border"]),
    td: Object.freeze(["colspan", "headers", "rowspan"]),
    textarea: Object.freeze(["rows", "cols", "disabled", "name", "readonly", "wrap"]),
    time: Object.freeze(["datetime"]),
    th: Object.freeze(["abbr", "colspan", "headers", "rowspan", "scope", "sorted"]),
    track: Object.freeze(["default", "kind", "label", "src", "srclang"]),
    video: Object.freeze(["controls", "height", "width", "loop", "muted", "poster", "src", "autoplay"])
  });

  /**
   * The list of trusted iframe domains.
   * @type {string[]}
   */
  const TRUSTED_IFRAME_DOMAINS = Object.freeze(["google.com", "youtube.com"]);

  /**
   * Available themes for the world join page.
   * @enum {string}
   */
  const WORLD_JOIN_THEMES = {
    default: "WORLD.JoinThemeDefault",
    minimal: "WORLD.JoinThemeMinimal"
  };

  /**
   * Setup page package progress protocol.
   * @type {{ACTIONS: Record<string, string>, STEPS: Record<string, string>}}
   */
  const SETUP_PACKAGE_PROGRESS = {
    ACTIONS: {
      CREATE_BACKUP: "createBackup",
      RESTORE_BACKUP: "restoreBackup",
      DELETE_BACKUP: "deleteBackup",
      CREATE_SNAPSHOT: "createSnapshot",
      RESTORE_SNAPSHOT: "restoreSnapshot",
      DELETE_SNAPSHOT: "deleteSnapshot",
      INSTALL_PKG: "installPackage",
      LAUNCH_WORLD: "launchWorld",
      UPDATE_CORE: "updateCore",
      UPDATE_DOWNLOAD: "updateDownload"
    },
    STEPS: {
      ARCHIVE: "archive",
      CHECK_DISK_SPACE: "checkDiskSpace",
      CONNECT_WORLD: "connectWorld",
      MIGRATE_WORLD: "migrateWorld",
      CONNECT_PKG: "connectPackage",
      MIGRATE_PKG: "migratePackage",
      MIGRATE_CORE: "migrateCore",
      MIGRATE_SYSTEM: "migrateSystem",
      DOWNLOAD: "download",
      EXTRACT: "extract",
      INSTALL: "install",
      CLEANUP: "cleanup",
      COMPLETE: "complete",
      DELETE: "delete",
      ERROR: "error",
      VEND: "vend",
      SNAPSHOT_MODULES: "snapshotModules",
      SNAPSHOT_SYSTEMS: "snapshotSystems",
      SNAPSHOT_WORLDS: "snapshotWorlds"
    }
  };

  /**
   * The combat announcements.
   * @type {string[]}
   */
  const COMBAT_ANNOUNCEMENTS = ["startEncounter", "nextUp", "yourTurn"];

  /**
   * The fit modes of {@link foundry.data.TextureData#fit}.
   * @type {string[]}
   */
  const TEXTURE_DATA_FIT_MODES = ["fill", "contain", "cover", "width", "height"];

  /**
   * The maximum depth to recurse to when embedding enriched text.
   * @type {number}
   */
  const TEXT_ENRICH_EMBED_MAX_DEPTH = 5;

  /**
   * The Region events that are supported by core.
   * @enum {string}
   */
  const REGION_EVENTS = {

    /**
     * Triggered when the shapes or bottom/top elevation of the Region are changed.
     */
    REGION_BOUNDARY: "regionBoundary",

    /**
     * Triggered when the behavior is enabled/disabled or the Scene its Region is in is viewed/unviewed.
     */
    BEHAVIOR_STATUS: "behaviorStatus",

    /**
     * Triggered when a Token enters a Region.
     */
    TOKEN_ENTER: "tokenEnter",

    /**
     * Triggered when a Token exists a Region.
     */
    TOKEN_EXIT: "tokenExit",

    /**
     * Triggered when a Token is about to move into, out of, through, or within a Region.
     */
    TOKEN_PRE_MOVE: "tokenPreMove",

    /**
     * Triggered when a Token moves into, out of, through, or within a Region.
     */
    TOKEN_MOVE: "tokenMove",

    /**
     * Triggered when a Token moves into a Region.
     */
    TOKEN_MOVE_IN: "tokenMoveIn",

    /**
     * Triggered when a Token moves out of a Region.
     */
    TOKEN_MOVE_OUT: "tokenMoveOut",

    /**
     * Triggered when a Token starts its Combat turn in a Region.
     */
    TOKEN_TURN_START: "tokenTurnStart",

    /**
     * Triggered when a Token ends its Combat turn in a Region.
     */
    TOKEN_TURN_END: "tokenTurnEnd",

    /**
     * Triggered when a Token starts the Combat round in a Region.
     */
    TOKEN_ROUND_START: "tokenRoundStart",

    /**
     * Triggered when a Token ends the Combat round in a Region.
     */
    TOKEN_ROUND_END: "tokenRoundEnd"
  };

  /**
   * The possible visibility state of Region.
   * @enum {string}
   */
  const REGION_VISIBILITY = {

    /**
     * Only visible on the RegionLayer.
     */
    LAYER: 0,

    /**
     * Only visible to Gamemasters.
     */
    GAMEMASTER: 1,

    /**
     * Visible to anyone.
     */
    ALWAYS: 2
  };

  /* -------------------------------------------- */
  /*  Deprecations and Compatibility              */
  /* -------------------------------------------- */

  /**
   * @deprecated since v12
   * @ignore
   */
  const CHAT_MESSAGE_TYPES = new Proxy(CHAT_MESSAGE_STYLES, {
    get(target, prop, receiver) {
      const msg = "CONST.CHAT_MESSAGE_TYPES is deprecated in favor of CONST.CHAT_MESSAGE_STYLES because the " +
        "ChatMessage#type field has been renamed to ChatMessage#style";
      foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return Reflect.get(...arguments);
    }
  });

  // Deprecated chat message styles
  Object.defineProperties(CHAT_MESSAGE_STYLES, {
    /**
     * @deprecated since v12
     * @ignore
     */
    ROLL: {
      get() {
        foundry.utils.logCompatibilityWarning("CONST.CHAT_MESSAGE_STYLES.ROLL is deprecated in favor of defining " +
          "rolls directly in ChatMessage#rolls", {since: 12, until: 14, once: true});
        return 0;
      }
    },
    /**
     * @deprecated since v12
     * @ignore
     */
    WHISPER: {
      get() {
        foundry.utils.logCompatibilityWarning("CONST.CHAT_MESSAGE_STYLES.WHISPER is deprecated in favor of defining " +
          "whisper recipients directly in ChatMessage#whisper", {since: 12, until: 14, once: true});
        return 0;
      }
    }
  });

  /**
   * @deprecated since v12
   * @ignore
   */
  const _DOCUMENT_TYPES = Object.freeze(WORLD_DOCUMENT_TYPES.filter(t => {
    const excluded = ["FogExploration", "Setting"];
    return !excluded.includes(t);
  }));

  /**
   * @deprecated since v12
   * @ignore
   */
  const DOCUMENT_TYPES = new Proxy(_DOCUMENT_TYPES, {
    get(target, prop, receiver) {
      const msg = "CONST.DOCUMENT_TYPES is deprecated in favor of either CONST.WORLD_DOCUMENT_TYPES or "
        + "CONST.COMPENDIUM_DOCUMENT_TYPES.";
      foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return Reflect.get(...arguments);
    }
  });

  var CONST$1 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    ACTIVE_EFFECT_MODES: ACTIVE_EFFECT_MODES,
    ALLOWED_HTML_ATTRIBUTES: ALLOWED_HTML_ATTRIBUTES,
    ALL_DOCUMENT_TYPES: ALL_DOCUMENT_TYPES,
    ASCII: ASCII,
    AUDIO_CHANNELS: AUDIO_CHANNELS,
    AUDIO_FILE_EXTENSIONS: AUDIO_FILE_EXTENSIONS,
    BASE_DOCUMENT_TYPE: BASE_DOCUMENT_TYPE,
    CANVAS_PERFORMANCE_MODES: CANVAS_PERFORMANCE_MODES,
    CARD_DRAW_MODES: CARD_DRAW_MODES,
    CHAT_MESSAGE_STYLES: CHAT_MESSAGE_STYLES,
    CHAT_MESSAGE_TYPES: CHAT_MESSAGE_TYPES,
    COMBAT_ANNOUNCEMENTS: COMBAT_ANNOUNCEMENTS,
    COMPATIBILITY_MODES: COMPATIBILITY_MODES,
    COMPENDIUM_DOCUMENT_TYPES: COMPENDIUM_DOCUMENT_TYPES,
    CORE_SUPPORTED_LANGUAGES: CORE_SUPPORTED_LANGUAGES,
    CSS_THEMES: CSS_THEMES,
    DEFAULT_TOKEN: DEFAULT_TOKEN,
    DICE_ROLL_MODES: DICE_ROLL_MODES,
    DIRECTORY_SEARCH_MODES: DIRECTORY_SEARCH_MODES,
    DOCUMENT_LINK_TYPES: DOCUMENT_LINK_TYPES,
    DOCUMENT_META_OWNERSHIP_LEVELS: DOCUMENT_META_OWNERSHIP_LEVELS,
    DOCUMENT_OWNERSHIP_LEVELS: DOCUMENT_OWNERSHIP_LEVELS,
    DOCUMENT_TYPES: DOCUMENT_TYPES,
    DRAWING_FILL_TYPES: DRAWING_FILL_TYPES,
    EMBEDDED_DOCUMENT_TYPES: EMBEDDED_DOCUMENT_TYPES,
    FILE_CATEGORIES: FILE_CATEGORIES,
    FOLDER_DOCUMENT_TYPES: FOLDER_DOCUMENT_TYPES,
    FOLDER_MAX_DEPTH: FOLDER_MAX_DEPTH,
    FONT_FILE_EXTENSIONS: FONT_FILE_EXTENSIONS,
    FONT_WEIGHTS: FONT_WEIGHTS,
    GAME_VIEWS: GAME_VIEWS,
    GRAPHICS_FILE_EXTENSIONS: GRAPHICS_FILE_EXTENSIONS,
    GRID_DIAGONALS: GRID_DIAGONALS,
    GRID_MIN_SIZE: GRID_MIN_SIZE,
    GRID_SNAPPING_MODES: GRID_SNAPPING_MODES,
    GRID_TYPES: GRID_TYPES,
    HTML_FILE_EXTENSIONS: HTML_FILE_EXTENSIONS,
    IMAGE_FILE_EXTENSIONS: IMAGE_FILE_EXTENSIONS,
    JOURNAL_ENTRY_PAGE_FORMATS: JOURNAL_ENTRY_PAGE_FORMATS,
    KEYBINDING_PRECEDENCE: KEYBINDING_PRECEDENCE,
    LIGHTING_LEVELS: LIGHTING_LEVELS,
    MACRO_SCOPES: MACRO_SCOPES,
    MACRO_TYPES: MACRO_TYPES,
    MEASURED_TEMPLATE_TYPES: MEASURED_TEMPLATE_TYPES,
    MEDIA_MIME_TYPES: MEDIA_MIME_TYPES,
    MOVEMENT_DIRECTIONS: MOVEMENT_DIRECTIONS,
    OCCLUSION_MODES: OCCLUSION_MODES,
    PACKAGE_AVAILABILITY_CODES: PACKAGE_AVAILABILITY_CODES,
    PACKAGE_TYPES: PACKAGE_TYPES,
    PASSWORD_SAFE_STRING: PASSWORD_SAFE_STRING,
    PLAYLIST_MODES: PLAYLIST_MODES,
    PLAYLIST_SORT_MODES: PLAYLIST_SORT_MODES,
    PRIMARY_DOCUMENT_TYPES: PRIMARY_DOCUMENT_TYPES,
    REGION_EVENTS: REGION_EVENTS,
    REGION_VISIBILITY: REGION_VISIBILITY,
    SETUP_PACKAGE_PROGRESS: SETUP_PACKAGE_PROGRESS,
    SETUP_VIEWS: SETUP_VIEWS,
    SHOWDOWN_OPTIONS: SHOWDOWN_OPTIONS,
    SOFTWARE_UPDATE_CHANNELS: SOFTWARE_UPDATE_CHANNELS,
    SORT_INTEGER_DENSITY: SORT_INTEGER_DENSITY,
    SYSTEM_SPECIFIC_COMPENDIUM_TYPES: SYSTEM_SPECIFIC_COMPENDIUM_TYPES,
    TABLE_RESULT_TYPES: TABLE_RESULT_TYPES,
    TEXTURE_DATA_FIT_MODES: TEXTURE_DATA_FIT_MODES,
    TEXT_ANCHOR_POINTS: TEXT_ANCHOR_POINTS,
    TEXT_ENRICH_EMBED_MAX_DEPTH: TEXT_ENRICH_EMBED_MAX_DEPTH,
    TEXT_FILE_EXTENSIONS: TEXT_FILE_EXTENSIONS,
    TILE_OCCLUSION_MODES: TILE_OCCLUSION_MODES,
    TIMEOUTS: TIMEOUTS,
    TOKEN_DISPLAY_MODES: TOKEN_DISPLAY_MODES,
    TOKEN_DISPOSITIONS: TOKEN_DISPOSITIONS,
    TOKEN_HEXAGONAL_SHAPES: TOKEN_HEXAGONAL_SHAPES,
    TOKEN_OCCLUSION_MODES: TOKEN_OCCLUSION_MODES,
    TRUSTED_IFRAME_DOMAINS: TRUSTED_IFRAME_DOMAINS,
    UPLOADABLE_FILE_EXTENSIONS: UPLOADABLE_FILE_EXTENSIONS,
    USER_PERMISSIONS: USER_PERMISSIONS,
    USER_ROLES: USER_ROLES,
    USER_ROLE_NAMES: USER_ROLE_NAMES,
    VIDEO_FILE_EXTENSIONS: VIDEO_FILE_EXTENSIONS,
    VTT: VTT,
    WALL_DIRECTIONS: WALL_DIRECTIONS,
    WALL_DOOR_INTERACTIONS: WALL_DOOR_INTERACTIONS,
    WALL_DOOR_STATES: WALL_DOOR_STATES,
    WALL_DOOR_TYPES: WALL_DOOR_TYPES,
    WALL_MOVEMENT_TYPES: WALL_MOVEMENT_TYPES,
    WALL_RESTRICTION_TYPES: WALL_RESTRICTION_TYPES,
    WALL_SENSE_TYPES: WALL_SENSE_TYPES,
    WEBSITE_API_URL: WEBSITE_API_URL,
    WEBSITE_URL: WEBSITE_URL,
    WORLD_DOCUMENT_TYPES: WORLD_DOCUMENT_TYPES,
    WORLD_JOIN_THEMES: WORLD_JOIN_THEMES,
    vtt: vtt$1
  });

  /** @module helpers */

  /**
   * Benchmark the performance of a function, calling it a requested number of iterations.
   * @param {Function} func       The function to benchmark
   * @param {number} iterations   The number of iterations to test
   * @param {...any} args         Additional arguments passed to the benchmarked function
   */
  async function benchmark(func, iterations, ...args) {
    const start = performance.now();
    for ( let i=0; i<iterations; i++ ) {
      await func(...args, i);
    }
    const end = performance.now();
    const t = Math.round((end - start) * 100) / 100;
    const name = func.name ?? "Evaluated Function";
    console.log(`${name} | ${iterations} iterations | ${t}ms | ${t / iterations}ms per`);
  }

  /* -------------------------------------------- */

  /**
   * A debugging function to test latency or timeouts by forcibly locking the thread for an amount of time.
   * @param {number} ms        A number of milliseconds to lock
   * @returns {Promise<void>}
   */
  async function threadLock(ms, debug=false) {
    const t0 = performance.now();
    let d = 0;
    while ( d < ms ) {
      d = performance.now() - t0;
      if ( debug && (d % 1000 === 0) ) {
        console.debug(`Thread lock for ${d / 1000} of ${ms / 1000} seconds`);
      }
    }
  }

  /* -------------------------------------------- */

  /**
   * Wrap a callback in a debounced timeout.
   * Delay execution of the callback function until the function has not been called for delay milliseconds
   * @param {Function} callback       A function to execute once the debounced threshold has been passed
   * @param {number} delay            An amount of time in milliseconds to delay
   * @return {Function}               A wrapped function which can be called to debounce execution
   */
  function debounce(callback, delay) {
    let timeoutId;
    return function(...args) {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => {
        callback.apply(this, args);
      }, delay);
    }
  }

  /* -------------------------------------------- */

  /**
   * Wrap a callback in a throttled timeout.
   * Delay execution of the callback function when the last time the function was called was delay milliseconds ago
   * @param {Function} callback       A function to execute once the throttled threshold has been passed
   * @param {number} delay            A maximum amount of time in milliseconds between to execution
   * @return {Function}               A wrapped function which can be called to throttle execution
   */
  function throttle(callback, delay) {
    let pending;
    let lastTime = -delay;
    return function(...args) {
      if ( pending ) {
        pending.thisArg = this;
        pending.args = args;
        return;
      }
      pending = {thisArg: this, args};
      setTimeout(() => {
        const {thisArg, args} = pending;
        pending = null;
        callback.apply(thisArg, args);
        lastTime = performance.now();
      }, Math.max(delay - (performance.now() - lastTime), 0));
    }
  }

  /* -------------------------------------------- */

  /**
   * A utility function to reload the page with a debounce.
   * @callback debouncedReload
   */
  const debouncedReload = debounce( () => window.location.reload(), 250);

  /* -------------------------------------------- */

  /**
   * Quickly clone a simple piece of data, returning a copy which can be mutated safely.
   * This method DOES support recursive data structures containing inner objects or arrays.
   * This method DOES NOT support advanced object types like Set, Map, or other specialized classes.
   * @param {*} original                     Some sort of data
   * @param {object} [options]               Options to configure the behaviour of deepClone
   * @param {boolean} [options.strict=false]  Throw an Error if deepClone is unable to clone something instead of
   *                                          returning the original
   * @param {number} [options._d]             An internal depth tracker
   * @return {*}                             The clone of that data
   */
  function deepClone(original, {strict=false, _d=0}={}) {
    if ( _d > 100 ) {
      throw new Error("Maximum depth exceeded. Be sure your object does not contain cyclical data structures.");
    }
    _d++;

    // Simple types
    if ( (typeof original !== "object") || (original === null) ) return original;

    // Arrays
    if ( original instanceof Array ) return original.map(o => deepClone(o, {strict, _d}));

    // Dates
    if ( original instanceof Date ) return new Date(original);

    // Unsupported advanced objects
    if ( original.constructor && (original.constructor !== Object) ) {
      if ( strict ) throw new Error("deepClone cannot clone advanced objects");
      return original;
    }

    // Other objects
    const clone = {};
    for ( let k of Object.keys(original) ) {
      clone[k] = deepClone(original[k], {strict, _d});
    }
    return clone;
  }

  /* -------------------------------------------- */

  /**
   * Deeply difference an object against some other, returning the update keys and values.
   * @param {object} original       An object comparing data against which to compare
   * @param {object} other          An object containing potentially different data
   * @param {object} [options={}]   Additional options which configure the diff operation
   * @param {boolean} [options.inner=false]  Only recognize differences in other for keys which also exist in original
   * @param {boolean} [options.deletionKeys=false] Apply special logic to deletion keys. They will only be kept if the
   *                                               original object has a corresponding key that could be deleted.
   * @param {number} [options._d]           An internal depth tracker
   * @return {object}               An object of the data in other which differs from that in original
   */
  function diffObject(original, other, {inner=false, deletionKeys=false, _d=0}={}) {
    if ( _d > 100 ) {
      throw new Error("Maximum depth exceeded. Be careful that your object does not contain a cyclical data structure.")
    }
    _d++;

    function _difference(v0, v1) {

      // Eliminate differences in types
      let t0 = getType(v0);
      let t1 = getType(v1);
      if ( t0 !== t1 ) return [true, v1];

      // null and undefined
      if ( ["null", "undefined"].includes(t0) ) return [v0 !== v1, v1];

      // If the prototype explicitly exposes an equality-testing method, use it
      if ( v0?.equals instanceof Function ) return [!v0.equals(v1), v1];

      // Recursively diff objects
      if ( t0 === "Object" ) {
        if ( isEmpty$1(v1) ) return [false, {}];
        if ( isEmpty$1(v0) ) return [true, v1];
        let d = diffObject(v0, v1, {inner, deletionKeys, _d});
        return [!isEmpty$1(d), d];
      }

      // Differences in primitives
      return [v0.valueOf() !== v1.valueOf(), v1];
    }

    // Recursively call the _difference function
    return Object.keys(other).reduce((obj, key) => {
      const isDeletionKey = key.startsWith("-=");
      if ( isDeletionKey && deletionKeys ) {
        const otherKey = key.substring(2);
        if ( otherKey in original ) obj[key] = other[key];
        return obj;
      }
      if ( inner && !(key in original) ) return obj;
      let [isDifferent, difference] = _difference(original[key], other[key]);
      if ( isDifferent ) obj[key] = difference;
      return obj;
    }, {});
  }

  /* -------------------------------------------- */

  /**
   * Test if two objects contain the same enumerable keys and values.
   * @param {object} a  The first object.
   * @param {object} b  The second object.
   * @returns {boolean}
   */
  function objectsEqual(a, b) {
    if ( (a == null) || (b == null) ) return a === b;
    if ( (getType(a) !== "Object") || (getType(b) !== "Object") ) return a === b;
    if ( Object.keys(a).length !== Object.keys(b).length ) return false;
    return Object.entries(a).every(([k, v0]) => {
      const v1 = b[k];
      const t0 = getType(v0);
      const t1 = getType(v1);
      if ( t0 !== t1 ) return false;
      if ( v0?.equals instanceof Function ) return v0.equals(v1);
      if ( t0 === "Object" ) return objectsEqual(v0, v1);
      return v0 === v1;
    });
  }

  /* -------------------------------------------- */

  /**
   * A cheap data duplication trick which is relatively robust.
   * For a subset of cases the deepClone function will offer better performance.
   * @param {Object} original   Some sort of data
   */
  function duplicate(original) {
    return JSON.parse(JSON.stringify(original));
  }

  /* -------------------------------------------- */

  /**
   * Test whether some class is a subclass of a parent.
   * Returns true if the classes are identical.
   * @param {Function} cls        The class to test
   * @param {Function} parent     Some other class which may be a parent
   * @returns {boolean}           Is the class a subclass of the parent?
   */
  function isSubclass(cls, parent) {
    if ( typeof cls !== "function" ) return false;
    if ( cls === parent ) return true;
    return parent.isPrototypeOf(cls);
  }

  /* -------------------------------------------- */

  /**
   * Search up the prototype chain and return the class that defines the given property.
   * @param {Object|Constructor} obj    A class instance or class definition which contains a property.
   *                                    If a class instance is passed the property is treated as an instance attribute.
   *                                    If a class constructor is passed the property is treated as a static attribute.
   * @param {string} property           The property name
   * @returns {Constructor}             The class that defines the property
   */
  function getDefiningClass(obj, property) {
    const isStatic = obj.hasOwnProperty("prototype");
    let target = isStatic ? obj : Object.getPrototypeOf(obj);
    while ( target ) {
      if ( target.hasOwnProperty(property) ) return isStatic ? target : target.constructor;
      target = Object.getPrototypeOf(target);
    }
  }

  /* -------------------------------------------- */

  /**
   * Encode a url-like string by replacing any characters which need encoding
   * To reverse this encoding, the native decodeURIComponent can be used on the whole encoded string, without adjustment.
   * @param {string} path     A fully-qualified URL or url component (like a relative path)
   * @return {string}         An encoded URL string
   */
  function encodeURL(path) {

    // Determine whether the path is a well-formed URL
    const url = URL.parseSafe(path);

    // If URL, remove the initial protocol
    if ( url ) path = path.replace(url.protocol, "");

    // Split and encode each URL part
    path = path.split("/").map(p => encodeURIComponent(p).replace(/'/g, "%27")).join("/");

    // Return the encoded URL
    return url ? url.protocol + path : path;
  }

  /* -------------------------------------------- */

  /**
   * Expand a flattened object to be a standard nested Object by converting all dot-notation keys to inner objects.
   * Only simple objects will be expanded. Other Object types like class instances will be retained as-is.
   * @param {object} obj      The object to expand
   * @return {object}         An expanded object
   */
  function expandObject(obj) {
    function _expand(value, depth) {
      if ( depth > 32 ) throw new Error("Maximum object expansion depth exceeded");
      if ( !value ) return value;
      if ( Array.isArray(value) ) return value.map(v => _expand(v, depth+1)); // Map arrays
      if ( value.constructor?.name !== "Object" ) return value;               // Return advanced objects directly
      const expanded = {};                                                    // Expand simple objects
      for ( let [k, v] of Object.entries(value) ) {
        setProperty(expanded, k, _expand(v, depth+1));
      }
      return expanded;
    }
    return _expand(obj, 0);
  }

  /* -------------------------------------------- */

  /**
   * Filter the contents of some source object using the structure of a template object.
   * Only keys which exist in the template are preserved in the source object.
   *
   * @param {object} source           An object which contains the data you wish to filter
   * @param {object} template         An object which contains the structure you wish to preserve
   * @param {object} [options={}]     Additional options which customize the filtration
   * @param {boolean} [options.deletionKeys=false]    Whether to keep deletion keys
   * @param {boolean} [options.templateValues=false]  Instead of keeping values from the source, instead draw values from the template
   *
   * @example Filter an object
   * ```js
   * const source = {foo: {number: 1, name: "Tim", topping: "olives"}, bar: "baz"};
   * const template = {foo: {number: 0, name: "Mit", style: "bold"}, other: 72};
   * filterObject(source, template); // {foo: {number: 1, name: "Tim"}};
   * filterObject(source, template, {templateValues: true}); // {foo: {number: 0, name: "Mit"}};
   * ```
   */
  function filterObject(source, template, {deletionKeys=false, templateValues=false}={}) {

    // Validate input
    const ts = getType(source);
    const tt = getType(template);
    if ( (ts !== "Object") || (tt !== "Object")) throw new Error("One of source or template are not Objects!");

    // Define recursive filtering function
    const _filter = function(s, t, filtered) {
      for ( let [k, v] of Object.entries(s) ) {
        let has = t.hasOwnProperty(k);
        let x = t[k];

        // Case 1 - inner object
        if ( has && (getType(v) === "Object") && (getType(x) === "Object") ) {
          filtered[k] = _filter(v, x, {});
        }

        // Case 2 - inner key
        else if ( has ) {
          filtered[k] = templateValues ? x : v;
        }

        // Case 3 - special key
        else if ( deletionKeys && k.startsWith("-=") ) {
          filtered[k] = v;
        }
      }
      return filtered;
    };

    // Begin filtering at the outer-most layer
    return _filter(source, template, {});
  }

  /* -------------------------------------------- */

  /**
   * Flatten a possibly multi-dimensional object to a one-dimensional one by converting all nested keys to dot notation
   * @param {object} obj        The object to flatten
   * @param {number} [_d=0]     Track the recursion depth to prevent overflow
   * @return {object}           A flattened object
   */
  function flattenObject(obj, _d=0) {
    const flat = {};
    if ( _d > 100 ) {
      throw new Error("Maximum depth exceeded");
    }
    for ( let [k, v] of Object.entries(obj) ) {
      let t = getType(v);
      if ( t === "Object" ) {
        if ( isEmpty$1(v) ) flat[k] = v;
        let inner = flattenObject(v, _d+1);
        for ( let [ik, iv] of Object.entries(inner) ) {
          flat[`${k}.${ik}`] = iv;
        }
      }
      else flat[k] = v;
    }
    return flat;
  }

  /* -------------------------------------------- */

  /**
   * Obtain references to the parent classes of a certain class.
   * @param {Function} cls            An class definition
   * @return {Array<typeof Object>}   An array of parent classes which the provided class extends
   */
  function getParentClasses(cls) {
    if ( typeof cls !== "function" ) {
      throw new Error("The provided class is not a type of Function");
    }
    const parents = [];
    let parent = Object.getPrototypeOf(cls);
    while ( parent ) {
      parents.push(parent);
      parent = Object.getPrototypeOf(parent);
    }
    return parents.slice(0, -2)
  }

  /* -------------------------------------------- */

  /**
   * Get the URL route for a certain path which includes a path prefix, if one is set
   * @param {string} path             The Foundry URL path
   * @param {string|null} [prefix]    A path prefix to apply
   * @returns {string}                The absolute URL path
   */
  function getRoute(path, {prefix}={}) {
    prefix = prefix === undefined ? globalThis.ROUTE_PREFIX : prefix || null;
    path = path.replace(/(^[\/]+)|([\/]+$)/g, ""); // Strip leading and trailing slashes
    let paths = [""];
    if ( prefix ) paths.push(prefix);
    paths = paths.concat([path.replace(/(^\/)|(\/$)/g, "")]);
    return paths.join("/");
  }

  /* -------------------------------------------- */

  /**
   * Learn the underlying data type of some variable. Supported identifiable types include:
   * undefined, null, number, string, boolean, function, Array, Set, Map, Promise, Error,
   * HTMLElement (client side only), Object (catchall for other object types)
   * @param {*} variable  A provided variable
   * @return {string}     The named type of the token
   */
  function getType(variable) {

    // Primitive types, handled with simple typeof check
    const typeOf = typeof variable;
    if ( typeOf !== "object" ) return typeOf;

    // Special cases of object
    if ( variable === null ) return "null";
    if ( !variable.constructor ) return "Object"; // Object with the null prototype.
    if ( variable.constructor.name === "Object" ) return "Object";  // simple objects

    // Match prototype instances
    const prototypes = [
      [Array, "Array"],
      [Set, "Set"],
      [Map, "Map"],
      [Promise, "Promise"],
      [Error, "Error"],
      [Color$1, "number"]
    ];
    if ( "HTMLElement" in globalThis ) prototypes.push([globalThis.HTMLElement, "HTMLElement"]);
    for ( const [cls, type] of prototypes ) {
      if ( variable instanceof cls ) return type;
    }

    // Unknown Object type
    return "Object";
  }

  /* -------------------------------------------- */

  /**
   * A helper function which tests whether an object has a property or nested property given a string key.
   * The method also supports arrays if the provided key is an integer index of the array.
   * The string key supports the notation a.b.c which would return true if object[a][b][c] exists
   * @param {object} object   The object to traverse
   * @param {string} key      An object property with notation a.b.c
   * @returns {boolean}       An indicator for whether the property exists
   */
  function hasProperty(object, key) {
    if ( !key || !object ) return false;
    if ( key in object ) return true;
    let target = object;
    for ( let p of key.split('.') ) {
      if ( !target || (typeof target !== "object") ) return false;
      if ( p in target ) target = target[p];
      else return false;
    }
    return true;
  }

  /* -------------------------------------------- */

  /**
   * A helper function which searches through an object to retrieve a value by a string key.
   * The method also supports arrays if the provided key is an integer index of the array.
   * The string key supports the notation a.b.c which would return object[a][b][c]
   * @param {object} object   The object to traverse
   * @param {string} key      An object property with notation a.b.c
   * @return {*}              The value of the found property
   */
  function getProperty(object, key) {
    if ( !key || !object ) return undefined;
    if ( key in object ) return object[key];
    let target = object;
    for ( let p of key.split('.') ) {
      if ( !target || (typeof target !== "object") ) return undefined;
      if ( p in target ) target = target[p];
      else return undefined;
    }
    return target;
  }

  /* -------------------------------------------- */

  /**
   * A helper function which searches through an object to assign a value using a string key
   * This string key supports the notation a.b.c which would target object[a][b][c]
   * @param {object} object   The object to update
   * @param {string} key      The string key
   * @param {*} value         The value to be assigned
   * @return {boolean}        Whether the value was changed from its previous value
   */
  function setProperty(object, key, value) {
    if ( !key ) return false;

    // Convert the key to an object reference if it contains dot notation
    let target = object;
    if ( key.indexOf('.') !== -1 ) {
      let parts = key.split('.');
      key = parts.pop();
      target = parts.reduce((o, i) => {
        if ( !o.hasOwnProperty(i) ) o[i] = {};
        return o[i];
      }, object);
    }

    // Update the target
    if ( !(key in target) || (target[key] !== value) ) {
      target[key] = value;
      return true;
    }
    return false;
  }

  /* -------------------------------------------- */

  /**
   * Invert an object by assigning its values as keys and its keys as values.
   * @param {object} obj    The original object to invert
   * @returns {object}      The inverted object with keys and values swapped
   */
  function invertObject(obj) {
    const inverted = {};
    for ( let [k, v] of Object.entries(obj) ) {
      if ( v in inverted ) throw new Error("The values of the provided object must be unique in order to invert it.");
      inverted[v] = k;
    }
    return inverted;
  }

  /* -------------------------------------------- */

  /**
   * Return whether a target version (v1) is more advanced than some other reference version (v0).
   * Supports either numeric or string version comparison with version parts separated by periods.
   * @param {number|string} v1    The target version
   * @param {number|string} v0    The reference version
   * @return {boolean}            Is v1 a more advanced version than v0?
   */
  function isNewerVersion(v1, v0) {

    // Handle numeric versions
    if ( (typeof v1 === "number") && (typeof v0 === "number") ) return v1 > v0;

    // Handle string parts
    let v1Parts = String(v1).split(".");
    let v0Parts = String(v0).split(".");

    // Iterate over version parts
    for ( let [i, p1] of v1Parts.entries() ) {
      let p0 = v0Parts[i];

      // If the prior version doesn't have a part, v1 wins
      if ( p0 === undefined ) return true;

      // If both parts are numbers, use numeric comparison to avoid cases like "12" < "5"
      if ( Number.isNumeric(p0) && Number.isNumeric(p1) ) {
        if ( Number(p1) !== Number(p0) ) return Number(p1) > Number(p0);
      }

      // Otherwise, compare as strings
      if ( p1 !== p0 ) return p1 > p0;
    }

    // If there are additional parts to v0, it is not newer
    if ( v0Parts.length > v1Parts.length ) return false;

    // If we have not returned false by now, it's either newer or the same
    return !v1Parts.equals(v0Parts);
  }

  /* -------------------------------------------- */

  /**
   * Test whether a value is empty-like; either undefined or a content-less object.
   * @param {*} value       The value to test
   * @returns {boolean}     Is the value empty-like?
   */
  function isEmpty$1(value) {
    const t = getType(value);
    switch ( t ) {
      case "undefined":
        return true;
      case "null":
        return true;
      case "Array":
        return !value.length;
      case "Object":
        return !Object.keys(value).length;
      case "Set":
      case "Map":
        return !value.size;
      default:
        return false;
    }
  }

  /* -------------------------------------------- */

  /**
   * Update a source object by replacing its keys and values with those from a target object.
   *
   * @param {object} original                           The initial object which should be updated with values from the
   *                                                    target
   * @param {object} [other={}]                         A new object whose values should replace those in the source
   * @param {object} [options={}]                       Additional options which configure the merge
   * @param {boolean} [options.insertKeys=true]         Control whether to insert new top-level objects into the resulting
   *                                                    structure which do not previously exist in the original object.
   * @param {boolean} [options.insertValues=true]       Control whether to insert new nested values into child objects in
   *                                                    the resulting structure which did not previously exist in the
   *                                                    original object.
   * @param {boolean} [options.overwrite=true]          Control whether to replace existing values in the source, or only
   *                                                    merge values which do not already exist in the original object.
   * @param {boolean} [options.recursive=true]          Control whether to merge inner-objects recursively (if true), or
   *                                                    whether to simply replace inner objects with a provided new value.
   * @param {boolean} [options.inplace=true]            Control whether to apply updates to the original object in-place
   *                                                    (if true), otherwise the original object is duplicated and the
   *                                                    copy is merged.
   * @param {boolean} [options.enforceTypes=false]      Control whether strict type checking requires that the value of a
   *                                                    key in the other object must match the data type in the original
   *                                                    data to be merged.
   * @param {boolean} [options.performDeletions=false]  Control whether to perform deletions on the original object if
   *                                                    deletion keys are present in the other object.
   * @param {number} [_d=0]                             A privately used parameter to track recursion depth.
   * @returns {object}                                  The original source object including updated, inserted, or
   *                                                    overwritten records.
   *
   * @example Control how new keys and values are added
   * ```js
   * mergeObject({k1: "v1"}, {k2: "v2"}, {insertKeys: false}); // {k1: "v1"}
   * mergeObject({k1: "v1"}, {k2: "v2"}, {insertKeys: true});  // {k1: "v1", k2: "v2"}
   * mergeObject({k1: {i1: "v1"}}, {k1: {i2: "v2"}}, {insertValues: false}); // {k1: {i1: "v1"}}
   * mergeObject({k1: {i1: "v1"}}, {k1: {i2: "v2"}}, {insertValues: true}); // {k1: {i1: "v1", i2: "v2"}}
   * ```
   *
   * @example Control how existing data is overwritten
   * ```js
   * mergeObject({k1: "v1"}, {k1: "v2"}, {overwrite: true}); // {k1: "v2"}
   * mergeObject({k1: "v1"}, {k1: "v2"}, {overwrite: false}); // {k1: "v1"}
   * ```
   *
   * @example Control whether merges are performed recursively
   * ```js
   * mergeObject({k1: {i1: "v1"}}, {k1: {i2: "v2"}}, {recursive: false}); // {k1: {i2: "v2"}}
   * mergeObject({k1: {i1: "v1"}}, {k1: {i2: "v2"}}, {recursive: true}); // {k1: {i1: "v1", i2: "v2"}}
   * ```
   *
   * @example Deleting an existing object key
   * ```js
   * mergeObject({k1: "v1", k2: "v2"}, {"-=k1": null}, {performDeletions: true});   // {k2: "v2"}
   * ```
   */
  function mergeObject(original, other={}, {
      insertKeys=true, insertValues=true, overwrite=true, recursive=true, inplace=true, enforceTypes=false,
      performDeletions=false
    }={}, _d=0) {
    other = other || {};
    if (!(original instanceof Object) || !(other instanceof Object)) {
      throw new Error("One of original or other are not Objects!");
    }
    const options = {insertKeys, insertValues, overwrite, recursive, inplace, enforceTypes, performDeletions};

    // Special handling at depth 0
    if ( _d === 0 ) {
      if ( Object.keys(other).some(k => /\./.test(k)) ) other = expandObject(other);
      if ( Object.keys(original).some(k => /\./.test(k)) ) {
        const expanded = expandObject(original);
        if ( inplace ) {
          Object.keys(original).forEach(k => delete original[k]);
          Object.assign(original, expanded);
        }
        else original = expanded;
      }
      else if ( !inplace ) original = deepClone(original);
    }

    // Iterate over the other object
    for ( let k of Object.keys(other) ) {
      const v = other[k];
      if ( original.hasOwnProperty(k) ) _mergeUpdate(original, k, v, options, _d+1);
      else _mergeInsert(original, k, v, options, _d+1);
    }
    return original;
  }

  /**
   * A helper function for merging objects when the target key does not exist in the original
   * @private
   */
  function _mergeInsert(original, k, v, {insertKeys, insertValues, performDeletions}={}, _d) {
    // Delete a key
    if ( k.startsWith("-=") && performDeletions ) {
      delete original[k.slice(2)];
      return;
    }

    const canInsert = ((_d <= 1) && insertKeys) || ((_d > 1) && insertValues);
    if ( !canInsert ) return;

    // Recursively create simple objects
    if ( v?.constructor === Object ) {
      original[k] = mergeObject({}, v, {insertKeys: true, inplace: true, performDeletions});
      return;
    }

    // Insert a key
    original[k] = v;
  }

  /**
   * A helper function for merging objects when the target key exists in the original
   * @private
   */
  function _mergeUpdate(original, k, v, {
      insertKeys, insertValues, enforceTypes, overwrite, recursive, performDeletions
    }={}, _d) {
    const x = original[k];
    const tv = getType(v);
    const tx = getType(x);

    // Recursively merge an inner object
    if ( (tv === "Object") && (tx === "Object") && recursive) {
      return mergeObject(x, v, {
        insertKeys, insertValues, overwrite, enforceTypes, performDeletions,
        inplace: true
      }, _d);
    }

    // Overwrite an existing value
    if ( overwrite ) {
      if ( (tx !== "undefined") && (tv !== tx) && enforceTypes ) {
        throw new Error(`Mismatched data types encountered during object merge.`);
      }
      original[k] = v;
    }
  }

  /* -------------------------------------------- */

  /**
   * Parse an S3 key to learn the bucket and the key prefix used for the request.
   * @param {string} key  A fully qualified key name or prefix path.
   * @returns {{bucket: string|null, keyPrefix: string}}
   */
  function parseS3URL(key) {
    const url = URL.parseSafe(key);
    if ( url ) return {
      bucket: url.host.split(".").shift(),
      keyPrefix: url.pathname.slice(1)
    };
    return {
      bucket: null,
      keyPrefix: ""
    };
  }

  /* -------------------------------------------- */

  /**
   * Generate a random alphanumeric string ID of a given requested length using `crypto.getRandomValues()`.
   * @param {number} length    The length of the random string to generate, which must be at most 16384.
   * @return {string}          A string containing random letters (A-Z, a-z) and numbers (0-9).
   */
  function randomID(length=16) {
    const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    const cutoff = 0x100000000 - (0x100000000 % chars.length);
    const random = new Uint32Array(length);
    do {
      crypto.getRandomValues(random);
    } while ( random.some(x => x >= cutoff) );
    let id = "";
    for ( let i = 0; i < length; i++ ) id += chars[random[i] % chars.length];
    return id;
  }

  /* -------------------------------------------- */

  /**
   * Express a timestamp as a relative string
   * @param {Date|string} timeStamp   A timestamp string or Date object to be formatted as a relative time
   * @return {string}                 A string expression for the relative time
   */
  function timeSince(timeStamp) {
    timeStamp = new Date(timeStamp);
    const now = new Date();
    const secondsPast = (now - timeStamp) / 1000;
    let since = "";

    // Format the time
    if (secondsPast < 60) {
      since = secondsPast;
      if ( since < 1 ) return game.i18n.localize("TIME.Now");
      else since = Math.round(since) + game.i18n.localize("TIME.SecondsAbbreviation");
    }
    else if (secondsPast < 3600) since = Math.round(secondsPast / 60) + game.i18n.localize("TIME.MinutesAbbreviation");
    else if (secondsPast <= 86400) since = Math.round(secondsPast / 3600) + game.i18n.localize("TIME.HoursAbbreviation");
    else {
      const hours = Math.round(secondsPast / 3600);
      const days = Math.floor(hours / 24);
      since = `${days}${game.i18n.localize("TIME.DaysAbbreviation")} ${hours % 24}${game.i18n.localize("TIME.HoursAbbreviation")}`;
    }

    // Return the string
    return game.i18n.format("TIME.Since", {since: since});
  }

  /* -------------------------------------------- */

  /**
   * Format a file size to an appropriate order of magnitude.
   * @param {number} size  The size in bytes.
   * @param {object} [options]
   * @param {number} [options.decimalPlaces=2]  The number of decimal places to round to.
   * @param {2|10} [options.base=10]            The base to use. In base 10 a kilobyte is 1000 bytes. In base 2 it is
   *                                            1024 bytes.
   * @returns {string}
   */
  function formatFileSize(size, { decimalPlaces=2, base=10 }={}) {
    const units = ["B", "kB", "MB", "GB", "TB"];
    const divisor = base === 2 ? 1024 : 1000;
    let iterations = 0;
    while ( (iterations < units.length) && (size > divisor) ) {
      size /= divisor;
      iterations++;
    }
    return `${size.toFixed(decimalPlaces)} ${units[iterations]}`;
  }

  /* -------------------------------------------- */

  /**
   * @typedef {object} ResolvedUUID
   * @property {string} uuid                      The original UUID.
   * @property {string} [type]                    The type of Document referenced. Legacy compendium UUIDs will not
   *                                              populate this field if the compendium is not active in the World.
   * @property {string} id                        The ID of the Document referenced.
   * @property {string} [primaryType]             The primary Document type of this UUID. Only present if the Document
   *                                              is embedded.
   * @property {string} [primaryId]               The primary Document ID of this UUID. Only present if the Document
   *                                              is embedded.
   * @property {DocumentCollection} [collection]  The collection that the primary Document belongs to.
   * @property {string[]} embedded                Additional Embedded Document parts.
   * @property {Document} [doc]                   An already-resolved parent Document.
   * @property {string} [documentType]            Either the document type or the parent type. Retained for backwards
   *                                              compatibility.
   * @property {string} [documentId]              Either the document id or the parent id. Retained for backwards
   *                                              compatibility.
   */

  /**
   * Parse a UUID into its constituent parts, identifying the type and ID of the referenced document.
   * The ResolvedUUID result also identifies a "primary" document which is a root-level document either in the game
   * World or in a Compendium pack which is a parent of the referenced document.
   * @param {string} uuid                  The UUID to parse.
   * @param {object} [options]             Options to configure parsing behavior.
   * @param {foundry.abstract.Document} [options.relative]  A document to resolve relative UUIDs against.
   * @returns {ResolvedUUID}               Returns the Collection, Document Type, and Document ID to resolve the parent
   *                                       document, as well as the remaining Embedded Document parts, if any.
   * @throws {Error}                       An error if the provided uuid string is incorrectly structured
   */
  function parseUuid(uuid, {relative}={}) {
    if ( !uuid ) throw new Error("A uuid string is required");
    const packs = game.packs;

    // Relative UUID
    if ( uuid.startsWith(".") && relative ) return _resolveRelativeUuid(uuid, relative);

    // Split UUID parts
    const parts = uuid.split(".");

    // Check for redirects.
    if ( game.compendiumUUIDRedirects ) {
      const node = game.compendiumUUIDRedirects.nodeAtPrefix(parts, { hasLeaves: true });
      const [redirect] = node?.[foundry.utils.StringTree.leaves];
      if ( redirect?.length ) parts.splice(0, redirect.length, ...redirect);
    }

    let id;
    let type;
    let primaryId;
    let primaryType;
    let collection;

    // Compendium Documents.
    if ( parts[0] === "Compendium" ) {
      const [, scope, packName] = parts.splice(0, 3);
      collection = packs.get(`${scope}.${packName}`);

      // Re-interpret legacy compendium UUIDs which did not explicitly include their parent document type
      if ( !(COMPENDIUM_DOCUMENT_TYPES.includes(parts[0]) || (parts[0] === "Folder")) ) {
        const type = collection?.documentName;
        parts.unshift(type);
        if ( type ) uuid = ["Compendium", scope, packName, ...parts].filterJoin(".");
      }
      [primaryType, primaryId] = parts.splice(0, 2);
    }

    // World Documents
    else {
      [primaryType, primaryId] = parts.splice(0, 2);
      collection = globalThis.db?.[primaryType] ?? CONFIG[primaryType]?.collection?.instance;
    }

    // Embedded Documents
    if ( parts.length ) {
      if ( parts.length % 2 ) throw new Error("Invalid number of embedded UUID parts");
      id = parts.at(-1);
      type = parts.at(-2);
    }

    // Primary Documents
    else {
      id = primaryId;
      type = primaryType;
      primaryId = primaryType = undefined;
    }

    // Return resolved UUID
    return {uuid, type, id, collection, embedded: parts, primaryType, primaryId,
      documentType: primaryType ?? type, documentId: primaryId ?? id};
  }

  /* -------------------------------------------- */

  /**
   * Resolve a UUID relative to another document.
   * The general-purpose algorithm for resolving relative UUIDs is as follows:
   * 1. If the number of parts is odd, remove the first part and resolve it against the current document and update the
   *    current document.
   * 2. If the number of parts is even, resolve embedded documents against the current document.
   * @param {string} uuid        The UUID to resolve.
   * @param {foundry.abstract.Document} relative  The document to resolve against.
   * @returns {ResolvedUUID}     A resolved UUID object
   * @private
   */
  function _resolveRelativeUuid(uuid, relative) {
    if ( !(relative instanceof foundry.abstract.Document) ) {
      throw new Error("A relative Document instance must be provided to _resolveRelativeUuid");
    }
    uuid = uuid.substring(1);
    const parts = uuid.split(".");
    if ( !parts.length ) throw new Error("Invalid relative UUID");
    let id;
    let type;
    let root;
    let primaryType;
    let primaryId;
    let collection;

    // Identify the root document and its collection
    const getRoot = (doc) => {
      if ( doc.parent ) parts.unshift(doc.documentName, doc.id);
      return doc.parent ? getRoot(doc.parent) : doc;
    };

    // Even-numbered parts include an explicit child document type
    if ( (parts.length % 2) === 0 ) {
      root = getRoot(relative);
      id = parts.at(-1);
      type = parts.at(-2);
      primaryType = root.documentName;
      primaryId = root.id;
      uuid = [primaryType, primaryId, ...parts].join(".");
    }

    // Relative Embedded Document
    else if ( relative.parent ) {
      root = getRoot(relative.parent);
      id = parts.at(-1);
      type = relative.documentName;
      parts.unshift(type);
      primaryType = root.documentName;
      primaryId = root.id;
      uuid = [primaryType, primaryId, ...parts].join(".");
    }

    // Relative Document
    else {
      root = relative;
      id = parts.pop();
      type = relative.documentName;
      uuid = [type, id].join(".");
    }

    // Recreate fully-qualified UUID and return the resolved result
    collection = root.pack ? root.compendium : root.collection;
    if ( root.pack ) uuid = `Compendium.${root.pack}.${uuid}`;
    return {uuid, type, id, collection, primaryType, primaryId, embedded: parts,
      documentType: primaryType ?? type, documentId: primaryId ?? id};
  }

  /**
   * Flatten nested arrays by concatenating their contents
   * @returns {any[]}    An array containing the concatenated inner values
   */
  function deepFlatten() {
    return this.reduce((acc, val) => Array.isArray(val) ? acc.concat(val.deepFlatten()) : acc.concat(val), []);
  }

  /**
   * Test element-wise equality of the values of this array against the values of another array
   * @param {any[]} other   Some other array against which to test equality
   * @returns {boolean}     Are the two arrays element-wise equal?
   */
  function equals$1(other) {
    if ( !(other instanceof Array) || (other.length !== this.length) ) return false;
    return this.every((v0, i) => {
      const v1 = other[i];
      const t0 = getType(v0);
      const t1 = getType(v1);
      if ( t0 !== t1 ) return false;
      if ( v0?.equals instanceof Function ) return v0.equals(v1);
      if ( t0 === "Object" ) return objectsEqual(v0, v1);
      return v0 === v1;
    });
  }

  /**
   * Partition an original array into two children array based on a logical test
   * Elements which test as false go into the first result while elements testing as true appear in the second
   * @param rule {Function}
   * @returns {Array}    An Array of length two whose elements are the partitioned pieces of the original
   */
  function partition(rule) {
    return this.reduce((acc, val) => {
      let test = rule(val);
      acc[Number(test)].push(val);
      return acc;
    }, [[], []]);
  }

  /**
   * Join an Array using a string separator, first filtering out any parts which return a false-y value
   * @param {string} sep    The separator string
   * @returns {string}      The joined string, filtered of any false values
   */
  function filterJoin(sep) {
    return this.filter(p => !!p).join(sep);
  }

  /**
   * Find an element within the Array and remove it from the array
   * @param {Function} find   A function to use as input to findIndex
   * @param {*} [replace]     A replacement for the spliced element
   * @returns {*|null}        The replacement element, the removed element, or null if no element was found.
   */
  function findSplice(find, replace) {
    const idx = this.findIndex(find);
    if ( idx === -1 ) return null;
    if ( replace !== undefined ) {
      this.splice(idx, 1, replace);
      return replace;
    } else {
      const item = this[idx];
      this.splice(idx, 1);
      return item;
    }
  }

  /**
   * Create and initialize an array of length n with integers from 0 to n-1
   * @memberof Array
   * @param {number} n        The desired array length
   * @param {number} [min=0]  A desired minimum number from which the created array starts
   * @returns {number[]}      An array of integers from min to min+n
   */
  function fromRange(n, min=0) {
    return Array.from({length: n}, (v, i) => i + min);
  }

  // Define primitives on the Array prototype
  Object.defineProperties(Array.prototype, {
    deepFlatten: {value: deepFlatten},
    equals: {value: equals$1},
    filterJoin: {value: filterJoin},
    findSplice: {value: findSplice},
    partition: {value: partition}
  });
  Object.defineProperties(Array,{
    fromRange: {value: fromRange}
  });

  /**
   * Test whether a Date instance is valid.
   * A valid date returns a number for its timestamp, and NaN otherwise.
   * NaN is never equal to itself.
   * @returns {boolean}
   */
  function isValid() {
    return this.getTime() === this.getTime();
  }

  /**
   * Return a standard YYYY-MM-DD string for the Date instance.
   * @returns {string}    The date in YYYY-MM-DD format
   */
  function toDateInputString() {
    const yyyy = this.getFullYear();
    const mm = (this.getMonth() + 1).paddedString(2);
    const dd = this.getDate().paddedString(2);
    return `${yyyy}-${mm}-${dd}`;
  }

  /**
   * Return a standard H:M:S.Z string for the Date instance.
   * @returns {string}    The time in H:M:S format
   */
  function toTimeInputString() {
    return this.toTimeString().split(" ")[0];
  }

  // Define primitives on the Date prototype
  Object.defineProperties(Date.prototype, {
    isValid: {value: isValid},
    toDateInputString: {value: toDateInputString},
    toTimeInputString: {value: toTimeInputString}
  });

  /**
   * √3
   * @type {number}
   */
  const SQRT3 = 1.7320508075688772;

  /**
   * √⅓
   * @type {number}
   */
  const SQRT1_3 = 0.5773502691896257;

  /**
   * Bound a number between some minimum and maximum value, inclusively.
   * @param {number} num    The current value
   * @param {number} min    The minimum allowed value
   * @param {number} max    The maximum allowed value
   * @return {number}       The clamped number
   * @memberof Math
   */
  function clamp(num, min, max) {
    return Math.min(Math.max(num, min), max);
  }

  /**
   * @deprecated since v12
   * @ignore
   */
  function clamped(num, min, max) {
    const msg = "Math.clamped is deprecated in favor of Math.clamp.";
    foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
    return clamp(num, min, max);
  }

  /**
   * Linear interpolation function
   * @param {number} a   An initial value when weight is 0.
   * @param {number} b   A terminal value when weight is 1.
   * @param {number} w   A weight between 0 and 1.
   * @return {number}    The interpolated value between a and b with weight w.
   */
  function mix(a, b, w) {
    return a * (1 - w) + b * w;
  }

  /**
   * Transform an angle in degrees to be bounded within the domain [0, 360)
   * @param {number} degrees  An angle in degrees
   * @returns {number}        The same angle on the range [0, 360)
   */
  function normalizeDegrees(degrees, base) {
    const d = degrees % 360;
    if ( base !== undefined ) {
      const msg = "Math.normalizeDegrees(degrees, base) is deprecated.";
      foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      if ( base === 360 ) return d <= 0 ? d + 360 : d;
    }
    return d < 0 ? d + 360 : d;
  }

  /**
   * Transform an angle in radians to be bounded within the domain [-PI, PI]
   * @param {number} radians  An angle in degrees
   * @return {number}         The same angle on the range [-PI, PI]
   */
  function normalizeRadians(radians) {
    const pi = Math.PI;
    const pi2 = pi * 2;
    return radians - (pi2 * Math.floor((radians + pi) / pi2));
  }

  /**
   * @deprecated since v12
   * @ignore
   */
  function roundDecimals(number, places) {
    const msg = "Math.roundDecimals is deprecated.";
    foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
    places = Math.max(Math.trunc(places), 0);
    let scl = Math.pow(10, places);
    return Math.round(number * scl) / scl;
  }

  /**
   * Transform an angle in radians to a number in degrees
   * @param {number} angle    An angle in radians
   * @return {number}         An angle in degrees
   */
  function toDegrees(angle) {
    return angle * (180 / Math.PI);
  }

  /**
   * Transform an angle in degrees to an angle in radians
   * @param {number} angle    An angle in degrees
   * @return {number}         An angle in radians
   */
  function toRadians(angle) {
    return angle * (Math.PI / 180);
  }

  /**
   * Returns the value of the oscillation between `a` and `b` at time `t`.
   * @param {number} a                              The minimium value of the oscillation
   * @param {number} b                              The maximum value of the oscillation
   * @param {number} t                              The time
   * @param {number} [p=1]                          The period (must be nonzero)
   * @param {(x: number) => number} [f=Math.cos]    The periodic function (its period must be 2π)
   * @returns {number}                              `((b - a) * (f(2π * t / p) + 1) / 2) + a`
   */
  function oscillation(a, b, t, p=1, f=Math.cos) {
    return ((b - a) * (f((2 * Math.PI * t) / p) + 1) / 2) + a;
  }

  // Define properties on the Math environment
  Object.defineProperties(Math, {
    SQRT3: {value: SQRT3},
    SQRT1_3: {value: SQRT1_3},
    clamp: {
      value: clamp,
      configurable: true,
      writable: true
    },
    clamped: {
      value: clamped,
      configurable: true,
      writable: true
    },
    mix: {
      value: mix,
      configurable: true,
      writable: true
    },
    normalizeDegrees: {
      value: normalizeDegrees,
      configurable: true,
      writable: true
    },
    normalizeRadians: {
      value: normalizeRadians,
      configurable: true,
      writable: true
    },
    roundDecimals: {
      value: roundDecimals,
      configurable: true,
      writable: true
    },
    toDegrees: {
      value: toDegrees,
      configurable: true,
      writable: true
    },
    toRadians: {
      value: toRadians,
      configurable: true,
      writable: true
    },
    oscillation: {
      value: oscillation,
      configurable: true,
      writable: true
    }
  });

  /**
   * Test for near-equivalence of two numbers within some permitted epsilon
   * @param {number} n      Some other number
   * @param {number} e      Some permitted epsilon, by default 1e-8
   * @returns {boolean}     Are the numbers almost equal?
   */
  function almostEqual(n, e=1e-8) {
    return Math.abs(this - n) < e;
  }

  /**
   * Transform a number to an ordinal string representation. i.e.
   * 1 => 1st
   * 2 => 2nd
   * 3 => 3rd
   * @returns {string}
   */
  function ordinalString() {
    const s = ["th","st","nd","rd"];
    const v = this % 100;
    return this + (s[(v-20)%10]||s[v]||s[0]);
  }

  /**
   * Return a string front-padded by zeroes to reach a certain number of numeral characters
   * @param {number} digits     The number of characters desired
   * @returns {string}          The zero-padded number
   */
  function paddedString(digits) {
    return this.toString().padStart(digits, "0");
  }

  /**
   * Return a string prefaced by the sign of the number (+) or (-)
   * @returns {string}          The signed number as a string
   */
  function signedString() {
    return (( this < 0 ) ? "" : "+") + this;
  }

  /**
   * Round a number to the closest number which is a multiple of the provided interval.
   * This is a convenience function intended to humanize issues of floating point precision.
   * The interval is treated as a standard string representation to determine the amount of decimal truncation applied.
   * @param {number} interval       The interval to round the number to the nearest multiple of
   * @param {string} [method=round] The rounding method in: round, ceil, floor
   * @returns {number}              The rounded number
   *
   * @example Round a number to the nearest step interval
   * ```js
   * let n = 17.18;
   * n.toNearest(5); // 15
   * n.toNearest(10); // 20
   * n.toNearest(10, "floor"); // 10
   * n.toNearest(10, "ceil"); // 20
   * n.toNearest(0.25); // 17.25
   * ```
   */
  function toNearest(interval=1, method="round") {
    if ( interval < 0 ) throw new Error(`Number#toNearest interval must be positive`);
    const float = Math[method](this / interval) * interval;
    const trunc = Number.isInteger(interval) ? 0 : String(interval).length - 2;
    return Number(float.toFixed(trunc));
  }

  /**
   * A faster numeric between check which avoids type coercion to the Number object.
   * Since this avoids coercion, if non-numbers are passed in unpredictable results will occur. Use with caution.
   * @param {number} a            The lower-bound
   * @param {number} b            The upper-bound
   * @param {boolean} inclusive   Include the bounding values as a true result?
   * @return {boolean}            Is the number between the two bounds?
   */
  function between(a, b, inclusive=true) {
    const min = Math.min(a, b);
    const max = Math.max(a, b);
    return inclusive ? (this >= min) && (this <= max) : (this > min) && (this < max);
  }

  /**
   * @see Number#between
   * @ignore
   */
  Number.between = function(num, a, b, inclusive=true) {
    let min = Math.min(a, b);
    let max = Math.max(a, b);
    return inclusive ? (num >= min) && (num <= max) : (num > min) && (num < max);
  };

  /**
   * Test whether a value is numeric.
   * This is the highest performing algorithm currently available, per https://jsperf.com/isnan-vs-typeof/5
   * @memberof Number
   * @param {*} n       A value to test
   * @return {boolean}  Is it a number?
   */
  function isNumeric(n) {
    if ( n instanceof Array ) return false;
    else if ( [null, ""].includes(n) ) return false;
    return +n === +n;
  }

  /**
   * Attempt to create a number from a user-provided string.
   * @memberof Number
   * @param {string|number} n   The value to convert; typically a string, but may already be a number.
   * @return {number}           The number that the string represents, or NaN if no number could be determined.
   */
  function fromString(n) {
    if ( typeof n === "number" ) return n;
    if ( (typeof n !== "string") || !n.length ) return NaN;
    n = n.replace(/\s+/g, "");
    return Number(n);
  }

  // Define properties on the Number environment
  Object.defineProperties(Number.prototype, {
    almostEqual: {value: almostEqual},
    between: {value: between},
    ordinalString: {value: ordinalString},
    paddedString: {value: paddedString},
    signedString: {value: signedString},
    toNearest: {value: toNearest}
  });
  Object.defineProperties(Number, {
    isNumeric: {value: isNumeric},
    fromString: {value: fromString}
  });

  /**
   * Return the difference of two sets.
   * @param {Set} other       Some other set to compare against
   * @returns {Set}           The difference defined as objects in this which are not present in other
   */
  function difference(other) {
    if ( !(other instanceof Set) ) throw new Error("Some other Set instance must be provided.");
    const difference = new Set();
    for ( const element of this ) {
      if ( !other.has(element) ) difference.add(element);
    }
    return difference;
  }

  /**
   * Return the symmetric difference of two sets.
   * @param {Set} other  Another set.
   * @returns {Set}      The set of elements that exist in this or other, but not both.
   */
  function symmetricDifference(other) {
    if ( !(other instanceof Set) ) throw new Error("Some other Set instance must be provided.");
    const difference = new Set(this);
    for ( const element of other ) {
      if ( difference.has(element) ) difference.delete(element);
      else difference.add(element);
    }
    return difference
  }

  /**
   * Test whether this set is equal to some other set.
   * Sets are equal if they share the same members, independent of order
   * @param {Set} other       Some other set to compare against
   * @returns {boolean}       Are the sets equal?
   */
  function equals(other) {
    if ( !(other instanceof Set ) ) return false;
    if ( other.size !== this.size ) return false;
    for ( let element of this ) {
      if ( !other.has(element) ) return false;
    }
    return true;
  }

  /**
   * Return the first value from the set.
   * @returns {*}             The first element in the set, or undefined
   */
  function first() {
    return this.values().next().value;
  }

  /**
   * Return the intersection of two sets.
   * @param {Set} other       Some other set to compare against
   * @returns {Set}           The intersection of both sets
   */
  function intersection(other) {
    const n = new Set();
    for ( let element of this ) {
      if ( other.has(element) ) n.add(element);
    }
    return n;
  }

  /**
   * Test whether this set has an intersection with another set.
   * @param {Set} other       Another set to compare against
   * @returns {boolean}       Do the sets intersect?
   */
  function intersects(other) {
    for ( let element of this ) {
      if ( other.has(element) ) return true;
    }
    return false;
  }

  /**
   * Return the union of two sets.
   * @param {Set} other  The other set.
   * @returns {Set}
   */
  function union(other) {
    if ( !(other instanceof Set) ) throw new Error("Some other Set instance must be provided.");
    const union = new Set(this);
    for ( const element of other ) union.add(element);
    return union;
  }

  /**
   * Test whether this set is a subset of some other set.
   * A set is a subset if all its members are also present in the other set.
   * @param {Set} other       Some other set that may be a subset of this one
   * @returns {boolean}       Is the other set a subset of this one?
   */
  function isSubset(other) {
    if ( !(other instanceof Set ) ) return false;
    if ( other.size < this.size ) return false;
    for ( let element of this ) {
      if ( !other.has(element) ) return false;
    }
    return true;
  }

  /**
   * Convert a set to a JSON object by mapping its contents to an array
   * @returns {Array}           The set elements as an array.
   */
  function toObject() {
    return Array.from(this);
  }

  /**
   * Test whether every element in this Set satisfies a certain test criterion.
   * @see Array#every
   * @param {function(*,number,Set): boolean} test   The test criterion to apply. Positional arguments are the value,
   * the index of iteration, and the set being tested.
   * @returns {boolean}  Does every element in the set satisfy the test criterion?
   */
  function every(test) {
    let i = 0;
    for ( const v of this ) {
      if ( !test(v, i, this) ) return false;
      i++;
    }
    return true;
  }

  /**
   * Filter this set to create a subset of elements which satisfy a certain test criterion.
   * @see Array#filter
   * @param {function(*,number,Set): boolean} test  The test criterion to apply. Positional arguments are the value,
   * the index of iteration, and the set being filtered.
   * @returns {Set}  A new Set containing only elements which satisfy the test criterion.
   */
  function filter(test) {
    const filtered = new Set();
    let i = 0;
    for ( const v of this ) {
      if ( test(v, i, this) ) filtered.add(v);
      i++;
    }
    return filtered;
  }

  /**
   * Find the first element in this set which satisfies a certain test criterion.
   * @see Array#find
   * @param {function(*,number,Set): boolean} test  The test criterion to apply. Positional arguments are the value,
   * the index of iteration, and the set being searched.
   * @returns {*|undefined}  The first element in the set which satisfies the test criterion, or undefined.
   */
  function find(test) {
    let i = 0;
    for ( const v of this ) {
      if ( test(v, i, this) ) return v;
      i++;
    }
    return undefined;
  }

  /**
   * Create a new Set where every element is modified by a provided transformation function.
   * @see Array#map
   * @param {function(*,number,Set): boolean} transform  The transformation function to apply.Positional arguments are
   * the value, the index of iteration, and the set being transformed.
   * @returns {Set}  A new Set of equal size containing transformed elements.
   */
  function map(transform) {
    const mapped = new Set();
    let i = 0;
    for ( const v of this ) {
      mapped.add(transform(v, i, this));
      i++;
    }
    if ( mapped.size !== this.size ) {
      throw new Error("The Set#map operation illegally modified the size of the set");
    }
    return mapped;
  }

  /**
   * Create a new Set with elements that are filtered and transformed by a provided reducer function.
   * @see Array#reduce
   * @param {function(*,*,number,Set): *} reducer  A reducer function applied to each value. Positional
   * arguments are the accumulator, the value, the index of iteration, and the set being reduced.
   * @param {*} accumulator       The initial value of the returned accumulator.
   * @returns {*}                 The final value of the accumulator.
   */
  function reduce(reducer, accumulator) {
    let i = 0;
    for ( const v of this ) {
      accumulator = reducer(accumulator, v, i, this);
      i++;
    }
    return accumulator;
  }

  /**
   * Test whether any element in this Set satisfies a certain test criterion.
   * @see Array#some
   * @param {function(*,number,Set): boolean} test   The test criterion to apply. Positional arguments are the value,
   * the index of iteration, and the set being tested.
   * @returns {boolean}         Does any element in the set satisfy the test criterion?
   */
  function some(test) {
    let i = 0;
    for ( const v of this ) {
      if ( test(v, i, this) ) return true;
      i++;
    }
    return false;
  }

  // Assign primitives to Set prototype
  Object.defineProperties(Set.prototype, {
    difference: {value: difference},
    symmetricDifference: {value: symmetricDifference},
    equals: {value: equals},
    every: {value: every},
    filter: {value: filter},
    find: {value: find},
    first: {value: first},
    intersection: {value: intersection},
    intersects: {value: intersects},
    union: {value: union},
    isSubset: {value: isSubset},
    map: {value: map},
    reduce: {value: reduce},
    some: {value: some},
    toObject: {value: toObject}
  });

  /**
   * Capitalize a string, transforming it's first character to a capital letter.
   * @returns {string}
   */
  function capitalize() {
    if ( !this.length ) return this;
    return this.charAt(0).toUpperCase() + this.slice(1);
  }

  /**
   * Compare this string (x) with the other string (y) by comparing each character's Unicode code point value.
   * Returns a negative Number if x < y, a positive Number if x > y, or a zero otherwise.
   * This is the same comparision function that used by Array#sort if the compare function argument is omitted.
   * The result is host/locale-independent.
   * @param {string} other    The other string to compare this string to.
   * @returns {number}
   */
  function compare(other) {
    return this < other ? -1 : this > other ? 1 : 0;
  }

  /**
   * Convert a string to Title Case where the first letter of each word is capitalized.
   * @returns {string}
   */
  function titleCase() {
    if (!this.length) return this;
    return this.toLowerCase().split(' ').reduce((parts, word) => {
      if ( !word ) return parts;
      const title = word.replace(word[0], word[0].toUpperCase());
      parts.push(title);
      return parts;
    }, []).join(' ');
  }

  /**
   * Strip any script tags which were included within a provided string.
   * @returns {string}
   */
  function stripScripts() {
    let el = document.createElement("div");
    el.innerHTML = this;
    for ( let s of el.getElementsByTagName("script") ) {
      s.parentNode.removeChild(s);
    }
    return el.innerHTML;
  }

  /**
   * Map characters to lower case ASCII
   * @type {Record<string, string>}
   */
  const CHAR_MAP = JSON.parse('{"$":"dollar","%":"percent","&":"and","<":"less",">":"greater","|":"or","¢":"cent","£":"pound","¤":"currency","¥":"yen","©":"(c)","ª":"a","®":"(r)","º":"o","À":"A","Á":"A","Â":"A","Ã":"A","Ä":"A","Å":"A","Æ":"AE","Ç":"C","È":"E","É":"E","Ê":"E","Ë":"E","Ì":"I","Í":"I","Î":"I","Ï":"I","Ð":"D","Ñ":"N","Ò":"O","Ó":"O","Ô":"O","Õ":"O","Ö":"O","Ø":"O","Ù":"U","Ú":"U","Û":"U","Ü":"U","Ý":"Y","Þ":"TH","ß":"ss","à":"a","á":"a","â":"a","ã":"a","ä":"a","å":"a","æ":"ae","ç":"c","è":"e","é":"e","ê":"e","ë":"e","ì":"i","í":"i","î":"i","ï":"i","ð":"d","ñ":"n","ò":"o","ó":"o","ô":"o","õ":"o","ö":"o","ø":"o","ù":"u","ú":"u","û":"u","ü":"u","ý":"y","þ":"th","ÿ":"y","Ā":"A","ā":"a","Ă":"A","ă":"a","Ą":"A","ą":"a","Ć":"C","ć":"c","Č":"C","č":"c","Ď":"D","ď":"d","Đ":"DJ","đ":"dj","Ē":"E","ē":"e","Ė":"E","ė":"e","Ę":"e","ę":"e","Ě":"E","ě":"e","Ğ":"G","ğ":"g","Ģ":"G","ģ":"g","Ĩ":"I","ĩ":"i","Ī":"i","ī":"i","Į":"I","į":"i","İ":"I","ı":"i","Ķ":"k","ķ":"k","Ļ":"L","ļ":"l","Ľ":"L","ľ":"l","Ł":"L","ł":"l","Ń":"N","ń":"n","Ņ":"N","ņ":"n","Ň":"N","ň":"n","Ő":"O","ő":"o","Œ":"OE","œ":"oe","Ŕ":"R","ŕ":"r","Ř":"R","ř":"r","Ś":"S","ś":"s","Ş":"S","ş":"s","Š":"S","š":"s","Ţ":"T","ţ":"t","Ť":"T","ť":"t","Ũ":"U","ũ":"u","Ū":"u","ū":"u","Ů":"U","ů":"u","Ű":"U","ű":"u","Ų":"U","ų":"u","Ŵ":"W","ŵ":"w","Ŷ":"Y","ŷ":"y","Ÿ":"Y","Ź":"Z","ź":"z","Ż":"Z","ż":"z","Ž":"Z","ž":"z","ƒ":"f","Ơ":"O","ơ":"o","Ư":"U","ư":"u","ǈ":"LJ","ǉ":"lj","ǋ":"NJ","ǌ":"nj","Ș":"S","ș":"s","Ț":"T","ț":"t","˚":"o","Ά":"A","Έ":"E","Ή":"H","Ί":"I","Ό":"O","Ύ":"Y","Ώ":"W","ΐ":"i","Α":"A","Β":"B","Γ":"G","Δ":"D","Ε":"E","Ζ":"Z","Η":"H","Θ":"8","Ι":"I","Κ":"K","Λ":"L","Μ":"M","Ν":"N","Ξ":"3","Ο":"O","Π":"P","Ρ":"R","Σ":"S","Τ":"T","Υ":"Y","Φ":"F","Χ":"X","Ψ":"PS","Ω":"W","Ϊ":"I","Ϋ":"Y","ά":"a","έ":"e","ή":"h","ί":"i","ΰ":"y","α":"a","β":"b","γ":"g","δ":"d","ε":"e","ζ":"z","η":"h","θ":"8","ι":"i","κ":"k","λ":"l","μ":"m","ν":"n","ξ":"3","ο":"o","π":"p","ρ":"r","ς":"s","σ":"s","τ":"t","υ":"y","φ":"f","χ":"x","ψ":"ps","ω":"w","ϊ":"i","ϋ":"y","ό":"o","ύ":"y","ώ":"w","Ё":"Yo","Ђ":"DJ","Є":"Ye","І":"I","Ї":"Yi","Ј":"J","Љ":"LJ","Њ":"NJ","Ћ":"C","Џ":"DZ","А":"A","Б":"B","В":"V","Г":"G","Д":"D","Е":"E","Ж":"Zh","З":"Z","И":"I","Й":"J","К":"K","Л":"L","М":"M","Н":"N","О":"O","П":"P","Р":"R","С":"S","Т":"T","У":"U","Ф":"F","Х":"H","Ц":"C","Ч":"Ch","Ш":"Sh","Щ":"Sh","Ъ":"U","Ы":"Y","Ь":"","Э":"E","Ю":"Yu","Я":"Ya","а":"a","б":"b","в":"v","г":"g","д":"d","е":"e","ж":"zh","з":"z","и":"i","й":"j","к":"k","л":"l","м":"m","н":"n","о":"o","п":"p","р":"r","с":"s","т":"t","у":"u","ф":"f","х":"h","ц":"c","ч":"ch","ш":"sh","щ":"sh","ъ":"u","ы":"y","ь":"","э":"e","ю":"yu","я":"ya","ё":"yo","ђ":"dj","є":"ye","і":"i","ї":"yi","ј":"j","љ":"lj","њ":"nj","ћ":"c","ѝ":"u","џ":"dz","Ґ":"G","ґ":"g","Ғ":"GH","ғ":"gh","Қ":"KH","қ":"kh","Ң":"NG","ң":"ng","Ү":"UE","ү":"ue","Ұ":"U","ұ":"u","Һ":"H","һ":"h","Ә":"AE","ә":"ae","Ө":"OE","ө":"oe","฿":"baht","ა":"a","ბ":"b","გ":"g","დ":"d","ე":"e","ვ":"v","ზ":"z","თ":"t","ი":"i","კ":"k","ლ":"l","მ":"m","ნ":"n","ო":"o","პ":"p","ჟ":"zh","რ":"r","ს":"s","ტ":"t","უ":"u","ფ":"f","ქ":"k","ღ":"gh","ყ":"q","შ":"sh","ჩ":"ch","ც":"ts","ძ":"dz","წ":"ts","ჭ":"ch","ხ":"kh","ჯ":"j","ჰ":"h","Ẁ":"W","ẁ":"w","Ẃ":"W","ẃ":"w","Ẅ":"W","ẅ":"w","ẞ":"SS","Ạ":"A","ạ":"a","Ả":"A","ả":"a","Ấ":"A","ấ":"a","Ầ":"A","ầ":"a","Ẩ":"A","ẩ":"a","Ẫ":"A","ẫ":"a","Ậ":"A","ậ":"a","Ắ":"A","ắ":"a","Ằ":"A","ằ":"a","Ẳ":"A","ẳ":"a","Ẵ":"A","ẵ":"a","Ặ":"A","ặ":"a","Ẹ":"E","ẹ":"e","Ẻ":"E","ẻ":"e","Ẽ":"E","ẽ":"e","Ế":"E","ế":"e","Ề":"E","ề":"e","Ể":"E","ể":"e","Ễ":"E","ễ":"e","Ệ":"E","ệ":"e","Ỉ":"I","ỉ":"i","Ị":"I","ị":"i","Ọ":"O","ọ":"o","Ỏ":"O","ỏ":"o","Ố":"O","ố":"o","Ồ":"O","ồ":"o","Ổ":"O","ổ":"o","Ỗ":"O","ỗ":"o","Ộ":"O","ộ":"o","Ớ":"O","ớ":"o","Ờ":"O","ờ":"o","Ở":"O","ở":"o","Ỡ":"O","ỡ":"o","Ợ":"O","ợ":"o","Ụ":"U","ụ":"u","Ủ":"U","ủ":"u","Ứ":"U","ứ":"u","Ừ":"U","ừ":"u","Ử":"U","ử":"u","Ữ":"U","ữ":"u","Ự":"U","ự":"u","Ỳ":"Y","ỳ":"y","Ỵ":"Y","ỵ":"y","Ỷ":"Y","ỷ":"y","Ỹ":"Y","ỹ":"y","‘":"\'","’":"\'","“":"\\\"","”":"\\\"","†":"+","•":"*","…":"...","₠":"ecu","₢":"cruzeiro","₣":"french franc","₤":"lira","₥":"mill","₦":"naira","₧":"peseta","₨":"rupee","₩":"won","₪":"new shequel","₫":"dong","€":"euro","₭":"kip","₮":"tugrik","₯":"drachma","₰":"penny","₱":"peso","₲":"guarani","₳":"austral","₴":"hryvnia","₵":"cedi","₸":"kazakhstani tenge","₹":"indian rupee","₽":"russian ruble","₿":"bitcoin","℠":"sm","™":"tm","∂":"d","∆":"delta","∑":"sum","∞":"infinity","♥":"love","元":"yuan","円":"yen","﷼":"rial"}');

  /**
   * Transform any string into an url-viable slug string
   * @param {object} [options]      Optional arguments which customize how the slugify operation is performed
   * @param {string} [options.replacement="-"]  The replacement character to separate terms, default is '-'
   * @param {boolean} [options.strict=false]    Replace all non-alphanumeric characters, or allow them? Default false
   * @param {boolean} [options.lowercase=true]  Lowercase the string.
   * @returns {string}              The slugified input string
   */
  function slugify({replacement='-', strict=false, lowercase=true}={}) {
    let slug = this.split("").reduce((result, char) => result + (CHAR_MAP[char] || char), "").trim();
    if ( lowercase ) slug = slug.toLowerCase();

    // Convert any spaces to the replacement character and de-dupe
    slug = slug.replace(new RegExp('[\\s' + replacement + ']+', 'g'), replacement);

    // If we're being strict, replace anything that is not alphanumeric
    if ( strict ) slug = slug.replace(new RegExp('[^a-zA-Z0-9' + replacement + ']', 'g'), '');
    return slug;
  }

  // Define properties on the String environment
  Object.defineProperties(String.prototype, {
    capitalize: {value: capitalize},
    compare: {value: compare},
    titleCase: {value: titleCase},
    stripScripts: {value: stripScripts},
    slugify: {value: slugify}
  });

  /**
   * Escape a given input string, prefacing special characters with backslashes for use in a regular expression
   * @param {string} string     The un-escaped input string
   * @returns {string}          The escaped string, suitable for use in regular expression
   */
  function escape$1(string) {
    return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  }

  // Define properties on the RegExp environment
  Object.defineProperties(RegExp, {
    escape: {value: escape$1}
  });

  /**
   * Attempt to parse a URL without throwing an error.
   * @param {string} url  The string to parse.
   * @returns {URL|null}  The parsed URL if successful, otherwise null.
   */
  function parseSafe(url) {
    try {
      return new URL(url);
    } catch (err) {}
    return null;
  }

  // Define properties on the URL environment
  Object.defineProperties(URL, {
    parseSafe: {value: parseSafe}
  });

  /**
   * @typedef {Object} DatabaseGetOperation
   * @property {Record<string, any>} query        A query object which identifies the set of Documents retrieved
   * @property {false} [broadcast]                Get requests are never broadcast
   * @property {boolean} [index]                  Return indices only instead of full Document records
   * @property {string[]} [indexFields]           An array of field identifiers which should be indexed
   * @property {string|null} [pack=null]          A compendium collection ID which contains the Documents
   * @property {foundry.abstract.Document|null} [parent=null] A parent Document within which Documents are embedded
   * @property {string} [parentUuid]              A parent Document UUID provided when the parent instance is unavailable
   */

  /**
   * @typedef {Object} DatabaseCreateOperation
   * @property {boolean} broadcast                Whether the database operation is broadcast to other connected clients
   * @property {object[]} data                    An array of data objects from which to create Documents
   * @property {boolean} [keepId=false]           Retain the _id values of provided data instead of generating new ids
   * @property {boolean} [keepEmbeddedIds=true]   Retain the _id values of embedded document data instead of generating
   *                                              new ids for each embedded document
   * @property {number} [modifiedTime]            The timestamp when the operation was performed
   * @property {boolean} [noHook=false]           Block the dispatch of hooks related to this operation
   * @property {boolean} [render=true]            Re-render Applications whose display depends on the created Documents
   * @property {boolean} [renderSheet=false]      Render the sheet Application for any created Documents
   * @property {foundry.abstract.Document|null} [parent=null] A parent Document within which Documents are embedded
   * @property {string|null} pack                 A compendium collection ID which contains the Documents
   * @property {string|null} [parentUuid]         A parent Document UUID provided when the parent instance is unavailable
   * @property {(string|object)[]} [_result]      An alias for 'data' used internally by the server-side backend
   */

  /**
   * @typedef {Object} DatabaseUpdateOperation
   * @property {boolean} broadcast                Whether the database operation is broadcast to other connected clients
   * @property {object[]} updates                 An array of data objects used to update existing Documents.
   *                                              Each update object must contain the _id of the target Document
   * @property {boolean} [diff=true]              Difference each update object against current Document data and only use
   *                                              differential data for the update operation
   * @property {number} [modifiedTime]            The timestamp when the operation was performed
   * @property {boolean} [recursive=true]         Merge objects recursively. If false, inner objects will be replaced
   *                                              explicitly. Use with caution!
   * @property {boolean} [render=true]            Re-render Applications whose display depends on the created Documents
   * @property {boolean} [noHook=false]           Block the dispatch of hooks related to this operation
   * @property {foundry.abstract.Document|null} [parent=null] A parent Document within which Documents are embedded
   * @property {string|null} pack                 A compendium collection ID which contains the Documents
   * @property {string|null} [parentUuid]         A parent Document UUID provided when the parent instance is unavailable
   * @property {(string|object)[]} [_result]      An alias for 'updates' used internally by the server-side backend
   *
   */

  /**
   * @typedef {Object} DatabaseDeleteOperation
   * @property {boolean} broadcast                Whether the database operation is broadcast to other connected clients
   * @property {string[]} ids                     An array of Document ids which should be deleted
   * @property {boolean} [deleteAll=false]        Delete all documents in the Collection, regardless of _id
   * @property {number} [modifiedTime]            The timestamp when the operation was performed
   * @property {boolean} [noHook=false]           Block the dispatch of hooks related to this operation
   * @property {boolean} [render=true]            Re-render Applications whose display depends on the deleted Documents
   * @property {foundry.abstract.Document|null} [parent=null] A parent Document within which Documents are embedded
   * @property {string|null} pack                 A compendium collection ID which contains the Documents
   * @property {string|null} [parentUuid]         A parent Document UUID provided when the parent instance is unavailable
   * @property {(string|object)[]} [_result]      An alias for 'ids' used internally by the server-side backend
   */

  /**
   * @typedef {"get"|"create"|"update"|"delete"} DatabaseAction
   */

  /**
   * @typedef {DatabaseGetOperation|DatabaseCreateOperation|DatabaseUpdateOperation|DatabaseDeleteOperation} DatabaseOperation
   */

  /**
   * @typedef {Object} DocumentSocketRequest
   * @property {string} type                      The type of Document being transacted
   * @property {DatabaseAction} action            The action of the request
   * @property {DatabaseOperation} operation      Operation parameters for the request
   * @property {string} userId                    The id of the requesting User
   * @property {boolean} broadcast                Should the response be broadcast to other connected clients?
   */

  var _types$4 = /*#__PURE__*/Object.freeze({
    __proto__: null
  });

  /** @module validators */

  /**
   * Test whether a string is a valid 16 character UID
   * @param {string} id
   * @return {boolean}
   */
  function isValidId(id) {
    return /^[a-zA-Z0-9]{16}$/.test(id);
  }

  /**
   * Test whether a file path has an extension in a list of provided extensions
   * @param {string} path
   * @param {string[]} extensions
   * @return {boolean}
   */
  function hasFileExtension(path, extensions) {
    const xts = extensions.map(ext => `\\.${ext}`).join("|");
    const rgx = new RegExp(`(${xts})(\\?.*)?$`, "i");
    return !!path && rgx.test(path);
  }

  /**
   * Test whether a string data blob contains base64 data, optionally of a specific type or types
   * @param {string} data       The candidate string data
   * @param {string[]} [types]  An array of allowed mime types to test
   * @return {boolean}
   */
  function isBase64Data(data, types) {
    if ( types === undefined ) return /^data:([a-z]+)\/([a-z0-9]+);base64,/.test(data);
    return types.some(type => data.startsWith(`data:${type};base64,`))
  }

  /**
   * Test whether an input represents a valid 6-character color string
   * @param {string} color      The input string to test
   * @return {boolean}          Is the string a valid color?
   */
  function isColorString(color) {
    return /^#[0-9A-Fa-f]{6}$/.test(color);
  }

  /**
   * Assert that the given value parses as a valid JSON string
   * @param {string} val        The value to test
   * @return {boolean}          Is the String valid JSON?
   */
  function isJSON(val) {
    try {
      JSON.parse(val);
      return true;
    } catch(err) {
      return false;
    }
  }

  var validators = /*#__PURE__*/Object.freeze({
    __proto__: null,
    hasFileExtension: hasFileExtension,
    isBase64Data: isBase64Data,
    isColorString: isColorString,
    isJSON: isJSON,
    isValidId: isValidId
  });

  /**
   * The messages that have been logged already and should not be logged again.
   * @type {Set<string>}
   */
  const loggedCompatibilityWarnings = new Set();

  /**
   * Log a compatibility warning which is filtered based on the client's defined compatibility settings.
   * @param {string} message            The original warning or error message
   * @param {object} [options={}]       Additional options which customize logging
   * @param {number} [options.mode]           A logging level in COMPATIBILITY_MODES which overrides the configured default
   * @param {number|string} [options.since]   A version identifier since which a change was made
   * @param {number|string} [options.until]   A version identifier until which a change remains supported
   * @param {string} [options.details]        Additional details to append to the logged message
   * @param {boolean} [options.stack=true]    Include the message stack trace
   * @param {boolean} [options.once=false]    Log this the message only once?
   * @throws                            An Error if the mode is ERROR
   */
  function logCompatibilityWarning(message, {mode, since, until, details, stack=true, once=false}={}) {

    // Determine the logging mode
    const modes = COMPATIBILITY_MODES;
    const compatibility = globalThis.CONFIG?.compatibility || {
      mode: modes.WARNING,
      includePatterns: [],
      excludePatterns: []
    };
    mode ??= compatibility.mode;
    if ( mode === modes.SILENT ) return;

    // Compose the message
    since = since ? `Deprecated since Version ${since}` : null;
    until = until ? `Backwards-compatible support will be removed in Version ${until}`: null;
    message = [message, since, until, details].filterJoin("\n");

    // Filter the message by its stack trace
    const error = new Error(message);
    if ( compatibility.includePatterns.length ) {
      if ( !compatibility.includePatterns.some(rgx => rgx.test(error.message) || rgx.test(error.stack)) ) return;
    }
    if ( compatibility.excludePatterns.length ) {
      if ( compatibility.excludePatterns.some(rgx => rgx.test(error.message) || rgx.test(error.stack)) ) return;
    }

    // Log the message
    const log = !(once && loggedCompatibilityWarnings.has(error.stack));
    switch ( mode ) {
      case modes.WARNING:
        if ( log ) globalThis.logger.warn(stack ? error : error.message);
        break;
      case modes.ERROR:
        if ( log ) globalThis.logger.error(stack ? error : error.message);
        break;
      case modes.FAILURE:
        throw error;
    }
    if ( log && once ) loggedCompatibilityWarnings.add(error.stack);
  }

  /**
   * A class responsible for recording information about a validation failure.
   */
  class DataModelValidationFailure {
    /**
     * @param {any} [invalidValue]       The value that failed validation for this field.
     * @param {any} [fallback]           The value it was replaced by, if any.
     * @param {boolean} [dropped=false]  Whether the value was dropped from some parent collection.
     * @param {string} [message]         The validation error message.
     * @param {boolean} [unresolved=false]     Whether this failure was unresolved
     */
    constructor({invalidValue, fallback, dropped=false, message, unresolved=false}={}) {
      this.invalidValue = invalidValue;
      this.fallback = fallback;
      this.dropped = dropped;
      this.message = message;
      this.unresolved = unresolved;
    }

    /**
     * The value that failed validation for this field.
     * @type {any}
     */
    invalidValue;

    /**
     * The value it was replaced by, if any.
     * @type {any}
     */
    fallback;

    /**
     * Whether the value was dropped from some parent collection.
     * @type {boolean}
     */
    dropped;

    /**
     * The validation error message.
     * @type {string}
     */
    message;

    /**
     * If this field contains other fields that are validated as part of its validation, their results are recorded here.
     * @type {Record<string, DataModelValidationFailure>}
     */
    fields = {};

    /**
     * @typedef {object} ElementValidationFailure
     * @property {string|number} id                    Either the element's index or some other identifier for it.
     * @property {string} [name]                       Optionally a user-friendly name for the element.
     * @property {DataModelValidationFailure} failure  The element's validation failure.
     */

    /**
     * If this field contains a list of elements that are validated as part of its validation, their results are recorded
     * here.
     * @type {ElementValidationFailure[]}
     */
    elements = [];

    /**
     * Record whether a validation failure is unresolved.
     * This reports as true if validation for this field or any hierarchically contained field is unresolved.
     * A failure is unresolved if the value was invalid and there was no valid fallback value available.
     * @type {boolean}
     */
    unresolved;

    /* -------------------------------------------- */

    /**
     * Return this validation failure as an Error object.
     * @returns {DataModelValidationError}
     */
    asError() {
      return new DataModelValidationError(this);
    }

    /* -------------------------------------------- */

    /**
     * Whether this failure contains other sub-failures.
     * @returns {boolean}
     */
    isEmpty() {
      return isEmpty$1(this.fields) && isEmpty$1(this.elements);
    }

    /* -------------------------------------------- */

    /**
     * Return the base properties of this failure, omitting any nested failures.
     * @returns {{invalidValue: any, fallback: any, dropped: boolean, message: string}}
     */
    toObject() {
      const {invalidValue, fallback, dropped, message} = this;
      return {invalidValue, fallback, dropped, message};
    }

    /* -------------------------------------------- */

    /**
     * Represent the DataModelValidationFailure as a string.
     * @returns {string}
     */
    toString() {
      return DataModelValidationFailure.#formatString(this);
    }

    /* -------------------------------------------- */

    /**
     * Format a DataModelValidationFailure instance as a string message.
     * @param {DataModelValidationFailure} failure    The failure instance
     * @param {number} _d                             An internal depth tracker
     * @returns {string}                              The formatted failure string
     */
    static #formatString(failure, _d=0) {
      let message = failure.message ?? "";
      _d++;
      if ( !isEmpty$1(failure.fields) ) {
        message += "\n";
        const messages = [];
        for ( const [name, subFailure] of Object.entries(failure.fields) ) {
          const subMessage = DataModelValidationFailure.#formatString(subFailure, _d);
          messages.push(`${" ".repeat(2 * _d)}${name}: ${subMessage}`);
        }
        message += messages.join("\n");
      }
      if ( !isEmpty$1(failure.elements) ) {
        message += "\n";
        const messages = [];
        for ( const element of failure.elements ) {
          const subMessage = DataModelValidationFailure.#formatString(element.failure, _d);
          messages.push(`${" ".repeat(2 * _d)}${element.id}: ${subMessage}`);
        }
        message += messages.join("\n");
      }
      return message;
    }
  }

  /* -------------------------------------------- */

  /**
   * A specialised Error to indicate a model validation failure.
   * @extends {Error}
   */
  class DataModelValidationError extends Error {
    /**
     * @param {DataModelValidationFailure|string} failure  The failure that triggered this error or an error message
     * @param {...any} [params]                            Additional Error constructor parameters
     */
    constructor(failure, ...params) {
      super(failure.toString(), ...params);
      if ( failure instanceof DataModelValidationFailure ) this.#failure = failure;
    }

    /**
     * The root validation failure that triggered this error.
     * @type {DataModelValidationFailure}
     */
    #failure;

    /* -------------------------------------------- */

    /**
     * Retrieve the root failure that caused this error, or a specific sub-failure via a path.
     * @param {string} [path]  The property path to the failure.
     * @returns {DataModelValidationFailure}
     *
     * @example Retrieving a failure.
     * ```js
     * const changes = {
     *   "foo.bar": "validValue",
     *   "foo.baz": "invalidValue"
     * };
     * try {
     *   doc.validate(expandObject(changes));
     * } catch ( err ) {
     *   const failure = err.getFailure("foo.baz");
     *   console.log(failure.invalidValue); // "invalidValue"
     * }
     * ```
     */
    getFailure(path) {
      if ( !this.#failure ) return;
      if ( !path ) return this.#failure;
      let failure = this.#failure;
      for ( const p of path.split(".") ) {
        if ( !failure ) return;
        if ( !isEmpty$1(failure.fields) ) failure = failure.fields[p];
        else if ( !isEmpty$1(failure.elements) ) failure = failure.elements.find(e => e.id?.toString() === p);
      }
      return failure;
    }

    /* -------------------------------------------- */

    /**
     * Retrieve a flattened object of all the properties that failed validation as part of this error.
     * @returns {Record<string, DataModelValidationFailure>}
     *
     * @example Removing invalid changes from an update delta.
     * ```js
     * const changes = {
     *   "foo.bar": "validValue",
     *   "foo.baz": "invalidValue"
     * };
     * try {
     *   doc.validate(expandObject(changes));
     * } catch ( err ) {
     *   const failures = err.getAllFailures();
     *   if ( failures ) {
     *     for ( const prop in failures ) delete changes[prop];
     *     doc.validate(expandObject(changes));
     *   }
     * }
     * ```
     */
    getAllFailures() {
      if ( !this.#failure || this.#failure.isEmpty() ) return;
      return DataModelValidationError.#aggregateFailures(this.#failure);
    }

    /* -------------------------------------------- */

    /**
     * Log the validation error as a table.
     */
    logAsTable() {
      const failures = this.getAllFailures();
      if ( isEmpty$1(failures) ) return;
      console.table(Object.entries(failures).reduce((table, [p, failure]) => {
        table[p] = failure.toObject();
        return table;
      }, {}));
    }

    /* -------------------------------------------- */

    /**
     * Generate a nested tree view of the error as an HTML string.
     * @returns {string}
     */
    asHTML() {
      const renderFailureNode = failure => {
        if ( failure.isEmpty() ) return `<li>${failure.message || ""}</li>`;
        const nodes = [];
        for ( const [field, subFailure] of Object.entries(failure.fields) ) {
          nodes.push(`<li><details><summary>${field}</summary><ul>${renderFailureNode(subFailure)}</ul></details></li>`);
        }
        for ( const element of failure.elements ) {
          const name = element.name || element.id;
          const html = `
          <li><details><summary>${name}</summary><ul>${renderFailureNode(element.failure)}</ul></details></li>
        `;
          nodes.push(html);
        }
        return nodes.join("");
      };
      return `<ul class="summary-tree">${renderFailureNode(this.#failure)}</ul>`;
    }

    /* -------------------------------------------- */

    /**
     * Collect nested failures into an aggregate object.
     * @param {DataModelValidationFailure} failure                               The failure.
     * @returns {DataModelValidationFailure|Record<string, DataModelValidationFailure>}  Returns the failure at the leaf of the
     *                                                                           tree, otherwise an object of
     *                                                                           sub-failures.
     */
    static #aggregateFailures(failure) {
      if ( failure.isEmpty() ) return failure;
      const failures = {};
      const recordSubFailures = (field, subFailures) => {
        if ( subFailures instanceof DataModelValidationFailure ) failures[field] = subFailures;
        else {
          for ( const [k, v] of Object.entries(subFailures) ) {
            failures[`${field}.${k}`] = v;
          }
        }
      };
      for ( const [field, subFailure] of Object.entries(failure.fields) ) {
        recordSubFailures(field, DataModelValidationError.#aggregateFailures(subFailure));
      }
      for ( const element of failure.elements ) {
        recordSubFailures(element.id, DataModelValidationError.#aggregateFailures(element.failure));
      }
      return failures;
    }
  }

  var validationFailure = /*#__PURE__*/Object.freeze({
    __proto__: null,
    DataModelValidationError: DataModelValidationError,
    DataModelValidationFailure: DataModelValidationFailure
  });

  /**
   * A reusable storage concept which blends the functionality of an Array with the efficient key-based lookup of a Map.
   * This concept is reused throughout Foundry VTT where a collection of uniquely identified elements is required.
   * @template {string} K
   * @template {*} V
   * @extends {Map<K, V>}
   */
  class Collection extends Map {
    constructor(entries) {
      super(entries);
    }

    /* -------------------------------------------- */

    /**
     * Then iterating over a Collection, we should iterate over its values instead of over its entries
     * @returns {IterableIterator<V>}
     */
    [Symbol.iterator]() {
      return this.values();
    }

    /* -------------------------------------------- */

    /**
     * Return an Array of all the entry values in the Collection
     * @type {V[]}
     */
    get contents() {
      return Array.from(this.values());
    }

    /* -------------------------------------------- */

    /**
     * Find an entry in the Map using a functional condition.
     * @see {Array#find}
     * @param {function(*,number,Collection): boolean} condition  The functional condition to test. Positional
     * arguments are the value, the index of iteration, and the collection being searched.
     * @return {*}  The value, if found, otherwise undefined
     *
     * @example Create a new Collection and reference its contents
     * ```js
     * let c = new Collection([["a", "A"], ["b", "B"], ["c", "C"]]);
     * c.get("a") === c.find(entry => entry === "A"); // true
     * ```
     */
    find(condition) {
      let i = 0;
      for ( let v of this.values() ) {
        if ( condition(v, i, this) ) return v;
        i++;
      }
      return undefined;
    }

    /* -------------------------------------------- */

    /**
     * Filter the Collection, returning an Array of entries which match a functional condition.
     * @see {Array#filter}
     * @param {function(*,number,Collection): boolean} condition  The functional condition to test. Positional
     * arguments are the value, the index of iteration, and the collection being filtered.
     * @return {Array<*>}           An Array of matched values
     *
     * @example Filter the Collection for specific entries
     * ```js
     * let c = new Collection([["a", "AA"], ["b", "AB"], ["c", "CC"]]);
     * let hasA = c.filters(entry => entry.slice(0) === "A");
     * ```
     */
    filter(condition) {
      const entries = [];
      let i = 0;
      for ( let v of this.values() ) {
        if ( condition(v, i , this) ) entries.push(v);
        i++;
      }
      return entries;
    }

    /* -------------------------------------------- */

    /**
     * Apply a function to each element of the collection
     * @see Array#forEach
     * @param {function(*): void} fn       A function to apply to each element
     *
     * @example Apply a function to each value in the collection
     * ```js
     * let c = new Collection([["a", {active: false}], ["b", {active: false}], ["c", {active: false}]]);
     * c.forEach(e => e.active = true);
     * ```
     */
    forEach(fn) {
      for ( let e of this.values() ) {
        fn(e);
      }
    }

    /* -------------------------------------------- */

    /**
     * Get an element from the Collection by its key.
     * @param {string} key      The key of the entry to retrieve
     * @param {object} [options]  Additional options that affect how entries are retrieved
     * @param {boolean} [options.strict=false] Throw an Error if the requested key does not exist. Default false.
     * @return {*|undefined}    The retrieved entry value, if the key exists, otherwise undefined
     *
     * @example Get an element from the Collection by key
     * ```js
     * let c = new Collection([["a", "Alfred"], ["b", "Bob"], ["c", "Cynthia"]]);
     * c.get("a"); // "Alfred"
     * c.get("d"); // undefined
     * c.get("d", {strict: true}); // throws Error
     * ```
     */
    get(key, {strict=false}={}) {
      const entry = super.get(key);
      if ( strict && (entry === undefined) ) {
        throw new Error(`The key ${key} does not exist in the ${this.constructor.name} Collection`);
      }
      return entry;
    }

    /* -------------------------------------------- */

    /**
     * Get an entry from the Collection by name.
     * Use of this method assumes that the objects stored in the collection have a "name" attribute.
     * @param {string} name       The name of the entry to retrieve
     * @param {object} [options]  Additional options that affect how entries are retrieved
     * @param {boolean} [options.strict=false] Throw an Error if the requested name does not exist. Default false.
     * @return {*}                The retrieved entry value, if one was found, otherwise undefined
     *
     * @example Get an element from the Collection by name (if applicable)
     * ```js
     * let c = new Collection([["a", "Alfred"], ["b", "Bob"], ["c", "Cynthia"]]);
     * c.getName("Alfred"); // "Alfred"
     * c.getName("D"); // undefined
     * c.getName("D", {strict: true}); // throws Error
     * ```
     */
    getName(name, {strict=false} = {}) {
      const entry = this.find(e => e.name === name);
      if ( strict && (entry === undefined) ) {
        throw new Error(`An entry with name ${name} does not exist in the collection`);
      }
      return entry ?? undefined;
    }

    /* -------------------------------------------- */

    /**
     * Transform each element of the Collection into a new form, returning an Array of transformed values
     * @param {function(*,number,Collection): *} transformer  A transformation function applied to each entry value.
     * Positional arguments are the value, the index of iteration, and the collection being mapped.
     * @return {Array<*>}  An Array of transformed values
     */
    map(transformer) {
      const transformed = [];
      let i = 0;
      for ( let v of this.values() ) {
        transformed.push(transformer(v, i, this));
        i++;
      }
      return transformed;
    }

    /* -------------------------------------------- */

    /**
     * Reduce the Collection by applying an evaluator function and accumulating entries
     * @see {Array#reduce}
     * @param {function(*,*,number,Collection): *} reducer  A reducer function applied to each entry value. Positional
     * arguments are the accumulator, the value, the index of iteration, and the collection being reduced.
     * @param {*} initial             An initial value which accumulates with each iteration
     * @return {*}                    The accumulated result
     *
     * @example Reduce a collection to an array of transformed values
     * ```js
     * let c = new Collection([["a", "A"], ["b", "B"], ["c", "C"]]);
     * let letters = c.reduce((s, l) => {
     *   return s + l;
     * }, ""); // "ABC"
     * ```
     */
    reduce(reducer, initial) {
      let accumulator = initial;
      let i = 0;
      for ( let v of this.values() ) {
        accumulator = reducer(accumulator, v, i, this);
        i++;
      }
      return accumulator;
    }

    /* -------------------------------------------- */

    /**
     * Test whether a condition is met by some entry in the Collection.
     * @see {Array#some}
     * @param {function(*,number,Collection): boolean} condition  The functional condition to test. Positional
     * arguments are the value, the index of iteration, and the collection being tested.
     * @return {boolean}  Was the test condition passed by at least one entry?
     */
    some(condition) {
      let i = 0;
      for ( let v of this.values() ) {
        const pass = condition(v, i, this);
        i++;
        if ( pass ) return true;
      }
      return false;
    }

    /* -------------------------------------------- */

    /**
     * Convert the Collection to a primitive array of its contents.
     * @returns {object[]}  An array of contained values
     */
    toJSON() {
      return this.map(e => e.toJSON ? e.toJSON() : e);
    }
  }

  /**
   * An extension of the Collection.
   * Used for the specific task of containing embedded Document instances within a parent Document.
   */
  class EmbeddedCollection extends Collection {
    /**
     * @param {string} name           The name of this collection in the parent Document.
     * @param {DataModel} parent      The parent DataModel instance to which this collection belongs.
     * @param {object[]} sourceArray  The source data array for the collection in the parent Document data.
     */
    constructor(name, parent, sourceArray) {
      if ( typeof name !== "string" ) throw new Error("The signature of EmbeddedCollection has changed in v11.");
      super();
      Object.defineProperties(this, {
        _source: {value: sourceArray, writable: false},
        documentClass: {value: parent.constructor.hierarchy[name].model, writable: false},
        name: {value: name, writable: false},
        model: {value: parent, writable: false}
      });
    }

    /**
     * The Document implementation used to construct instances within this collection.
     * @type {typeof foundry.abstract.Document}
     */
    documentClass;

    /**
     * The name of this collection in the parent Document.
     * @type {string}
     */
    name;

    /**
     * The parent DataModel to which this EmbeddedCollection instance belongs.
     * @type {DataModel}
     */
    model;

    /**
     * Has this embedded collection been initialized as a one-time workflow?
     * @type {boolean}
     * @protected
     */
    _initialized = false;

    /**
     * The source data array from which the embedded collection is created
     * @type {object[]}
     * @private
     */
    _source;

    /**
     * Record the set of document ids where the Document was not initialized because of invalid source data
     * @type {Set<string>}
     */
    invalidDocumentIds = new Set();

    /* -------------------------------------------- */

    /**
     * Instantiate a Document for inclusion in the Collection.
     * @param {object} data       The Document data.
     * @param {DocumentConstructionContext} [context]  Document creation context.
     * @returns {Document}
     */
    createDocument(data, context={}) {
      return new this.documentClass(data, {
        ...context,
        parent: this.model,
        parentCollection: this.name,
        pack: this.model.pack
      });
    }

    /* -------------------------------------------- */

    /**
     * Initialize the EmbeddedCollection object by constructing its contained Document instances
     * @param {DocumentConstructionContext} [options]  Initialization options.
     */
    initialize(options={}) {

      // Repeat initialization
      if ( this._initialized ) {
        for ( const doc of this ) doc._initialize(options);
        return;
      }

      // First-time initialization
      this.clear();
      for ( const d of this._source ) this._initializeDocument(d, options);
      this._initialized = true;
    }

    /* -------------------------------------------- */

    /**
     * Initialize an embedded document and store it in the collection.
     * @param {object} data                    The Document data.
     * @param {DocumentConstructionContext} [context]  Context to configure Document initialization.
     * @protected
     */
    _initializeDocument(data, context) {
      if ( !data._id ) data._id = randomID(16);
      let doc;
      try {
        doc = this.createDocument(data, context);
        super.set(doc.id, doc);
      } catch(err) {
        this._handleInvalidDocument(data._id, err, context);
      }
    }

    /* -------------------------------------------- */

    /**
     * Log warnings or errors when a Document is found to be invalid.
     * @param {string} id                      The invalid Document's ID.
     * @param {Error} err                      The validation error.
     * @param {object} [options]               Options to configure invalid Document handling.
     * @param {boolean} [options.strict=true]  Whether to throw an error or only log a warning.
     * @protected
     */
    _handleInvalidDocument(id, err, {strict=true}={}) {
      const docName = this.documentClass.documentName;
      const parent = this.model;
      this.invalidDocumentIds.add(id);

      // Wrap the error with more information
      const uuid = `${parent.uuid}.${docName}.${id}`;
      const msg = `Failed to initialize ${docName} [${uuid}]:\n${err.message}`;
      const error = new Error(msg, {cause: err});

      if ( strict ) globalThis.logger.error(error);
      else globalThis.logger.warn(error);
      if ( globalThis.Hooks && strict ) {
        Hooks.onError(`${this.constructor.name}#_initializeDocument`, error, {id, documentName: docName});
      }
    }

    /* -------------------------------------------- */

    /**
     * Get an element from the EmbeddedCollection by its ID.
     * @param {string} id                        The ID of the Embedded Document to retrieve.
     * @param {object} [options]                 Additional options to configure retrieval.
     * @param {boolean} [options.strict=false]   Throw an Error if the requested Embedded Document does not exist.
     * @param {boolean} [options.invalid=false]  Allow retrieving an invalid Embedded Document.
     * @returns {Document}
     * @throws If strict is true and the Embedded Document cannot be found.
     */
    get(id, {invalid=false, strict=false}={}) {
      let result = super.get(id);
      if ( !result && invalid ) result = this.getInvalid(id, { strict: false });
      if ( !result && strict ) throw new Error(`${this.constructor.documentName} id [${id}] does not exist in the `
        + `${this.constructor.name} collection.`);
      return result;
    }

    /* ---------------------------------------- */

    /**
     * Add an item to the collection.
     * @param {string} key                           The embedded Document ID.
     * @param {Document} value                       The embedded Document instance.
     * @param {object} [options]                     Additional options to the set operation.
     * @param {boolean} [options.modifySource=true]  Whether to modify the collection's source as part of the operation.
     * */
    set(key, value, {modifySource=true, ...options}={}) {
      if ( modifySource ) this._set(key, value, options);
      return super.set(key, value);
    }

    /* -------------------------------------------- */

    /**
     * Modify the underlying source array to include the Document.
     * @param {string} key      The Document ID key.
     * @param {Document} value  The Document.
     * @protected
     */
    _set(key, value) {
      if ( this.has(key) || this.invalidDocumentIds.has(key) ) this._source.findSplice(d => d._id === key, value._source);
      else this._source.push(value._source);
    }

    /* ---------------------------------------- */

    /**
     * @param {string} key                           The embedded Document ID.
     * @param {object} [options]                     Additional options to the delete operation.
     * @param {boolean} [options.modifySource=true]  Whether to modify the collection's source as part of the operation.
     * */
    delete(key, {modifySource=true, ...options}={}) {
      if ( modifySource ) this._delete(key, options);
      return super.delete(key);
    }

    /* -------------------------------------------- */

    /**
     * Remove the value from the underlying source array.
     * @param {string} key        The Document ID key.
     * @param {object} [options]  Additional options to configure deletion behavior.
     * @protected
     */
    _delete(key, options={}) {
      if ( this.has(key) || this.invalidDocumentIds.has(key) ) this._source.findSplice(d => d._id === key);
    }

    /* ---------------------------------------- */

    /**
     * Update an EmbeddedCollection using an array of provided document data.
     * @param {DataModel[]} changes         An array of provided Document data
     * @param {object} [options={}]         Additional options which modify how the collection is updated
     */
    update(changes, options={}) {
      const updated = new Set();

      // Create or update documents within the collection
      for ( let data of changes ) {
        if ( !data._id ) data._id = randomID(16);
        this._createOrUpdate(data, options);
        updated.add(data._id);
      }

      // If the update was not recursive, remove all non-updated documents
      if ( options.recursive === false ) {
        for ( const id of this._source.map(d => d._id) ) {
          if ( !updated.has(id) ) this.delete(id, options);
        }
      }
    }

    /* -------------------------------------------- */

    /**
     * Create or update an embedded Document in this collection.
     * @param {DataModel} data       The update delta.
     * @param {object} [options={}]  Additional options which modify how the collection is updated.
     * @protected
     */
    _createOrUpdate(data, options) {
      const current = this.get(data._id);
      if ( current ) current.updateSource(data, options);
      else {
        const doc = this.createDocument(data);
        this.set(doc.id, doc);
      }
    }

    /* ---------------------------------------- */

    /**
     * Obtain a temporary Document instance for a document id which currently has invalid source data.
     * @param {string} id                      A document ID with invalid source data.
     * @param {object} [options]               Additional options to configure retrieval.
     * @param {boolean} [options.strict=true]  Throw an Error if the requested ID is not in the set of invalid IDs for
     *                                         this collection.
     * @returns {Document}                     An in-memory instance for the invalid Document
     * @throws If strict is true and the requested ID is not in the set of invalid IDs for this collection.
     */
    getInvalid(id, {strict=true}={}) {
      if ( !this.invalidDocumentIds.has(id) ) {
        if ( strict ) throw new Error(`${this.constructor.documentName} id [${id}] is not in the set of invalid ids`);
        return;
      }
      const data = this._source.find(d => d._id === id);
      return this.documentClass.fromSource(foundry.utils.deepClone(data), {parent: this.model});
    }

    /* ---------------------------------------- */

    /**
     * Convert the EmbeddedCollection to an array of simple objects.
     * @param {boolean} [source=true]     Draw data for contained Documents from the underlying data source?
     * @returns {object[]}                The extracted array of primitive objects
     */
    toObject(source=true) {
      const arr = [];
      for ( let doc of this.values() ) {
        arr.push(doc.toObject(source));
      }
      return arr;
    }

    /* -------------------------------------------- */

    /**
     * Follow-up actions to take when a database operation modifies Documents in this EmbeddedCollection.
     * @param {DatabaseAction} action                   The database action performed
     * @param {foundry.abstract.Document[]} documents   The array of modified Documents
     * @param {any[]} result                            The result of the database operation
     * @param {DatabaseOperation} operation             Database operation details
     * @param {foundry.documents.BaseUser} user         The User who performed the operation
     * @internal
     */
    _onModifyContents(action, documents, result, operation, user) {}
  }

  /**
   * This class provides a {@link Collection} wrapper around a singleton embedded Document so that it can be interacted
   * with via a common interface.
   */
  class SingletonEmbeddedCollection extends EmbeddedCollection {
    /** @inheritdoc */
    set(key, value) {
      if ( this.size && !this.has(key) ) {
        const embeddedName = this.documentClass.documentName;
        const parentName = this.model.documentName;
        throw new Error(`Cannot create singleton embedded ${embeddedName} [${key}] in parent ${parentName} `
          + `[${this.model.id}] as it already has one assigned.`);
      }
      return super.set(key, value);
    }

    /* -------------------------------------------- */

    /** @override */
    _set(key, value) {
      this.model._source[this.name] = value?._source ?? null;
    }

    /* -------------------------------------------- */

    /** @override */
    _delete(key) {
      this.model._source[this.name] = null;
    }
  }

  /**
   * An embedded collection delta contains delta source objects that can be compared against other objects inside a base
   * embedded collection, and generate new embedded Documents by combining them.
   */
  class EmbeddedCollectionDelta extends EmbeddedCollection {
    /**
     * Maintain a list of IDs that are managed by this collection delta to distinguish from those IDs that are inherited
     * from the base collection.
     * @type {Set<string>}
     */
    #managedIds = new Set();

    /* -------------------------------------------- */

    /**
     * Maintain a list of IDs that are tombstone Documents.
     * @type {Set<string>}
     */
    #tombstones = new Set();

    /* -------------------------------------------- */

    /**
     * A convenience getter to return the corresponding base collection.
     * @type {EmbeddedCollection}
     */
    get baseCollection() {
      return this.model.getBaseCollection?.(this.name);
    }

    /* -------------------------------------------- */

    /**
     * A convenience getter to return the corresponding synthetic collection.
     * @type {EmbeddedCollection}
     */
    get syntheticCollection() {
      return this.model.syntheticActor?.getEmbeddedCollection(this.name);
    }

    /* -------------------------------------------- */

    /** @override */
    createDocument(data, context={}) {
      return new this.documentClass(data, {
        ...context,
        parent: this.model.syntheticActor ?? this.model,
        parentCollection: this.name,
        pack: this.model.pack
      });
    }

    /* -------------------------------------------- */

    /** @override */
    initialize({full=false, ...options} = {}) {
      // Repeat initialization.
      if ( this._initialized && !full ) return;

      // First-time initialization.
      this.clear();
      if ( !this.baseCollection ) return;

      // Initialize the deltas.
      for ( const d of this._source ) {
        if ( d._tombstone ) this.#tombstones.add(d._id);
        else this._initializeDocument(d, options);
        this.#managedIds.add(d._id);
      }

      // Include the Documents from the base collection.
      for ( const d of this.baseCollection._source ) {
        if ( this.has(d._id) || this.isTombstone(d._id) ) continue;
        this._initializeDocument(deepClone(d), options);
      }

      this._initialized = true;
    }

    /* -------------------------------------------- */

    /** @override */
    _initializeDocument(data, context) {
      if ( !data._id ) data._id = randomID(16);
      let doc;
      if ( this.syntheticCollection ) doc = this.syntheticCollection.get(data._id);
      else {
        try {
          doc = this.createDocument(data, context);
        } catch(err) {
          this._handleInvalidDocument(data._id, err, context);
        }
      }
      if ( doc ) super.set(doc.id, doc, {modifySource: false});
    }

    /* -------------------------------------------- */

    /** @override */
    _createOrUpdate(data, options) {
      if ( options.recursive === false ) {
        if ( data._tombstone ) return this.delete(data._id);
        else if ( this.isTombstone(data._id) ) return this.set(data._id, this.createDocument(data));
      }
      else if ( this.isTombstone(data._id) || data._tombstone ) return;
      let doc = this.get(data._id);
      if ( doc ) doc.updateSource(data, options);
      else doc = this.createDocument(data);
      this.set(doc.id, doc);
    }

    /* -------------------------------------------- */

    /**
     * Determine whether a given ID is managed directly by this collection delta or inherited from the base collection.
     * @param {string} key  The Document ID.
     * @returns {boolean}
     */
    manages(key) {
      return this.#managedIds.has(key);
    }

    /* -------------------------------------------- */

    /**
     * Determine whether a given ID exists as a tombstone Document in the collection delta.
     * @param {string} key  The Document ID.
     * @returns {boolean}
     */
    isTombstone(key) {
      return this.#tombstones.has(key);
    }

    /* -------------------------------------------- */

    /**
     * Restore a Document so that it is no longer managed by the collection delta and instead inherits from the base
     * Document.
     * @param {string} id            The Document ID.
     * @returns {Promise<Document>}  The restored Document.
     */
    async restoreDocument(id) {
      const docs = await this.restoreDocuments([id]);
      return docs.shift();
    }

    /* -------------------------------------------- */

    /**
     * Restore the given Documents so that they are no longer managed by the collection delta and instead inherit directly
     * from their counterparts in the base Actor.
     * @param {string[]} ids           The IDs of the Documents to restore.
     * @returns {Promise<Document[]>}  An array of updated Document instances.
     */
    async restoreDocuments(ids) {
      if ( !this.model.syntheticActor ) return [];
      const baseActor = this.model.parent.baseActor;
      const embeddedName = this.documentClass.documentName;
      const {deltas, tombstones} = ids.reduce((obj, id) => {
        if ( !this.manages(id) ) return obj;
        const doc = baseActor.getEmbeddedCollection(this.name).get(id);
        if ( this.isTombstone(id) ) obj.tombstones.push(doc.toObject());
        else obj.deltas.push(doc.toObject());
        return obj;
      }, {deltas: [], tombstones: []});

      // For the benefit of downstream CRUD workflows, we emulate events from the perspective of the synthetic Actor.
      // Restoring an Item to the version on the base Actor is equivalent to updating that Item on the synthetic Actor
      // with the version of the Item on the base Actor.
      // Restoring an Item that has been deleted on the synthetic Actor is equivalent to creating a new Item on the
      // synthetic Actor with the contents of the version on the base Actor.
      // On the ActorDelta, those Items are removed from this collection delta so that they are once again 'linked' to the
      // base Actor's Item, as though they had never been modified from the original in the first place.

      let updated = [];
      if ( deltas.length ) {
        updated = await this.model.syntheticActor.updateEmbeddedDocuments(embeddedName, deltas, {
          diff: false, recursive: false, restoreDelta: true
        });
      }

      let created = [];
      if ( tombstones.length ) {
        created = await this.model.syntheticActor.createEmbeddedDocuments(embeddedName, tombstones, {
          keepId: true, restoreDelta: true
        });
      }

      return updated.concat(created);
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    set(key, value, options={}) {
      super.set(key, value, options);
      this.syntheticCollection?.set(key, value, options);
    }

    /* -------------------------------------------- */

    /** @override */
    _set(key, value, {restoreDelta=false}={}) {
      if ( restoreDelta ) {
        this._source.findSplice(entry => entry._id === key);
        this.#managedIds.delete(key);
        this.#tombstones.delete(key);
        return;
      }

      if ( this.manages(key) ) this._source.findSplice(d => d._id === key, value._source);
      else this._source.push(value._source);
      this.#managedIds.add(key);
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    delete(key, options={}) {
      super.delete(key, options);
      this.syntheticCollection?.delete(key, options);
    }

    /* -------------------------------------------- */

    /** @override */
    _delete(key, {restoreDelta=false}={}) {
      if ( !this.baseCollection ) return;

      // Remove the document from this collection, if it exists.
      if ( this.manages(key) ) {
        this._source.findSplice(entry => entry._id === key);
        this.#managedIds.delete(key);
        this.#tombstones.delete(key);
      }

      // If the document exists in the base collection, push a tombstone in its place.
      if ( !restoreDelta && this.baseCollection.has(key) ) {
        this._source.push({_id: key, _tombstone: true});
        this.#managedIds.add(key);
        this.#tombstones.add(key);
      }
    }
  }

  /**
   * Determine the relative orientation of three points in two-dimensional space.
   * The result is also an approximation of twice the signed area of the triangle defined by the three points.
   * This method is fast - but not robust against issues of floating point precision. Best used with integer coordinates.
   * Adapted from https://github.com/mourner/robust-predicates.
   * @param {Point} a     An endpoint of segment AB, relative to which point C is tested
   * @param {Point} b     An endpoint of segment AB, relative to which point C is tested
   * @param {Point} c     A point that is tested relative to segment AB
   * @returns {number}    The relative orientation of points A, B, and C
   *                      A positive value if the points are in counter-clockwise order (C lies to the left of AB)
   *                      A negative value if the points are in clockwise order (C lies to the right of AB)
   *                      Zero if the points A, B, and C are collinear.
   */
  function orient2dFast(a, b, c) {
    return (a.y - c.y) * (b.x - c.x) - (a.x - c.x) * (b.y - c.y);
  }

  /* -------------------------------------------- */

  /**
   * Quickly test whether the line segment AB intersects with the line segment CD.
   * This method does not determine the point of intersection, for that use lineLineIntersection.
   * @param {Point} a                   The first endpoint of segment AB
   * @param {Point} b                   The second endpoint of segment AB
   * @param {Point} c                   The first endpoint of segment CD
   * @param {Point} d                   The second endpoint of segment CD
   * @returns {boolean}                 Do the line segments intersect?
   */
  function lineSegmentIntersects(a, b, c, d) {

    // First test the orientation of A and B with respect to CD to reject collinear cases
    const xa = foundry.utils.orient2dFast(a, b, c);
    const xb = foundry.utils.orient2dFast(a, b, d);
    if ( !xa && !xb ) return false;
    const xab = (xa * xb) <= 0;

    // Also require an intersection of CD with respect to AB
    const xcd = (foundry.utils.orient2dFast(c, d, a) * foundry.utils.orient2dFast(c, d, b)) <= 0;
    return xab && xcd;
  }

  /* -------------------------------------------- */

  /**
   * @typedef {Object}                  LineIntersection
   * @property {number} x               The x-coordinate of intersection
   * @property {number} y               The y-coordinate of intersection
   * @property {number} t0              The vector distance from A to B on segment AB
   * @property {number} [t1]            The vector distance from C to D on segment CD
   */

  /**
   * An internal helper method for computing the intersection between two infinite-length lines.
   * Adapted from http://paulbourke.net/geometry/pointlineplane/.
   * @param {Point} a                   The first endpoint of segment AB
   * @param {Point} b                   The second endpoint of segment AB
   * @param {Point} c                   The first endpoint of segment CD
   * @param {Point} d                   The second endpoint of segment CD
   * @param {object} [options]          Options which affect the intersection test
   * @param {boolean} [options.t1=false]    Return the optional vector distance from C to D on CD
   * @returns {LineIntersection|null}   An intersection point, or null if no intersection occurred
   */
  function lineLineIntersection(a, b, c, d, {t1=false}={}) {

    // If either line is length 0, they cannot intersect
    if (((a.x === b.x) && (a.y === b.y)) || ((c.x === d.x) && (c.y === d.y))) return null;

    // Check denominator - avoid parallel lines where d = 0
    const dnm = ((d.y - c.y) * (b.x - a.x) - (d.x - c.x) * (b.y - a.y));
    if (dnm === 0) return null;

    // Vector distances
    const t0 = ((d.x - c.x) * (a.y - c.y) - (d.y - c.y) * (a.x - c.x)) / dnm;
    t1 = t1 ? ((b.x - a.x) * (a.y - c.y) - (b.y - a.y) * (a.x - c.x)) / dnm : undefined;

    // Return the point of intersection
    return {
      x: a.x + t0 * (b.x - a.x),
      y: a.y + t0 * (b.y - a.y),
      t0: t0,
      t1: t1
    }
  }

  /* -------------------------------------------- */

  /**
   * An internal helper method for computing the intersection between two finite line segments.
   * Adapted from http://paulbourke.net/geometry/pointlineplane/
   * @param {Point} a                   The first endpoint of segment AB
   * @param {Point} b                   The second endpoint of segment AB
   * @param {Point} c                   The first endpoint of segment CD
   * @param {Point} d                   The second endpoint of segment CD
   * @param {number} [epsilon]          A small epsilon which defines a tolerance for near-equality
   * @returns {LineIntersection|null}   An intersection point, or null if no intersection occurred
   */
  function lineSegmentIntersection(a, b, c, d, epsilon=1e-8) {

    // If either line is length 0, they cannot intersect
    if (((a.x === b.x) && (a.y === b.y)) || ((c.x === d.x) && (c.y === d.y))) return null;

    // Check denominator - avoid parallel lines where d = 0
    const dnm = ((d.y - c.y) * (b.x - a.x) - (d.x - c.x) * (b.y - a.y));
    if (dnm === 0) return null;

    // Vector distance from a
    const t0 = ((d.x - c.x) * (a.y - c.y) - (d.y - c.y) * (a.x - c.x)) / dnm;
    if ( !Number.between(t0, 0-epsilon, 1+epsilon) ) return null;

    // Vector distance from c
    const t1 = ((b.x - a.x) * (a.y - c.y) - (b.y - a.y) * (a.x - c.x)) / dnm;
    if ( !Number.between(t1, 0-epsilon, 1+epsilon) ) return null;

    // Return the point of intersection and the vector distance from both line origins
    return {
      x: a.x + t0 * (b.x - a.x),
      y: a.y + t0 * (b.y - a.y),
      t0: Math.clamp(t0, 0, 1),
      t1: Math.clamp(t1, 0, 1)
    }
  }

  /* -------------------------------------------- */

  /**
   * @typedef {Object} LineCircleIntersection
   * @property {boolean} aInside        Is point A inside the circle?
   * @property {boolean} bInside        Is point B inside the circle?
   * @property {boolean} contained      Is the segment AB contained within the circle?
   * @property {boolean} outside        Is the segment AB fully outside the circle?
   * @property {boolean} tangent        Is the segment AB tangent to the circle?
   * @property {Point[]} intersections  Intersection points: zero, one, or two
   */

  /**
   * Determine the intersection between a line segment and a circle.
   * @param {Point} a                   The first vertex of the segment
   * @param {Point} b                   The second vertex of the segment
   * @param {Point} center              The center of the circle
   * @param {number} radius             The radius of the circle
   * @param {number} epsilon            A small tolerance for floating point precision
   * @returns {LineCircleIntersection}  The intersection of the segment AB with the circle
   */
  function lineCircleIntersection(a, b, center, radius, epsilon=1e-8) {
    const r2 = Math.pow(radius, 2);
    let intersections = [];

    // Test whether endpoint A is contained
    const ar2 = Math.pow(a.x - center.x, 2) + Math.pow(a.y - center.y, 2);
    const aInside = ar2 < r2 - epsilon;

    // Test whether endpoint B is contained
    const br2 = Math.pow(b.x - center.x, 2) + Math.pow(b.y - center.y, 2);
    const bInside = br2 < r2 - epsilon;

    // Find quadratic intersection points
    const contained = aInside && bInside;
    if ( !contained ) intersections = quadraticIntersection(a, b, center, radius, epsilon);

    // Return the intersection data
    return {
      aInside,
      bInside,
      contained,
      outside: !contained && !intersections.length,
      tangent: !aInside && !bInside && intersections.length === 1,
      intersections
    };
  }

  /* -------------------------------------------- */

  /**
   * Identify the point closest to C on segment AB
   * @param {Point} c     The reference point C
   * @param {Point} a     Point A on segment AB
   * @param {Point} b     Point B on segment AB
   * @returns {Point}     The closest point to C on segment AB
   */
  function closestPointToSegment(c, a, b) {
    const dx = b.x - a.x;
    const dy = b.y - a.y;
    if (( dx === 0 ) && ( dy === 0 )) {
      throw new Error("Zero-length segment AB not supported");
    }
    const u = (((c.x - a.x) * dx) + ((c.y - a.y) * dy)) / (dx * dx + dy * dy);
    if ( u < 0 ) return a;
    if ( u > 1 ) return b;
    else return {
      x: a.x + (u * dx),
      y: a.y + (u * dy)
    }
  }

  /* -------------------------------------------- */

  /**
   * Determine the points of intersection between a line segment (p0,p1) and a circle.
   * There will be zero, one, or two intersections
   * See https://math.stackexchange.com/a/311956.
   * @param {Point} p0            The initial point of the line segment
   * @param {Point} p1            The terminal point of the line segment
   * @param {Point} center        The center of the circle
   * @param {number} radius       The radius of the circle
   * @param {number} [epsilon=0]  A small tolerance for floating point precision
   */
  function quadraticIntersection(p0, p1, center, radius, epsilon=0) {
    const dx = p1.x - p0.x;
    const dy = p1.y - p0.y;

    // Quadratic terms where at^2 + bt + c = 0
    const a = Math.pow(dx, 2) + Math.pow(dy, 2);
    const b = (2 * dx * (p0.x - center.x)) + (2 * dy * (p0.y - center.y));
    const c = Math.pow(p0.x - center.x, 2) + Math.pow(p0.y - center.y, 2) - Math.pow(radius, 2);

    // Discriminant
    let disc2 = Math.pow(b, 2) - (4 * a * c);
    if ( disc2.almostEqual(0) ) disc2 = 0; // segment endpoint touches the circle; 1 intersection
    else if ( disc2 < 0 ) return []; // no intersections

    // Roots
    const disc = Math.sqrt(disc2);
    const t1 = (-b - disc) / (2 * a);

    // If t1 hits (between 0 and 1) it indicates an "entry"
    const intersections = [];
    if ( t1.between(0-epsilon, 1+epsilon) ) {
      intersections.push({
        x: p0.x + (dx * t1),
        y: p0.y + (dy * t1)
      });
    }
    if ( !disc2 ) return intersections; // 1 intersection

    // If t2 hits (between 0 and 1) it indicates an "exit"
    const t2 = (-b + disc) / (2 * a);
    if ( t2.between(0-epsilon, 1+epsilon) ) {
      intersections.push({
        x: p0.x + (dx * t2),
        y: p0.y + (dy * t2)
      });
    }
    return intersections;
  }

  /* -------------------------------------------- */

  /**
   * Calculate the centroid non-self-intersecting closed polygon.
   * See https://en.wikipedia.org/wiki/Centroid#Of_a_polygon.
   * @param {Point[]|number[]} points    The points of the polygon
   * @returns {Point}                    The centroid of the polygon
   */
  function polygonCentroid(points) {
    const n = points.length;
    if ( n === 0 ) return {x: 0, y: 0};
    let x = 0;
    let y = 0;
    let a = 0;
    if ( typeof points[0] === "number" ) {
      let x0 = points[n - 2];
      let y0 = points[n - 1];
      for ( let i = 0; i < n; i += 2 ) {
        const x1 = points[i];
        const y1 = points[i + 1];
        const z = (x0 * y1) - (x1 * y0);
        x += (x0 + x1) * z;
        y += (y0 + y1) * z;
        x0 = x1;
        y0 = y1;
        a += z;
      }
    } else {
      let {x: x0, y: y0} = points[n - 1];
      for ( let i = 0; i < n; i++ ) {
        const {x: x1, y: y1} = points[i];
        const z = (x0 * y1) - (x1 * y0);
        x += (x0 + x1) * z;
        y += (y0 + y1) * z;
        x0 = x1;
        y0 = y1;
        a += z;
      }
    }
    a *= 3;
    x /= a;
    y /= a;
    return {x, y};
  }

  /* -------------------------------------------- */

  /**
   * Test whether the circle given by the center and radius intersects the path (open or closed).
   * @param {Point[]|number[]} points    The points of the path
   * @param {boolean} close              If true, the edge from the last to the first point is tested
   * @param {Point} center               The center of the circle
   * @param {number} radius              The radius of the circle
   * @returns {boolean}                  Does the circle intersect the path?
   */
  function pathCircleIntersects(points, close, center, radius) {
    const n = points.length;
    if ( n === 0 ) return false;
    const {x: cx, y: cy} = center;
    const rr = radius * radius;
    let i;
    let x0;
    let y0;
    if ( typeof points[0] === "number" ) {
      if ( close ) {
        i = 0;
        x0 = points[n - 2];
        y0 = points[n - 1];
      } else {
        i = 2;
        x0 = points[0];
        y0 = points[1];
      }
      for ( ; i < n; i += 2 ) {
        const x1 = points[i];
        const y1 = points[i + 1];
        let dx = cx - x0;
        let dy = cy - y0;
        const nx = x1 - x0;
        const ny = y1 - y0;
        const t = Math.clamp(((dx * nx) + (dy * ny)) / ((nx * nx) + (ny * ny)), 0, 1);
        dx = (t * nx) - dx;
        dy = (t * ny) - dy;
        if ( (dx * dx) + (dy * dy) <= rr ) return true;
        x0 = x1;
        y0 = y1;
      }
    } else {
      if ( close ) {
        i = 0;
        ({x: x0, y: y0} = points[n - 1]);
      } else {
        i = 1;
        ({x: x0, y: y0} = points[0]);
      }
      for ( ; i < n; i++ ) {
        const {x: x2, y: y2} = points[i];
        let dx = cx - x0;
        let dy = cy - y0;
        const nx = x1 - x0;
        const ny = y1 - y0;
        const t = Math.clamp(((dx * nx) + (dy * ny)) / ((nx * nx) + (ny * ny)), 0, 1);
        dx = (t * nx) - dx;
        dy = (t * ny) - dy;
        if ( (dx * dx) + (dy * dy) <= rr ) return true;
        x0 = x2;
        y0 = y2;
      }
    }
    return false;
  }

  /* -------------------------------------------- */

  /**
   * Test whether two circles (with position and radius) intersect.
   * @param {number} x0    x center coordinate of circle A.
   * @param {number} y0    y center coordinate of circle A.
   * @param {number} r0    radius of circle A.
   * @param {number} x1    x center coordinate of circle B.
   * @param {number} y1    y center coordinate of circle B.
   * @param {number} r1    radius of circle B.
   * @returns {boolean}    True if the two circles intersect, false otherwise.
   */
  function circleCircleIntersects(x0, y0, r0, x1, y1, r1) {
    return Math.hypot(x0 - x1, y0 - y1) <= (r0 + r1);
  }

  /**
   * A wrapper method around `fetch` that attaches an AbortController signal to the `fetch` call for clean timeouts
   * @see https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal#aborting_a_fetch_with_timeout_or_explicit_abort
   * @param {string} url            The URL to make the Request to
   * @param {Object} data           The data of the Request
   * @param {number|null} timeoutMs How long to wait for a Response before cleanly aborting.
   *                                If null, no timeout is applied
   * @param {function} onTimeout    A method to invoke if and when the timeout is reached
   * @return {Promise<Response>}
   * @throws {HttpError}
   */
  async function fetchWithTimeout(url, data = {}, {timeoutMs=30000, onTimeout = () => {}} = {}) {
    const controller = new AbortController();
    data.signal = controller.signal;
    let timedOut = false;
    const enforceTimeout = timeoutMs !== null;

    // Enforce a timeout
    let timeout;
    if ( enforceTimeout ) {
      timeout = setTimeout(() => {
        timedOut = true;
        controller.abort();
        onTimeout();
      }, timeoutMs);
    }

    // Attempt the request
    let response;
    try {
      response = await fetch(url, data);
    } catch(err) {
      if ( timedOut ) {
        const timeoutS = Math.round(timeoutMs / 1000);
        const msg = game.i18n
          ? game.i18n.format("SETUP.ErrorTimeout", { url, timeout: timeoutS })
          : `The request to ${url} timed out after ${timeoutS}s.`;
        throw new HttpError("Timed Out", 408, msg);
      }
      throw err;
    } finally {
      if ( enforceTimeout ) clearTimeout(timeout);
    }

    // Return the response
    if ( !response.ok && (response.type !== "opaqueredirect") ) {
      const responseBody = response.body ? await response.text() : "";
      throw new HttpError(response.statusText, response.status, responseBody);
    }
    return response;
  }

  /* ----------------------------------------- */

  /**
   * A small wrapper that automatically asks for JSON with a Timeout
   * @param {string} url          The URL to make the Request to
   * @param {Object} data         The data of the Request
   * @param {int} timeoutMs       How long to wait for a Response before cleanly aborting
   * @param {function} onTimeout  A method to invoke if and when the timeout is reached
   * @returns {Promise<*>}
   */
  async function fetchJsonWithTimeout(url, data = {}, {timeoutMs=30000, onTimeout = () => {}} = {}) {
    let response = await fetchWithTimeout(url, data, {timeoutMs: timeoutMs, onTimeout: onTimeout});
    return response.json();
  }

  /* ----------------------------------------- */

  /**
   * Represents an HTTP Error when a non-OK response is returned by Fetch
   * @extends {Error}
   */
  class HttpError extends Error {
    constructor(statusText, code, displayMessage="") {
      super(statusText);
      this.code = code;
      this.displayMessage = displayMessage;
    }

    /* -------------------------------------------- */

    /** @override */
    toString() {
      return this.displayMessage;
    }
  }

  /**
   * @typedef {import("../types.mjs").Constructor} Constructor
   */

  /**
   * @callback EmittedEventListener
   * @param {Event} event         The emitted event
   * @returns {any}
   */

  /**
   * Augment a base class with EventEmitter behavior.
   * @template {Constructor} BaseClass
   * @param {BaseClass} BaseClass         Some base class augmented with event emitter functionality
   */
  function EventEmitterMixin(BaseClass) {
    /**
     * A mixin class which implements the behavior of EventTarget.
     * This is useful in cases where a class wants EventTarget-like behavior but needs to extend some other class.
     * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
     */
    class EventEmitter extends BaseClass {

      /**
       * An array of event types which are valid for this class.
       * @type {string[]}
       */
      static emittedEvents = [];

      /**
       * A mapping of registered events.
       * @type {Record<string, Map<EmittedEventListener, {fn: EmittedEventListener, once: boolean}>>}
       */
      #events = {};

      /* -------------------------------------------- */

      /**
       * Add a new event listener for a certain type of event.
       * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
       * @param {string} type                     The type of event being registered for
       * @param {EmittedEventListener} listener   The listener function called when the event occurs
       * @param {object} [options={}]             Options which configure the event listener
       * @param {boolean} [options.once=false]      Should the event only be responded to once and then removed
       */
      addEventListener(type, listener, {once = false} = {}) {
        if ( !this.constructor.emittedEvents.includes(type) ) {
          throw new Error(`"${type}" is not a supported event of the ${this.constructor.name} class`);
        }
        this.#events[type] ||= new Map();
        this.#events[type].set(listener, {fn: listener, once});
      }

      /* -------------------------------------------- */

      /**
       * Remove an event listener for a certain type of event.
       * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
       * @param {string} type                     The type of event being removed
       * @param {EmittedEventListener} listener   The listener function being removed
       */
      removeEventListener(type, listener) {
        this.#events[type]?.delete(listener);
      }

      /* -------------------------------------------- */

      /**
       * Dispatch an event on this target.
       * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent
       * @param {Event} event                     The Event to dispatch
       * @returns {boolean}                       Was default behavior for the event prevented?
       */
      dispatchEvent(event) {
        if ( !(event instanceof Event) ) {
          throw new Error("EventEmitter#dispatchEvent must be provided an Event instance");
        }
        if ( !this.constructor.emittedEvents.includes(event?.type) ) {
          throw new Error(`"${event.type}" is not a supported event of the ${this.constructor.name} class`);
        }
        const listeners = this.#events[event.type];
        if ( !listeners ) return true;

        // Extend and configure the Event
        Object.defineProperties(event, {
          target: {value: this},
          stopPropagation: {value: function() {
            event.propagationStopped = true;
            Event.prototype.stopPropagation.call(this);
          }},
          stopImmediatePropagation: {value: function() {
            event.propagationStopped = true;
            Event.prototype.stopImmediatePropagation.call(this);
          }}
        });

        // Call registered listeners
        for ( const listener of listeners.values() ) {
          listener.fn(event);
          if ( listener.once ) this.removeEventListener(event.type, listener.fn);
          if ( event.propagationStopped ) break;
        }
        return event.defaultPrevented;
      }
    }
    return EventEmitter;
  }

  /**
   * Stores a map of objects with weak references to the keys, allowing them to be garbage collected. Both keys and values
   * can be iterated over, unlike a WeakMap.
   */
  class IterableWeakMap extends WeakMap {
    /**
     * @typedef {object} IterableWeakMapHeldValue
     * @property {Set<WeakRef<any>>} set  The set to be cleaned.
     * @property {WeakRef<any>} ref       The ref to remove.
     */

    /**
     * @typedef {object} IterableWeakMapValue
     * @property {any} value         The value.
     * @property {WeakRef<any>} ref  The weak ref of the key.
     */

    /**
     * A set of weak refs to the map's keys, allowing enumeration.
     * @type {Set<WeakRef<any>>}
     */
    #refs = new Set();

    /**
     * A FinalizationRegistry instance to clean up the ref set when objects are garbage collected.
     * @type {FinalizationRegistry<IterableWeakMapHeldValue>}
     */
    #finalizer = new FinalizationRegistry(IterableWeakMap.#cleanup);

    /**
     * @param {Iterable<[any, any]>} [entries]  The initial entries.
     */
    constructor(entries=[]) {
      super();
      for ( const [key, value] of entries ) this.set(key, value);
    }

    /* -------------------------------------------- */

    /**
     * Clean up the corresponding ref in the set when its value is garbage collected.
     * @param {IterableWeakMapHeldValue} heldValue  The value held by the finalizer.
     */
    static #cleanup({ set, ref }) {
      set.delete(ref);
    }

    /* -------------------------------------------- */

    /**
     * Remove a key from the map.
     * @param {any} key  The key to remove.
     * @returns {boolean}
     */
    delete(key) {
      const entry = super.get(key);
      if ( !entry ) return false;
      super.delete(key);
      this.#refs.delete(entry.ref);
      this.#finalizer.unregister(key);
      return true;
    }

    /* -------------------------------------------- */

    /**
     * Retrieve a value from the map.
     * @param {any} key  The value's key.
     * @returns {any}
     */
    get(key) {
      const entry = super.get(key);
      return entry && entry.value;
    }

    /* -------------------------------------------- */

    /**
     * Place a value in the map.
     * @param {any} key    The key.
     * @param {any} value  The value.
     * @returns {IterableWeakMap}
     */
    set(key, value) {
      const entry = super.get(key);
      if ( entry ) this.#refs.delete(entry.ref);
      const ref = new WeakRef(key);
      super.set(key, { value, ref });
      this.#refs.add(ref);
      this.#finalizer.register(key, { ref, set: this.#refs }, key);
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Clear all values from the map.
     */
    clear() {
      for ( const ref of this.#refs ) {
        const key = ref.deref();
        if ( key ) this.delete(key);
        else this.#refs.delete(ref);
      }
    }

    /* -------------------------------------------- */

    /**
     * Enumerate the entries.
     * @returns {Generator<[any, any], void, any>}
     */
    *[Symbol.iterator]() {
      for ( const ref of this.#refs ) {
        const key = ref.deref();
        if ( !key ) continue;
        const { value } = super.get(key);
        yield [key, value];
      }
    }

    /* -------------------------------------------- */

    /**
     * Enumerate the entries.
     * @returns {Generator<[any, any], void, any>}
     */
    entries() {
      return this[Symbol.iterator]();
    }

    /* -------------------------------------------- */

    /**
     * Enumerate the keys.
     * @returns {Generator<any, void, any>}
     */
    *keys() {
      for ( const [key] of this ) yield key;
    }

    /* -------------------------------------------- */

    /**
     * Enumerate the values.
     * @returns {Generator<any, void, any>}
     */
    *values() {
      for ( const [, value] of this ) yield value;
    }
  }

  /**
   * Stores a set of objects with weak references to them, allowing them to be garbage collected. Can be iterated over,
   * unlike a WeakSet.
   */
  class IterableWeakSet extends WeakSet {
    /**
     * The backing iterable weak map.
     * @type {IterableWeakMap<any, any>}
     */
    #map = new IterableWeakMap();

    /**
     * @param {Iterable<any>} [entries]  The initial entries.
     */
    constructor(entries=[]) {
      super();
      for ( const entry of entries ) this.add(entry);
    }

    /* -------------------------------------------- */

    /**
     * Enumerate the values.
     * @returns {Generator<any, void, any>}
     */
    [Symbol.iterator]() {
      return this.values();
    }

    /* -------------------------------------------- */

    /**
     * Add a value to the set.
     * @param {any} value  The value to add.
     * @returns {IterableWeakSet}
     */
    add(value) {
      this.#map.set(value, value);
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Delete a value from the set.
     * @param {any} value  The value to delete.
     * @returns {boolean}
     */
    delete(value) {
      return this.#map.delete(value);
    }

    /* -------------------------------------------- */

    /**
     * Whether this set contains the given value.
     * @param {any} value  The value to test.
     * @returns {boolean}
     */
    has(value) {
      return this.#map.has(value);
    }

    /* -------------------------------------------- */

    /**
     * Enumerate the collection.
     * @returns {Generator<any, void, any>}
     */
    values() {
      return this.#map.values();
    }

    /* -------------------------------------------- */

    /**
     * Clear all values from the set.
     */
    clear() {
      this.#map.clear();
    }
  }

  /**
   * A simple Semaphore implementation which provides a limited queue for ensuring proper concurrency.
   * @param {number} [max=1]    The maximum number of tasks which are allowed concurrently.
   *
   * @example Using a Semaphore
   * ```js
   * // Some async function that takes time to execute
   * function fn(x) {
   *   return new Promise(resolve => {
   *     setTimeout(() => {
   *       console.log(x);
   *       resolve(x);
   *     }, 1000));
   *   }
   * };
   *
   * // Create a Semaphore and add many concurrent tasks
   * const semaphore = new Semaphore(1);
   * for ( let i of Array.fromRange(100) ) {
   *   semaphore.add(fn, i);
   * }
   * ```
   */
  class Semaphore {
    constructor(max=1) {

      /**
       * The maximum number of tasks which can be simultaneously attempted.
       * @type {number}
       */
      this.max = max;

      /**
       * A queue of pending function signatures
       * @type {Array<Array<Function|*>>}
       * @private
       */
      this._queue = [];

      /**
       * The number of tasks which are currently underway
       * @type {number}
       * @private
       */
      this._active = 0;
    }

    /**
     * The number of pending tasks remaining in the queue
     * @type {number}
     */
    get remaining() {
      return this._queue.length;
    }

    /**
     * The number of actively executing tasks
     * @type {number}
     */
    get active() {
      return this._active;
    }

    /**
     * Add a new tasks to the managed queue
     * @param {Function} fn     A callable function
     * @param {...*} [args]     Function arguments
     * @returns {Promise}       A promise that resolves once the added function is executed
     */
    add(fn, ...args) {
      return new Promise((resolve, reject) => {
        this._queue.push([fn, args, resolve, reject]);
        return this._try();
      });
    }

    /**
     * Abandon any tasks which have not yet concluded
     */
    clear() {
      this._queue = [];
    }

    /**
     * Attempt to perform a task from the queue.
     * If all workers are busy, do nothing.
     * If successful, try again.
     * @private
     */
    async _try() {
      if ( (this.active === this.max) || !this.remaining ) return false;

      // Obtain the next task from the queue
      const next = this._queue.shift();
      if ( !next ) return;
      this._active += 1;

      // Try and execute it, resolving its promise
      const [fn, args, resolve, reject] = next;
      try {
        const r = await fn(...args);
        resolve(r);
      }
      catch(err) {
        reject(err);
      }

      // Try the next function in the queue
      this._active -= 1;
      return this._try();
    }
  }

  /**
   * Create a new BitMask instance.
   * @param {Record<string, boolean>} [states=null] An object containing valid states and their corresponding initial boolean values (default is null).
   */
  class BitMask extends Number {
    constructor(states=null) {
      super();
      this.#generateValidStates(states);
      this.#generateEnum();
      this.#value = this.#computeValue(states);
    }

    /**
     * The real value behind the bitmask instance.
     * @type {number}
     */
    #value;

    /**
     * The structure of valid states and their associated values.
     * @type {Map<string, number>}
     */
    #validStates;

    /**
     * The enum associated with this structure.
     * @type {Record<string, string>}
     * @readonly
     */
    states;

    /* -------------------------------------------- */
    /*  Internals                                   */
    /* -------------------------------------------- */

    /**
     * Generates the valid states and their associated values.
     * @param {Record<string, boolean>} [states=null] The structure defining the valid states and their associated values.
     */
    #generateValidStates(states) {
      this.#validStates = new Map();
      let bitIndex = 0;
      for ( const state of Object.keys(states || {}) ) {
        if ( bitIndex >= 32 ) throw new Error("A bitmask can't handle more than 32 states");
        this.#validStates.set(state, 1 << bitIndex++);
      }
    }

    /* -------------------------------------------- */

    /**
     * Generates an enum based on the provided valid states.
     */
    #generateEnum() {
      this.states = {};
      for ( const state of this.#validStates.keys() ) this.states[state] = state;
      Object.freeze(this.states);
    }

    /* -------------------------------------------- */

    /**
     * Calculate the default value of the bitmask based on the initial states
     * @param {Record<string, boolean>} [initialStates={}] The structure defining the valid states and their associated values.
     * @returns {number}
     */
    #computeValue(initialStates={}) {
      let defaultValue = 0;
      for ( const state in initialStates ) {
        if ( !initialStates.hasOwnProperty(state) ) continue;
        this.#checkState(state);
        if ( initialStates[state] ) defaultValue |= this.#validStates.get(state);
      }
      return defaultValue;
    }

    /* -------------------------------------------- */

    /**
     * Checks a state and throws an error if it doesn't exist.
     * @param {string} state   Name of the state to check.
     */
    #checkState(state) {
      if ( !this.#validStates.has(state) ) {
        throw new Error(`${state} is an invalid state for this BitMask instance: ${this.toJSON()}`);
      }
    }

    /* -------------------------------------------- */
    /*  Properties                                  */
    /* -------------------------------------------- */

    /**
     * True if this bitmask is empty (no active states).
     * @type {boolean}
     */
    get isEmpty() {
      return this.#value === 0;
    }

    /* -------------------------------------------- */
    /*  Methods for Handling states                 */
    /* -------------------------------------------- */

    /**
     * Check if a specific state is active.
     * @param {string} state The state to check.
     * @returns {boolean} True if the state is active, false otherwise.
     */
    hasState(state) {
      return (this.#value & this.#validStates.get(state)) !== 0;
    }

    /* -------------------------------------------- */

    /**
     * Add a state to the bitmask.
     * @param {string} state The state to add.
     * @throws {Error} Throws an error if the provided state is not valid.
     */
    addState(state) {
      this.#checkState(state);
      this.#value |= this.#validStates.get(state);
    }

    /* -------------------------------------------- */

    /**
     * Remove a state from the bitmask.
     * @param {string} state The state to remove.
     * @throws {Error} Throws an error if the provided state is not valid.
     */
    removeState(state) {
      this.#checkState(state);
      this.#value &= ~this.#validStates.get(state);
    }

    /* -------------------------------------------- */

    /**
     * Toggle the state of a specific state in the bitmask.
     * @param {string} state The state to toggle.
     * @param {boolean} [enabled] Toggle on (true) or off (false)? If undefined, the state is switched automatically.
     * @throws {Error} Throws an error if the provided state is not valid.
     */
    toggleState(state, enabled) {
      this.#checkState(state);
      if ( enabled === undefined ) return (this.#value ^= this.#validStates.get(state));
      if ( enabled ) this.addState(state);
      else this.removeState(state);
    }

    /* -------------------------------------------- */

    /**
     * Clear the bitmask, setting all states to inactive.
     */
    clear() {
      this.#value = 0;
    }

    /* -------------------------------------------- */
    /*  bitmask representations                     */
    /* -------------------------------------------- */

    /**
     * Get the current value of the bitmask.
     * @returns {number} The current value of the bitmask.
     */
    valueOf() {
      return this.#value;
    }

    /* -------------------------------------------- */

    /**
     * Get a string representation of the bitmask in binary format.
     * @returns {string} The string representation of the bitmask.
     */
    toString() {
      return String(this.#value.toString(2)).padStart(this.#validStates.size, '0');
    }

    /* -------------------------------------------- */

    /**
     * Checks if two bitmasks structures are compatible (the same valid states).
     * @param {BitMask} otherBitMask The bitmask structure to compare with.
     * @returns {boolean} True if the two bitmasks have the same structure, false otherwise.
     */
    isCompatible(otherBitMask) {
      const states1 = Array.from(this.#validStates.keys()).sort().join(',');
      const states2 = Array.from(otherBitMask.#validStates.keys()).sort().join(',');
      return states1 === states2;
    }

    /* -------------------------------------------- */

    /**
     * Serializes the bitmask to a JSON string.
     * @returns {string} The JSON string representing the bitmask.
     */
    toJSON() {
      return JSON.stringify(this.toObject());
    }

    /* -------------------------------------------- */

    /**
     * Creates a new BitMask instance from a JSON string.
     * @param {string} jsonString The JSON string representing the bitmask.
     * @returns {BitMask} A new BitMask instance created from the JSON string.
     */
    static fromJSON(jsonString) {
      const data = JSON.parse(jsonString);
      return new BitMask(data);
    }

    /* -------------------------------------------- */

    /**
     * Convert value of this BitMask to object representation according to structure.
     * @returns {Object} The data represented by the bitmask.
     */
    toObject() {
      const result = {};
      for ( const [validState, value] of this.#validStates ) result[validState] = ((this.#value & value) !== 0);
      return result;
    }

    /* -------------------------------------------- */

    /**
     * Creates a clone of this BitMask instance.
     * @returns {BitMask} A new BitMask instance with the same value and valid states as this instance.
     */
    clone() {
      return new BitMask(this.toObject());
    }

    /* -------------------------------------------- */
    /*  Static Helpers                              */
    /* -------------------------------------------- */

    /**
     * Generates shader constants based on the provided states.
     * @param {string[]} states An array containing valid states.
     * @returns {string} Shader bit mask constants generated from the states.
     */
    static generateShaderBitMaskConstants(states) {
      let shaderConstants = '';
      let bitIndex = 0;
      for ( const state of states ) {
        shaderConstants += `const uint ${state.toUpperCase()} = 0x${(1 << bitIndex).toString(16).toUpperCase()}U;\n`;
        bitIndex++;
      }
      return shaderConstants;
    }
  }

  /**
   * A string tree node consists of zero-or-more string keys, and a leaves property that contains any objects that
   * terminate at the current node.
   * @typedef {object} StringTreeNode
   */

  /**
   * @callback StringTreeEntryFilter
   * @param {any} entry  The entry to filter.
   * @returns {boolean}  Whether the entry should be included in the result set.
   */

  /**
   * A data structure representing a tree of string nodes with arbitrary object leaves.
   */
  class StringTree {
    /**
     * The key symbol that stores the leaves of any given node.
     * @type {symbol}
     */
    static get leaves() {
      return StringTree.#leaves;
    }

    static #leaves = Symbol();

    /* -------------------------------------------- */

    /**
     * The tree's root.
     * @type {StringTreeNode}
     */
    #root = this.#createNode();

    /* -------------------------------------------- */

    /**
     * Create a new node.
     * @returns {StringTreeNode}
     */
    #createNode() {
      return { [StringTree.leaves]: [] };
    }

    /* -------------------------------------------- */

    /**
     * Insert an entry into the tree.
     * @param {string[]} strings  The string parents for the entry.
     * @param {any} entry         The entry to store.
     * @returns {StringTreeNode}  The node the entry was added to.
     */
    addLeaf(strings, entry) {
      let node = this.#root;
      for ( const string of strings ) {
        node[string] ??= this.#createNode();
        node = node[string];
      }

      // Once we've traversed the tree, we add our entry.
      node[StringTree.leaves].push(entry);
      return node;
    }

    /* -------------------------------------------- */

    /**
     * Traverse the tree along the given string path and return any entries reachable from the node.
     * @param {string[]} strings                               The string path to the desired node.
     * @param {object} [options]
     * @param {number} [options.limit]                         The maximum number of items to retrieve.
     * @param {StringTreeEntryFilter} [options.filterEntries]  A filter function to apply to each candidate entry.
     * @returns {any[]}
     */
    lookup(strings, { limit, filterEntries }={}) {
      const entries = [];
      const node = this.nodeAtPrefix(strings);
      if ( !node ) return []; // No matching entries.
      const queue = [node];
      while ( queue.length ) {
        if ( limit && (entries.length >= limit) ) break;
        this._breadthFirstSearch(queue.shift(), entries, queue, { limit, filterEntries });
      }
      return entries;
    }

    /* -------------------------------------------- */

    /**
     * Returns the node at the given path through the tree.
     * @param {string[]} strings                    The string path to the desired node.
     * @param {object} [options]
     * @param {boolean} [options.hasLeaves=false]   Only return the most recently visited node that has leaves, otherwise
     *                                              return the exact node at the prefix, if it exists.
     * @returns {StringTreeNode|void}
     */
    nodeAtPrefix(strings, { hasLeaves=false }={}) {
      let node = this.#root;
      let withLeaves = node;
      for ( const string of strings ) {
        if ( !(string in node) ) return hasLeaves ? withLeaves : undefined;
        node = node[string];
        if ( node[StringTree.leaves].length ) withLeaves = node;
      }
      return hasLeaves ? withLeaves : node;
    }

    /* -------------------------------------------- */

    /**
     * Perform a breadth-first search starting from the given node and retrieving any entries reachable from that node,
     * until we reach the limit.
     * @param {StringTreeNode} node                            The starting node.
     * @param {any[]} entries                                  The accumulated entries.
     * @param {StringTreeNode[]} queue                         The working queue of nodes to search.
     * @param {object} [options]
     * @param {number} [options.limit]                         The maximum number of entries to retrieve before stopping.
     * @param {StringTreeEntryFilter} [options.filterEntries]  A filter function to apply to each candidate entry.
     * @protected
     */
    _breadthFirstSearch(node, entries, queue, { limit, filterEntries }={}) {
      // Retrieve the entries at this node.
      let leaves = node[StringTree.leaves];
      if ( filterEntries instanceof Function ) leaves = leaves.filter(filterEntries);
      entries.push(...leaves);
      if ( limit && (entries.length >= limit) ) return;
      // Push this node's children onto the end of the queue.
      for ( const key of Object.keys(node) ) {
        if ( typeof key === "string" ) queue.push(node[key]);
      }
    }
  }

  /**
   * @typedef {import("./string-tree.mjs").StringTreeNode} StringTreeNode
   */

  /**
   * A leaf entry in the tree.
   * @typedef {object} WordTreeEntry
   * @property {Document|object} entry  An object that this entry represents.
   * @property {string} documentName    The document type.
   * @property {string} uuid            The document's UUID.
   * @property {string} [pack]          The pack ID.
   */

  /**
   * A data structure for quickly retrieving objects by a string prefix.
   * Note that this works well for languages with alphabets (latin, cyrillic, korean, etc.), but may need more nuanced
   * handling for languages that compose characters and letters.
   * @extends {StringTree}
   */
  class WordTree extends StringTree {
    /**
     * Insert an entry into the tree.
     * @param {string} string        The string key for the entry.
     * @param {WordTreeEntry} entry  The entry to store.
     * @returns {StringTreeNode}     The node the entry was added to.
     */
    addLeaf(string, entry) {
      string = string.toLocaleLowerCase(game.i18n.lang);
      return super.addLeaf(Array.from(string), entry);
    }

    /* -------------------------------------------- */

    /**
     * Return entries that match the given string prefix.
     * @param {string} prefix              The prefix.
     * @param {object} [options]           Additional options to configure behaviour.
     * @param {number} [options.limit=10]  The maximum number of items to retrieve. It is important to set this value as
     *                                     very short prefixes will naturally match large numbers of entries.
     * @param {StringTreeEntryFilter} [options.filterEntries]  A filter function to apply to each candidate entry.
     * @returns {WordTreeEntry[]}          A number of entries that have the given prefix.
     */
    lookup(prefix, { limit=10, filterEntries }={}) {
      return super.lookup(prefix, { limit, filterEntries });
    }

    /* -------------------------------------------- */

    /**
     * Returns the node at the given prefix.
     * @param {string} prefix  The prefix.
     * @returns {StringTreeNode}
     */
    nodeAtPrefix(prefix) {
      prefix = prefix.toLocaleLowerCase(game.i18n.lang);
      return super.nodeAtPrefix(Array.from(prefix));
    }
  }

  /**
   * The constructor of an async function.
   * @type {typeof AsyncFunction}
   */
  const AsyncFunction = (async function() {}).constructor;

  var utils = /*#__PURE__*/Object.freeze({
    __proto__: null,
    AsyncFunction: AsyncFunction,
    BitMask: BitMask,
    Collection: Collection,
    Color: Color$1,
    EventEmitterMixin: EventEmitterMixin,
    HttpError: HttpError,
    IterableWeakMap: IterableWeakMap,
    IterableWeakSet: IterableWeakSet,
    Semaphore: Semaphore,
    StringTree: StringTree,
    WordTree: WordTree,
    benchmark: benchmark,
    circleCircleIntersects: circleCircleIntersects,
    closestPointToSegment: closestPointToSegment,
    debounce: debounce,
    debouncedReload: debouncedReload,
    deepClone: deepClone,
    diffObject: diffObject,
    duplicate: duplicate,
    encodeURL: encodeURL,
    expandObject: expandObject,
    fetchJsonWithTimeout: fetchJsonWithTimeout,
    fetchWithTimeout: fetchWithTimeout,
    filterObject: filterObject,
    flattenObject: flattenObject,
    formatFileSize: formatFileSize,
    getDefiningClass: getDefiningClass,
    getParentClasses: getParentClasses,
    getProperty: getProperty,
    getRoute: getRoute,
    getType: getType,
    hasProperty: hasProperty,
    invertObject: invertObject,
    isEmpty: isEmpty$1,
    isNewerVersion: isNewerVersion,
    isSubclass: isSubclass,
    lineCircleIntersection: lineCircleIntersection,
    lineLineIntersection: lineLineIntersection,
    lineSegmentIntersection: lineSegmentIntersection,
    lineSegmentIntersects: lineSegmentIntersects,
    logCompatibilityWarning: logCompatibilityWarning,
    mergeObject: mergeObject,
    objectsEqual: objectsEqual,
    orient2dFast: orient2dFast,
    parseS3URL: parseS3URL,
    parseUuid: parseUuid,
    pathCircleIntersects: pathCircleIntersects,
    polygonCentroid: polygonCentroid,
    quadraticIntersection: quadraticIntersection,
    randomID: randomID,
    setProperty: setProperty,
    threadLock: threadLock,
    throttle: throttle,
    timeSince: timeSince
  });

  /**
   * This module contains data field classes which are used to define a data schema.
   * A data field is responsible for cleaning, validation, and initialization of the value assigned to it.
   * Each data field extends the [DataField]{@link DataField} class to implement logic specific to its
   * contained data type.
   * @module fields
   */


  /* ---------------------------------------- */
  /*  Abstract Data Field                     */
  /* ---------------------------------------- */

  /**
   * @callback DataFieldValidator
   * A Custom DataField validator function.
   *
   * A boolean return value indicates that the value is valid (true) or invalid (false) with certainty. With an explicit
   * boolean return value no further validation functions will be evaluated.
   *
   * An undefined return indicates that the value may be valid but further validation functions should be performed,
   * if defined.
   *
   * An Error may be thrown which provides a custom error message explaining the reason the value is invalid.
   *
   * @param {any} value                     The value provided for validation
   * @param {DataFieldValidationOptions} options  Validation options
   * @returns {boolean|void}
   * @throws {Error}
   */

  /**
   * @typedef {Object} DataFieldOptions
   * @property {boolean} [required=false]   Is this field required to be populated?
   * @property {boolean} [nullable=false]   Can this field have null values?
   * @property {boolean} [gmOnly=false]     Can this field only be modified by a gamemaster or assistant gamemaster?
   * @property {Function|*} [initial]       The initial value of a field, or a function which assigns that initial value.
   * @property {string} [label]             A localizable label displayed on forms which render this field.
   * @property {string} [hint]              Localizable help text displayed on forms which render this field.
   * @property {DataFieldValidator} [validate] A custom data field validation function.
   * @property {string} [validationError]   A custom validation error string. When displayed will be prepended with the
   *                                        document name, field name, and candidate value. This error string is only
   *                                        used when the return type of the validate function is a boolean. If an Error
   *                                        is thrown in the validate function, the string message of that Error is used.
   */

  /**
   * @typedef {Object} DataFieldContext
   * @property {string} [name]               A field name to assign to the constructed field
   * @property {DataField} [parent]          Another data field which is a hierarchical parent of this one
   */

  /**
   * @typedef {object} DataFieldValidationOptions
   * @property {boolean} [partial]   Whether this is a partial schema validation, or a complete one.
   * @property {boolean} [fallback]  Whether to allow replacing invalid values with valid fallbacks.
   * @property {object} [source]     The full source object being evaluated.
   * @property {boolean} [dropInvalidEmbedded]  If true, invalid embedded documents will emit a warning and be placed in
   *                                            the invalidDocuments collection rather than causing the parent to be
   *                                            considered invalid.
   */

  /**
   * An abstract class that defines the base pattern for a data field within a data schema.
   * @abstract
   * @property {string} name                The name of this data field within the schema that contains it.
   * @mixes DataFieldOptions
   */
  class DataField {
    /**
     * @param {DataFieldOptions} [options]    Options which configure the behavior of the field
     * @param {DataFieldContext} [context]    Additional context which describes the field
     */
    constructor(options={}, {name, parent}={}) {
      this.name = name;
      this.parent = parent;
      this.options = options;
      for ( let k in this.constructor._defaults ) {
        this[k] = k in this.options ? this.options[k] : this.constructor._defaults[k];
      }
    }

    /**
     * The field name of this DataField instance.
     * This is assigned by SchemaField#initialize.
     * @internal
     */
    name;

    /**
     * A reference to the parent schema to which this DataField belongs.
     * This is assigned by SchemaField#initialize.
     * @internal
     */
    parent;

    /**
     * The initially provided options which configure the data field
     * @type {DataFieldOptions}
     */
    options;

    /**
     * Whether this field defines part of a Document/Embedded Document hierarchy.
     * @type {boolean}
     */
    static hierarchical = false;

    /**
     * Does this field type contain other fields in a recursive structure?
     * Examples of recursive fields are SchemaField, ArrayField, or TypeDataField
     * Examples of non-recursive fields are StringField, NumberField, or ObjectField
     * @type {boolean}
     */
    static recursive = false;

    /**
     * Default parameters for this field type
     * @return {DataFieldOptions}
     * @protected
     */
    static get _defaults() {
      return {
        required: false,
        nullable: false,
        initial: undefined,
        readonly: false,
        gmOnly: false,
        label: "",
        hint: "",
        validationError: "is not a valid value"
      }
    }

    /**
     * A dot-separated string representation of the field path within the parent schema.
     * @type {string}
     */
    get fieldPath() {
      return [this.parent?.fieldPath, this.name].filterJoin(".");
    }

    /**
     * Apply a function to this DataField which propagates through recursively to any contained data schema.
     * @param {string|function} fn          The function to apply
     * @param {*} value                     The current value of this field
     * @param {object} [options={}]         Additional options passed to the applied function
     * @returns {object}                    The results object
     */
    apply(fn, value, options={}) {
      if ( typeof fn === "string" ) fn = this[fn];
      return fn.call(this, value, options);
    }

    /* -------------------------------------------- */
    /*  Field Cleaning                              */
    /* -------------------------------------------- */

    /**
     * Coerce source data to ensure that it conforms to the correct data type for the field.
     * Data coercion operations should be simple and synchronous as these are applied whenever a DataModel is constructed.
     * For one-off cleaning of user-provided input the sanitize method should be used.
     * @param {*} value           The initial value
     * @param {object} [options]  Additional options for how the field is cleaned
     * @param {boolean} [options.partial]   Whether to perform partial cleaning?
     * @param {object} [options.source]     The root data model being cleaned
     * @returns {*}               The cast value
     */
    clean(value, options={}) {

      // Permit explicitly null values for nullable fields
      if ( value === null ) {
        if ( this.nullable ) return value;
        value = undefined;
      }

      // Get an initial value for the field
      if ( value === undefined ) return this.getInitialValue(options.source);

      // Cast a provided value to the correct type
      value = this._cast(value);

      // Cleaning logic specific to the DataField.
      return this._cleanType(value, options);
    }

    /* -------------------------------------------- */

    /**
     * Apply any cleaning logic specific to this DataField type.
     * @param {*} value           The appropriately coerced value.
     * @param {object} [options]  Additional options for how the field is cleaned.
     * @returns {*}               The cleaned value.
     * @protected
     */
    _cleanType(value, options) {
      return value;
    }

    /* -------------------------------------------- */

    /**
     * Cast a non-default value to ensure it is the correct type for the field
     * @param {*} value       The provided non-default value
     * @returns {*}           The standardized value
     * @protected
     */
    _cast(value) {
      throw new Error(`Subclasses of DataField must implement the _cast method`);
    }

    /* -------------------------------------------- */

    /**
     * Attempt to retrieve a valid initial value for the DataField.
     * @param {object} data   The source data object for which an initial value is required
     * @returns {*}           A valid initial value
     * @throws                An error if there is no valid initial value defined
     */
    getInitialValue(data) {
      return this.initial instanceof Function ? this.initial(data) : this.initial;
    }

    /* -------------------------------------------- */
    /*  Field Validation                            */
    /* -------------------------------------------- */

    /**
     * Validate a candidate input for this field, ensuring it meets the field requirements.
     * A validation failure can be provided as a raised Error (with a string message), by returning false, or by returning
     * a DataModelValidationFailure instance.
     * A validator which returns true denotes that the result is certainly valid and further validations are unnecessary.
     * @param {*} value                                  The initial value
     * @param {DataFieldValidationOptions} [options={}]  Options which affect validation behavior
     * @returns {DataModelValidationFailure}             Returns a DataModelValidationFailure if a validation failure
     *                                                   occurred.
     */
    validate(value, options={}) {
      const validators = [this._validateSpecial, this._validateType];
      if ( this.options.validate ) validators.push(this.options.validate);
      try {
        for ( const validator of validators ) {
          const isValid = validator.call(this, value, options);
          if ( isValid === true ) return undefined;
          if ( isValid === false ) {
            return new DataModelValidationFailure({
              invalidValue: value,
              message: this.validationError,
              unresolved: true
            });
          }
          if ( isValid instanceof DataModelValidationFailure ) return isValid;
        }
      } catch(err) {
        return new DataModelValidationFailure({invalidValue: value, message: err.message, unresolved: true});
      }
    }

    /* -------------------------------------------- */

    /**
     * Special validation rules which supersede regular field validation.
     * This validator screens for certain values which are otherwise incompatible with this field like null or undefined.
     * @param {*} value               The candidate value
     * @returns {boolean|void}        A boolean to indicate with certainty whether the value is valid.
     *                                Otherwise, return void.
     * @throws                        May throw a specific error if the value is not valid
     * @protected
     */
    _validateSpecial(value) {

      // Allow null values for explicitly nullable fields
      if ( value === null ) {
        if ( this.nullable ) return true;
        else throw new Error("may not be null");
      }

      // Allow undefined if the field is not required
      if ( value === undefined ) {
        if ( this.required ) throw new Error("may not be undefined");
        else return true;
      }
    }

    /* -------------------------------------------- */

    /**
     * A default type-specific validator that can be overridden by child classes
     * @param {*} value                                    The candidate value
     * @param {DataFieldValidationOptions} [options={}]    Options which affect validation behavior
     * @returns {boolean|DataModelValidationFailure|void}  A boolean to indicate with certainty whether the value is
     *                                                     valid, or specific DataModelValidationFailure information,
     *                                                     otherwise void.
     * @throws                                             May throw a specific error if the value is not valid
     * @protected
     */
    _validateType(value, options={}) {}

    /* -------------------------------------------- */

    /**
     * Certain fields may declare joint data validation criteria.
     * This method will only be called if the field is designated as recursive.
     * @param {object} data       Candidate data for joint model validation
     * @param {object} options    Options which modify joint model validation
     * @throws  An error if joint model validation fails
     * @internal
     */
    _validateModel(data, options={}) {}

    /* -------------------------------------------- */
    /*  Initialization and Serialization            */
    /* -------------------------------------------- */

    /**
     * Initialize the original source data into a mutable copy for the DataModel instance.
     * @param {*} value                   The source value of the field
     * @param {Object} model              The DataModel instance that this field belongs to
     * @param {object} [options]          Initialization options
     * @returns {*}                       An initialized copy of the source data
     */
    initialize(value, model, options={}) {
      return value;
    }

    /**
     * Export the current value of the field into a serializable object.
     * @param {*} value                   The initialized value of the field
     * @returns {*}                       An exported representation of the field
     */
    toObject(value) {
      return value;
    }

    /**
     * Recursively traverse a schema and retrieve a field specification by a given path
     * @param {string[]} path             The field path as an array of strings
     * @internal
     */
    _getField(path) {
      return path.length ? undefined : this;
    }

    /* -------------------------------------------- */
    /*  Form Field Integration                      */
    /* -------------------------------------------- */

    /**
     * Does this form field class have defined form support?
     * @type {boolean}
     */
    static get hasFormSupport() {
      return this.prototype._toInput !== DataField.prototype._toInput;
    }

    /* -------------------------------------------- */

    /**
     * Render this DataField as an HTML element.
     * @param {FormInputConfig} config        Form element configuration parameters
     * @throws {Error}                        An Error if this DataField subclass does not support input rendering
     * @returns {HTMLElement|HTMLCollection}  A rendered HTMLElement for the field
     */
    toInput(config={}) {
      const inputConfig = {name: this.fieldPath, ...config};
      if ( inputConfig.input instanceof Function ) return config.input(this, inputConfig);
      return this._toInput(inputConfig);
    }

    /* -------------------------------------------- */

    /**
     * Render this DataField as an HTML element.
     * Subclasses should implement this method rather than the public toInput method which wraps it.
     * @param {FormInputConfig} config        Form element configuration parameters
     * @throws {Error}                        An Error if this DataField subclass does not support input rendering
     * @returns {HTMLElement|HTMLCollection}  A rendered HTMLElement for the field
     * @protected
     */
    _toInput(config) {
      throw new Error(`The ${this.constructor.name} class does not implement the _toInput method`);
    }

    /* -------------------------------------------- */

    /**
     * Render this DataField as a standardized form-group element.
     * @param {FormGroupConfig} groupConfig   Configuration options passed to the wrapping form-group
     * @param {FormInputConfig} inputConfig   Input element configuration options passed to DataField#toInput
     * @returns {HTMLDivElement}              The rendered form group element
     */
    toFormGroup(groupConfig={}, inputConfig={}) {
      if ( groupConfig.widget instanceof Function ) return groupConfig.widget(this, groupConfig, inputConfig);
      groupConfig.label ??= this.label ?? this.fieldPath;
      groupConfig.hint ??= this.hint;
      groupConfig.input ??= this.toInput(inputConfig);
      return foundry.applications.fields.createFormGroup(groupConfig);
    }

    /* -------------------------------------------- */
    /*  Active Effect Integration                   */
    /* -------------------------------------------- */

    /**
     * Apply an ActiveEffectChange to this field.
     * @param {*} value                  The field's current value.
     * @param {DataModel} model          The model instance.
     * @param {EffectChangeData} change  The change to apply.
     * @returns {*}                      The updated value.
     */
    applyChange(value, model, change) {
      const delta = this._castChangeDelta(change.value);
      switch ( change.mode ) {
        case CONST.ACTIVE_EFFECT_MODES.ADD: return this._applyChangeAdd(value, delta, model, change);
        case CONST.ACTIVE_EFFECT_MODES.MULTIPLY: return this._applyChangeMultiply(value, delta, model, change);
        case CONST.ACTIVE_EFFECT_MODES.OVERRIDE: return this._applyChangeOverride(value, delta, model, change);
        case CONST.ACTIVE_EFFECT_MODES.UPGRADE: return this._applyChangeUpgrade(value, delta, model, change);
        case CONST.ACTIVE_EFFECT_MODES.DOWNGRADE: return this._applyChangeDowngrade(value, delta, model, change);
      }
      return this._applyChangeCustom(value, delta, model, change);
    }

    /* -------------------------------------------- */

    /**
     * Cast a change delta into an appropriate type to be applied to this field.
     * @param {*} delta  The change delta.
     * @returns {*}
     * @internal
     */
    _castChangeDelta(delta) {
      return this._cast(delta);
    }

    /* -------------------------------------------- */

    /**
     * Apply an ADD change to this field.
     * @param {*} value                  The field's current value.
     * @param {*} delta                  The change delta.
     * @param {DataModel} model          The model instance.
     * @param {EffectChangeData} change  The original change data.
     * @returns {*}                      The updated value.
     * @protected
     */
    _applyChangeAdd(value, delta, model, change) {
      return value + delta;
    }

    /* -------------------------------------------- */

    /**
     * Apply a MULTIPLY change to this field.
     * @param {*} value                  The field's current value.
     * @param {*} delta                  The change delta.
     * @param {DataModel} model          The model instance.
     * @param {EffectChangeData} change  The original change data.
     * @returns {*}                      The updated value.
     * @protected
     */
    _applyChangeMultiply(value, delta, model, change) {}

    /* -------------------------------------------- */

    /**
     * Apply an OVERRIDE change to this field.
     * @param {*} value                  The field's current value.
     * @param {*} delta                  The change delta.
     * @param {DataModel} model          The model instance.
     * @param {EffectChangeData} change  The original change data.
     * @returns {*}                      The updated value.
     * @protected
     */
    _applyChangeOverride(value, delta, model, change) {
      return delta;
    }

    /* -------------------------------------------- */

    /**
     * Apply an UPGRADE change to this field.
     * @param {*} value                  The field's current value.
     * @param {*} delta                  The change delta.
     * @param {DataModel} model          The model instance.
     * @param {EffectChangeData} change  The original change data.
     * @returns {*}                      The updated value.
     * @protected
     */
    _applyChangeUpgrade(value, delta, model, change) {}

    /* -------------------------------------------- */

    /**
     * Apply a DOWNGRADE change to this field.
     * @param {*} value                  The field's current value.
     * @param {*} delta                  The change delta.
     * @param {DataModel} model          The model instance.
     * @param {EffectChangeData} change  The original change data.
     * @returns {*}                      The updated value.
     * @protected
     */
    _applyChangeDowngrade(value, delta, model, change) {}

    /* -------------------------------------------- */

    /**
     * Apply a CUSTOM change to this field.
     * @param {*} value                  The field's current value.
     * @param {*} delta                  The change delta.
     * @param {DataModel} model          The model instance.
     * @param {EffectChangeData} change  The original change data.
     * @returns {*}                      The updated value.
     * @protected
     */
    _applyChangeCustom(value, delta, model, change) {
      const preHook = foundry.utils.getProperty(model, change.key);
      Hooks.call("applyActiveEffect", model, change, value, delta, {});
      const postHook = foundry.utils.getProperty(model, change.key);
      if ( postHook !== preHook ) return postHook;
    }
  }

  /* -------------------------------------------- */
  /*  Data Schema Field                           */
  /* -------------------------------------------- */

  /**
   * A special class of {@link DataField} which defines a data schema.
   */
  class SchemaField extends DataField {
    /**
     * @param {DataSchema} fields                 The contained field definitions
     * @param {DataFieldOptions} [options]        Options which configure the behavior of the field
     * @param {DataFieldContext} [context]        Additional context which describes the field
     */
    constructor(fields, options, context={}) {
      super(options, context);
      this.fields = this._initialize(fields);
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        required: true,
        nullable: false,
        initial() { return this.clean({}); }
      });
    }

    /** @override */
    static recursive = true;

    /* -------------------------------------------- */

    /**
     * The contained field definitions.
     * @type {DataSchema}
     */
    fields;

    /**
     * Any unknown keys encountered during the last cleaning.
     * @type {string[]}
     */
    unknownKeys;

    /* -------------------------------------------- */

    /**
     * Initialize and validate the structure of the provided field definitions.
     * @param {DataSchema} fields     The provided field definitions
     * @returns {DataSchema}          The validated schema
     * @protected
     */
    _initialize(fields) {
      if ( (typeof fields !== "object") ) {
        throw new Error("A DataSchema must be an object with string keys and DataField values.");
      }
      fields = {...fields};
      for ( const [name, field] of Object.entries(fields) ) {
        if ( !(field instanceof DataField) ) {
          throw new Error(`The "${name}" field is not an instance of the DataField class.`);
        }
        if ( field.parent !== undefined ) {
          throw new Error(`The "${field.fieldPath}" field already belongs to some other parent and may not be reused.`);
        }
        field.name = name;
        field.parent = this;
      }
      return fields;
    }

    /* -------------------------------------------- */
    /*  Schema Iteration                            */
    /* -------------------------------------------- */

    /**
     * Iterate over a SchemaField by iterating over its fields.
     * @type {Iterable<DataField>}
     */
    *[Symbol.iterator]() {
      for ( const field of Object.values(this.fields) ) {
        yield field;
      }
    }

    /**
     * An array of field names which are present in the schema.
     * @returns {string[]}
     */
    keys() {
      return Object.keys(this.fields);
    }

    /**
     * An array of DataField instances which are present in the schema.
     * @returns {DataField[]}
     */
    values() {
      return Object.values(this.fields);
    }

    /**
     * An array of [name, DataField] tuples which define the schema.
     * @returns {Array<[string, DataField]>}
     */
    entries() {
      return Object.entries(this.fields);
    }

    /**
     * Test whether a certain field name belongs to this schema definition.
     * @param {string} fieldName    The field name
     * @returns {boolean}           Does the named field exist in this schema?
     */
    has(fieldName) {
      return fieldName in this.fields;
    }

    /**
     * Get a DataField instance from the schema by name
     * @param {string} fieldName    The field name
     * @returns {DataField}         The DataField instance or undefined
     */
    get(fieldName) {
      return this.fields[fieldName];
    }

    /**
     * Traverse the schema, obtaining the DataField definition for a particular field.
     * @param {string[]|string} fieldName       A field path like ["abilities", "strength"] or "abilities.strength"
     * @returns {SchemaField|DataField}         The corresponding DataField definition for that field, or undefined
     */
    getField(fieldName) {
      let path;
      if ( typeof fieldName === "string" ) path = fieldName.split(".");
      else if ( Array.isArray(fieldName) ) path = fieldName.slice();
      else throw new Error("A field path must be an array of strings or a dot-delimited string");
      return this._getField(path);
    }

    /** @override */
    _getField(path) {
      if ( !path.length ) return this;
      const field = this.get(path.shift());
      return field?._getField(path);
    }

    /* -------------------------------------------- */
    /*  Data Field Methods                          */
    /* -------------------------------------------- */

    /** @override */
    _cast(value) {
      return typeof value === "object" ? value : {};
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    _cleanType(data, options={}) {
      options.source = options.source || data;

      // Clean each field which belongs to the schema
      for ( const [name, field] of this.entries() ) {
        if ( !(name in data) && options.partial ) continue;
        data[name] = field.clean(data[name], options);
      }

      // Delete any keys which do not
      this.unknownKeys = [];
      for ( const k of Object.keys(data) ) {
        if ( this.has(k) ) continue;
        this.unknownKeys.push(k);
        delete data[k];
      }
      return data;
    }

    /* -------------------------------------------- */

    /** @override */
    initialize(value, model, options={}) {
      if ( !value ) return value;
      const data = {};
      for ( let [name, field] of this.entries() ) {
        const v = field.initialize(value[name], model, options);

        // Readonly fields
        if ( field.readonly ) {
          Object.defineProperty(data, name, {value: v, writable: false});
        }

        // Getter fields
        else if ( (typeof v === "function") && !v.prototype ) {
          Object.defineProperty(data, name, {get: v, set() {}, configurable: true});
        }

        // Writable fields
        else data[name] = v;
      }
      return data;
    }

    /* -------------------------------------------- */

    /** @override */
    _validateType(data, options={}) {
      if ( !(data instanceof Object) ) throw new Error("must be an object");
      options.source = options.source || data;
      const schemaFailure = new DataModelValidationFailure();
      for ( const [key, field] of this.entries() ) {
        if ( options.partial && !(key in data) ) continue;

        // Validate the field's current value
        const value = data[key];
        const failure = field.validate(value, options);

        // Failure may be permitted if fallback replacement is allowed
        if ( failure ) {
          schemaFailure.fields[field.name] = failure;

          // If the field internally applied fallback logic
          if ( !failure.unresolved ) continue;

          // If fallback is allowed at the schema level
          if ( options.fallback ) {
            const initial = field.getInitialValue(options.source);
            if ( field.validate(initial, {source: options.source}) === undefined ) {  // Ensure initial is valid
              data[key] = initial;
              failure.fallback = initial;
              failure.unresolved = false;
            }
            else failure.unresolved = schemaFailure.unresolved = true;
          }

          // Otherwise the field-level failure is unresolved
          else failure.unresolved = schemaFailure.unresolved = true;
        }
      }
      if ( !isEmpty$1(schemaFailure.fields) ) return schemaFailure;
    }

    /* ---------------------------------------- */

    /** @override */
    _validateModel(changes, options={}) {
      options.source = options.source || changes;
      if ( !changes ) return;
      for ( const [name, field] of this.entries() ) {
        const change = changes[name];  // May be nullish
        if ( change && field.constructor.recursive ) field._validateModel(change, options);
      }
    }

    /* -------------------------------------------- */

    /** @override */
    toObject(value) {
      if ( (value === undefined) || (value === null) ) return value;
      const data = {};
      for ( const [name, field] of this.entries() ) {
        data[name] = field.toObject(value[name]);
      }
      return data;
    }

    /* -------------------------------------------- */

    /** @override */
    apply(fn, data={}, options={}) {

      // Apply to this SchemaField
      const thisFn = typeof fn === "string" ? this[fn] : fn;
      thisFn?.call(this, data, options);

      // Recursively apply to inner fields
      const results = {};
      for ( const [key, field] of this.entries() ) {
        if ( options.partial && !(key in data) ) continue;
        const r = field.apply(fn, data[key], options);
        if ( !options.filter || !isEmpty$1(r) ) results[key] = r;
      }
      return results;
    }

    /* -------------------------------------------- */

    /**
     * Migrate this field's candidate source data.
     * @param {object} sourceData   Candidate source data of the root model
     * @param {any} fieldData       The value of this field within the source data
     */
    migrateSource(sourceData, fieldData) {
      for ( const [key, field] of this.entries() ) {
        const canMigrate = field.migrateSource instanceof Function;
        if ( canMigrate && fieldData[key] ) field.migrateSource(sourceData, fieldData[key]);
      }
    }
  }

  /* -------------------------------------------- */
  /*  Basic Field Types                           */
  /* -------------------------------------------- */

  /**
   * A subclass of [DataField]{@link DataField} which deals with boolean-typed data.
   */
  class BooleanField extends DataField {

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        required: true,
        nullable: false,
        initial: false
      });
    }

    /** @override */
    _cast(value) {
      if ( typeof value === "string" ) return value === "true";
      if ( typeof value === "object" ) return false;
      return Boolean(value);
    }

    /** @override */
    _validateType(value) {
      if (typeof value !== "boolean") throw new Error("must be a boolean");
    }

    /** @override */
    _toInput(config) {
      return foundry.applications.fields.createCheckboxInput(config);
    }

    /* -------------------------------------------- */
    /*  Active Effect Integration                   */
    /* -------------------------------------------- */

    /** @override */
    _applyChangeAdd(value, delta, model, change) {
      return value || delta;
    }

    /** @override */
    _applyChangeMultiply(value, delta, model, change) {
      return value && delta;
    }

    /** @override */
    _applyChangeUpgrade(value, delta, model, change) {
      return delta > value ? delta : value;
    }

    _applyChangeDowngrade(value, delta, model, change) {
      return delta < value ? delta : value;
    }
  }

  /* ---------------------------------------- */

  /**
   * @typedef {DataFieldOptions} NumberFieldOptions
   * @property {number} [min]               A minimum allowed value
   * @property {number} [max]               A maximum allowed value
   * @property {number} [step]              A permitted step size
   * @property {boolean} [integer=false]    Must the number be an integer?
   * @property {number} [positive=false]    Must the number be positive?
   * @property {number[]|object|function} [choices]  An array of values or an object of values/labels which represent
   *                                        allowed choices for the field. A function may be provided which dynamically
   *                                        returns the array of choices.
   */

  /**
   * A subclass of [DataField]{@link DataField} which deals with number-typed data.
   *
   * @property {number} min                 A minimum allowed value
   * @property {number} max                 A maximum allowed value
   * @property {number} step                A permitted step size
   * @property {boolean} integer=false      Must the number be an integer?
   * @property {number} positive=false      Must the number be positive?
   * @property {number[]|object|function} [choices]  An array of values or an object of values/labels which represent
   *                                        allowed choices for the field. A function may be provided which dynamically
   *                                        returns the array of choices.
   */
  class NumberField extends DataField {
    /**
     * @param {NumberFieldOptions} options  Options which configure the behavior of the field
     * @param {DataFieldContext} [context]  Additional context which describes the field
     */
    constructor(options={}, context={}) {
      super(options, context);
      // If choices are provided, the field should not be null by default
      if ( this.choices ) {
        this.nullable = options.nullable ?? false;
      }
      if ( Number.isFinite(this.min) && Number.isFinite(this.max) && (this.min > this.max) ) {
        throw new Error("NumberField minimum constraint cannot exceed its maximum constraint");
      }
    }

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        initial: null,
        nullable: true,
        min: undefined,
        max: undefined,
        step: undefined,
        integer: false,
        positive: false,
        choices: undefined
      });
    }

    /** @override */
    _cast(value) {
      return Number(value);
    }

    /** @inheritdoc */
    _cleanType(value, options) {
      value = super._cleanType(value, options);
      if ( typeof value !== "number" ) return value;
      if ( this.integer ) value = Math.round(value);
      if ( Number.isFinite(this.min) ) value = Math.max(value, this.min);
      if ( Number.isFinite(this.max) ) value = Math.min(value, this.max);
      if ( Number.isFinite(this.step) ) value = value.toNearest(this.step);
      return value;
    }

    /** @override */
    _validateType(value) {
      if ( typeof value !== "number" ) throw new Error("must be a number");
      if ( this.positive && (value <= 0) ) throw new Error("must be a positive number");
      if ( Number.isFinite(this.min) && (value < this.min) ) throw new Error(`must be at least ${this.min}`);
      if ( Number.isFinite(this.max) && (value > this.max) ) throw new Error(`must be at most ${this.max}`);
      if ( Number.isFinite(this.step) && (value.toNearest(this.step) !== value) ) {
        throw new Error(`must be an increment of ${this.step}`);
      }
      if ( this.choices && !this.#isValidChoice(value) ) throw new Error(`${value} is not a valid choice`);
      if ( this.integer ) {
        if ( !Number.isInteger(value) ) throw new Error("must be an integer");
      }
      else if ( !Number.isFinite(value) ) throw new Error("must be a finite number");
    }

    /**
     * Test whether a provided value is a valid choice from the allowed choice set
     * @param {number} value      The provided value
     * @returns {boolean}         Is the choice valid?
     */
    #isValidChoice(value) {
      let choices = this.choices;
      if ( choices instanceof Function ) choices = choices();
      if ( choices instanceof Array ) return choices.includes(value);
      return String(value) in choices;
    }

    /* -------------------------------------------- */
    /*  Form Field Integration                      */
    /* -------------------------------------------- */

    /** @override */
    _toInput(config) {
      config.min ??= this.min;
      config.max ??= this.max;
      config.step ??= this.step;
      if ( config.value === undefined ) config.value = this.getInitialValue({});
      if ( this.integer ) {
        if ( Number.isNumeric(config.value) ) config.value = Math.round(config.value);
        config.step ??= 1;
      }
      if ( this.positive && Number.isFinite(config.step) ) config.min ??= config.step;

      // Number Select
      config.choices ??= this.choices;
      if ( config.choices && !config.options ) {
        config.options = StringField._getChoices(config);
        delete config.valueAttr;
        delete config.labelAttr;
        config.dataset ||= {};
        config.dataset.dtype = "Number";
      }
      if ( config.options ) return foundry.applications.fields.createSelectInput(config);

      // Range Slider
      if ( ["min", "max", "step"].every(k => config[k] !== undefined) && (config.type !== "number") ) {
        return foundry.applications.elements.HTMLRangePickerElement.create(config);
      }

      // Number Input
      return foundry.applications.fields.createNumberInput(config);
    }

    /* -------------------------------------------- */
    /*  Active Effect Integration                   */
    /* -------------------------------------------- */

    /** @override */
    _applyChangeMultiply(value, delta, model, change) {
      return value * delta;
    }

    /** @override */
    _applyChangeUpgrade(value, delta, model, change) {
      return delta > value ? delta : value;
    }

    /** @override */
    _applyChangeDowngrade(value, delta, model, change) {
      return delta < value ? delta : value;
    }
  }

  /* ---------------------------------------- */

  /**
   * @typedef {Object} StringFieldParams
   * @property {boolean} [blank=true]       Is the string allowed to be blank (empty)?
   * @property {boolean} [trim=true]        Should any provided string be trimmed as part of cleaning?
   * @property {string[]|object|function} [choices]  An array of values or an object of values/labels which represent
   *                                        allowed choices for the field. A function may be provided which dynamically
   *                                        returns the array of choices.
   * @property {boolean} [textSearch=false] Is this string field a target for text search?
   * @typedef {DataFieldOptions&StringFieldParams} StringFieldOptions
   */

  /**
   * A subclass of {@link DataField} which deals with string-typed data.
   */
  class StringField extends DataField {
    /**
     * @param {StringFieldOptions} [options]  Options which configure the behavior of the field
     * @param {DataFieldContext} [context]    Additional context which describes the field
     */
    constructor(options={}, context={}) {
      super(options, context);

      // If choices are provided, the field should not be null or blank by default
      if ( this.choices ) {
        this.nullable = options.nullable ?? false;
        this.blank = options.blank ?? false;
      }
    }

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        blank: true,
        trim: true,
        nullable: false,
        initial() {
          // The initial value depends on the field configuration
          if ( !this.required ) return undefined;
          else if ( this.blank ) return "";
          else if ( this.nullable ) return null;
          return undefined;
        },
        choices: undefined,
        textSearch: false
      });
    }

    /**
     * Is the string allowed to be blank (empty)?
     * @type {boolean}
     */
    blank = this.blank;

    /**
     * Should any provided string be trimmed as part of cleaning?
     * @type {boolean}
     */
    trim = this.trim;

    /**
     * An array of values or an object of values/labels which represent
     * allowed choices for the field. A function may be provided which dynamically
     * returns the array of choices.
     * @type {string[]|object|function}
     */
    choices = this.choices;

    /**
     * Is this string field a target for text search?
     * @type {boolean}
     */
    textSearch = this.textSearch;

    /** @inheritdoc */
    clean(value, options) {
      if ( (typeof value === "string") && this.trim ) value = value.trim(); // Trim input strings
      if ( value === "" ) {  // Permit empty strings for blank fields
        if ( this.blank ) return value;
        value = undefined;
      }
      return super.clean(value, options);
    }

    /** @override */
    _cast(value) {
      return String(value);
    }

    /** @inheritdoc */
    _validateSpecial(value) {
      if ( value === "" ) {
        if ( this.blank ) return true;
        else throw new Error("may not be a blank string");
      }
      return super._validateSpecial(value);
    }

    /** @override */
    _validateType(value) {
      if ( typeof value !== "string" ) throw new Error("must be a string");
      else if ( this.choices ) {
        if ( this._isValidChoice(value) ) return true;
        else throw new Error(`${value} is not a valid choice`);
      }
    }

    /**
     * Test whether a provided value is a valid choice from the allowed choice set
     * @param {string} value      The provided value
     * @returns {boolean}         Is the choice valid?
     * @protected
     */
    _isValidChoice(value) {
      let choices = this.choices;
      if ( choices instanceof Function ) choices = choices();
      if ( choices instanceof Array ) return choices.includes(value);
      return String(value) in choices;
    }

    /* -------------------------------------------- */
    /*  Form Field Integration                      */
    /* -------------------------------------------- */

    /**
     * Get a record of eligible choices for the field.
     * @param {object} [options]
     * @param {Record<any, any>|Array<any>} options.choices
     * @param {string} [options.labelAttr="label"]   The property in the choice object values to use as the option label.
     * @param {string} [options.valueAttr]
     * @param {boolean} [options.localize=false]     Pass each label through string localization?
     * @returns {FormSelectOption[]}
     * @internal
     */
    static _getChoices({choices, labelAttr="label", valueAttr, localize=false}={}) {
      if ( choices instanceof Function ) choices = choices();
      if ( typeof choices === "object" ) {
        choices = Object.entries(choices).reduce((arr, [value, label]) => {
          if ( typeof label !== "string" ) {
            if ( valueAttr && (valueAttr in label) ) value = label[valueAttr];
            label = label[labelAttr] ?? "undefined";
          }
          if ( localize ) label = game.i18n.localize(label);
          arr.push({value, label});
          return arr;
        }, []);
      }
      return choices;
    }

    /* -------------------------------------------- */

    /** @override */
    _toInput(config) {
      if ( config.value === undefined ) config.value = this.getInitialValue({});
      config.choices ??= this.choices;
      if ( config.choices && !config.options ) {
        config.options = StringField._getChoices(config);
        delete config.choices;
        delete config.valueAttr;
        delete config.labelAttr;
        if ( this.blank || !this.required ) config.blank ??= "";
      }
      if ( config.options ) return foundry.applications.fields.createSelectInput(config);
      return foundry.applications.fields.createTextInput(config);
    }
  }

  /* ---------------------------------------- */

  /**
   * A subclass of [DataField]{@link DataField} which deals with object-typed data.
   */
  class ObjectField extends DataField {

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        required: true,
        nullable: false
      });
    }

    /* -------------------------------------------- */

    /** @override */
    getInitialValue(data) {
      const initial = super.getInitialValue(data);
      if ( initial ) return initial;          // Explicit initial value defined by subclass
      if ( !this.required ) return undefined; // The ObjectField may be undefined
      if ( this.nullable ) return null;       // The ObjectField may be null
      return {};                              // Otherwise an empty object
    }

    /** @override */
    _cast(value) {
      return getType(value) === "Object" ? value : {};
    }

    /** @override */
    initialize(value, model, options={}) {
      if ( !value ) return value;
      return deepClone(value);
    }

    /** @override */
    toObject(value) {
      return deepClone(value);
    }

    /** @override */
    _validateType(value, options={}) {
      if ( getType(value) !== "Object" ) throw new Error("must be an object");
    }
  }

  /* -------------------------------------------- */

  /**
   * @typedef {DataFieldOptions} ArrayFieldOptions
   * @property {number} [min]          The minimum number of elements.
   * @property {number} [max]          The maximum number of elements.
   */

  /**
   * A subclass of [DataField]{@link DataField} which deals with array-typed data.
   * @property {number} min     The minimum number of elements.
   * @property {number} max     The maximum number of elements.
   */
  class ArrayField extends DataField {
    /**
     * @param {DataField} element            A DataField instance which defines the type of element contained in the Array
     * @param {ArrayFieldOptions} [options]  Options which configure the behavior of the field
     * @param {DataFieldContext} [context]   Additional context which describes the field
     */
    constructor(element, options={}, context={}) {
      super(options, context);
      /**
       * The data type of each element in this array
       * @type {DataField}
       */
      this.element = this.constructor._validateElementType(element);
      if ( this.min > this.max ) throw new Error("ArrayField minimum length cannot exceed maximum length");
    }

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        required: true,
        nullable: false,
        empty: true,
        exact: undefined,
        min: 0,
        max: Infinity,
        initial: () => []
      });
    }

    /** @override */
    static recursive = true;

    /* ---------------------------------------- */

    /**
     * Validate the contained element type of the ArrayField
     * @param {*} element       The type of Array element
     * @returns {*}             The validated element type
     * @throws                  An error if the element is not a valid type
     * @protected
     */
    static _validateElementType(element) {
      if ( !(element instanceof DataField) ) {
        throw new Error(`${this.name} must have a DataField as its contained element`);
      }
      return element;
    }

    /* ---------------------------------------- */

    /** @override */
    _validateModel(changes, options) {
      if ( !this.element.constructor.recursive ) return;
      for ( const element of changes ) {
        this.element._validateModel(element, options);
      }
    }

    /* ---------------------------------------- */

    /** @override */
    _cast(value) {
      const t = getType(value);
      if ( t === "Object" ) {
        const arr = [];
        for ( const [k, v] of Object.entries(value) ) {
          const i = Number(k);
          if ( Number.isInteger(i) && (i >= 0) ) arr[i] = v;
        }
        return arr;
      }
      else if ( t === "Set" ) return Array.from(value);
      return value instanceof Array ? value : [value];
    }

    /** @override */
    _cleanType(value, options) {
      // Force partial as false for array cleaning. Arrays are updated by replacing the entire array, so partial data
      // must be initialized.
      return value.map(v => this.element.clean(v, { ...options, partial: false }));
    }

    /** @override */
    _validateType(value, options={}) {
      if ( !(value instanceof Array) ) throw new Error("must be an Array");
      if ( value.length < this.min ) throw new Error(`cannot have fewer than ${this.min} elements`);
      if ( value.length > this.max ) throw new Error(`cannot have more than ${this.max} elements`);
      return this._validateElements(value, options);
    }

    /**
     * Validate every element of the ArrayField
     * @param {Array} value                         The array to validate
     * @param {DataFieldValidationOptions} options  Validation options
     * @returns {DataModelValidationFailure|void}   A validation failure if any of the elements failed validation,
     *                                              otherwise void.
     * @protected
     */
    _validateElements(value, options) {
      const arrayFailure = new DataModelValidationFailure();
      for ( let i=0; i<value.length; i++ ) {
        // Force partial as false for array validation. Arrays are updated by replacing the entire array, so there cannot
        // be partial data in the elements.
        const failure = this._validateElement(value[i], { ...options, partial: false });
        if ( failure ) {
          arrayFailure.elements.push({id: i, failure});
          arrayFailure.unresolved ||= failure.unresolved;
        }
      }
      if ( arrayFailure.elements.length ) return arrayFailure;
    }

    /**
     * Validate a single element of the ArrayField.
     * @param {*} value                       The value of the array element
     * @param {DataFieldValidationOptions} options  Validation options
     * @returns {DataModelValidationFailure}  A validation failure if the element failed validation
     * @protected
     */
    _validateElement(value, options) {
      return this.element.validate(value, options);
    }

    /** @override */
    initialize(value, model, options={}) {
      if ( !value ) return value;
      return value.map(v => this.element.initialize(v, model, options));
    }

    /** @override */
    toObject(value) {
      if ( !value ) return value;
      return value.map(v => this.element.toObject(v));
    }

    /** @override */
    apply(fn, value=[], options={}) {

      // Apply to this ArrayField
      const thisFn = typeof fn === "string" ? this[fn] : fn;
      thisFn?.call(this, value, options);

      // Recursively apply to array elements
      const results = [];
      if ( !value.length && options.initializeArrays ) value = [undefined];
      for ( const v of value ) {
        const r = this.element.apply(fn, v, options);
        if ( !options.filter || !isEmpty$1(r) ) results.push(r);
      }
      return results;
    }

    /** @override */
    _getField(path) {
      if ( !path.length ) return this;
      if ( path[0] === "element" ) path.shift();
      return this.element._getField(path);
    }

    /**
     * Migrate this field's candidate source data.
     * @param {object} sourceData   Candidate source data of the root model
     * @param {any} fieldData       The value of this field within the source data
     */
    migrateSource(sourceData, fieldData) {
      const canMigrate = this.element.migrateSource instanceof Function;
      if ( canMigrate && (fieldData instanceof Array) ) {
        for ( const entry of fieldData ) this.element.migrateSource(sourceData, entry);
      }
    }

    /* -------------------------------------------- */
    /*  Active Effect Integration                   */
    /* -------------------------------------------- */

    /** @override */
    _castChangeDelta(raw) {
      let delta;
      try {
        delta = JSON.parse(raw);
        delta = Array.isArray(delta) ? delta : [delta];
      } catch {
        delta = [raw];
      }
      return delta.map(value => this.element._castChangeDelta(value));
    }

    /** @override */
    _applyChangeAdd(value, delta, model, change) {
      value.push(...delta);
      return value;
    }
  }

  /* -------------------------------------------- */
  /*  Specialized Field Types                     */
  /* -------------------------------------------- */

  /**
   * A subclass of [ArrayField]{@link ArrayField} which supports a set of contained elements.
   * Elements in this set are treated as fungible and may be represented in any order or discarded if invalid.
   */
  class SetField extends ArrayField {

    /** @override */
    _validateElements(value, options) {
      const setFailure = new DataModelValidationFailure();
      for ( let i=value.length-1; i>=0; i-- ) {  // iterate backwards so we can splice as we go
        const failure = this._validateElement(value[i], options);
        if ( failure ) {
          setFailure.elements.unshift({id: i, failure});

          // The failure may have been internally resolved by fallback logic
          if ( !failure.unresolved && failure.fallback ) continue;

          // If fallback is allowed, remove invalid elements from the set
          if ( options.fallback ) {
            value.splice(i, 1);
            failure.dropped = true;
          }

          // Otherwise the set failure is unresolved
          else setFailure.unresolved = true;
        }
      }

      // Return a record of any failed set elements
      if ( setFailure.elements.length ) {
        if ( options.fallback && !setFailure.unresolved ) setFailure.fallback = value;
        return setFailure;
      }
    }

    /** @override */
    initialize(value, model, options={}) {
      if ( !value ) return value;
      return new Set(super.initialize(value, model, options));
    }

    /** @override */
    toObject(value) {
      if ( !value ) return value;
      return Array.from(value).map(v => this.element.toObject(v));
    }

    /* -------------------------------------------- */
    /*  Form Field Integration                      */
    /* -------------------------------------------- */

    /** @override */
    _toInput(config) {
      const e = this.element;

      // Document UUIDs
      if ( e instanceof DocumentUUIDField ) {
        Object.assign(config, {type: e.type, single: false});
        return foundry.applications.elements.HTMLDocumentTagsElement.create(config);
      }

      // Multi-Select Input
      if ( e.choices && !config.options ) {
        config.options = StringField._getChoices({choices: e.choices, ...config});
      }
      if ( config.options ) return foundry.applications.fields.createMultiSelectInput(config);

      // Arbitrary String Tags
      if ( e instanceof StringField ) return foundry.applications.elements.HTMLStringTagsElement.create(config);
      throw new Error(`SetField#toInput is not supported for a ${e.constructor.name} element type`);
    }

    /* -------------------------------------------- */
    /*  Active Effect Integration                   */
    /* -------------------------------------------- */

    /** @inheritDoc */
    _castChangeDelta(raw) {
      return new Set(super._castChangeDelta(raw));
    }

    /** @override */
    _applyChangeAdd(value, delta, model, change) {
      for ( const element of delta ) value.add(element);
      return value;
    }
  }

  /* ---------------------------------------- */

  /**
   * A subclass of [ObjectField]{@link ObjectField} which embeds some other DataModel definition as an inner object.
   */
  class EmbeddedDataField extends SchemaField {
    /**
     * @param {typeof DataModel} model          The class of DataModel which should be embedded in this field
     * @param {DataFieldOptions} [options]      Options which configure the behavior of the field
     * @param {DataFieldContext} [context]      Additional context which describes the field
     */
    constructor(model, options={}, context={}) {
      if ( !isSubclass(model, DataModel) ) {
        throw new Error("An EmbeddedDataField must specify a DataModel class as its type");
      }

      // Create an independent copy of the model schema
      const fields = model.defineSchema();
      super(fields, options, context);

      /**
       * The base DataModel definition which is contained in this field.
       * @type {typeof DataModel}
       */
      this.model = model;
    }

    /** @inheritdoc */
    clean(value, options) {
      return super.clean(value, {...options, source: value});
    }

    /** @inheritdoc */
    validate(value, options) {
      return super.validate(value, {...options, source: value});
    }

    /** @override */
    initialize(value, model, options={}) {
      if ( !value ) return value;
      const m = new this.model(value, {parent: model, ...options});
      Object.defineProperty(m, "schema", {value: this});
      return m;
    }

    /** @override */
    toObject(value) {
      if ( !value ) return value;
      return value.toObject(false);
    }

    /** @override */
    migrateSource(sourceData, fieldData) {
      if ( fieldData ) this.model.migrateDataSafe(fieldData);
    }

    /** @override */
    _validateModel(changes, options) {
      this.model.validateJoint(changes);
    }
  }

  /* ---------------------------------------- */

  /**
   * A subclass of [ArrayField]{@link ArrayField} which supports an embedded Document collection.
   * Invalid elements will be dropped from the collection during validation rather than failing for the field entirely.
   */
  class EmbeddedCollectionField extends ArrayField {
    /**
     * @param {typeof foundry.abstract.Document} element  The type of Document which belongs to this embedded collection
     * @param {DataFieldOptions} [options]  Options which configure the behavior of the field
     * @param {DataFieldContext} [context]  Additional context which describes the field
     */
    constructor(element, options={}, context={}) {
      super(element, options, context);
      this.readonly = true; // Embedded collections are always immutable
    }

    /** @override */
    static _validateElementType(element) {
      if ( isSubclass(element, foundry.abstract.Document) ) return element;
      throw new Error("An EmbeddedCollectionField must specify a Document subclass as its type");
    }

    /**
     * The Collection implementation to use when initializing the collection.
     * @type {typeof EmbeddedCollection}
     */
    static get implementation() {
      return EmbeddedCollection;
    }

    /** @override */
    static hierarchical = true;

    /**
     * A reference to the DataModel subclass of the embedded document element
     * @type {typeof foundry.abstract.Document}
     */
    get model() {
      return this.element.implementation;
    }

    /**
     * The DataSchema of the contained Document model.
     * @type {SchemaField}
     */
    get schema() {
      return this.model.schema;
    }

    /** @inheritDoc */
    _cast(value) {
      if ( getType(value) !== "Map" ) return super._cast(value);
      const arr = [];
      for ( const [id, v] of value.entries() ) {
        if ( !("_id" in v) ) v._id = id;
        arr.push(v);
      }
      return super._cast(arr);
    }

    /** @override */
    _cleanType(value, options) {
      return value.map(v => this.schema.clean(v, {...options, source: v}));
    }

    /** @override */
    _validateElements(value, options) {
      const collectionFailure = new DataModelValidationFailure();
      for ( const v of value ) {
        const failure = this.schema.validate(v, {...options, source: v});
        if ( failure && !options.dropInvalidEmbedded ) {
          collectionFailure.elements.push({id: v._id, name: v.name, failure});
          collectionFailure.unresolved ||= failure.unresolved;
        }
      }
      if ( collectionFailure.elements.length ) return collectionFailure;
    }

    /** @override */
    initialize(value, model, options={}) {
      const collection = model.collections[this.name];
      collection.initialize(options);
      return collection;
    }

    /** @override */
    toObject(value) {
      return value.toObject(false);
    }

    /** @override */
    apply(fn, value=[], options={}) {

      // Apply to this EmbeddedCollectionField
      const thisFn = typeof fn === "string" ? this[fn] : fn;
      thisFn?.call(this, value, options);

      // Recursively apply to inner fields
      const results = [];
      if ( !value.length && options.initializeArrays ) value = [undefined];
      for ( const v of value ) {
        const r = this.schema.apply(fn, v, options);
        if ( !options.filter || !isEmpty$1(r) ) results.push(r);
      }
      return results;
    }

    /**
     * Migrate this field's candidate source data.
     * @param {object} sourceData   Candidate source data of the root model
     * @param {any} fieldData       The value of this field within the source data
     */
    migrateSource(sourceData, fieldData) {
      if ( fieldData instanceof Array ) {
        for ( const entry of fieldData ) this.model.migrateDataSafe(entry);
      }
    }

    /* -------------------------------------------- */
    /*  Embedded Document Operations                */
    /* -------------------------------------------- */

    /**
     * Return the embedded document(s) as a Collection.
     * @param {foundry.abstract.Document} parent  The parent document.
     * @returns {DocumentCollection}
     */
    getCollection(parent) {
      return parent[this.name];
    }
  }

  /* -------------------------------------------- */

  /**
   * A subclass of {@link EmbeddedCollectionField} which manages a collection of delta objects relative to another
   * collection.
   */
  class EmbeddedCollectionDeltaField extends EmbeddedCollectionField {
    /** @override */
    static get implementation() {
      return EmbeddedCollectionDelta;
    }

    /** @override */
    _cleanType(value, options) {
      return value.map(v => {
        if ( v._tombstone ) return foundry.data.TombstoneData.schema.clean(v, {...options, source: v});
        return this.schema.clean(v, {...options, source: v});
      });
    }

    /** @override */
    _validateElements(value, options) {
      const collectionFailure = new DataModelValidationFailure();
      for ( const v of value ) {
        const validationOptions = {...options, source: v};
        const failure = v._tombstone
          ? foundry.data.TombstoneData.schema.validate(v, validationOptions)
          : this.schema.validate(v, validationOptions);
        if ( failure && !options.dropInvalidEmbedded ) {
          collectionFailure.elements.push({id: v._id, name: v.name, failure});
          collectionFailure.unresolved ||= failure.unresolved;
        }
      }
      if ( collectionFailure.elements.length ) return collectionFailure;
    }
  }

  /* -------------------------------------------- */

  /**
   * A subclass of {@link EmbeddedDataField} which supports a single embedded Document.
   */
  class EmbeddedDocumentField extends EmbeddedDataField {
    /**
     * @param {typeof foundry.abstract.Document} model The type of Document which is embedded.
     * @param {DataFieldOptions} [options]  Options which configure the behavior of the field.
     * @param {DataFieldContext} [context]  Additional context which describes the field
     */
    constructor(model, options={}, context={}) {
      if ( !isSubclass(model, foundry.abstract.Document) ) {
        throw new Error("An EmbeddedDocumentField must specify a Document subclass as its type.");
      }
      super(model.implementation, options, context);
    }

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        nullable: true
      });
    }

    /** @override */
    static hierarchical = true;

    /** @override */
    initialize(value, model, options={}) {
      if ( !value ) return value;
      if ( model[this.name] ) {
        model[this.name]._initialize(options);
        return model[this.name];
      }
      const m = new this.model(value, {...options, parent: model, parentCollection: this.name});
      Object.defineProperty(m, "schema", {value: this});
      return m;
    }

    /* -------------------------------------------- */
    /*  Embedded Document Operations                */
    /* -------------------------------------------- */

    /**
     * Return the embedded document(s) as a Collection.
     * @param {Document} parent  The parent document.
     * @returns {Collection<Document>}
     */
    getCollection(parent) {
      const collection = new SingletonEmbeddedCollection(this.name, parent, []);
      const doc = parent[this.name];
      if ( !doc ) return collection;
      collection.set(doc.id, doc);
      return collection;
    }
  }

  /* -------------------------------------------- */
  /*  Special Field Types                         */
  /* -------------------------------------------- */

  /**
   * A subclass of [StringField]{@link StringField} which provides the primary _id for a Document.
   * The field may be initially null, but it must be non-null when it is saved to the database.
   */
  class DocumentIdField extends StringField {

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        required: true,
        blank: false,
        nullable: true,
        initial: null,
        readonly: true,
        validationError: "is not a valid Document ID string"
      });
    }

    /** @override */
    _cast(value) {
      if ( value instanceof foundry.abstract.Document ) return value._id;
      else return String(value);
    }

    /** @override */
    _validateType(value) {
      if ( !isValidId(value) ) throw new Error("must be a valid 16-character alphanumeric ID");
    }
  }

  /* ---------------------------------------- */


  /**
   * @typedef {Object} DocumentUUIDFieldOptions
   * @property {string} [type]            A specific document type in CONST.ALL_DOCUMENT_TYPES required by this field
   * @property {boolean} [embedded]       Does this field require (or prohibit) embedded documents?
   */

  /**
   * A subclass of {@link StringField} which supports referencing some other Document by its UUID.
   * This field may not be blank, but may be null to indicate that no UUID is referenced.
   */
  class DocumentUUIDField extends StringField {
    /**
     * @param {StringFieldOptions & DocumentUUIDFieldOptions} [options] Options which configure the behavior of the field
     * @param {DataFieldContext} [context]    Additional context which describes the field
     */
    constructor(options, context) {
      super(options, context);
    }

    /** @inheritdoc */
    static get _defaults() {
      return Object.assign(super._defaults, {
        required: true,
        blank: false,
        nullable: true,
        initial: null,
        type: undefined,
        embedded: undefined
      });
    }

    /** @override */
    _validateType(value) {
      const p = parseUuid(value);
      if ( this.type ) {
        if ( p.type !== this.type ) throw new Error(`Invalid document type "${p.type}" which must be a "${this.type}"`);
      }
      else if ( p.type && !ALL_DOCUMENT_TYPES.includes(p.type) ) throw new Error(`Invalid document type "${p.type}"`);
      if ( (this.embedded === true) && !p.embedded.length ) throw new Error("must be an embedded document");
      if ( (this.embedded === false) && p.embedded.length ) throw new Error("may not be an embedded document");
      if ( !isValidId(p.documentId) ) throw new Error(`Invalid document ID "${p.documentId}"`);
    }

    /* -------------------------------------------- */
    /*  Form Field Integration                      */
    /* -------------------------------------------- */

    /** @override */
    _toInput(config) {
      Object.assign(config, {type: this.type, single: true});
      return foundry.applications.elements.HTMLDocumentTagsElement.create(config);
    }
  }

  /* ---------------------------------------- */

  /**
   * A special class of [StringField]{@link StringField} field which references another DataModel by its id.
   * This field may also be null to indicate that no foreign model is linked.
   */
  class ForeignDocumentField extends DocumentIdField {
    /**
     * @param {typeof foundry.abstract.Document} model  The foreign DataModel class definition which this field links to
     * @param {StringFieldOptions} [options]    Options which configure the behavior of the field
     * @param {DataFieldContext} [context]      Additional context which describes the field
     */
    constructor(model, options={}, context={}) {
      super(options, context);
      if ( !isSubclass(model, DataModel) ) {
        throw new Error("A ForeignDocumentField must specify a DataModel subclass as its type");
      }
      /**
       * A reference to the model class which is stored in this field
       * @type {typeof foundry.abstract.Document}
       */
      this.model = model;
    }

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        nullable: true,
        readonly: false,
        idOnly: false
      });
    }

    /** @override */
    _cast(value) {
      if ( typeof value === "string" ) return value;
      if ( (value instanceof this.model) ) return value._id;
      throw new Error(`The value provided to a ForeignDocumentField must be a ${this.model.name} instance.`);
    }

    /** @inheritdoc */
    initialize(value, model, options={}) {
      if ( this.idOnly ) return value;
      if ( model?.pack && !foundry.utils.isSubclass(this.model, foundry.documents.BaseFolder) ) return null;
      if ( !game.collections ) return value; // server-side
      return () => this.model?.get(value, {pack: model?.pack, ...options}) ?? null;
    }

    /** @inheritdoc */
    toObject(value) {
      return value?._id ?? value
    }

    /* -------------------------------------------- */
    /*  Form Field Integration                      */
    /* -------------------------------------------- */

    /** @override */
    _toInput(config) {

      // Prepare array of visible options
      const collection = game.collections.get(this.model.documentName);
      const current = collection.get(config.value);
      let hasCurrent = false;
      const options = collection.reduce((arr, doc) => {
        if ( !doc.visible ) return arr;
        if ( doc === current ) hasCurrent = true;
        arr.push({value: doc.id, label: doc.name});
        return arr;
      }, []);
      if ( current && !hasCurrent ) options.unshift({value: config.value, label: current.name});
      Object.assign(config, {options});

      // Allow blank
      if ( !this.required || this.nullable ) config.blank = "";

      // Create select input
      return foundry.applications.fields.createSelectInput(config);
    }
  }

  /* -------------------------------------------- */

  /**
   * A special [StringField]{@link StringField} which records a standardized CSS color string.
   */
  class ColorField extends StringField {

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        nullable: true,
        initial: null,
        blank: false,
        validationError: "is not a valid hexadecimal color string"
      });
    }

    /** @override */
    initialize(value, model, options={}) {
      if ( (value === null) || (value === undefined) ) return value;
      return Color.from(value);
    }

    /** @override */
    getInitialValue(data) {
      const value = super.getInitialValue(data);
      if ( (value === undefined) || (value === null) || (value === "") ) return value;
      const color = Color.from(value);
      if ( !color.valid ) throw new Error("Invalid initial value for ColorField");
      return color.css;
    }

    /** @override */
    _cast(value) {
      if ( value === "" ) return value;
      return Color.from(value);
    }

    /** @override */
    _cleanType(value, options) {
      if ( value === "" ) return value;
      if ( value.valid ) return value.css;
      return this.getInitialValue(options.source);
    }

    /** @inheritdoc */
    _validateType(value, options) {
      const result = super._validateType(value, options);
      if ( result !== undefined ) return result;
      if ( !isColorString(value) ) throw new Error("must be a valid color string");
    }

    /* -------------------------------------------- */
    /*  Form Field Integration                      */
    /* -------------------------------------------- */

    /** @override */
    _toInput(config) {
      if ( (config.placeholder === undefined) && !this.nullable && !(this.initial instanceof Function) ) {
        config.placeholder = this.initial;
      }
      return foundry.applications.elements.HTMLColorPickerElement.create(config);
    }
  }

  /* -------------------------------------------- */

  /**
   * @typedef {StringFieldOptions} FilePathFieldOptions
   * @property {string[]} [categories]    A set of categories in CONST.FILE_CATEGORIES which this field supports
   * @property {boolean} [base64=false]   Is embedded base64 data supported in lieu of a file path?
   * @property {boolean} [wildcard=false] Does this file path field allow wildcard characters?
   * @property {object} [initial]         The initial values of the fields
   */

  /**
   * A special [StringField]{@link StringField} which records a file path or inline base64 data.
   * @property {string[]} categories      A set of categories in CONST.FILE_CATEGORIES which this field supports
   * @property {boolean} base64=false     Is embedded base64 data supported in lieu of a file path?
   * @property {boolean} wildcard=false   Does this file path field allow wildcard characters?
   */
  class FilePathField extends StringField {
    /**
     * @param {FilePathFieldOptions} [options]  Options which configure the behavior of the field
     * @param {DataFieldContext} [context]      Additional context which describes the field
     */
    constructor(options={}, context={}) {
      super(options, context);
      if ( !this.categories.length || this.categories.some(c => !(c in FILE_CATEGORIES)) ) {
        throw new Error("The categories of a FilePathField must be keys in CONST.FILE_CATEGORIES");
      }
    }

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        categories: [],
        base64: false,
        wildcard: false,
        nullable: true,
        blank: false,
        initial: null
      });
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    _validateType(value) {

      // Wildcard paths
      if ( this.wildcard && value.includes("*") ) return true;

      // Allowed extension or base64
      const isValid = this.categories.some(c => {
        const category = FILE_CATEGORIES[c];
        if ( hasFileExtension(value, Object.keys(category)) ) return true;
        /**
         * If the field contains base64 data, it is allowed (for now) regardless of the base64 setting for the field.
         * Eventually, this will become more strict and only be valid if base64 is configured as true for the field.
         * @deprecated since v10
         */
        return isBase64Data(value, Object.values(category));
      });

      // Throw an error for invalid paths
      if ( !isValid ) {
        let err = "does not have a valid file extension";
        if ( this.base64 ) err += " or provide valid base64 data";
        throw new Error(err);
      }
    }

    /* -------------------------------------------- */
    /*  Form Field Integration                      */
    /* -------------------------------------------- */

    /** @override */
    _toInput(config) {
      // FIXME: This logic is fragile and would require a mapping between CONST.FILE_CATEGORIES and FilePicker.TYPES
      config.type = this.categories.length === 1 ? this.categories[0].toLowerCase() : "any";
      return foundry.applications.elements.HTMLFilePickerElement.create(config);
    }
  }

  /* -------------------------------------------- */

  /**
   * A special {@link NumberField} which represents an angle of rotation in degrees between 0 and 360.
   * @property {boolean} normalize    Whether the angle should be normalized to [0,360) before being clamped to [0,360]. The default is true.
   */
  class AngleField extends NumberField {
    constructor(options={}, context={}) {
      super(options, context);
      if ( "base" in this.options ) this.base = this.options.base;
    }

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        required: true,
        nullable: false,
        initial: 0,
        normalize: true,
        min: 0,
        max: 360,
        validationError: "is not a number between 0 and 360"
      });
    }

    /** @inheritdoc */
    _cast(value) {
      value = super._cast(value);
      if ( !this.normalize ) return value;
      value = Math.normalizeDegrees(value);
      /** @deprecated since v12 */
      if ( (this.#base === 360) && (value === 0) ) value = 360;
      return value;
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get base() {
      const msg = "The AngleField#base is deprecated in favor of AngleField#normalize.";
      foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
      return this.#base;
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    set base(v) {
      const msg = "The AngleField#base is deprecated in favor of AngleField#normalize.";
      foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
      this.#base = v;
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    #base = 0;
  }

  /* -------------------------------------------- */

  /**
   * A special [NumberField]{@link NumberField} represents a number between 0 and 1.
   */
  class AlphaField extends NumberField {
    static get _defaults() {
      return mergeObject(super._defaults, {
        required: true,
        nullable: false,
        initial: 1,
        min: 0,
        max: 1,
        validationError: "is not a number between 0 and 1"
      });
    }
  }

  /* -------------------------------------------- */

  /**
   * A special [NumberField]{@link NumberField} represents a number between 0 (inclusive) and 1 (exclusive).
   * Its values are normalized (modulo 1) to the range [0, 1) instead of being clamped.
   */
  class HueField extends NumberField {
    static get _defaults() {
      return mergeObject(super._defaults, {
        required: true,
        nullable: false,
        initial: 0,
        min: 0,
        max: 1,
        validationError: "is not a number between 0 (inclusive) and 1 (exclusive)"
      });
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    _cast(value) {
      value = super._cast(value) % 1;
      if ( value < 0 ) value += 1;
      return value;
    }
  }

  /* -------------------------------------------- */

  /**
   * A special [ObjectField]{@link ObjectField} which captures a mapping of User IDs to Document permission levels.
   */
  class DocumentOwnershipField extends ObjectField {

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        initial: {"default": DOCUMENT_OWNERSHIP_LEVELS.NONE},
        validationError: "is not a mapping of user IDs and document permission levels"
      });
    }

    /** @override */
    _validateType(value) {
      for ( let [k, v] of Object.entries(value) ) {
        if ( k.startsWith("-=") ) return isValidId(k.slice(2)) && (v === null);   // Allow removals
        if ( (k !== "default") && !isValidId(k) ) return false;
        if ( !Object.values(DOCUMENT_OWNERSHIP_LEVELS).includes(v) ) return false;
      }
    }
  }

  /* -------------------------------------------- */

  /**
   * A special [StringField]{@link StringField} which contains serialized JSON data.
   */
  class JSONField extends StringField {
    constructor(options, context) {
      super(options, context);
      this.blank = false;
      this.trim = false;
      this.choices = undefined;
    }

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        blank: false,
        trim: false,
        initial: undefined,
        validationError: "is not a valid JSON string"
      });
    }

    /** @inheritdoc */
    clean(value, options) {
      if ( value === "" ) return '""';  // Special case for JSON fields
      return super.clean(value, options);
    }

    /** @override */
    _cast(value) {
      if ( (typeof value !== "string") || !isJSON(value) ) return JSON.stringify(value);
      return value;
    }

    /** @override */
    _validateType(value, options) {
      if ( (typeof value !== "string") || !isJSON(value) ) throw new Error("must be a serialized JSON string");
    }

    /** @override */
    initialize(value, model, options={}) {
      if ( (value === undefined) || (value === null) ) return value;
      return JSON.parse(value);
    }

    /** @override */
    toObject(value) {
      if ( (value === undefined) || (this.nullable && (value === null)) ) return value;
      return JSON.stringify(value);
    }

    /* -------------------------------------------- */
    /*  Form Field Integration                      */
    /* -------------------------------------------- */

    /** @override */
    _toInput(config) {
      if ( config.value !== "" ) config.value = JSON.stringify(config.value, null, 2);
      return foundry.applications.fields.createTextareaInput(config);
    }
  }

  /* -------------------------------------------- */

  /**
   * A special subclass of {@link DataField} which can contain any value of any type.
   * Any input is accepted and is treated as valid.
   * It is not recommended to use this class except for very specific circumstances.
   */
  class AnyField extends DataField {

    /** @override */
    _cast(value) {
      return value;
    }

    /** @override */
    _validateType(value) {
      return true;
    }
  }


  /* -------------------------------------------- */

  /**
   * A subclass of [StringField]{@link StringField} which contains a sanitized HTML string.
   * This class does not override any StringField behaviors, but is used by the server-side to identify fields which
   * require sanitization of user input.
   */
  class HTMLField extends StringField {

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        required: true,
        blank: true
      });
    }

    /** @override */
    toFormGroup(groupConfig={}, inputConfig) {
      groupConfig.stacked ??= true;
      return super.toFormGroup(groupConfig, inputConfig);
    }

    /** @override */
    _toInput(config) {
      return foundry.applications.elements.HTMLProseMirrorElement.create(config);
    }
  }

  /* ---------------------------------------- */

  /**
   * A subclass of {@link NumberField} which is used for storing integer sort keys.
   */
  class IntegerSortField extends NumberField {
    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        required: true,
        nullable: false,
        integer: true,
        initial: 0,
        label: "FOLDER.DocumentSort",
        hint: "FOLDER.DocumentSortHint"
      });
    }
  }

  /* ---------------------------------------- */

  /**
   * @typedef {Object} DocumentStats
   * @property {string|null} coreVersion       The core version whose schema the Document data is in.
   *                                           It is NOT the version the Document was created or last modified in.
   * @property {string|null} systemId          The package name of the system the Document was created in.
   * @property {string|null} systemVersion     The version of the system the Document was created or last modified in.
   * @property {number|null} createdTime       A timestamp of when the Document was created.
   * @property {number|null} modifiedTime      A timestamp of when the Document was last modified.
   * @property {string|null} lastModifiedBy    The ID of the user who last modified the Document.
   * @property {string|null} compendiumSource  The UUID of the compendium Document this one was imported from.
   * @property {string|null} duplicateSource   The UUID of the Document this one is a duplicate of.
   */

  /**
   * A subclass of {@link SchemaField} which stores document metadata in the _stats field.
   * @mixes DocumentStats
   */
  class DocumentStatsField extends SchemaField {
    /**
     * @param {DataFieldOptions} [options]        Options which configure the behavior of the field
     * @param {DataFieldContext} [context]        Additional context which describes the field
     */
    constructor(options={}, context={}) {
      super({
        coreVersion: new StringField({required: true, blank: false, nullable: true, initial: () => game.release.version}),
        systemId: new StringField({required: true, blank: false, nullable: true, initial: () => game.system?.id ?? null}),
        systemVersion: new StringField({required: true, blank: false, nullable: true, initial: () => game.system?.version ?? null}),
        createdTime: new NumberField(),
        modifiedTime: new NumberField(),
        lastModifiedBy: new ForeignDocumentField(foundry.documents.BaseUser, {idOnly: true}),
        compendiumSource: new DocumentUUIDField(),
        duplicateSource: new DocumentUUIDField()
      }, options, context);
    }

    /**
     * All Document stats.
     * @type {string[]}
     */
    static fields = [
      "coreVersion", "systemId", "systemVersion", "createdTime", "modifiedTime", "lastModifiedBy", "compendiumSource",
      "duplicateSource"
    ];

    /**
     * These fields are managed by the server and are ignored if they appear in creation or update data.
     * @type {string[]}
     */
    static managedFields = ["coreVersion", "systemId", "systemVersion", "createdTime", "modifiedTime", "lastModifiedBy"];
  }

  /* ---------------------------------------- */

  /**
   * A subclass of [StringField]{@link StringField} that is used specifically for the Document "type" field.
   */
  class DocumentTypeField extends StringField {
    /**
     * @param {typeof foundry.abstract.Document} documentClass  The base document class which belongs in this field
     * @param {StringFieldOptions} [options]  Options which configure the behavior of the field
     * @param {DataFieldContext} [context]    Additional context which describes the field
     */
    constructor(documentClass, options={}, context={}) {
      options.choices = () => documentClass.TYPES;
      options.validationError = `is not a valid type for the ${documentClass.documentName} Document class`;
      super(options, context);
    }

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        required: true,
        nullable: false,
        blank: false
      });
    }

    /** @override */
    _validateType(value, options) {
      if ( (typeof value !== "string") || !value ) throw new Error("must be a non-blank string");
      if ( this._isValidChoice(value) ) return true;
      // Allow unrecognized types if we are allowed to fallback (non-strict validation)
      if (options.fallback ) return true;
      throw new Error(`"${value}" ${this.options.validationError}`);
    }
  }

  /* ---------------------------------------- */

  /**
   * A subclass of [ObjectField]{@link ObjectField} which supports a type-specific data object.
   */
  class TypeDataField extends ObjectField {
    /**
     * @param {typeof foundry.abstract.Document} document  The base document class which belongs in this field
     * @param {DataFieldOptions} [options]    Options which configure the behavior of the field
     * @param {DataFieldContext} [context]    Additional context which describes the field
     */
    constructor(document, options={}, context={}) {
      super(options, context);
      /**
       * The canonical document name of the document type which belongs in this field
       * @type {typeof foundry.abstract.Document}
       */
      this.document = document;
    }

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {required: true});
    }

    /** @override */
    static recursive = true;

    /**
     * Return the package that provides the sub-type for the given model.
     * @param {DataModel} model       The model instance created for this sub-type.
     * @returns {System|Module|null}
     */
    static getModelProvider(model) {
      const document = model.parent;
      if ( !document ) return null;
      const documentClass = document.constructor;
      const documentName = documentClass.documentName;
      const type = document.type;

      // Unrecognized type
      if ( !documentClass.TYPES.includes(type) ) return null;

      // Core-defined sub-type
      const coreTypes = documentClass.metadata.coreTypes;
      if ( coreTypes.includes(type) ) return null;

      // System-defined sub-type
      const systemTypes = game.system.documentTypes[documentName];
      if ( systemTypes && (type in systemTypes) ) return game.system;

      // Module-defined sub-type
      const moduleId = type.substring(0, type.indexOf("."));
      return game.modules.get(moduleId) ?? null;
    }

    /**
     * A convenience accessor for the name of the document type associated with this TypeDataField
     * @type {string}
     */
    get documentName() {
      return this.document.documentName;
    }

    /**
     * Get the DataModel definition that should be used for this type of document.
     * @param {string} type              The Document instance type
     * @returns {typeof DataModel|null}  The DataModel class or null
     */
    getModelForType(type) {
      if ( !type ) return null;
      return globalThis.CONFIG?.[this.documentName]?.dataModels?.[type] ?? null;
    }

    /** @override */
    getInitialValue(data) {
      const cls = this.getModelForType(data.type);
      if ( cls ) return cls.cleanData();
      const template = game?.model[this.documentName]?.[data.type];
      if ( template ) return foundry.utils.deepClone(template);
      return {};
    }

    /** @override */
    _cleanType(value, options) {
      if ( !(typeof value === "object") ) value = {};

      // Use a defined DataModel
      const type = options.source?.type;
      const cls = this.getModelForType(type);
      if ( cls ) return cls.cleanData(value, {...options, source: value});
      if ( options.partial ) return value;

      // Use the defined template.json
      const template = this.getInitialValue(options.source);
      const insertKeys = (type === BASE_DOCUMENT_TYPE) || !game?.system?.strictDataCleaning;
      return mergeObject(template, value, {insertKeys, inplace: true});
    }

    /** @override */
    initialize(value, model, options={}) {
      const cls = this.getModelForType(model._source.type);
      if ( cls ) {
        const instance = new cls(value, {parent: model, ...options});
        if ( !("modelProvider" in instance) ) Object.defineProperty(instance, "modelProvider", {
          value: this.constructor.getModelProvider(instance),
          writable: false
        });
        return instance;
      }
      return deepClone(value);
    }

    /** @inheritdoc */
    _validateType(data, options={}) {
      const result = super._validateType(data, options);
      if ( result !== undefined ) return result;
      const cls = this.getModelForType(options.source?.type);
      const schema = cls?.schema;
      return schema?.validate(data, {...options, source: data});
    }

    /* ---------------------------------------- */

    /** @override */
    _validateModel(changes, options={}) {
      const cls = this.getModelForType(options.source?.type);
      return cls?.validateJoint(changes);
    }

    /* ---------------------------------------- */

    /** @override */
    toObject(value) {
      return value.toObject instanceof Function ? value.toObject(false) : deepClone(value);
    }

    /**
     * Migrate this field's candidate source data.
     * @param {object} sourceData   Candidate source data of the root model
     * @param {any} fieldData       The value of this field within the source data
     */
    migrateSource(sourceData, fieldData) {
      const cls = this.getModelForType(sourceData.type);
      if ( cls ) cls.migrateDataSafe(fieldData);
    }
  }

  /* ---------------------------------------- */

  /**
   * A subclass of [DataField]{@link DataField} which allows to typed schemas.
   */
  class TypedSchemaField extends DataField {
    /**
     * @param {{[type: string]: DataSchema|SchemaField|typeof DataModel}} types    The different types this field can represent.
     * @param {DataFieldOptions} [options]                                         Options which configure the behavior of the field
     * @param {DataFieldContext} [context]                                         Additional context which describes the field
     */
    constructor(types, options, context) {
      super(options, context);
      this.types = this.#configureTypes(types);
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {required: true});
    }

    /* ---------------------------------------- */

    /**
    * The types of this field.
    * @type {{[type: string]: SchemaField}}
    */
    types;

    /* -------------------------------------------- */

    /**
     * Initialize and validate the structure of the provided type definitions.
     * @param {{[type: string]: DataSchema|SchemaField|typeof DataModel}} types    The provided field definitions
     * @returns {{[type: string]: SchemaField}}                                     The validated fields
     */
    #configureTypes(types) {
      if ( (typeof types !== "object") ) {
        throw new Error("A DataFields must be an object with string keys and DataField values.");
      }
      types = {...types};
      for ( let [type, field] of Object.entries(types) ) {
        if ( isSubclass(field, DataModel) ) field = new EmbeddedDataField(field);
        if ( field?.constructor?.name === "Object" ) {
          const schema = {...field};
          if ( !("type" in schema) ) {
            schema.type = new StringField({required: true, blank: false, initial: field,
              validate: value => value === type, validationError: `must be equal to "${type}"`});
          }
          field = new SchemaField(schema);
        }
        if ( !(field instanceof SchemaField)  ) {
          throw new Error(`The "${type}" field is not an instance of the SchemaField class or a subclass of DataModel.`);
        }
        if ( field.name !== undefined ) throw new Error(`The "${field.fieldPath}" field must not have a name.`);
        if ( field.parent !== undefined ) {
          throw new Error(`The "${field.fieldPath}" field already belongs to some other parent and may not be reused.`);
        }
        types[type] = field;
        field.parent = this;
        if ( !field.required ) throw new Error(`The "${field.fieldPath}" field must be required.`);
        if ( field.nullable ) throw new Error(`The "${field.fieldPath}" field must not be nullable.`);
        const typeField = field.fields.type;
        if ( !(typeField instanceof StringField) ) throw new Error(`The "${field.fieldPath}" field must have a "type" StringField.`);
        if ( !typeField.required ) throw new Error(`The "${typeField.fieldPath}" field must be required.`);
        if ( typeField.nullable ) throw new Error(`The "${typeField.fieldPath}" field must not be nullable.`);
        if ( typeField.blank ) throw new Error(`The "${typeField.fieldPath}" field must not be blank.`);
        if ( typeField.validate(type, {fallback: false}) !== undefined ) throw new Error(`"${type}" must be a valid type of "${typeField.fieldPath}".`);
      }
      return types;
    }

    /* ---------------------------------------- */

    /** @override */
    _getField(path) {
      if ( !path.length ) return this;
      return this.types[path.shift()]?._getField(path);
    }

    /* -------------------------------------------- */
    /*  Data Field Methods                          */
    /* -------------------------------------------- */

    /** @override */
    _cleanType(value, options) {
      const field = this.types[value?.type];
      if ( !field ) return value;
      return field.clean(value, options);
    }

    /* ---------------------------------------- */

    /** @override */
    _cast(value) {
      return typeof value === "object" ? value : {};
    }

    /* ---------------------------------------- */

    /** @override */
    _validateSpecial(value) {
      const result = super._validateSpecial(value);
      if ( result !== undefined ) return result;
      const field = this.types[value?.type];
      if ( !field ) throw new Error("does not have a valid type");
    }

    /* ---------------------------------------- */

    /** @override */
    _validateType(value, options) {
      return this.types[value.type].validate(value, options);
    }

    /* ---------------------------------------- */

    /** @override */
    initialize(value, model, options) {
      const field = this.types[value?.type];
      if ( !field ) return value;
      return field.initialize(value, model, options);
    }

    /* ---------------------------------------- */

    /** @override */
    toObject(value) {
      if ( !value ) return value;
      return this.types[value.type]?.toObject(value) ?? value;
    }

    /* -------------------------------------------- */

    /** @override */
    apply(fn, data, options) {

      // Apply to this TypedSchemaField
      const thisFn = typeof fn === "string" ? this[fn] : fn;
      thisFn?.call(this, data, options);

      // Apply to the inner typed field
      const typeField = this.types[data?.type];
      return typeField?.apply(fn, data, options);
    }

    /* -------------------------------------------- */

    /**
     * Migrate this field's candidate source data.
     * @param {object} sourceData   Candidate source data of the root model
     * @param {any} fieldData       The value of this field within the source data
     */
    migrateSource(sourceData, fieldData) {
      const field = this.types[fieldData?.type];
      const canMigrate = field?.migrateSource instanceof Function;
      if ( canMigrate ) field.migrateSource(sourceData, fieldData);
    }
  }

  /* ---------------------------------------- */
  /*  DEPRECATIONS                            */
  /* ---------------------------------------- */

  /**
   * @deprecated since v11
   * @see DataModelValidationError
   * @ignore
   */
  class ModelValidationError extends Error {
    constructor(errors) {
      logCompatibilityWarning(
        "ModelValidationError is deprecated. Please use DataModelValidationError instead.",
        {since: 11, until: 13});
      const message = ModelValidationError.formatErrors(errors);
      super(message);
      this.errors = errors;
    }

    /**
     * Collect all the errors into a single message for consumers who do not handle the ModelValidationError specially.
     * @param {Record<string, Error>|Error[]|string} errors   The raw error structure
     * @returns {string}                              A formatted error message
     */
    static formatErrors(errors) {
      if ( typeof errors === "string" ) return errors;
      const message = ["Model Validation Errors"];
      if ( errors instanceof Array ) message.push(...errors.map(e => e.message));
      else message.push(...Object.entries(errors).map(([k, e]) => `[${k}]: ${e.message}`));
      return message.join("\n");
    }
  }

  /* -------------------------------------------- */


  /**
   * @typedef {Object} JavaScriptFieldOptions
   * @property {boolean} [async=false]            Does the field allow async code?
   */

  /**
   * A subclass of {@link StringField} which contains JavaScript code.
   */
  class JavaScriptField extends StringField {
    /**
     * @param {StringFieldOptions & JavaScriptFieldOptions} [options] Options which configure the behavior of the field
     * @param {DataFieldContext} [context]    Additional context which describes the field
     */
    constructor(options, context) {
      super(options, context);
      this.choices = undefined;
    }

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        required: true,
        blank: true,
        nullable: false,
        async: false
      });
    }

    /** @inheritdoc */
    _validateType(value, options) {
      const result = super._validateType(value, options);
      if ( result !== undefined ) return result;
      try {
        new (this.async ? AsyncFunction : Function)(value);
      } catch(err) {
        const scope = this.async ? "an asynchronous" : "a synchronous";
        err.message = `must be valid JavaScript for ${scope} scope:\n${err.message}`;
        throw new Error(err);
      }
    }

    /* -------------------------------------------- */
    /*  Form Field Integration                      */
    /* -------------------------------------------- */

    /** @override */
    toFormGroup(groupConfig={}, inputConfig) {
      groupConfig.stacked ??= true;
      return super.toFormGroup(groupConfig, inputConfig);
    }

    /** @override */
    _toInput(config) {
      return foundry.applications.fields.createTextareaInput(config);
    }
  }

  var fields$1 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    AlphaField: AlphaField,
    AngleField: AngleField,
    AnyField: AnyField,
    ArrayField: ArrayField,
    BooleanField: BooleanField,
    ColorField: ColorField,
    DataField: DataField,
    DocumentIdField: DocumentIdField,
    DocumentOwnershipField: DocumentOwnershipField,
    DocumentStatsField: DocumentStatsField,
    DocumentTypeField: DocumentTypeField,
    DocumentUUIDField: DocumentUUIDField,
    EmbeddedCollectionDeltaField: EmbeddedCollectionDeltaField,
    EmbeddedCollectionField: EmbeddedCollectionField,
    EmbeddedDataField: EmbeddedDataField,
    EmbeddedDocumentField: EmbeddedDocumentField,
    FilePathField: FilePathField,
    ForeignDocumentField: ForeignDocumentField,
    HTMLField: HTMLField,
    HueField: HueField,
    IntegerSortField: IntegerSortField,
    JSONField: JSONField,
    JavaScriptField: JavaScriptField,
    ModelValidationError: ModelValidationError,
    NumberField: NumberField,
    ObjectField: ObjectField,
    SchemaField: SchemaField,
    SetField: SetField,
    StringField: StringField,
    TypeDataField: TypeDataField,
    TypedSchemaField: TypedSchemaField
  });

  /**
   * @typedef {Record<string, DataField>}  DataSchema
   */

  /**
   * @typedef {Object} DataValidationOptions
   * @property {boolean} [strict=true]     Throw an error if validation fails.
   * @property {boolean} [fallback=false]  Attempt to replace invalid values with valid defaults?
   * @property {boolean} [partial=false]   Allow partial source data, ignoring absent fields?
   * @property {boolean} [dropInvalidEmbedded=false]  If true, invalid embedded documents will emit a warning and be
   *                                                  placed in the invalidDocuments collection rather than causing the
   *                                                  parent to be considered invalid.
   */

  /**
   * The abstract base class which defines the data schema contained within a Document.
   * @param {object} [data={}]                    Initial data used to construct the data object. The provided object
   *                                              will be owned by the constructed model instance and may be mutated.
   * @param {DataValidationOptions} [options={}]  Options which affect DataModel construction
   * @param {Document} [options.parent]           A parent DataModel instance to which this DataModel belongs
   * @abstract
   */
  class DataModel {
    constructor(data={}, {parent=null, strict=true, ...options}={}) {

      // Parent model
      Object.defineProperty(this, "parent", {
        value: (() => {
          if ( parent === null ) return null;
          if ( parent instanceof DataModel ) return parent;
          throw new Error("The provided parent must be a DataModel instance");
        })(),
        writable: false,
        enumerable: false
      });

      // Source data
      Object.defineProperty(this, "_source", {
        value: this._initializeSource(data, {strict, ...options}),
        writable: false,
        enumerable: false
      });
      Object.seal(this._source);

      // Additional subclass configurations
      this._configure(options);

      // Data validation and initialization
      const fallback = options.fallback ?? !strict;
      const dropInvalidEmbedded = options.dropInvalidEmbedded ?? !strict;
      this.validate({strict, fallback, dropInvalidEmbedded, fields: true, joint: true});
      this._initialize({strict, ...options});
    }

    /**
     * Configure the data model instance before validation and initialization workflows are performed.
     * @protected
     */
    _configure(options={}) {}

    /* -------------------------------------------- */

    /**
     * The source data object for this DataModel instance.
     * Once constructed, the source object is sealed such that no keys may be added nor removed.
     * @type {object}
     */
    _source;

    /**
     * The defined and cached Data Schema for all instances of this DataModel.
     * @type {SchemaField}
     * @private
     */
    static _schema;

    /**
     * An immutable reverse-reference to a parent DataModel to which this model belongs.
     * @type {DataModel|null}
     */
    parent;

    /* ---------------------------------------- */
    /*  Data Schema                             */
    /* ---------------------------------------- */

    /**
     * Define the data schema for documents of this type.
     * The schema is populated the first time it is accessed and cached for future reuse.
     * @virtual
     * @returns {DataSchema}
     */
    static defineSchema() {
      throw new Error(`The ${this["name"]} subclass of DataModel must define its Document schema`);
    }

    /* ---------------------------------------- */

    /**
     * The Data Schema for all instances of this DataModel.
     * @type {SchemaField}
     */
    static get schema() {
      if ( this.hasOwnProperty("_schema") ) return this._schema;
      const schema = new SchemaField(Object.freeze(this.defineSchema()));
      Object.defineProperty(this, "_schema", {value: schema, writable: false});
      return schema;
    }

    /* ---------------------------------------- */

    /**
     * Define the data schema for this document instance.
     * @type {SchemaField}
     */
    get schema() {
      return this.constructor.schema;
    }

    /* ---------------------------------------- */

    /**
     * Is the current state of this DataModel invalid?
     * The model is invalid if there is any unresolved failure.
     * @type {boolean}
     */
    get invalid() {
      return Object.values(this.#validationFailures).some(f => f?.unresolved);
    }

    /**
     * An array of validation failure instances which may have occurred when this instance was last validated.
     * @type {{fields: DataModelValidationFailure|null, joint: DataModelValidationFailure|null}}
     */
    get validationFailures() {
      return this.#validationFailures;
    }

    #validationFailures = Object.seal({fields: null, joint: null });

    /**
     * A set of localization prefix paths which are used by this DataModel.
     * @type {string[]}
     */
    static LOCALIZATION_PREFIXES = [];

    /* ---------------------------------------- */
    /*  Data Cleaning Methods                   */
    /* ---------------------------------------- */

    /**
     * Initialize the source data for a new DataModel instance.
     * One-time migrations and initial cleaning operations are applied to the source data.
     * @param {object|DataModel} data   The candidate source data from which the model will be constructed
     * @param {object} [options]        Options provided to the model constructor
     * @returns {object}                Migrated and cleaned source data which will be stored to the model instance
     * @protected
     */
    _initializeSource(data, options={}) {
      if ( data instanceof DataModel ) data = data.toObject();
      const dt = getType(data);
      if ( dt !== "Object" ) {
        logger.error(`${this.constructor.name} was incorrectly constructed with a ${dt} instead of an object. 
      Attempting to fall back to default values.`);
        data = {};
      }
      data = this.constructor.migrateDataSafe(data);    // Migrate old data to the new format
      data = this.constructor.cleanData(data);          // Clean the data in the new format
      return this.constructor.shimData(data);           // Apply shims which preserve backwards compatibility
    }

    /* ---------------------------------------- */

    /**
     * Clean a data source object to conform to a specific provided schema.
     * @param {object} [source]         The source data object
     * @param {object} [options={}]     Additional options which are passed to field cleaning methods
     * @returns {object}                The cleaned source data
     */
    static cleanData(source={}, options={}) {
      return this.schema.clean(source, options);
    }

    /* ---------------------------------------- */
    /*  Data Initialization                     */
    /* ---------------------------------------- */

    /**
     * A generator that orders the DataFields in the DataSchema into an expected initialization order.
     * @returns {Generator<[string,DataField]>}
     * @protected
     */
    static *_initializationOrder() {
      for ( const entry of this.schema.entries() ) yield entry;
    }

    /* ---------------------------------------- */

    /**
     * Initialize the instance by copying data from the source object to instance attributes.
     * This mirrors the workflow of SchemaField#initialize but with some added functionality.
     * @param {object} [options]        Options provided to the model constructor
     * @protected
     */
    _initialize(options={}) {
      for ( let [name, field] of this.constructor._initializationOrder() ) {
        const sourceValue = this._source[name];

        // Field initialization
        const value = field.initialize(sourceValue, this, options);

        // Special handling for Document IDs.
        if ( (name === "_id") && (!Object.getOwnPropertyDescriptor(this, "_id") || (this._id === null)) ) {
          Object.defineProperty(this, name, {value, writable: false, configurable: true});
        }

        // Readonly fields
        else if ( field.readonly ) {
          if ( this[name] !== undefined ) continue;
          Object.defineProperty(this, name, {value, writable: false});
        }

        // Getter fields
        else if ( value instanceof Function ) {
          Object.defineProperty(this, name, {get: value, set() {}, configurable: true});
        }

        // Writable fields
        else this[name] = value;
      }
    }

    /* ---------------------------------------- */

    /**
     * Reset the state of this data instance back to mirror the contained source data, erasing any changes.
     */
    reset() {
      this._initialize();
    }

    /* ---------------------------------------- */

    /**
     * Clone a model, creating a new data model by combining current data with provided overrides.
     * @param {Object} [data={}]                    Additional data which overrides current document data at the time of creation
     * @param {object} [context={}]                 Context options passed to the data model constructor
     * @returns {Document|Promise<Document>}        The cloned Document instance
     */
    clone(data={}, context={}) {
      data = mergeObject(this.toObject(), data, {insertKeys: false, performDeletions: true, inplace: true});
      return new this.constructor(data, {parent: this.parent, ...context});
    }

    /* ---------------------------------------- */
    /*  Data Validation Methods                 */
    /* ---------------------------------------- */

    /**
     * Validate the data contained in the document to check for type and content
     * This function throws an error if data within the document is not valid
     *
     * @param {object} options                    Optional parameters which customize how validation occurs.
     * @param {object} [options.changes]          A specific set of proposed changes to validate, rather than the full
     *                                            source data of the model.
     * @param {boolean} [options.clean=false]     If changes are provided, attempt to clean the changes before validating
     *                                            them?
     * @param {boolean} [options.fallback=false]  Allow replacement of invalid values with valid defaults?
     * @param {boolean} [options.dropInvalidEmbedded=false]  If true, invalid embedded documents will emit a warning and
     *                                                       be placed in the invalidDocuments collection rather than
     *                                                       causing the parent to be considered invalid.
     * @param {boolean} [options.strict=true]     Throw if an invalid value is encountered, otherwise log a warning?
     * @param {boolean} [options.fields=true]     Perform validation on individual fields?
     * @param {boolean} [options.joint]           Perform joint validation on the full data model?
     *                                            Joint validation will be performed by default if no changes are passed.
     *                                            Joint validation will be disabled by default if changes are passed.
     *                                            Joint validation can be performed on a complete set of changes (for
     *                                            example testing a complete data model) by explicitly passing true.
     * @return {boolean}                          An indicator for whether the document contains valid data
     */
    validate({changes, clean=false, fallback=false, dropInvalidEmbedded=false, strict=true, fields=true, joint}={}) {
      const source = changes ?? this._source;
      this.#validationFailures.fields = this.#validationFailures.joint = null; // Remove any prior failures

      // Determine whether we are performing partial or joint validation
      const partial = !!changes;
      joint = joint ?? !changes;
      if ( partial && joint ) {
        throw new Error("It is not supported to perform joint data model validation with only a subset of changes");
      }

      // Optionally clean the data before validating
      if ( partial && clean ) this.constructor.cleanData(source, {partial});

      // Validate individual fields in the data or in a specific change-set, throwing errors if validation fails
      if ( fields ) {
        const failure = this.schema.validate(source, {partial, fallback, dropInvalidEmbedded});
        if ( failure ) {
          const id = this._source._id ? `[${this._source._id}] ` : "";
          failure.message = `${this.constructor.name} ${id}validation errors:`;
          this.#validationFailures.fields = failure;
          if ( strict && failure.unresolved ) throw failure.asError();
          else logger.warn(failure.asError());
        }
      }

      // Perform joint document-level validations which consider all fields together
      if ( joint ) {
        try {
          this.schema._validateModel(source);     // Validate inner models
          this.constructor.validateJoint(source); // Validate this model
        } catch (err) {
          const id = this._source._id ? `[${this._source._id}] ` : "";
          const message = [this.constructor.name, id, `Joint Validation Error:\n${err.message}`].filterJoin(" ");
          const failure = new DataModelValidationFailure({message, unresolved: true});
          this.#validationFailures.joint = failure;
          if ( strict ) throw failure.asError();
          else logger.warn(failure.asError());
        }
      }
      return !this.invalid;
    }

    /* ---------------------------------------- */

    /**
     * Evaluate joint validation rules which apply validation conditions across multiple fields of the model.
     * Field-specific validation rules should be defined as part of the DataSchema for the model.
     * This method allows for testing aggregate rules which impose requirements on the overall model.
     * @param {object} data     Candidate data for the model
     * @throws                  An error if a validation failure is detected
     */
    static validateJoint(data) {
      /**
       * @deprecated since v11
       * @ignore
       */
      if ( this.prototype._validateModel instanceof Function ) {
        const msg = `${this.name} defines ${this.name}.prototype._validateModel instance method which should now be`
                  + ` declared as ${this.name}.validateJoint static method.`;
        foundry.utils.logCompatibilityWarning(msg, {from: 11, until: 13});
        return this.prototype._validateModel.call(this, data);
      }
    }

    /* ---------------------------------------- */
    /*  Data Management                         */
    /* ---------------------------------------- */

    /**
     * Update the DataModel locally by applying an object of changes to its source data.
     * The provided changes are cleaned, validated, and stored to the source data object for this model.
     * The source data is then re-initialized to apply those changes to the prepared data.
     * The method returns an object of differential changes which modified the original data.
     *
     * @param {object} changes          New values which should be applied to the data model
     * @param {object} [options={}]     Options which determine how the new data is merged
     * @returns {object}                An object containing the changed keys and values
     */
    updateSource(changes={}, options={}) {
      const schema = this.schema;
      const source = this._source;
      const _diff = {};
      const _backup = {};
      const _collections = this.collections;
      const _singletons = this.singletons;

      // Expand the object, if dot-notation keys are provided
      if ( Object.keys(changes).some(k => /\./.test(k)) ) changes = expandObject(changes);

      // Clean and validate the provided changes, throwing an error if any change is invalid
      this.validate({changes, clean: true, fallback: options.fallback, strict: true, fields: true, joint: false});

      // Update the source data for all fields and validate the final combined model
      let error;
      try {
        DataModel.#updateData(schema, source, changes, {_backup, _collections, _singletons, _diff, ...options});
        this.validate({fields: this.invalid, joint: true, strict: true});
      } catch(err) {
        error = err;
      }

      // Restore the backup data
      if ( error || options.dryRun ) {
        mergeObject(this._source, _backup, { recursive: false });
        if ( error ) throw error;
      }

      // Initialize the updated data
      if ( !options.dryRun ) this._initialize();
      return _diff;
    }

    /* ---------------------------------------- */

    /**
     * Update the source data for a specific DataSchema.
     * This method assumes that both source and changes are valid objects.
     * @param {SchemaField} schema      The data schema to update
     * @param {object} source           Source data to be updated
     * @param {object} changes          Changes to apply to the source data
     * @param {object} [options={}]     Options which modify the update workflow
     * @returns {object}                The updated source data
     * @throws                          An error if the update operation was unsuccessful
     * @private
     */
    static #updateData(schema, source, changes, options) {
      const {_backup, _diff} = options;
      for ( let [name, value] of Object.entries(changes) ) {
        const field = schema.get(name);
        if ( !field ) continue;

        // Skip updates where the data is unchanged
        const prior = source[name];
        if ( (value?.equals instanceof Function) && value.equals(prior) ) continue;  // Arrays, Sets, etc...
        if ( (prior === value) ) continue; // Direct comparison
        _backup[name] = deepClone(prior);
        _diff[name] = value;

        // Field-specific updating logic
        this.#updateField(name, field, source, value, options);
      }
      return source;
    }

    /* ---------------------------------------- */

    /**
     * Update the source data for a specific DataField.
     * @param {string} name             The field name being updated
     * @param {DataField} field         The field definition being updated
     * @param {object} source           The source object being updated
     * @param {*} value                 The new value for the field
     * @param {object} options          Options which modify the update workflow
     * @throws                          An error if the new candidate value is invalid
     * @private
     */
    static #updateField(name, field, source, value, options) {
      const {dryRun, fallback, recursive, restoreDelta, _collections, _singletons, _diff, _backup} = options;
      let current = source?.[name];   // The current value may be null or undefined

      // Special Case: Update Embedded Collection
      if ( field instanceof EmbeddedCollectionField ) {
        _backup[name] = current;
        if ( !dryRun ) _collections[name].update(value, {fallback, recursive, restoreDelta});
        return;
      }

      // Special Case: Update Embedded Document
      if ( (field instanceof EmbeddedDocumentField) && _singletons[name] ) {
        _diff[name] = _singletons[name].updateSource(value ?? {}, {dryRun, fallback, recursive, restoreDelta});
        if ( isEmpty$1(_diff[name]) ) delete _diff[name];
        return;
      }

      // Special Case: Inner Data Schema
      let innerSchema;
      if ( (field instanceof SchemaField) || (field instanceof EmbeddedDataField) ) innerSchema = field;
      else if ( field instanceof TypeDataField ) {
        const cls = field.getModelForType(source.type);
        if ( cls ) {
          innerSchema = cls.schema;
          if ( dryRun ) {
            _backup[name] = current;
            current = deepClone(current);
          }
        }
      }
      if ( innerSchema && current && value ) {
        _diff[name] = {};
        const recursiveOptions = {fallback, recursive, _backup: current, _collections, _diff: _diff[name]};
        this.#updateData(innerSchema, current, value, recursiveOptions);
        if ( isEmpty$1(_diff[name]) ) delete _diff[name];
      }

      // Special Case: Object Field
      else if ( (field instanceof ObjectField) && current && value && (recursive !== false) ) {
        _diff[name] = diffObject(current, value);
        mergeObject(current, value, {insertKeys: true, insertValues: true, performDeletions: true});
        if ( isEmpty$1(_diff[name]) ) delete _diff[name];
      }

      // Standard Case: Update Directly
      else source[name] = value;
    }

    /* ---------------------------------------- */
    /*  Serialization and Storage               */
    /* ---------------------------------------- */

    /**
     * Copy and transform the DataModel into a plain object.
     * Draw the values of the extracted object from the data source (by default) otherwise from its transformed values.
     * @param {boolean} [source=true]     Draw values from the underlying data source rather than transformed values
     * @returns {object}                  The extracted primitive object
     */
    toObject(source=true) {
      if ( source ) return deepClone(this._source);

      // We have use the schema of the class instead of the schema of the instance to prevent an infinite recursion:
      // the EmbeddedDataField replaces the schema of its model instance with itself
      // and EmbeddedDataField#toObject calls DataModel#toObject.
      return this.constructor.schema.toObject(this);
    }

    /* ---------------------------------------- */

    /**
     * Extract the source data for the DataModel into a simple object format that can be serialized.
     * @returns {object}          The document source data expressed as a plain object
     */
    toJSON() {
      return this.toObject(true);
    }

    /* -------------------------------------------- */

    /**
     * Create a new instance of this DataModel from a source record.
     * The source is presumed to be trustworthy and is not strictly validated.
     * @param {object} source                    Initial document data which comes from a trusted source.
     * @param {DocumentConstructionContext & DataValidationOptions} [context]  Model construction context
     * @param {boolean} [context.strict=false]   Models created from trusted source data are validated non-strictly
     * @returns {DataModel}
     */
    static fromSource(source, {strict=false, ...context}={}) {
      return new this(source, {strict, ...context});
    }

    /* ---------------------------------------- */

    /**
     * Create a DataModel instance using a provided serialized JSON string.
     * @param {string} json       Serialized document data in string format
     * @returns {DataModel}       A constructed data model instance
     */
    static fromJSON(json) {
      return this.fromSource(JSON.parse(json))
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * Migrate candidate source data for this DataModel which may require initial cleaning or transformations.
     * @param {object} source           The candidate source data from which the model will be constructed
     * @returns {object}                Migrated source data, if necessary
     */
    static migrateData(source) {
      if ( !source ) return source;
      this.schema.migrateSource(source, source);
      return source;
    }

    /* ---------------------------------------- */

    /**
     * Wrap data migration in a try/catch which attempts it safely
     * @param {object} source           The candidate source data from which the model will be constructed
     * @returns {object}                Migrated source data, if necessary
     */
    static migrateDataSafe(source) {
      try {
        this.migrateData(source);
      } catch(err) {
        err.message = `Failed data migration for ${this.name}: ${err.message}`;
        logger.warn(err);
      }
      return source;
    }

    /* ---------------------------------------- */

    /**
     * Take data which conforms to the current data schema and add backwards-compatible accessors to it in order to
     * support older code which uses this data.
     * @param {object} data         Data which matches the current schema
     * @param {object} [options={}] Additional shimming options
     * @param {boolean} [options.embedded=true] Apply shims to embedded models?
     * @returns {object}            Data with added backwards-compatible properties
     */
    static shimData(data, {embedded=true}={}) {
      if ( Object.isSealed(data) ) return data;
      const schema = this.schema;
      if ( embedded ) {
        for ( const [name, value] of Object.entries(data) ) {
          const field = schema.get(name);
          if ( (field instanceof EmbeddedDataField) && !Object.isSealed(value) ) {
            data[name] = field.model.shimData(value || {});
          }
          else if ( field instanceof EmbeddedCollectionField ) {
            for ( const d of (value || []) ) {
              if ( !Object.isSealed(d) ) field.model.shimData(d);
            }
          }
        }
      }
      return data;
    }
  }

  /**
   * A specialized subclass of DataModel, intended to represent a Document's type-specific data.
   * Systems or Modules that provide DataModel implementations for sub-types of Documents (such as Actors or Items)
   * should subclass this class instead of the base DataModel class.
   *
   * @see {@link Document}
   * @extends {DataModel}
   * @abstract
   *
   * @example Registering a custom sub-type for a Module.
   *
   * **module.json**
   * ```json
   * {
   *   "id": "my-module",
   *   "esmodules": ["main.mjs"],
   *   "documentTypes": {
   *     "Actor": {
   *       "sidekick": {},
   *       "villain": {}
   *     },
   *     "JournalEntryPage": {
   *       "dossier": {},
   *       "quest": {
   *         "htmlFields": ["description"]
   *       }
   *     }
   *   }
   * }
   * ```
   *
   * **main.mjs**
   * ```js
   * Hooks.on("init", () => {
   *   Object.assign(CONFIG.Actor.dataModels, {
   *     "my-module.sidekick": SidekickModel,
   *     "my-module.villain": VillainModel
   *   });
   *   Object.assign(CONFIG.JournalEntryPage.dataModels, {
   *     "my-module.dossier": DossierModel,
   *     "my-module.quest": QuestModel
   *   });
   * });
   *
   * class QuestModel extends foundry.abstract.TypeDataModel {
   *   static defineSchema() {
   *     const fields = foundry.data.fields;
   *     return {
   *       description: new fields.HTMLField({required: false, blank: true, initial: ""}),
   *       steps: new fields.ArrayField(new fields.StringField())
   *     };
   *   }
   *
   *   prepareDerivedData() {
   *     this.totalSteps = this.steps.length;
   *   }
   * }
   * ```
   */
  class TypeDataModel extends DataModel {

    /** @inheritdoc */
    constructor(data={}, options={}) {
      super(data, options);

      /**
       * The package that is providing this DataModel for the given sub-type.
       * @type {System|Module|null}
       */
      Object.defineProperty(this, "modelProvider", {value: TypeDataField.getModelProvider(this), writable: false});
    }

    /**
     * A set of localization prefix paths which are used by this data model.
     * @type {string[]}
     */
    static LOCALIZATION_PREFIXES = [];

    /* ---------------------------------------- */

    /** @override */
    static get schema() {
      if ( this.hasOwnProperty("_schema") ) return this._schema;
      const schema = super.schema;
      schema.name = "system";
      return schema;
    }

    /* -------------------------------------------- */

    /**
     * Prepare data related to this DataModel itself, before any derived data is computed.
     *
     * Called before {@link ClientDocument#prepareBaseData} in {@link ClientDocument#prepareData}.
     */
    prepareBaseData() {}

    /* -------------------------------------------- */

    /**
     * Apply transformations of derivations to the values of the source data object.
     * Compute data fields whose values are not stored to the database.
     *
     * Called before {@link ClientDocument#prepareDerivedData} in {@link ClientDocument#prepareData}.
     */
    prepareDerivedData() {}

    /* -------------------------------------------- */

    /**
     * Convert this Document to some HTML display for embedding purposes.
     * @param {DocumentHTMLEmbedConfig} config  Configuration for embedding behavior.
     * @param {EnrichmentOptions} [options]     The original enrichment options for cases where the Document embed content
     *                                          also contains text that must be enriched.
     * @returns {Promise<HTMLElement|HTMLCollection|null>}
     */
    async toEmbed(config, options={}) {
      return null;
    }

    /* -------------------------------------------- */
    /*  Database Operations                         */
    /* -------------------------------------------- */

    /**
     * Called by {@link ClientDocument#_preCreate}.
     *
     * @param {object} data                         The initial data object provided to the document creation request
     * @param {object} options                      Additional options which modify the creation request
     * @param {documents.BaseUser} user             The User requesting the document creation
     * @returns {Promise<boolean|void>}             Return false to exclude this Document from the creation operation
     * @internal
     */
    async _preCreate(data, options, user) {}

    /* -------------------------------------------- */

    /**
     * Called by {@link ClientDocument#_onCreate}.
     *
     * @param {object} data                         The initial data object provided to the document creation request
     * @param {object} options                      Additional options which modify the creation request
     * @param {string} userId                       The id of the User requesting the document update
     * @protected
     * @internal
     */
    _onCreate(data, options, userId) {}

    /* -------------------------------------------- */

    /**
     * Called by {@link ClientDocument#_preUpdate}.
     *
     * @param {object} changes            The candidate changes to the Document
     * @param {object} options            Additional options which modify the update request
     * @param {documents.BaseUser} user   The User requesting the document update
     * @returns {Promise<boolean|void>}   A return value of false indicates the update operation should be cancelled.
     * @protected
     * @internal
     */
    async _preUpdate(changes, options, user) {}

    /* -------------------------------------------- */

    /**
     * Called by {@link ClientDocument#_onUpdate}.
     *
     * @param {object} changed            The differential data that was changed relative to the documents prior values
     * @param {object} options            Additional options which modify the update request
     * @param {string} userId             The id of the User requesting the document update
     * @protected
     * @internal
     */
    _onUpdate(changed, options, userId) {}

    /* -------------------------------------------- */


    /**
     * Called by {@link ClientDocument#_preDelete}.
     *
     * @param {object} options            Additional options which modify the deletion request
     * @param {documents.BaseUser} user   The User requesting the document deletion
     * @returns {Promise<boolean|void>}   A return value of false indicates the deletion operation should be cancelled.
     * @protected
     * @internal
     */
    async _preDelete(options, user) {}

    /* -------------------------------------------- */

    /**
     * Called by {@link ClientDocument#_onDelete}.
     *
     * @param {object} options            Additional options which modify the deletion request
     * @param {string} userId             The id of the User requesting the document update
     * @protected
     * @internal
     */
    _onDelete(options, userId) {}
  }

  /**
   * An extension of the base DataModel which defines a Document.
   * Documents are special in that they are persisted to the database and referenced by _id.
   * @memberof abstract
   * @abstract
   * @alias foundry.abstract.Document
   *
   * @param {object} data                           Initial data from which to construct the Document
   * @param {DocumentConstructionContext} context   Construction context options
   *
   * @property {string|null} _id                    The document identifier, unique within its Collection, or null if the
   *                                                Document has not yet been assigned an identifier
   * @property {string} [name]                      Documents typically have a human-readable name
   * @property {DataModel} [system]                 Certain document types may have a system data model which contains
   *                                                subtype-specific data defined by the game system or a module
   * @property {DocumentStats} [_stats]             Primary document types have a _stats object which provides metadata
   *                                                about their status
   * @property {Record<string, any>} flags          Documents each have an object of arbitrary flags which are used by
   *                                                systems or modules to store additional Document-specific data
   */
  class Document extends DataModel {

    /** @override */
    _configure({pack=null, parentCollection=null}={}) {
      /**
       * An immutable reverse-reference to the name of the collection that this Document exists in on its parent, if any.
       * @type {string|null}
       */
      Object.defineProperty(this, "parentCollection", {
        value: this._getParentCollection(parentCollection),
        writable: false
      });

      /**
       * An immutable reference to a containing Compendium collection to which this Document belongs.
       * @type {string|null}
       */
      Object.defineProperty(this, "pack", {
        value: (() => {
          if ( typeof pack === "string" ) return pack;
          if ( this.parent?.pack ) return this.parent.pack;
          if ( pack === null ) return null;
          throw new Error("The provided compendium pack ID must be a string");
        })(),
        writable: false
      });

      // Construct Embedded Collections
      const collections = {};
      for ( const [fieldName, field] of Object.entries(this.constructor.hierarchy) ) {
        if ( !field.constructor.implementation ) continue;
        const data = this._source[fieldName];
        const c = collections[fieldName] = new field.constructor.implementation(fieldName, this, data);
        Object.defineProperty(this, fieldName, {value: c, writable: false});
      }

      /**
       * A mapping of embedded Document collections which exist in this model.
       * @type {Record<string, EmbeddedCollection>}
       */
      Object.defineProperty(this, "collections", {value: Object.seal(collections), writable: false});
    }

    /* ---------------------------------------- */

    /**
     * Ensure that all Document classes share the same schema of their base declaration.
     * @type {SchemaField}
     * @override
     */
    static get schema() {
      if ( this._schema ) return this._schema;
      const base = this.baseDocument;
      if ( !base.hasOwnProperty("_schema") ) {
        const schema = new SchemaField(Object.freeze(base.defineSchema()));
        Object.defineProperty(base, "_schema", {value: schema, writable: false});
      }
      Object.defineProperty(this, "_schema", {value: base._schema, writable: false});
      return base._schema;
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    _initialize(options={}) {
      super._initialize(options);

      const singletons = {};
      for ( const [fieldName, field] of Object.entries(this.constructor.hierarchy) ) {
        if ( field instanceof foundry.data.fields.EmbeddedDocumentField ) {
          Object.defineProperty(singletons, fieldName, { get: () => this[fieldName] });
        }
      }

      /**
       * A mapping of singleton embedded Documents which exist in this model.
       * @type {Record<string, Document>}
       */
      Object.defineProperty(this, "singletons", {value: Object.seal(singletons), configurable: true});
    }

    /* -------------------------------------------- */

    /** @override */
    static *_initializationOrder() {
      const hierarchy = this.hierarchy;

      // Initialize non-hierarchical fields first
      for ( const [name, field] of this.schema.entries() ) {
        if ( name in hierarchy ) continue;
        yield [name, field];
      }

      // Initialize hierarchical fields last
      for ( const [name, field] of Object.entries(hierarchy) ) {
        yield [name, field];
      }
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /**
     * Default metadata which applies to each instance of this Document type.
     * @type {object}
     */
    static metadata = Object.freeze({
      name: "Document",
      collection: "documents",
      indexed: false,
      compendiumIndexFields: [],
      label: "DOCUMENT.Document",
      coreTypes: [BASE_DOCUMENT_TYPE],
      embedded: {},
      permissions: {
        create: "ASSISTANT",
        update: "ASSISTANT",
        delete: "ASSISTANT"
      },
      preserveOnImport: ["_id", "sort", "ownership"],
      /*
       * The metadata has to include the version of this Document schema, which needs to be increased
       * whenever the schema is changed such that Document data created before this version
       * would come out different if `fromSource(data).toObject()` was applied to it so that
       * we always vend data to client that is in the schema of the current core version.
       * The schema version needs to be bumped if
       *   - a field was added or removed,
       *   - the class/type of any field was changed,
       *   - the casting or cleaning behavior of any field class was changed,
       *   - the data model of an embedded data field was changed,
       *   - certain field properties are changed (e.g. required, nullable, blank, ...), or
       *   - there have been changes to cleanData or migrateData of the Document.
       *
       * Moreover, the schema version needs to be bumped if the sanitization behavior
       * of any field in the schema was changed.
       */
      schemaVersion: undefined
    });

    /* -------------------------------------------- */

    /**
     * The database backend used to execute operations and handle results.
     * @type {abstract.DatabaseBackend}
     */
    static get database() {
      return globalThis.CONFIG.DatabaseBackend;
    }

    /* -------------------------------------------- */

    /**
     * Return a reference to the configured subclass of this base Document type.
     * @type {typeof Document}
     */
    static get implementation() {
      return globalThis.CONFIG[this.documentName]?.documentClass || this;
    }

    /* -------------------------------------------- */

    /**
     * The base document definition that this document class extends from.
     * @type {typeof Document}
     */
    static get baseDocument() {
      let cls;
      let parent = this;
      while ( parent ) {
        cls = parent;
        parent = Object.getPrototypeOf(cls);
        if ( parent === Document ) return cls;
      }
      throw new Error(`Base Document class identification failed for "${this.documentName}"`);
    }

    /* -------------------------------------------- */

    /**
     * The named collection to which this Document belongs.
     * @type {string}
     */
    static get collectionName() {
      return this.metadata.collection;
    }
    get collectionName() {
      return this.constructor.collectionName;
    }

    /* -------------------------------------------- */

    /**
     * The canonical name of this Document type, for example "Actor".
     * @type {string}
     */
    static get documentName() {
      return this.metadata.name;
    }
    get documentName() {
      return this.constructor.documentName;
    }

    /* ---------------------------------------- */

    /**
     * The allowed types which may exist for this Document class.
     * @type {string[]}
     */
    static get TYPES() {
      return Object.keys(game.model[this.metadata.name]);
    }

    /* -------------------------------------------- */

    /**
     * Does this Document support additional subtypes?
     * @type {boolean}
     */
    static get hasTypeData() {
      return this.metadata.hasTypeData;
    }

    /* -------------------------------------------- */
    /*  Model Properties                            */
    /* -------------------------------------------- */

    /**
     * The Embedded Document hierarchy for this Document.
     * @returns {Readonly<Record<string, EmbeddedCollectionField|EmbeddedDocumentField>>}
     */
    static get hierarchy() {
      const hierarchy = {};
      for ( const [fieldName, field] of this.schema.entries() ) {
        if ( field.constructor.hierarchical ) hierarchy[fieldName] = field;
      }
      Object.defineProperty(this, "hierarchy", {value: Object.freeze(hierarchy), writable: false});
      return hierarchy;
    }

    /* -------------------------------------------- */

    /**
     * Identify the collection in a parent Document that this Document belongs to, if any.
     * @param {string|null} [parentCollection]  An explicitly provided parent collection name.
     * @returns {string|null}
     * @internal
     */
    _getParentCollection(parentCollection) {
      if ( !this.parent ) return null;
      if ( parentCollection ) return parentCollection;
      return this.parent.constructor.getCollectionName(this.documentName);
    }

    /**
     * The canonical identifier for this Document.
     * @type {string|null}
     */
    get id() {
      return this._id;
    }

    /**
     * Test whether this Document is embedded within a parent Document
     * @type {boolean}
     */
    get isEmbedded() {
      return !!(this.parent && this.parentCollection);
    }

    /* -------------------------------------------- */

    /**
     * A Universally Unique Identifier (uuid) for this Document instance.
     * @type {string}
     */
    get uuid() {
      let parts = [this.documentName, this.id];
      if ( this.parent ) parts = [this.parent.uuid].concat(parts);
      else if ( this.pack ) parts = ["Compendium", this.pack].concat(parts);
      return parts.join(".");
    }

    /* ---------------------------------------- */
    /*  Model Permissions                       */
    /* ---------------------------------------- */

    /**
     * Test whether a given User has a sufficient role in order to create Documents of this type in general.
     * @param {documents.BaseUser} user       The User being tested
     * @return {boolean}                      Does the User have a sufficient role to create?
     */
    static canUserCreate(user) {
      // TODO: https://github.com/foundryvtt/foundryvtt/issues/11280
      const perm = this.metadata.permissions.create;
      if ( perm instanceof Function ) {
        throw new Error('Document.canUserCreate is not supported for this document type. ' +
          'Use Document#canUserModify(user, "create") to test whether a user is permitted to create a ' +
          'specific document instead.');
      }
      return user.hasPermission(perm) || user.hasRole(perm, {exact: false});
    }

    /* ---------------------------------------- */

    /**
     * Get the explicit permission level that a User has over this Document, a value in CONST.DOCUMENT_OWNERSHIP_LEVELS.
     * This method returns the value recorded in Document ownership, regardless of the User's role.
     * To test whether a user has a certain capability over the document, testUserPermission should be used.
     * @param {documents.BaseUser} [user=game.user] The User being tested
     * @returns {number|null}               A numeric permission level from CONST.DOCUMENT_OWNERSHIP_LEVELS or null
     */
    getUserLevel(user) {
      user = user || game.user;

      // Compendium content uses role-based ownership
      if ( this.pack ) return this.compendium.getUserLevel(user);

      // World content uses granular per-User ownership
      const ownership = this["ownership"] || {};
      return ownership[user.id] ?? ownership.default ?? null;
    }

    /* ---------------------------------------- */

    /**
     * Test whether a certain User has a requested permission level (or greater) over the Document
     * @param {documents.BaseUser} user       The User being tested
     * @param {string|number} permission      The permission level from DOCUMENT_OWNERSHIP_LEVELS to test
     * @param {object} options                Additional options involved in the permission test
     * @param {boolean} [options.exact=false]     Require the exact permission level requested?
     * @return {boolean}                      Does the user have this permission level over the Document?
     */
    testUserPermission(user, permission, {exact=false}={}) {
      const perms = DOCUMENT_OWNERSHIP_LEVELS;

      let level;
      if ( user.isGM ) level = perms.OWNER;
      else if ( user.isBanned ) level = perms.NONE;
      else level = this.getUserLevel(user);

      const target = (typeof permission === "string") ? (perms[permission] ?? perms.OWNER) : permission;
      return exact ? level === target : level >= target;
    }

    /* ---------------------------------------- */

    /**
     * Test whether a given User has permission to perform some action on this Document
     * @param {documents.BaseUser} user   The User attempting modification
     * @param {string} action             The attempted action
     * @param {object} [data]             Data involved in the attempted action
     * @return {boolean}                  Does the User have permission?
     */
    canUserModify(user, action, data={}) {
      const permissions = this.constructor.metadata.permissions;
      const perm = permissions[action];

      // Specialized permission test function
      if ( perm instanceof Function ) return perm(user, this, data);

      // User-level permission
      else if ( perm in USER_PERMISSIONS ) return user.hasPermission(perm);

      // Document-level permission
      const isOwner = this.testUserPermission(user, "OWNER");
      const hasRole = (perm in USER_ROLES) && user.hasRole(perm);
      return isOwner || hasRole;
    }

    /* ---------------------------------------- */
    /*  Model Methods                           */
    /* ---------------------------------------- */

    /**
     * Clone a document, creating a new document by combining current data with provided overrides.
     * The cloned document is ephemeral and not yet saved to the database.
     * @param {Object} [data={}]                         Additional data which overrides current document data at the time
     *                                                   of creation
     * @param {DocumentConstructionContext} [context={}] Additional context options passed to the create method
     * @param {boolean} [context.save=false]             Save the clone to the World database?
     * @param {boolean} [context.keepId=false]           Keep the same ID of the original document
     * @param {boolean} [context.addSource=false]        Track the clone source.
     * @returns {Document|Promise<Document>}             The cloned Document instance
     */
    clone(data={}, {save=false, keepId=false, addSource=false, ...context}={}) {
      if ( !keepId ) data["-=_id"] = null;
      if ( addSource ) data["_stats.duplicateSource"] = this.uuid;
      context.parent = this.parent;
      context.pack = this.pack;
      context.strict = false;
      const doc = super.clone(data, context);
      return save ? this.constructor.create(doc, context) : doc;
    }

    /* -------------------------------------------- */

    /**
     * For Documents which include game system data, migrate the system data object to conform to its latest data model.
     * The data model is defined by the template.json specification included by the game system.
     * @returns {object}              The migrated system data object
     */
    migrateSystemData() {
      if ( !this.constructor.hasTypeData ) {
        throw new Error(`The ${this.documentName} Document does not include a TypeDataField.`);
      }
      if ( (this.system instanceof DataModel) && !(this.system.modelProvider instanceof System) ) {
        throw new Error(`The ${this.documentName} Document does not have system-provided package data.`);
      }
      const model = game.model[this.documentName]?.[this["type"]] || {};
      return mergeObject(model, this["system"], {
        insertKeys: false,
        insertValues: true,
        enforceTypes: false,
        overwrite: true,
        inplace: false
      });
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    toObject(source=true) {
      const data = super.toObject(source);
      return this.constructor.shimData(data);
    }

    /* -------------------------------------------- */
    /*  Database Operations                         */
    /* -------------------------------------------- */

    /**
     * Create multiple Documents using provided input data.
     * Data is provided as an array of objects where each individual object becomes one new Document.
     *
     * @param {Array<object|Document>} data  An array of data objects or existing Documents to persist.
     * @param {Partial<Omit<DatabaseCreateOperation, "data">>} [operation={}]  Parameters of the requested creation
     *                                  operation
     * @return {Promise<Document[]>}         An array of created Document instances
     *
     * @example Create a single Document
     * ```js
     * const data = [{name: "New Actor", type: "character", img: "path/to/profile.jpg"}];
     * const created = await Actor.createDocuments(data);
     * ```
     *
     * @example Create multiple Documents
     * ```js
     * const data = [{name: "Tim", type: "npc"], [{name: "Tom", type: "npc"}];
     * const created = await Actor.createDocuments(data);
     * ```
     *
     * @example Create multiple embedded Documents within a parent
     * ```js
     * const actor = game.actors.getName("Tim");
     * const data = [{name: "Sword", type: "weapon"}, {name: "Breastplate", type: "equipment"}];
     * const created = await Item.createDocuments(data, {parent: actor});
     * ```
     *
     * @example Create a Document within a Compendium pack
     * ```js
     * const data = [{name: "Compendium Actor", type: "character", img: "path/to/profile.jpg"}];
     * const created = await Actor.createDocuments(data, {pack: "mymodule.mypack"});
     * ```
     */
    static async createDocuments(data=[], operation={}) {
      if ( operation.parent?.pack ) operation.pack = operation.parent.pack;
      operation.data = data;
      const created = await this.database.create(this.implementation, operation);

      /** @deprecated since v12 */
      if ( getDefiningClass(this, "_onCreateDocuments") !== Document ) {
        foundry.utils.logCompatibilityWarning("The Document._onCreateDocuments static method is deprecated in favor of "
          + "Document._onCreateOperation", {since: 12, until: 14});
        await this._onCreateDocuments(created, operation);
      }
      return created;
    }

    /* -------------------------------------------- */

    /**
     * Update multiple Document instances using provided differential data.
     * Data is provided as an array of objects where each individual object updates one existing Document.
     *
     * @param {object[]} updates          An array of differential data objects, each used to update a single Document
     * @param {Partial<Omit<DatabaseUpdateOperation, "updates">>} [operation={}] Parameters of the database update
     *                                    operation
     * @return {Promise<Document[]>}      An array of updated Document instances
     *
     * @example Update a single Document
     * ```js
     * const updates = [{_id: "12ekjf43kj2312ds", name: "Timothy"}];
     * const updated = await Actor.updateDocuments(updates);
     * ```
     *
     * @example Update multiple Documents
     * ```js
     * const updates = [{_id: "12ekjf43kj2312ds", name: "Timothy"}, {_id: "kj549dk48k34jk34", name: "Thomas"}]};
     * const updated = await Actor.updateDocuments(updates);
     * ```
     *
     * @example Update multiple embedded Documents within a parent
     * ```js
     * const actor = game.actors.getName("Timothy");
     * const updates = [{_id: sword.id, name: "Magic Sword"}, {_id: shield.id, name: "Magic Shield"}];
     * const updated = await Item.updateDocuments(updates, {parent: actor});
     * ```
     *
     * @example Update Documents within a Compendium pack
     * ```js
     * const actor = await pack.getDocument(documentId);
     * const updated = await Actor.updateDocuments([{_id: actor.id, name: "New Name"}], {pack: "mymodule.mypack"});
     * ```
     */
    static async updateDocuments(updates=[], operation={}) {
      if ( operation.parent?.pack ) operation.pack = operation.parent.pack;
      operation.updates = updates;
      const updated = await this.database.update(this.implementation, operation);

      /** @deprecated since v12 */
      if ( getDefiningClass(this, "_onUpdateDocuments") !== Document ) {
        foundry.utils.logCompatibilityWarning("The Document._onUpdateDocuments static method is deprecated in favor of "
          + "Document._onUpdateOperation", {since: 12, until: 14});
        await this._onUpdateDocuments(updated, operation);
      }
      return updated;
    }

    /* -------------------------------------------- */

    /**
     * Delete one or multiple existing Documents using an array of provided ids.
     * Data is provided as an array of string ids for the documents to delete.
     *
     * @param {string[]} ids              An array of string ids for the documents to be deleted
     * @param {Partial<Omit<DatabaseDeleteOperation, "ids">>} [operation={}]  Parameters of the database deletion
     *                                    operation
     * @return {Promise<Document[]>}      An array of deleted Document instances
     *
     * @example Delete a single Document
     * ```js
     * const tim = game.actors.getName("Tim");
     * const deleted = await Actor.deleteDocuments([tim.id]);
     * ```
     *
     * @example Delete multiple Documents
     * ```js
     * const tim = game.actors.getName("Tim");
     * const tom = game.actors.getName("Tom");
     * const deleted = await Actor.deleteDocuments([tim.id, tom.id]);
     * ```
     *
     * @example Delete multiple embedded Documents within a parent
     * ```js
     * const tim = game.actors.getName("Tim");
     * const sword = tim.items.getName("Sword");
     * const shield = tim.items.getName("Shield");
     * const deleted = await Item.deleteDocuments([sword.id, shield.id], parent: actor});
     * ```
     *
     * @example Delete Documents within a Compendium pack
     * ```js
     * const actor = await pack.getDocument(documentId);
     * const deleted = await Actor.deleteDocuments([actor.id], {pack: "mymodule.mypack"});
     * ```
     */
    static async deleteDocuments(ids=[], operation={}) {
      if ( operation.parent?.pack ) operation.pack = operation.parent.pack;
      operation.ids = ids;
      const deleted = await this.database.delete(this.implementation, operation);

      /** @deprecated since v12 */
      if ( getDefiningClass(this, "_onDeleteDocuments") !== Document ) {
        foundry.utils.logCompatibilityWarning("The Document._onDeleteDocuments static method is deprecated in favor of "
          + "Document._onDeleteOperation", {since: 12, until: 14});
        await this._onDeleteDocuments(deleted, operation);
      }
      return deleted;
    }

    /* -------------------------------------------- */

    /**
     * Create a new Document using provided input data, saving it to the database.
     * @see Document.createDocuments
     * @param {object|Document|(object|Document)[]} [data={}] Initial data used to create this Document, or a Document
     *                                                        instance to persist.
     * @param {Partial<Omit<DatabaseCreateOperation, "data">>} [operation={}]  Parameters of the creation operation
     * @returns {Promise<Document | Document[] | undefined>}        The created Document instance
     *
     * @example Create a World-level Item
     * ```js
     * const data = [{name: "Special Sword", type: "weapon"}];
     * const created = await Item.create(data);
     * ```
     *
     * @example Create an Actor-owned Item
     * ```js
     * const data = [{name: "Special Sword", type: "weapon"}];
     * const actor = game.actors.getName("My Hero");
     * const created = await Item.create(data, {parent: actor});
     * ```
     *
     * @example Create an Item in a Compendium pack
     * ```js
     * const data = [{name: "Special Sword", type: "weapon"}];
     * const created = await Item.create(data, {pack: "mymodule.mypack"});
     * ```
     */
    static async create(data, operation={}) {
      const createData = data instanceof Array ? data : [data];
      const created = await this.createDocuments(createData, operation);
      return data instanceof Array ? created : created.shift();
    }

    /* -------------------------------------------- */

    /**
     * Update this Document using incremental data, saving it to the database.
     * @see Document.updateDocuments
     * @param {object} [data={}]          Differential update data which modifies the existing values of this document
     * @param {Partial<Omit<DatabaseUpdateOperation, "updates">>} [operation={}]  Parameters of the update operation
     * @returns {Promise<Document>}       The updated Document instance
     */
    async update(data={}, operation={}) {
      data._id = this.id;
      operation.parent = this.parent;
      operation.pack = this.pack;
      const updates = await this.constructor.updateDocuments([data], operation);
      return updates.shift();
    }

    /* -------------------------------------------- */

    /**
     * Delete this Document, removing it from the database.
     * @see Document.deleteDocuments
     * @param {Partial<Omit<DatabaseDeleteOperation, "ids">>} [operation={}]  Parameters of the deletion operation
     * @returns {Promise<Document>}       The deleted Document instance
     */
    async delete(operation={}) {
      operation.parent = this.parent;
      operation.pack = this.pack;
      const deleted = await this.constructor.deleteDocuments([this.id], operation);
      return deleted.shift();
    }

    /* -------------------------------------------- */

    /**
     * Get a World-level Document of this type by its id.
     * @param {string} documentId         The Document ID
     * @param {DatabaseGetOperation} [operation={}] Parameters of the get operation
     * @returns {abstract.Document|null}  The retrieved Document, or null
     */
    static get(documentId, operation={}) {
      if ( !documentId ) return null;
      if ( operation.pack ) {
        const pack = game.packs.get(operation.pack);
        return pack?.index.get(documentId) || null;
      }
      else {
        const collection = game.collections?.get(this.documentName);
        return collection?.get(documentId) || null;
      }
    }

    /* -------------------------------------------- */
    /*  Embedded Operations                         */
    /* -------------------------------------------- */

    /**
     * A compatibility method that returns the appropriate name of an embedded collection within this Document.
     * @param {string} name    An existing collection name or a document name.
     * @returns {string|null}  The provided collection name if it exists, the first available collection for the
     *                         document name provided, or null if no appropriate embedded collection could be found.
     * @example Passing an existing collection name.
     * ```js
     * Actor.getCollectionName("items");
     * // returns "items"
     * ```
     *
     * @example Passing a document name.
     * ```js
     * Actor.getCollectionName("Item");
     * // returns "items"
     * ```
     */
    static getCollectionName(name) {
      if ( name in this.hierarchy ) return name;
      for ( const [collectionName, field] of Object.entries(this.hierarchy) ) {
        if ( field.model.documentName === name ) return collectionName;
      }
      return null;
    }

    /* -------------------------------------------- */

    /**
     * Obtain a reference to the Array of source data within the data object for a certain embedded Document name
     * @param {string} embeddedName   The name of the embedded Document type
     * @return {DocumentCollection}   The Collection instance of embedded Documents of the requested type
     */
    getEmbeddedCollection(embeddedName) {
      const collectionName = this.constructor.getCollectionName(embeddedName);
      if ( !collectionName ) {
        throw new Error(`${embeddedName} is not a valid embedded Document within the ${this.documentName} Document`);
      }
      const field = this.constructor.hierarchy[collectionName];
      return field.getCollection(this);
    }

    /* -------------------------------------------- */

    /**
     * Get an embedded document by its id from a named collection in the parent document.
     * @param {string} embeddedName              The name of the embedded Document type
     * @param {string} id                        The id of the child document to retrieve
     * @param {object} [options]                 Additional options which modify how embedded documents are retrieved
     * @param {boolean} [options.strict=false]   Throw an Error if the requested id does not exist. See Collection#get
     * @param {boolean} [options.invalid=false]  Allow retrieving an invalid Embedded Document.
     * @return {Document}                        The retrieved embedded Document instance, or undefined
     * @throws If the embedded collection does not exist, or if strict is true and the Embedded Document could not be
     *         found.
     */
    getEmbeddedDocument(embeddedName, id, {invalid=false, strict=false}={}) {
      const collection = this.getEmbeddedCollection(embeddedName);
      return collection.get(id, {invalid, strict});
    }

    /* -------------------------------------------- */

    /**
     * Create multiple embedded Document instances within this parent Document using provided input data.
     * @see Document.createDocuments
     * @param {string} embeddedName                     The name of the embedded Document type
     * @param {object[]} data                           An array of data objects used to create multiple documents
     * @param {DatabaseCreateOperation} [operation={}]  Parameters of the database creation workflow
     * @return {Promise<Document[]>}                    An array of created Document instances
     */
    async createEmbeddedDocuments(embeddedName, data=[], operation={}) {
      this.getEmbeddedCollection(embeddedName); // Validation only
      operation.parent = this;
      operation.pack = this.pack;
      const cls = getDocumentClass(embeddedName);
      return cls.createDocuments(data, operation);
    }

    /* -------------------------------------------- */

    /**
     * Update multiple embedded Document instances within a parent Document using provided differential data.
     * @see Document.updateDocuments
     * @param {string} embeddedName                     The name of the embedded Document type
     * @param {object[]} updates                        An array of differential data objects, each used to update a
     *                                                  single Document
     * @param {DatabaseUpdateOperation} [operation={}]  Parameters of the database update workflow
     * @return {Promise<Document[]>}                    An array of updated Document instances
     */
    async updateEmbeddedDocuments(embeddedName, updates=[], operation={}) {
      this.getEmbeddedCollection(embeddedName); // Validation only
      operation.parent = this;
      operation.pack = this.pack;
      const cls = getDocumentClass(embeddedName);
      return cls.updateDocuments(updates, operation);
    }

    /* -------------------------------------------- */

    /**
     * Delete multiple embedded Document instances within a parent Document using provided string ids.
     * @see Document.deleteDocuments
     * @param {string} embeddedName                     The name of the embedded Document type
     * @param {string[]} ids                            An array of string ids for each Document to be deleted
     * @param {DatabaseDeleteOperation} [operation={}]  Parameters of the database deletion workflow
     * @return {Promise<Document[]>}                    An array of deleted Document instances
     */
    async deleteEmbeddedDocuments(embeddedName, ids, operation={}) {
      this.getEmbeddedCollection(embeddedName); // Validation only
      operation.parent = this;
      operation.pack = this.pack;
      const cls = getDocumentClass(embeddedName);
      return cls.deleteDocuments(ids, operation);
    }

    /* -------------------------------------------- */

    /**
     * Iterate over all embedded Documents that are hierarchical children of this Document.
     * @param {string} [_parentPath]                      A parent field path already traversed
     * @returns {Generator<[string, Document]>}
     */
    * traverseEmbeddedDocuments(_parentPath) {
      for ( const [fieldName, field] of Object.entries(this.constructor.hierarchy) ) {
        let fieldPath = _parentPath ? `${_parentPath}.${fieldName}` : fieldName;

        // Singleton embedded document
        if ( field instanceof foundry.data.fields.EmbeddedDocumentField ) {
          const document = this[fieldName];
          if ( document ) {
            yield [fieldPath, document];
            yield* document.traverseEmbeddedDocuments(fieldPath);
          }
        }

        // Embedded document collection
        else if ( field instanceof foundry.data.fields.EmbeddedCollectionField ) {
          const collection = this[fieldName];
          const isDelta = field instanceof foundry.data.fields.EmbeddedCollectionDeltaField;
          for ( const document of collection.values() ) {
            if ( isDelta && !collection.manages(document.id) ) continue;
            yield [fieldPath, document];
            yield* document.traverseEmbeddedDocuments(fieldPath);
          }
        }
      }
    }

    /* -------------------------------------------- */
    /*  Flag Operations                             */
    /* -------------------------------------------- */

    /**
     * Get the value of a "flag" for this document
     * See the setFlag method for more details on flags
     *
     * @param {string} scope        The flag scope which namespaces the key
     * @param {string} key          The flag key
     * @return {*}                  The flag value
     */
    getFlag(scope, key) {
      const scopes = this.constructor.database.getFlagScopes();
      if ( !scopes.includes(scope) ) throw new Error(`Flag scope "${scope}" is not valid or not currently active`);

      /** @deprecated since v12 */
      if ( (scope === "core") && (key === "sourceId") ) {
        foundry.utils.logCompatibilityWarning("The core.sourceId flag has been deprecated. "
          + "Please use the _stats.compendiumSource property instead.", { since: 12, until: 14 });
        return this._stats?.compendiumSource;
      }

      if ( !this.flags || !(scope in this.flags) ) return undefined;
      return getProperty(this.flags?.[scope], key);
    }

    /* -------------------------------------------- */

    /**
     * Assign a "flag" to this document.
     * Flags represent key-value type data which can be used to store flexible or arbitrary data required by either
     * the core software, game systems, or user-created modules.
     *
     * Each flag should be set using a scope which provides a namespace for the flag to help prevent collisions.
     *
     * Flags set by the core software use the "core" scope.
     * Flags set by game systems or modules should use the canonical name attribute for the module
     * Flags set by an individual world should "world" as the scope.
     *
     * Flag values can assume almost any data type. Setting a flag value to null will delete that flag.
     *
     * @param {string} scope        The flag scope which namespaces the key
     * @param {string} key          The flag key
     * @param {*} value             The flag value
     * @return {Promise<Document>}  A Promise resolving to the updated document
     */
    async setFlag(scope, key, value) {
      const scopes = this.constructor.database.getFlagScopes();
      if ( !scopes.includes(scope) ) throw new Error(`Flag scope "${scope}" is not valid or not currently active`);
      return this.update({
        flags: {
          [scope]: {
            [key]: value
          }
        }
      });
    }

    /* -------------------------------------------- */

    /**
     * Remove a flag assigned to the document
     * @param {string} scope        The flag scope which namespaces the key
     * @param {string} key          The flag key
     * @return {Promise<Document>}  The updated document instance
     */
    async unsetFlag(scope, key) {
      const scopes = this.constructor.database.getFlagScopes();
      if ( !scopes.includes(scope) ) throw new Error(`Flag scope "${scope}" is not valid or not currently active`);
      const head = key.split(".");
      const tail = `-=${head.pop()}`;
      key = ["flags", scope, ...head, tail].join(".");
      return this.update({[key]: null});
    }

    /* -------------------------------------------- */
    /*  Database Creation Operations                */
    /* -------------------------------------------- */

    /**
     * Pre-process a creation operation for a single Document instance. Pre-operation events only occur for the client
     * which requested the operation.
     *
     * Modifications to the pending Document instance must be performed using {@link Document#updateSource}.
     *
     * @param {object} data                         The initial data object provided to the document creation request
     * @param {object} options                      Additional options which modify the creation request
     * @param {documents.BaseUser} user             The User requesting the document creation
     * @returns {Promise<boolean|void>}             Return false to exclude this Document from the creation operation
     * @internal
     */
    async _preCreate(data, options, user) {}

    /**
     * Post-process a creation operation for a single Document instance. Post-operation events occur for all connected
     * clients.
     *
     * @param {object} data                         The initial data object provided to the document creation request
     * @param {object} options                      Additional options which modify the creation request
     * @param {string} userId                       The id of the User requesting the document update
     * @internal
     */
    _onCreate(data, options, userId) {}

    /**
     * Pre-process a creation operation, potentially altering its instructions or input data. Pre-operation events only
     * occur for the client which requested the operation.
     *
     * This batch-wise workflow occurs after individual {@link Document#_preCreate} workflows and provides a final
     * pre-flight check before a database operation occurs.
     *
     * Modifications to pending documents must mutate the documents array or alter individual document instances using
     * {@link Document#updateSource}.
     *
     * @param {Document[]} documents                Pending document instances to be created
     * @param {DatabaseCreateOperation} operation   Parameters of the database creation operation
     * @param {documents.BaseUser} user             The User requesting the creation operation
     * @returns {Promise<boolean|void>}             Return false to cancel the creation operation entirely
     * @internal
     */
    static async _preCreateOperation(documents, operation, user) {}

    /**
     * Post-process a creation operation, reacting to database changes which have occurred. Post-operation events occur
     * for all connected clients.
     *
     * This batch-wise workflow occurs after individual {@link Document#_onCreate} workflows.
     *
     * @param {Document[]} documents                The Document instances which were created
     * @param {DatabaseCreateOperation} operation   Parameters of the database creation operation
     * @param {documents.BaseUser} user             The User who performed the creation operation
     * @returns {Promise<void>}
     * @internal
     */
    static async _onCreateOperation(documents, operation, user) {}

    /* -------------------------------------------- */
    /*  Database Update Operations                  */
    /* -------------------------------------------- */

    /**
     * Pre-process an update operation for a single Document instance. Pre-operation events only occur for the client
     * which requested the operation.
     *
     * @param {object} changes            The candidate changes to the Document
     * @param {object} options            Additional options which modify the update request
     * @param {documents.BaseUser} user   The User requesting the document update
     * @returns {Promise<boolean|void>}   A return value of false indicates the update operation should be cancelled.
     * @internal
     */
    async _preUpdate(changes, options, user) {}

    /**
     * Post-process an update operation for a single Document instance. Post-operation events occur for all connected
     * clients.
     *
     * @param {object} changed            The differential data that was changed relative to the documents prior values
     * @param {object} options            Additional options which modify the update request
     * @param {string} userId             The id of the User requesting the document update
     * @internal
     */
    _onUpdate(changed, options, userId) {}

    /**
     * Pre-process an update operation, potentially altering its instructions or input data. Pre-operation events only
     * occur for the client which requested the operation.
     *
     * This batch-wise workflow occurs after individual {@link Document#_preUpdate} workflows and provides a final
     * pre-flight check before a database operation occurs.
     *
     * Modifications to the requested updates are performed by mutating the data array of the operation.
     * {@link Document#updateSource}.
     *
     * @param {Document[]} documents                Document instances to be updated
     * @param {DatabaseUpdateOperation} operation   Parameters of the database update operation
     * @param {documents.BaseUser} user             The User requesting the update operation
     * @returns {Promise<boolean|void>}             Return false to cancel the update operation entirely
     * @internal
     */
    static async _preUpdateOperation(documents, operation, user) {}

    /**
     * Post-process an update operation, reacting to database changes which have occurred. Post-operation events occur
     * for all connected clients.
     *
     * This batch-wise workflow occurs after individual {@link Document#_onUpdate} workflows.
     *
     * @param {Document[]} documents                The Document instances which were updated
     * @param {DatabaseUpdateOperation} operation   Parameters of the database update operation
     * @param {documents.BaseUser} user             The User who performed the update operation
     * @returns {Promise<void>}
     * @internal
     */
    static async _onUpdateOperation(documents, operation, user) {}

    /* -------------------------------------------- */
    /*  Database Delete Operations                  */
    /* -------------------------------------------- */

    /**
     * Pre-process a deletion operation for a single Document instance. Pre-operation events only occur for the client
     * which requested the operation.
     *
     * @param {object} options            Additional options which modify the deletion request
     * @param {documents.BaseUser} user   The User requesting the document deletion
     * @returns {Promise<boolean|void>}   A return value of false indicates the deletion operation should be cancelled.
     * @internal
     */
    async _preDelete(options, user) {}

    /**
     * Post-process a deletion operation for a single Document instance. Post-operation events occur for all connected
     * clients.
     *
     * @param {object} options            Additional options which modify the deletion request
     * @param {string} userId             The id of the User requesting the document update
     * @internal
     */
    _onDelete(options, userId) {}

    /**
     * Pre-process a deletion operation, potentially altering its instructions or input data. Pre-operation events only
     * occur for the client which requested the operation.
     *
     * This batch-wise workflow occurs after individual {@link Document#_preDelete} workflows and provides a final
     * pre-flight check before a database operation occurs.
     *
     * Modifications to the requested deletions are performed by mutating the operation object.
     * {@link Document#updateSource}.
     *
     * @param {Document[]} documents                Document instances to be deleted
     * @param {DatabaseDeleteOperation} operation   Parameters of the database update operation
     * @param {documents.BaseUser} user             The User requesting the deletion operation
     * @returns {Promise<boolean|void>}             Return false to cancel the deletion operation entirely
     * @internal
     */
    static async _preDeleteOperation(documents, operation, user) {}

    /**
     * Post-process a deletion operation, reacting to database changes which have occurred. Post-operation events occur
     * for all connected clients.
     *
     * This batch-wise workflow occurs after individual {@link Document#_onDelete} workflows.
     *
     * @param {Document[]} documents                The Document instances which were deleted
     * @param {DatabaseDeleteOperation} operation   Parameters of the database deletion operation
     * @param {documents.BaseUser} user             The User who performed the deletion operation
     * @returns {Promise<void>}
     * @internal
     */
    static async _onDeleteOperation(documents, operation, user) {}

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * @deprecated since v10
     * @ignore
     */
    get data() {
      if ( this.constructor.schema.has("system") ) {
        throw new Error(`You are accessing the ${this.constructor.name} "data" field of which was deprecated in v10 and `
          + `replaced with "system". Continued usage of pre-v10 ".data" paths is no longer supported"`);
      }
    }

    /* -------------------------------------------- */


    /**
     * @deprecated since v11
     * @ignore
     */
    static get hasSystemData() {
      foundry.utils.logCompatibilityWarning(`You are accessing ${this.name}.hasSystemData which is deprecated. `
      + `Please use ${this.name}.hasTypeData instead.`, {since: 11, until: 13});
      return this.hasTypeData;
    }

    /* ---------------------------------------- */

    /**
     * A reusable helper for adding migration shims.
     * @protected
     * @ignore
     */
    static _addDataFieldShims(data, shims, options) {
      for ( const [oldKey, newKey] of Object.entries(shims) ) {
        this._addDataFieldShim(data, oldKey, newKey, options);
      }
    }

    /* ---------------------------------------- */

    /**
     * A reusable helper for adding a migration shim
     * @protected
     * @ignore
     */
    static _addDataFieldShim(data, oldKey, newKey, options={}) {
      if ( data.hasOwnProperty(oldKey) ) return;
      Object.defineProperty(data, oldKey, {
        get: () => {
          if ( options.warning ) logCompatibilityWarning(options.warning);
          else this._logDataFieldMigration(oldKey, newKey, options);
          return ("value" in options) ? options.value : getProperty(data, newKey);
        },
        set: value => {
          if ( newKey ) setProperty(data, newKey, value);
        },
        configurable: true,
        enumerable: false
      });
    }

    /* ---------------------------------------- */

    /**
     * Define a simple migration from one field name to another.
     * The value of the data can be transformed during the migration by an optional application function.
     * @param {object} data     The data object being migrated
     * @param {string} oldKey   The old field name
     * @param {string} newKey   The new field name
     * @param {function(data: object): any} [apply] An application function, otherwise the old value is applied
     * @returns {boolean}       Whether a migration was applied.
     * @internal
     */
    static _addDataFieldMigration(data, oldKey, newKey, apply) {
      if ( !hasProperty(data, newKey) && hasProperty(data, oldKey) ) {
        const prop = Object.getOwnPropertyDescriptor(data, oldKey);
        if ( prop && !prop.writable ) return false;
        setProperty(data, newKey, apply ? apply(data) : getProperty(data, oldKey));
        delete data[oldKey];
        return true;
      }
      return false;
    }

    /* ---------------------------------------- */

    /** @protected */
    static _logDataFieldMigration(oldKey, newKey, options={}) {
      const msg = `You are accessing ${this.name}#${oldKey} which has been migrated to ${this.name}#${newKey}`;
      return logCompatibilityWarning(msg, {...options})
    }

    /* ---------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    static async _onCreateDocuments(documents, operation) {}

    /**
     * @deprecated since v12
     * @ignore
     */
    static async _onUpdateDocuments(documents, operation) {}

    /**
     * @deprecated since v12
     * @ignore
     */
    static async _onDeleteDocuments(documents, operation) {}
  }

  /**
   * @typedef {import("./_types.mjs").DatabaseAction} DatabaseAction
   * @typedef {import("./_types.mjs").DatabaseOperation} DatabaseOperation
   * @typedef {import("./_types.mjs").DocumentSocketRequest} DocumentSocketRequest
   */

  /**
   * The data structure of a modifyDocument socket response.
   * @alias foundry.abstract.DocumentSocketResponse
   */
  class DocumentSocketResponse {
    /**
     * Prepare a response for an incoming request.
     * @param {DocumentSocketRequest} request     The incoming request that is being responded to
     */
    constructor(request) {
      for ( const [k, v] of Object.entries(request) ) {
        if ( this.hasOwnProperty(k) ) this[k] = v;
      }
    }

    /**
     * The type of Document being transacted.
     * @type {string}
     */
    type;

    /**
     * The database action that was performed.
     * @type {DatabaseAction}
     */
    action;

    /**
     * Was this response broadcast to other connected clients?
     * @type {boolean}
     */
    broadcast;

    /**
     * The database operation that was requested.
     * @type {DatabaseOperation}
     */
    operation;

    /**
     * The identifier of the requesting user.
     * @type {string}
     */
    userId;

    /**
     * The result of the request. Present if successful
     * @type {object[]|string[]}
     */
    result;

    /**
     * An error that occurred. Present if unsuccessful
     * @type {Error}
     */
    error;
  }

  /**
   * @typedef {import("./_types.mjs").DatabaseGetOperation} DatabaseGetOperation
   * @typedef {import("./_types.mjs").DatabaseCreateOperation} DatabaseCreateOperation
   * @typedef {import("./_types.mjs").DatabaseUpdateOperation} DatabaseUpdateOperation
   * @typedef {import("./_types.mjs").DatabaseDeleteOperation} DatabaseDeleteOperation
   */

  /**
   * An abstract base class extended on both the client and server which defines how Documents are retrieved, created,
   * updated, and deleted.
   * @alias foundry.abstract.DatabaseBackend
   * @abstract
   */
  class DatabaseBackend {

    /* -------------------------------------------- */
    /*  Get Operations                              */
    /* -------------------------------------------- */

    /**
     * Retrieve Documents based on provided query parameters.
     * It recommended to use CompendiumCollection#getDocuments or CompendiumCollection#getIndex rather
     * than calling this method directly.
     * @param {typeof Document} documentClass           The Document class definition
     * @param {DatabaseGetOperation} operation          Parameters of the get operation
     * @param {BaseUser} [user]                         The requesting User
     * @returns {Promise<Document[]|object[]>}          An array of retrieved Document instances or index objects
     */
    async get(documentClass, operation, user) {
      operation = await this.#configureGet(operation);
      return this._getDocuments(documentClass, operation, user);
    }

    /* -------------------------------------------- */

    /**
     * Validate and configure the parameters of the get operation.
     * @param {DatabaseGetOperation} operation          The requested operation
     */
    async #configureGet(operation) {
      await this.#configureOperation(operation);
      operation.broadcast = false; // Get requests are never broadcast
      return operation;
    }

    /* -------------------------------------------- */

    /**
     * Retrieve Document instances using the specified operation parameters.
     * @param {typeof Document} documentClass           The Document class definition
     * @param {DatabaseGetOperation} operation          Parameters of the get operation
     * @param {BaseUser} [user]                         The requesting User
     * @returns {Promise<Document[]|object[]>}          An array of retrieved Document instances or index objects
     * @abstract
     * @internal
     * @ignore
     */
    async _getDocuments(documentClass, operation, user) {}

    /* -------------------------------------------- */
    /*  Create Operations                           */
    /* -------------------------------------------- */

    /**
     * Create new Documents using provided data and context.
     * It is recommended to use {@link Document.createDocuments} or {@link Document.create} rather than calling this
     * method directly.
     * @param {typeof Document} documentClass           The Document class definition
     * @param {DatabaseCreateOperation} operation       Parameters of the create operation
     * @param {BaseUser} [user]                         The requesting User
     * @returns {Promise<Document[]>}                   An array of created Document instances
     */
    async create(documentClass, operation, user) {
      operation = await this.#configureCreate(operation);
      return this._createDocuments(documentClass, operation, user);
    }

    /* -------------------------------------------- */

    /**
     * Validate and configure the parameters of the create operation.
     * @param {DatabaseCreateOperation} operation       The requested operation
     */
    async #configureCreate(operation) {
      if ( !Array.isArray(operation.data) ) {
        throw new Error("The data provided to the DatabaseBackend#create operation must be an array of data objects");
      }
      await this.#configureOperation(operation);
      operation.render ??= true;
      operation.renderSheet ??= false;
      return operation;
    }

    /* -------------------------------------------- */

    /**
     * Create Document instances using provided data and operation parameters.
     * @param {typeof Document} documentClass           The Document class definition
     * @param {DatabaseCreateOperation} operation       Parameters of the create operation
     * @param {BaseUser} [user]                         The requesting User
     * @returns {Promise<Document[]>}                   An array of created Document instances
     * @abstract
     * @internal
     * @ignore
     */
    async _createDocuments(documentClass, operation, user) {}

    /* -------------------------------------------- */
    /*  Update Operations                           */
    /* -------------------------------------------- */

    /**
     * Update Documents using provided data and context.
     * It is recommended to use {@link Document.updateDocuments} or {@link Document#update} rather than calling this
     * method directly.
     * @param {typeof Document} documentClass           The Document class definition
     * @param {DatabaseUpdateOperation} operation       Parameters of the update operation
     * @param {BaseUser} [user]                         The requesting User
     * @returns {Promise<Document[]>}                   An array of updated Document instances
     */
    async update(documentClass, operation, user) {
      operation = await this.#configureUpdate(operation);
      return this._updateDocuments(documentClass, operation, user);
    }

    /* -------------------------------------------- */

    /**
     * Validate and configure the parameters of the update operation.
     * @param {DatabaseUpdateOperation} operation       The requested operation
     */
    async #configureUpdate(operation) {
      if ( !Array.isArray(operation.updates) ) {
        throw new Error("The updates provided to the DatabaseBackend#update operation must be an array of data objects");
      }
      await this.#configureOperation(operation);
      operation.diff ??= true;
      operation.recursive ??= true;
      operation.render ??= true;
      return operation;
    }

    /* -------------------------------------------- */

    /**
     * Update Document instances using provided data and operation parameters.
     * @param {typeof Document} documentClass           The Document class definition
     * @param {DatabaseUpdateOperation} operation       Parameters of the update operation
     * @param {BaseUser} [user]                         The requesting User
     * @returns {Promise<Document[]>}                   An array of updated Document instances
     * @abstract
     * @internal
     * @ignore
     */
    async _updateDocuments(documentClass, operation, user) {}

    /* -------------------------------------------- */
    /*  Delete Operations                           */
    /* -------------------------------------------- */

    /**
     * Delete Documents using provided ids and context.
     * It is recommended to use {@link foundry.abstract.Document.deleteDocuments} or
     * {@link foundry.abstract.Document#delete} rather than calling this method directly.
     * @param {typeof Document} documentClass           The Document class definition
     * @param {DatabaseDeleteOperation} operation       Parameters of the delete operation
     * @param {BaseUser} [user]                         The requesting User
     * @returns {Promise<Document[]>}                   An array of deleted Document instances
     */
    async delete(documentClass, operation, user) {
      operation = await this.#configureDelete(operation);
      return this._deleteDocuments(documentClass, operation, user);
    }

    /* -------------------------------------------- */

    /**
     * Validate and configure the parameters of the delete operation.
     * @param {DatabaseDeleteOperation} operation       The requested operation
     */
    async #configureDelete(operation) {
      if ( !Array.isArray(operation.ids) ) {
        throw new Error("The document ids provided to the DatabaseBackend#delete operation must be an array of strings");
      }
      await this.#configureOperation(operation);
      operation.deleteAll ??= false;
      operation.render ??= true;
      return operation;
    }

    /* -------------------------------------------- */

    /**
     * Delete Document instances using provided ids and operation parameters.
     * @param {typeof Document} documentClass           The Document class definition
     * @param {DatabaseDeleteOperation} operation       Parameters of the delete operation
     * @param {BaseUser} [user]                         The requesting User
     * @returns {Promise<Document[]>}                   An array of deleted Document instances
     * @abstract
     * @internal
     * @ignore
     */
    async _deleteDocuments(documentClass, operation, user) {}

    /* -------------------------------------------- */
    /*  Helper Methods                              */
    /* -------------------------------------------- */

    /**
     * Common database operation configuration steps.
     * @param {DatabaseOperation} operation           The requested operation
     * @returns {Promise<void>}
     */
    async #configureOperation(operation) {
      if ( operation.pack && !this.getCompendiumScopes().includes(operation.pack) ) {
        throw new Error(`Compendium pack "${operation.pack}" is not a valid Compendium identifier`);
      }
      operation.parent = await this._getParent(operation);
      operation.modifiedTime = Date.now();
    }

    /* -------------------------------------------- */

    /**
     * Get the parent Document (if any) associated with a request context.
     * @param {DatabaseOperation} operation           The requested database operation
     * @return {Promise<Document|null>}               The parent Document, or null
     * @internal
     * @ignore
     */
    async _getParent(operation) {
      if ( operation.parent && !(operation.parent instanceof Document) ) {
        throw new Error("A parent Document provided to the database operation must be a Document instance");
      }
      else if ( operation.parent ) return operation.parent;
      if ( operation.parentUuid ) return globalThis.fromUuid(operation.parentUuid, {invalid: true});
      return null;
    }

    /* -------------------------------------------- */

    /**
     * Describe the scopes which are suitable as the namespace for a flag key
     * @returns {string[]}
     */
    getFlagScopes() {}

    /* -------------------------------------------- */

    /**
     * Describe the scopes which are suitable as the namespace for a flag key
     * @returns {string[]}
     */
    getCompendiumScopes() {}

    /* -------------------------------------------- */

    /**
     * Log a database operations message.
     * @param {string} level      The logging level
     * @param {string} message    The message
     * @abstract
     * @protected
     */
    _log(level, message) {}

    /* -------------------------------------------- */

    /**
     * Log a database operation for an embedded document, capturing the action taken and relevant IDs
     * @param {string} action                       The action performed
     * @param {string} type                         The document type
     * @param {abstract.Document[]} documents       The documents modified
     * @param {string} [level=info]                 The logging level
     * @param {abstract.Document} [parent]          A parent document
     * @param {string} [pack]                       A compendium pack within which the operation occurred
     * @protected
     */
    _logOperation(action, type, documents, {parent, pack, level="info"}={}) {
      let msg = (documents.length === 1) ? `${action} ${type}` : `${action} ${documents.length} ${type} documents`;
      if (documents.length === 1) msg += ` with id [${documents[0].id}]`;
      else if (documents.length <= 5) msg += ` with ids: [${documents.map(d => d.id)}]`;
      msg += this.#logContext(parent, pack);
      this._log(level, msg);
    }

    /* -------------------------------------------- */

    /**
     * Construct a standardized error message given the context of an attempted operation
     * @returns {string}
     * @protected
     */
    _logError(user, action, subject, {parent, pack}={}) {
      if ( subject instanceof Document ) {
        subject = subject.id ? `${subject.documentName} [${subject.id}]` : `a new ${subject.documentName}`;
      }
      let msg = `User ${user.name} lacks permission to ${action} ${subject}`;
      return msg + this.#logContext(parent, pack);
    }

    /* -------------------------------------------- */

    /**
     * Determine a string suffix for a log message based on the parent and/or compendium context.
     * @param {Document|null} parent
     * @param {string|null} pack
     * @returns {string}
     */
    #logContext(parent, pack) {
      let context = "";
      if ( parent ) context += ` in parent ${parent.constructor.metadata.name} [${parent.id}]`;
      if ( pack ) context += ` in Compendium ${pack}`;
      return context;
    }
  }

  var abstract = /*#__PURE__*/Object.freeze({
    __proto__: null,
    DataModel: DataModel,
    DatabaseBackend: DatabaseBackend,
    Document: Document,
    DocumentSocketResponse: DocumentSocketResponse,
    EmbeddedCollection: EmbeddedCollection,
    EmbeddedCollectionDelta: EmbeddedCollectionDelta,
    SingletonEmbeddedCollection: SingletonEmbeddedCollection,
    TypeDataModel: TypeDataModel,
    types: _types$4
  });

  /**
   * @typedef {import("./_types.mjs").ActiveEffectData} ActiveEffectData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The ActiveEffect Document.
   * Defines the DataSchema and common behaviors for an ActiveEffect which are shared between both client and server.
   * @mixes {@link ActiveEffectData}
   */
  class BaseActiveEffect extends Document {
    /**
     * Construct an ActiveEffect document using provided data and context.
     * @param {Partial<ActiveEffectData>} data        Initial data from which to construct the ActiveEffect
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "ActiveEffect",
      collection: "effects",
      hasTypeData: true,
      label: "DOCUMENT.ActiveEffect",
      labelPlural: "DOCUMENT.ActiveEffects",
      schemaVersion: "12.324"
    }, {inplace: false}));

    /* -------------------------------------------- */

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, label: "EFFECT.Name", textSearch: true}),
        img: new FilePathField({categories: ["IMAGE"], label: "EFFECT.Image"}),
        type: new DocumentTypeField(this, {initial: BASE_DOCUMENT_TYPE}),
        system: new TypeDataField(this),
        changes: new ArrayField(new SchemaField({
          key: new StringField({required: true, label: "EFFECT.ChangeKey"}),
          value: new StringField({required: true, label: "EFFECT.ChangeValue"}),
          mode: new NumberField({integer: true, initial: ACTIVE_EFFECT_MODES.ADD,
            label: "EFFECT.ChangeMode"}),
          priority: new NumberField()
        })),
        disabled: new BooleanField(),
        duration: new SchemaField({
          startTime: new NumberField({initial: null, label: "EFFECT.StartTime"}),
          seconds: new NumberField({integer: true, min: 0, label: "EFFECT.DurationSecs"}),
          combat: new ForeignDocumentField(BaseCombat, {label: "EFFECT.Combat"}),
          rounds: new NumberField({integer: true, min: 0}),
          turns: new NumberField({integer: true, min: 0, label: "EFFECT.DurationTurns"}),
          startRound: new NumberField({integer: true, min: 0}),
          startTurn: new NumberField({integer: true, min: 0, label: "EFFECT.StartTurns"})
        }),
        description: new HTMLField({label: "EFFECT.Description", textSearch: true}),
        origin: new StringField({nullable: true, blank: false, initial: null, label: "EFFECT.Origin"}),
        tint: new ColorField({nullable: false, initial: "#ffffff", label: "EFFECT.Tint"}),
        transfer: new BooleanField({initial: true, label: "EFFECT.Transfer"}),
        statuses: new SetField(new StringField({required: true, blank: false})),
        sort: new IntegerSortField(),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      }
    }

    /* -------------------------------------------- */
    /*  Model Methods                               */
    /* -------------------------------------------- */

    /** @inheritdoc */
    canUserModify(user, action, data={}) {
      if ( this.isEmbedded ) return this.parent.canUserModify(user, "update");
      return super.canUserModify(user, action, data);
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    testUserPermission(user, permission, {exact=false}={}) {
      if ( this.isEmbedded ) return this.parent.testUserPermission(user, permission, {exact});
      return super.testUserPermission(user, permission, {exact});
    }

    /* -------------------------------------------- */
    /*  Database Event Handlers                     */
    /* -------------------------------------------- */

    /** @inheritDoc */
    async _preCreate(data, options, user) {
      const allowed = await super._preCreate(data, options, user);
      if ( allowed === false ) return false;
      if ( this.parent instanceof BaseActor ) {
        this.updateSource({transfer: false});
      }
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /** @inheritDoc */
    static migrateData(data) {
      /**
       * label -> name
       * @deprecated since v11
       */
      this._addDataFieldMigration(data, "label", "name", d => d.label || "Unnamed Effect");
      /**
       * icon -> img
       * @deprecated since v12
       */
      this._addDataFieldMigration(data, "icon", "img");
      return super.migrateData(data);
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    static shimData(data, options) {
      this._addDataFieldShim(data, "label", "name", {since: 11, until: 13});
      this._addDataFieldShim(data, "icon", "img", {since: 12, until: 14});
      return super.shimData(data, options);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v11
     * @ignore
     */
    get label() {
      this.constructor._logDataFieldMigration("label", "name", {since: 11, until: 13, once: true});
      return this.name;
    }

    /**
     * @deprecated since v11
     * @ignore
     */
    set label(value) {
      this.constructor._logDataFieldMigration("label", "name", {since: 11, until: 13, once: true});
      this.name = value;
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get icon() {
      this.constructor._logDataFieldMigration("icon", "img", {since: 12, until: 14, once: true});
      return this.img;
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    set icon(value) {
      this.constructor._logDataFieldMigration("icon", "img", {since: 12, until: 14, once: true});
      this.img = value;
    }
  }

  /**
   * The collection of data schema and document definitions for primary documents which are shared between the both the
   * client and the server.
   * @namespace data
   */


  /**
   * @typedef {import("./fields.mjs").DataFieldOptions} DataFieldOptions
   * @typedef {import("./fields.mjs").FilePathFieldOptions} FilePathFieldOptions
   */

  /**
   * @typedef {Object} LightAnimationData
   * @property {string} type          The animation type which is applied
   * @property {number} speed         The speed of the animation, a number between 0 and 10
   * @property {number} intensity     The intensity of the animation, a number between 1 and 10
   * @property {boolean} reverse      Reverse the direction of animation.
   */

  /**
   * A reusable document structure for the internal data used to render the appearance of a light source.
   * This is re-used by both the AmbientLightData and TokenData classes.
   * @extends DataModel
   * @memberof data
   *
   * @property {boolean} negative           Is this light source a negative source? (i.e. darkness source)
   * @property {number} alpha               An opacity for the emitted light, if any
   * @property {number} angle               The angle of emission for this point source
   * @property {number} bright              The allowed radius of bright vision or illumination
   * @property {number} color               A tint color for the emitted light, if any
   * @property {number} coloration          The coloration technique applied in the shader
   * @property {number} contrast            The amount of contrast this light applies to the background texture
   * @property {number} dim                 The allowed radius of dim vision or illumination
   * @property {number} attenuation         Fade the difference between bright, dim, and dark gradually?
   * @property {number} luminosity          The luminosity applied in the shader
   * @property {number} saturation          The amount of color saturation this light applies to the background texture
   * @property {number} shadows             The depth of shadows this light applies to the background texture
   * @property {LightAnimationData} animation  An animation configuration for the source
   * @property {{min: number, max: number}} darkness  A darkness range (min and max) for which the source should be active
   */
  class LightData extends DataModel {
    static defineSchema() {
      return {
        negative: new BooleanField(),
        priority: new NumberField({required: true, nullable: false, integer: true, initial: 0, min: 0}),
        alpha: new AlphaField({initial: 0.5}),
        angle: new AngleField({initial: 360, normalize: false}),
        bright: new NumberField({required: true,  nullable: false, initial: 0, min: 0, step: 0.01}),
        color: new ColorField({}),
        coloration: new NumberField({required: true, integer: true, initial: 1}),
        dim: new NumberField({required: true, nullable: false, initial: 0, min: 0, step: 0.01}),
        attenuation: new AlphaField({initial: 0.5}),
        luminosity: new NumberField({required: true, nullable: false, initial: 0.5, min: 0, max: 1}),
        saturation: new NumberField({required: true, nullable: false, initial: 0, min: -1, max: 1}),
        contrast: new NumberField({required: true, nullable: false, initial: 0, min: -1, max: 1}),
        shadows: new NumberField({required: true, nullable: false, initial: 0, min: 0, max: 1}),
        animation: new SchemaField({
          type: new StringField({nullable: true, blank: false, initial: null}),
          speed: new NumberField({required: true, nullable: false, integer: true, initial: 5, min: 0, max: 10,
            validationError: "Light animation speed must be an integer between 0 and 10"}),
          intensity: new NumberField({required: true, nullable: false, integer: true, initial: 5, min: 1, max: 10,
            validationError: "Light animation intensity must be an integer between 1 and 10"}),
          reverse: new BooleanField()
        }),
        darkness: new SchemaField({
          min: new AlphaField({initial: 0}),
          max: new AlphaField({initial: 1})
        }, {
          validate: d => (d.min ?? 0) <= (d.max ?? 1),
          validationError: "darkness.max may not be less than darkness.min"
        })
      }
    }

    /** @override */
    static LOCALIZATION_PREFIXES = ["LIGHT"];

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /** @inheritDoc */
    static migrateData(data) {
      /**
       * Migration of negative luminosity
       * @deprecated since v12
       */
      const luminosity = data.luminosity;
      if ( luminosity < 0) {
        data.luminosity = 1 - luminosity;
        data.negative = true;
      }
      return super.migrateData(data);
    }
  }

  /* ---------------------------------------- */

  /**
   * A data model intended to be used as an inner EmbeddedDataField which defines a geometric shape.
   * @extends DataModel
   * @memberof data
   *
   * @property {string} type                The type of shape, a value in ShapeData.TYPES.
   *                                        For rectangles, the x/y coordinates are the top-left corner.
   *                                        For circles, the x/y coordinates are the center of the circle.
   *                                        For polygons, the x/y coordinates are the first point of the polygon.
   * @property {number} [width]             For rectangles, the pixel width of the shape.
   * @property {number} [height]            For rectangles, the pixel width of the shape.
   * @property {number} [radius]            For circles, the pixel radius of the shape.
   * @property {number[]} [points]          For polygons, the array of polygon coordinates which comprise the shape.
   */
  class ShapeData extends DataModel {
    static defineSchema() {
      return {
        type: new StringField({required: true, blank: false, choices: Object.values(this.TYPES), initial: "r"}),
        width: new NumberField({required: false, integer: true, min: 0}),
        height: new NumberField({required: false, integer: true, min: 0}),
        radius: new NumberField({required: false, integer: true, positive: true}),
        points: new ArrayField(new NumberField({nullable: false}))
      }
    }

    /**
     * The primitive shape types which are supported
     * @enum {string}
     */
    static TYPES = {
      RECTANGLE: "r",
      CIRCLE: "c",
      ELLIPSE: "e",
      POLYGON: "p"
    }
  }

  /* ---------------------------------------- */

  /**
   * A data model intended to be used as an inner EmbeddedDataField which defines a geometric shape.
   * @extends DataModel
   * @memberof data
   * @abstract
   *
   * @property {string} type                                          The type of shape, a value in BaseShapeData.TYPES.
   * @property {{bottom: number|null, top: number|null}} [elevation]  The bottom and top elevation of the shape.
   *                                                                  A value of null means -/+Infinity.
   * @property {boolean} [hole=false]                                 Is this shape a hole?
   */
  class BaseShapeData extends DataModel {

    /**
     * The possible shape types.
     * @type {Readonly<{
     *   rectangle: RectangleShapeData,
     *   circle: CircleShapeData,
     *   ellipse: EllipseShapeData,
     *   polygon: PolygonShapeData
     * }>}
     */
    static get TYPES() {
      return BaseShapeData.#TYPES ??= Object.freeze({
        [RectangleShapeData.TYPE]: RectangleShapeData,
        [CircleShapeData.TYPE]: CircleShapeData,
        [EllipseShapeData.TYPE]: EllipseShapeData,
        [PolygonShapeData.TYPE]: PolygonShapeData
      });
    }

    static #TYPES;

    /* -------------------------------------------- */

    /**
     * The type of this shape.
     * @type {string}
     */
    static TYPE = "";

    /* -------------------------------------------- */

    /** @override */
    static defineSchema() {
      return {
        type: new StringField({required: true, blank: false, initial: this.TYPE,
          validate: value => value === this.TYPE, validationError: `must be equal to "${this.TYPE}"`}),
        hole: new BooleanField()
      }
    }
  }

  /* -------------------------------------------- */

  /**
   * The data model for a rectangular shape.
   * @extends DataModel
   * @memberof data
   *
   * @property {number} x               The top-left x-coordinate in pixels before rotation.
   * @property {number} y               The top-left y-coordinate in pixels before rotation.
   * @property {number} width           The width of the rectangle in pixels.
   * @property {number} height          The height of the rectangle in pixels.
   * @property {number} [rotation=0]    The rotation around the center of the rectangle in degrees.
   */
  class RectangleShapeData extends BaseShapeData {

    static {
      Object.defineProperty(this, "TYPE", {value: "rectangle"});
    }

    /** @inheritdoc */
    static defineSchema() {
      return Object.assign(super.defineSchema(), {
        x: new NumberField({required: true, nullable: false, initial: undefined}),
        y: new NumberField({required: true, nullable: false, initial: undefined}),
        width: new NumberField({required: true, nullable: false, initial: undefined, positive: true}),
        height: new NumberField({required: true, nullable: false, initial: undefined, positive: true}),
        rotation: new AngleField()
      });
    }
  }

  /* -------------------------------------------- */

  /**
   * The data model for a circle shape.
   * @extends DataModel
   * @memberof data
   *
   * @property {number} x         The x-coordinate of the center point in pixels.
   * @property {number} y         The y-coordinate of the center point in pixels.
   * @property {number} radius    The radius of the circle in pixels.
   */
  class CircleShapeData extends BaseShapeData {

    static {
      Object.defineProperty(this, "TYPE", {value: "circle"});
    }

    /** @inheritdoc */
    static defineSchema() {
      return Object.assign(super.defineSchema(), {
        x: new NumberField({required: true, nullable: false, initial: undefined}),
        y: new NumberField({required: true, nullable: false, initial: undefined}),
        radius: new NumberField({required: true, nullable: false, initial: undefined, positive: true})
      });
    }
  }

  /* -------------------------------------------- */

  /**
   * The data model for an ellipse shape.
   * @extends DataModel
   * @memberof data
   *
   * @property {number} x               The x-coordinate of the center point in pixels.
   * @property {number} y               The y-coordinate of the center point in pixels.
   * @property {number} radiusX         The x-radius of the circle in pixels.
   * @property {number} radiusY         The y-radius of the circle in pixels.
   * @property {number} [rotation=0]    The rotation around the center of the rectangle in degrees.
   */
  class EllipseShapeData extends BaseShapeData {

    static {
      Object.defineProperty(this, "TYPE", {value: "ellipse"});
    }

    /** @inheritdoc */
    static defineSchema() {
      return Object.assign(super.defineSchema(), {
        x: new NumberField({required: true, nullable: false, initial: undefined}),
        y: new NumberField({required: true, nullable: false, initial: undefined}),
        radiusX: new NumberField({required: true, nullable: false, initial: undefined, positive: true}),
        radiusY: new NumberField({required: true, nullable: false, initial: undefined, positive: true}),
        rotation: new AngleField()
      });
    }
  }

  /* -------------------------------------------- */

  /**
   * The data model for a polygon shape.
   * @extends DataModel
   * @memberof data
   *
   * @property {number[]} points      The points of the polygon ([x0, y0, x1, y1, ...]).
   *                                  The polygon must not be self-intersecting.
   */
  class PolygonShapeData extends BaseShapeData {

    static {
      Object.defineProperty(this, "TYPE", {value: "polygon"});
    }

    /** @inheritdoc */
    static defineSchema() {
      return Object.assign(super.defineSchema(), {
        points: new ArrayField(new NumberField({required: true, nullable: false, initial: undefined}),
          {validate: value => {
            if ( value.length % 2 !== 0 ) throw new Error("must have an even length");
            if ( value.length < 6 ) throw new Error("must have at least 3 points");
          }}),
      });
    }
  }

  /* ---------------------------------------- */

  /**
   * A {@link fields.SchemaField} subclass used to represent texture data.
   * @property {string|null} src              The URL of the texture source.
   * @property {number} [anchorX=0]           The X coordinate of the texture anchor.
   * @property {number} [anchorY=0]           The Y coordinate of the texture anchor.
   * @property {number} [scaleX=1]            The scale of the texture in the X dimension.
   * @property {number} [scaleY=1]            The scale of the texture in the Y dimension.
   * @property {number} [offsetX=0]           The X offset of the texture with (0,0) in the top left.
   * @property {number} [offsetY=0]           The Y offset of the texture with (0,0) in the top left.
   * @property {number} [rotation=0]           An angle of rotation by which this texture is rotated around its center.
   * @property {string} [tint="#ffffff"]      The tint applied to the texture.
   * @property {number} [alphaThreshold=0]    Only pixels with an alpha value at or above this value are consider solid
   *                                          w.r.t. to occlusion testing and light/weather blocking.
   */
  class TextureData extends SchemaField {
    /**
     * @param {DataFieldOptions} options        Options which are forwarded to the SchemaField constructor
     * @param {FilePathFieldOptions} srcOptions Additional options for the src field
     */
    constructor(options={}, {categories=["IMAGE", "VIDEO"], initial={}, wildcard=false, label=""}={}) {
      /** @deprecated since v12 */
      if ( typeof initial === "string" ) {
        const msg = "Passing the initial value of the src field as a string is deprecated. Pass {src} instead.";
        logCompatibilityWarning(msg, {since: 12, until: 14});
        initial = {src: initial};
      }
      super({
        src: new FilePathField({categories, initial: initial.src ?? null, label, wildcard}),
        anchorX: new NumberField({nullable: false, initial: initial.anchorX ?? 0}),
        anchorY: new NumberField({nullable: false, initial: initial.anchorY ?? 0}),
        offsetX: new NumberField({nullable: false, integer: true, initial: initial.offsetX ?? 0}),
        offsetY: new NumberField({nullable: false, integer: true, initial: initial.offsetY ?? 0}),
        fit: new StringField({initial: initial.fit ?? "fill", choices: CONST.TEXTURE_DATA_FIT_MODES}),
        scaleX: new NumberField({nullable: false, initial: initial.scaleX ?? 1}),
        scaleY: new NumberField({nullable: false, initial: initial.scaleY ?? 1}),
        rotation: new AngleField({initial: initial.rotation ?? 0}),
        tint: new ColorField({nullable: false, initial: initial.tint ?? "#ffffff"}),
        alphaThreshold: new AlphaField({nullable: false, initial: initial.alphaThreshold ?? 0})
      }, options);
    }
  }

  /* ---------------------------------------- */

  /**
   * Extend the base TokenData to define a PrototypeToken which exists within a parent Actor.
   * @extends abstract.DataModel
   * @memberof data
   * @property {boolean} randomImg      Does the prototype token use a random wildcard image?
   * @alias {PrototypeToken}
   */
  class PrototypeToken extends DataModel {
    constructor(data={}, options={}) {
      super(data, options);
      Object.defineProperty(this, "apps", {value: {}});
    }

    /** @override */
    static defineSchema() {
      const schema = BaseToken.defineSchema();
      const excluded = ["_id", "actorId", "delta", "x", "y", "elevation", "sort", "hidden", "locked", "_regions"];
      for ( let x of excluded ) {
        delete schema[x];
      }
      schema.name.textSearch = schema.name.options.textSearch = false;
      schema.randomImg = new BooleanField();
      PrototypeToken.#applyDefaultTokenSettings(schema);
      return schema;
    }

    /** @override */
    static LOCALIZATION_PREFIXES = ["TOKEN"];

    /**
     * The Actor which owns this Prototype Token
     * @type {documents.BaseActor}
     */
    get actor() {
      return this.parent;
    }

    /** @inheritdoc */
    toObject(source=true) {
      const data = super.toObject(source);
      data["actorId"] = this.document?.id;
      return data;
    }

    /**
     * @see ClientDocument.database
     * @ignore
     */
    static get database() {
      return globalThis.CONFIG.DatabaseBackend;
    }

    /* -------------------------------------------- */

    /**
     * Apply configured default token settings to the schema.
     * @param {DataSchema} [schema]  The schema to apply the settings to.
     */
    static #applyDefaultTokenSettings(schema) {
      if ( typeof DefaultTokenConfig === "undefined" ) return;
      const settings = foundry.utils.flattenObject(game.settings.get("core", DefaultTokenConfig.SETTING) ?? {});
      for ( const [k, v] of Object.entries(settings) ) {
        const path = k.split(".");
        let field = schema[path.shift()];
        if ( path.length ) field = field._getField(path);
        if ( field ) field.initial = v;
      }
    }

    /* -------------------------------------------- */
    /*  Document Compatibility Methods              */
    /* -------------------------------------------- */

    /**
     * @see abstract.Document#update
     * @ignore
     */
    update(data, options) {
      return this.actor.update({prototypeToken: data}, options);
    }

    /* -------------------------------------------- */

    /**
     * @see abstract.Document#getFlag
     * @ignore
     */
    getFlag(...args) {
      return foundry.abstract.Document.prototype.getFlag.call(this, ...args);
    }

    /* -------------------------------------------- */

    /**
     * @see abstract.Document#getFlag
     * @ignore
     */
    setFlag(...args) {
      return foundry.abstract.Document.prototype.setFlag.call(this, ...args);
    }

    /* -------------------------------------------- */

    /**
     * @see abstract.Document#unsetFlag
     * @ignore
     */
    async unsetFlag(...args) {
      return foundry.abstract.Document.prototype.unsetFlag.call(this, ...args);
    }

    /* -------------------------------------------- */

    /**
     * @see abstract.Document#testUserPermission
     * @ignore
     */
    testUserPermission(user, permission, {exact=false}={}) {
      return this.actor.testUserPermission(user, permission, {exact});
    }

    /* -------------------------------------------- */

    /**
     * @see documents.BaseActor#isOwner
     * @ignore
     */
    get isOwner() {
      return this.actor.isOwner;
    }
  }

  /* -------------------------------------------- */

  /**
   * A minimal data model used to represent a tombstone entry inside an EmbeddedCollectionDelta.
   * @see {EmbeddedCollectionDelta}
   * @extends DataModel
   * @memberof data
   *
   * @property {string} _id              The _id of the base Document that this tombstone represents.
   * @property {boolean} _tombstone      A property that identifies this entry as a tombstone.
   */
  class TombstoneData extends DataModel {
    /** @override */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        _tombstone: new BooleanField({initial: true, validate: v => v === true, validationError: "must be true"})
      };
    }
  }

  /**
   * @typedef {import("./_types.mjs").ActorData} ActorData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Actor Document.
   * Defines the DataSchema and common behaviors for an Actor which are shared between both client and server.
   * @mixes ActorData
   */
  class BaseActor extends Document {
    /**
     * Construct an Actor document using provided data and context.
     * @param {Partial<ActorData>} data               Initial data from which to construct the Actor
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Actor",
      collection: "actors",
      indexed: true,
      compendiumIndexFields: ["_id", "name", "img", "type", "sort", "folder"],
      embedded: {ActiveEffect: "effects", Item: "items"},
      hasTypeData: true,
      label: "DOCUMENT.Actor",
      labelPlural: "DOCUMENT.Actors",
      permissions: {
        create: this.#canCreate,
        update: this.#canUpdate
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /* ---------------------------------------- */

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, textSearch: true}),
        img: new FilePathField({categories: ["IMAGE"], initial: data => {
          return this.implementation.getDefaultArtwork(data).img;
        }}),
        type: new DocumentTypeField(this),
        system: new TypeDataField(this),
        prototypeToken: new EmbeddedDataField(PrototypeToken),
        items: new EmbeddedCollectionField(BaseItem),
        effects: new EmbeddedCollectionField(BaseActiveEffect),
        folder: new ForeignDocumentField(BaseFolder),
        sort: new IntegerSortField(),
        ownership: new DocumentOwnershipField(),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      };
    }

    /* ---------------------------------------- */

    /**
     * The default icon used for newly created Actor documents.
     * @type {string}
     */
    static DEFAULT_ICON = DEFAULT_TOKEN;

    /* -------------------------------------------- */

    /**
     * Determine default artwork based on the provided actor data.
     * @param {ActorData} actorData                      The source actor data.
     * @returns {{img: string, texture: {src: string}}}  Candidate actor image and prototype token artwork.
     */
    static getDefaultArtwork(actorData) {
      return {
        img: this.DEFAULT_ICON,
        texture: {
          src: this.DEFAULT_ICON
        }
      };
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    _initializeSource(source, options) {
      source = super._initializeSource(source, options);
      source.prototypeToken.name = source.prototypeToken.name || source.name;
      source.prototypeToken.texture.src = source.prototypeToken.texture.src || source.img;
      return source;
    }

    /* -------------------------------------------- */

    /** @override */
    static canUserCreate(user) {
      return user.hasPermission("ACTOR_CREATE");
    }

    /* ---------------------------------------- */

    /**
     * Is a user able to create this actor?
     * @param {User} user  The user attempting the creation operation.
     * @param {Actor} doc  The Actor being created.
     */
    static #canCreate(user, doc) {
      if ( !user.hasPermission("ACTOR_CREATE") ) return false;      // User cannot create actors at all
      if ( doc._source.prototypeToken.randomImg && !user.hasPermission("FILES_BROWSE") ) return false;
      return true;
    }

    /* -------------------------------------------- */

    /**
     * Is a user able to update an existing actor?
     * @param {User} user    The user attempting the update operation.
     * @param {Actor} doc    The Actor being updated.
     * @param {object} data  The update delta being applied.
     */
    static #canUpdate(user, doc, data) {
      if ( !doc.testUserPermission(user, "OWNER") ) return false; // Ownership is required.

      // Users can only enable token wildcard images if they have FILES_BROWSE permission.
      const tokenChange = data?.prototypeToken || {};
      const enablingRandomImage = tokenChange.randomImg === true;
      if ( enablingRandomImage ) return user.hasPermission("FILES_BROWSE");

      // Users can only change a token wildcard path if they have FILES_BROWSE permission.
      const randomImageEnabled = doc._source.prototypeToken.randomImg && (tokenChange.randomImg !== false);
      const changingRandomImage = ("img" in tokenChange) && randomImageEnabled;
      if ( changingRandomImage ) return user.hasPermission("FILES_BROWSE");
      return true;
    }

    /* ---------------------------------------- */

    /** @inheritDoc */
    async _preCreate(data, options, user) {
      const allowed = await super._preCreate(data, options, user);
      if ( allowed === false ) return false;
      if ( !this.prototypeToken.name ) this.prototypeToken.updateSource({name: this.name});
      if ( !this.prototypeToken.texture.src || (this.prototypeToken.texture.src === DEFAULT_TOKEN)) {
        const { texture } = this.constructor.getDefaultArtwork(this.toObject());
        this.prototypeToken.updateSource("img" in data ? { texture: { src: this.img } } : { texture });
      }
    }

    /* ---------------------------------------- */

    /** @inheritDoc */
    async _preUpdate(changed, options, user) {
      const allowed = await super._preUpdate(changed, options, user);
      if ( allowed === false ) return false;
      if ( changed.img && !getProperty(changed, "prototypeToken.texture.src") ) {
        const { texture } = this.constructor.getDefaultArtwork(foundry.utils.mergeObject(this.toObject(), changed));
        if ( !this.prototypeToken.texture.src || (this.prototypeToken.texture.src === texture?.src) ) {
          setProperty(changed, "prototypeToken.texture.src", changed.img);
        }
      }
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    static migrateData(source) {
      /**
       * Migrate sourceId.
       * @deprecated since v12
       */
      this._addDataFieldMigration(source, "flags.core.sourceId", "_stats.compendiumSource");

      return super.migrateData(source);
    }
  }

  /**
   * @typedef {import("./_types.mjs").ActorDeltaData} ActorDeltaData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The ActorDelta Document.
   * Defines the DataSchema and common behaviors for an ActorDelta which are shared between both client and server.
   * ActorDeltas store a delta that can be applied to a particular Actor in order to produce a new Actor.
   * @mixes ActorDeltaData
   */
  class BaseActorDelta extends Document {
    /**
     * Construct an ActorDelta document using provided data and context.
     * @param {Partial<ActorDeltaData>} data         Initial data used to construct the ActorDelta.
     * @param {DocumentConstructionContext} context  Construction context options.
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "ActorDelta",
      collection: "delta",
      label: "DOCUMENT.ActorDelta",
      labelPlural: "DOCUMENT.ActorDeltas",
      isEmbedded: true,
      embedded: {
        Item: "items",
        ActiveEffect: "effects"
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @override */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: false, nullable: true, initial: null}),
        type: new StringField({required: false, nullable: true, initial: null}),
        img: new FilePathField({categories: ["IMAGE"], nullable: true, initial: null, required: false}),
        system: new ObjectField(),
        items: new EmbeddedCollectionDeltaField(BaseItem),
        effects: new EmbeddedCollectionDeltaField(BaseActiveEffect),
        ownership: new DocumentOwnershipField({required: false, nullable: true, initial: null}),
        flags: new ObjectField()
      };
    }

    /* -------------------------------------------- */

    /** @override */
    canUserModify(user, action, data={}) {
      return this.parent.canUserModify(user, action, data);
    }

    /* -------------------------------------------- */

    /** @override */
    testUserPermission(user, permission, { exact=false }={}) {
      return this.parent.testUserPermission(user, permission, { exact });
    }

    /* -------------------------------------------- */
    /*  Methods                                     */
    /* -------------------------------------------- */

    /**
     * Retrieve the base actor's collection, if it exists.
     * @param {string} collectionName  The collection name.
     * @returns {Collection}
     */
    getBaseCollection(collectionName) {
      const baseActor = this.parent?.baseActor;
      return baseActor?.getEmbeddedCollection(collectionName);
    }

    /* -------------------------------------------- */

    /**
     * Apply an ActorDelta to an Actor and return the resultant synthetic Actor.
     * @param {ActorDelta} delta  The ActorDelta.
     * @param {Actor} baseActor   The base Actor.
     * @param {object} [context]  Context to supply to synthetic Actor instantiation.
     * @returns {Actor|null}
     */
    static applyDelta(delta, baseActor, context={}) {
      if ( !baseActor ) return null;
      if ( delta.parent?.isLinked ) return baseActor;

      // Get base actor data.
      const cls = game?.actors?.documentClass ?? db.Actor;
      const actorData = baseActor.toObject();
      const deltaData = delta.toObject();
      delete deltaData._id;

      // Merge embedded collections.
      BaseActorDelta.#mergeEmbeddedCollections(cls, actorData, deltaData);

      // Merge the rest of the delta.
      mergeObject(actorData, deltaData);
      return new cls(actorData, {parent: delta.parent, ...context});
    }

    /* -------------------------------------------- */

    /**
     * Merge delta Document embedded collections with the base Document.
     * @param {typeof Document} documentClass  The parent Document class.
     * @param {object} baseData                The base Document data.
     * @param {object} deltaData               The delta Document data.
     */
    static #mergeEmbeddedCollections(documentClass, baseData, deltaData) {
      for ( const collectionName of Object.keys(documentClass.hierarchy) ) {
        const baseCollection = baseData[collectionName];
        const deltaCollection = deltaData[collectionName];
        baseData[collectionName] = BaseActorDelta.#mergeEmbeddedCollection(baseCollection, deltaCollection);
        delete deltaData[collectionName];
      }
    }

    /* -------------------------------------------- */

    /**
     * Apply an embedded collection delta.
     * @param {object[]} base   The base embedded collection.
     * @param {object[]} delta  The delta embedded collection.
     * @returns {object[]}
     */
    static #mergeEmbeddedCollection(base=[], delta=[]) {
      const deltaIds = new Set();
      const records = [];
      for ( const record of delta ) {
        if ( !record._tombstone ) records.push(record);
        deltaIds.add(record._id);
      }
      for ( const record of base ) {
        if ( !deltaIds.has(record._id) ) records.push(record);
      }
      return records;
    }

    /* -------------------------------------------- */

    /** @override */
    static migrateData(source) {
      return BaseActor.migrateData(source);
    }

    /* -------------------------------------------- */
    /*  Serialization                               */
    /* -------------------------------------------- */

    /** @override */
    toObject(source=true) {
      const data = {};
      const value = source ? this._source : this;
      for ( const [name, field] of this.schema.entries() ) {
        const v = value[name];
        if ( !field.required && ((v === undefined) || (v === null)) ) continue; // Drop optional fields
        data[name] = source ? deepClone(value[name]) : field.toObject(value[name]);
      }
      return data;
    }
  }

  /**
   * @typedef {import("./_types.mjs").AdventureData} AdventureData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Adventure Document.
   * Defines the DataSchema and common behaviors for an Adventure which are shared between both client and server.
   * @mixes AdventureData
   */
  class BaseAdventure extends Document {
    /**
     * Construct an Adventure document using provided data and context.
     * @param {Partial<AdventureData>} data         Initial data used to construct the Adventure.
     * @param {DocumentConstructionContext} context  Construction context options.
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Adventure",
      collection: "adventures",
      compendiumIndexFields: ["_id", "name", "description", "img", "sort", "folder"],
      label: "DOCUMENT.Adventure",
      labelPlural: "DOCUMENT.Adventures",
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, label: "ADVENTURE.Name", hint: "ADVENTURE.NameHint", textSearch: true}),
        img: new FilePathField({categories: ["IMAGE"], label: "ADVENTURE.Image", hint: "ADVENTURE.ImageHint"}),
        caption: new HTMLField({label: "ADVENTURE.Caption", hint: "ADVENTURE.CaptionHint"}),
        description: new HTMLField({label: "ADVENTURE.Description", hint: "ADVENTURE.DescriptionHint", textSearch: true}),
        actors: new SetField(new EmbeddedDataField(BaseActor)),
        combats: new SetField(new EmbeddedDataField(BaseCombat)),
        items: new SetField(new EmbeddedDataField(BaseItem)),
        journal: new SetField(new EmbeddedDataField(BaseJournalEntry)),
        scenes: new SetField(new EmbeddedDataField(BaseScene)),
        tables: new SetField(new EmbeddedDataField(BaseRollTable)),
        macros: new SetField(new EmbeddedDataField(BaseMacro)),
        cards: new SetField(new EmbeddedDataField(BaseCards)),
        playlists: new SetField(new EmbeddedDataField(BasePlaylist)),
        folders: new SetField(new EmbeddedDataField(BaseFolder)),
        folder: new ForeignDocumentField(BaseFolder),
        sort: new IntegerSortField(),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      };
    }

    /* -------------------------------------------- */
    /*  Model Properties                            */
    /* -------------------------------------------- */

    /**
     * An array of the fields which provide imported content from the Adventure.
     * @type {Record<string, typeof Document>}
     */
    static get contentFields() {
      const content = {};
      for ( const field of this.schema ) {
        if ( field instanceof SetField ) content[field.name] = field.element.model.implementation;
      }
      return content;
    }

    /**
     * Provide a thumbnail image path used to represent the Adventure document.
     * @type {string}
     */
    get thumbnail() {
      return this.img;
    }
  }

  /**
   * @typedef {import("./_types.mjs").AmbientLightData} AmbientLightData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The AmbientLight Document.
   * Defines the DataSchema and common behaviors for an AmbientLight which are shared between both client and server.
   * @mixes AmbientLightData
   */
  class BaseAmbientLight extends Document {
    /**
     * Construct an AmbientLight document using provided data and context.
     * @param {Partial<AmbientLightData>} data        Initial data from which to construct the AmbientLight
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "AmbientLight",
      collection: "lights",
      label: "DOCUMENT.AmbientLight",
      labelPlural: "DOCUMENT.AmbientLights",
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        x: new NumberField({required: true, integer: true, nullable: false, initial: 0}),
        y: new NumberField({required: true, integer: true, nullable: false, initial: 0}),
        elevation: new NumberField({required: true, nullable: false, initial: 0}),
        rotation: new AngleField(),
        walls: new BooleanField({initial: true}),
        vision: new BooleanField(),
        config: new EmbeddedDataField(LightData),
        hidden: new BooleanField(),
        flags: new ObjectField()
      }
    }

    /** @override */
    static LOCALIZATION_PREFIXES = ["AMBIENT_LIGHT"];
  }

  /**
   * @typedef {import("./_types.mjs").AmbientSoundData} AmbientSoundData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The AmbientSound Document.
   * Defines the DataSchema and common behaviors for an AmbientSound which are shared between both client and server.
   * @mixes AmbientSoundData
   */
  class BaseAmbientSound extends Document {
    /**
     * Construct an AmbientSound document using provided data and context.
     * @param {Partial<AmbientSoundData>} data        Initial data from which to construct the AmbientSound
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "AmbientSound",
      collection: "sounds",
      label: "DOCUMENT.AmbientSound",
      labelPlural: "DOCUMENT.AmbientSounds",
      isEmbedded: true,
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        x: new NumberField({required: true, integer: true, nullable: false, initial: 0}),
        y: new NumberField({required: true, integer: true, nullable: false, initial: 0}),
        elevation: new NumberField({required: true, nullable: false, initial: 0}),
        radius: new NumberField({required: true, nullable: false, initial: 0, min: 0, step: 0.01}),
        path: new FilePathField({categories: ["AUDIO"]}),
        repeat: new BooleanField(),
        volume: new AlphaField({initial: 0.5, step: 0.01}),
        walls: new BooleanField({initial: true}),
        easing: new BooleanField({initial: true}),
        hidden: new BooleanField(),
        darkness: new SchemaField({
          min: new AlphaField({initial: 0}),
          max: new AlphaField({initial: 1})
        }),
        effects: new SchemaField({
          base: new SchemaField({
            type: new StringField(),
            intensity: new NumberField({required: true, integer: true, initial: 5, min: 1, max: 10, step: 1})
          }),
          muffled: new SchemaField({
            type: new StringField(),
            intensity: new NumberField({required: true, integer: true, initial: 5, min: 1, max: 10, step: 1})
          })
        }),
        flags: new ObjectField()
      }
    }

    /** @override */
    static LOCALIZATION_PREFIXES = ["AMBIENT_SOUND"];
  }

  /**
   * @typedef {import("./_types.mjs").CardData} CardData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Card Document.
   * Defines the DataSchema and common behaviors for a Card which are shared between both client and server.
   * @mixes CardData
   */
  class BaseCard extends Document {
    /**
     * Construct a Card document using provided data and context.
     * @param {Partial<CardData>} data                Initial data from which to construct the Card
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Card",
      collection: "cards",
      hasTypeData: true,
      indexed: true,
      label: "DOCUMENT.Card",
      labelPlural: "DOCUMENT.Cards",
      permissions: {
        create: this.#canCreate,
        update: this.#canUpdate
      },
      compendiumIndexFields: ["name", "type", "suit", "sort"],
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, label: "CARD.Name", textSearch: true}),
        description: new HTMLField({label: "CARD.Description"}),
        type: new DocumentTypeField(this, {initial: BASE_DOCUMENT_TYPE}),
        system: new TypeDataField(this),
        suit: new StringField({label: "CARD.Suit"}),
        value: new NumberField({label: "CARD.Value"}),
        back: new SchemaField({
          name: new StringField({label: "CARD.BackName"}),
          text: new HTMLField({label: "CARD.BackText"}),
          img: new FilePathField({categories: ["IMAGE", "VIDEO"], label: "CARD.BackImage"}),
        }),
        faces: new ArrayField(new SchemaField({
          name: new StringField({label: "CARD.FaceName"}),
          text: new HTMLField({label: "CARD.FaceText"}),
          img: new FilePathField({categories: ["IMAGE", "VIDEO"], initial: () => this.DEFAULT_ICON,
            label: "CARD.FaceImage"}),
        })),
        face: new NumberField({required: true, initial: null, integer: true, min: 0, label: "CARD.Face"}),
        drawn: new BooleanField({label: "CARD.Drawn"}),
        origin: new ForeignDocumentField(BaseCards),
        width: new NumberField({integer: true, positive: true, label: "Width"}),
        height: new NumberField({integer: true, positive: true, label: "Height"}),
        rotation: new AngleField({label: "Rotation"}),
        sort: new IntegerSortField(),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      }
    }

    /**
     * The default icon used for a Card face that does not have a custom image set
     * @type {string}
     */
    static DEFAULT_ICON = "icons/svg/card-joker.svg";

    /**
     * Is a User able to create a new Card within this parent?
     * @private
     */
    static #canCreate(user, doc, data) {
      if ( user.isGM ) return true;                             // GM users can always create
      if ( doc.parent.type !== "deck" ) return true;            // Users can pass cards to card hands or piles
      return doc.parent.canUserModify(user, "create", data);    // Otherwise require parent document permission
    }

    /**
     * Is a user able to update an existing Card?
     * @private
     */
    static #canUpdate(user, doc, data) {
      if ( user.isGM ) return true;                               // GM users can always update
      const wasDrawn = new Set(["drawn", "_id"]);                 // Users can draw cards from a deck
      if ( new Set(Object.keys(data)).equals(wasDrawn) ) return true;
      return doc.parent.canUserModify(user, "update", data);      // Otherwise require parent document permission
    }

    /* -------------------------------------------- */
    /*  Model Methods                               */
    /* -------------------------------------------- */

    /** @inheritdoc */
    testUserPermission(user, permission, {exact=false}={}) {
      if ( this.isEmbedded ) return this.parent.testUserPermission(user, permission, {exact});
      return super.testUserPermission(user, permission, {exact});
    }
  }

  /**
   * @typedef {import("./_types.mjs").CardsData} CardsData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Cards Document.
   * Defines the DataSchema and common behaviors for a Cards Document which are shared between both client and server.
   * @mixes CardsData
   */
  class BaseCards extends Document {
    /**
     * Construct a Cards document using provided data and context.
     * @param {Partial<CardsData>} data               Initial data from which to construct the Cards
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Cards",
      collection: "cards",
      indexed: true,
      compendiumIndexFields: ["_id", "name", "description", "img", "type", "sort", "folder"],
      embedded: {Card: "cards"},
      hasTypeData: true,
      label: "DOCUMENT.Cards",
      labelPlural: "DOCUMENT.CardsPlural",
      permissions: {create: "CARDS_CREATE"},
      coreTypes: ["deck", "hand", "pile"],
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, label: "CARDS.Name", textSearch: true}),
        type: new DocumentTypeField(this),
        description: new HTMLField({label: "CARDS.Description", textSearch: true}),
        img: new FilePathField({categories: ["IMAGE", "VIDEO"], initial: () => this.DEFAULT_ICON,
          label: "CARDS.Image"}),
        system: new TypeDataField(this),
        cards: new EmbeddedCollectionField(BaseCard),
        width: new NumberField({integer: true, positive: true, label: "Width"}),
        height: new NumberField({integer: true, positive: true, label: "Height"}),
        rotation: new AngleField({label: "Rotation"}),
        displayCount: new BooleanField(),
        folder: new ForeignDocumentField(BaseFolder),
        sort: new IntegerSortField(),
        ownership: new DocumentOwnershipField(),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      }
    }

    /**
     * The default icon used for a cards stack that does not have a custom image set
     * @type {string}
     */
    static DEFAULT_ICON = "icons/svg/card-hand.svg";

    /* -------------------------------------------- */

    /** @inheritDoc */
    static migrateData(source) {
      /**
       * Migrate sourceId.
       * @deprecated since v12
       */
      this._addDataFieldMigration(source, "flags.core.sourceId", "_stats.compendiumSource");

      return super.migrateData(source);
    }
  }

  /**
   * @typedef {import("./_types.mjs").ChatMessageData} ChatMessageData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The ChatMessage Document.
   * Defines the DataSchema and common behaviors for a ChatMessage which are shared between both client and server.
   * @mixes ChatMessageData
   */
  class BaseChatMessage extends Document {
    /**
     * Construct a Cards document using provided data and context.
     * @param {Partial<ChatMessageData>} data         Initial data from which to construct the ChatMessage
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "ChatMessage",
      collection: "messages",
      label: "DOCUMENT.ChatMessage",
      labelPlural: "DOCUMENT.ChatMessages",
      hasTypeData: true,
      isPrimary: true,
      permissions: {
        create: this.#canCreate,
        update: this.#canUpdate
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        type: new DocumentTypeField(this, {initial: BASE_DOCUMENT_TYPE}),
        system: new TypeDataField(this),
        style: new NumberField({required: true, choices: Object.values(CHAT_MESSAGE_STYLES),
          initial: CHAT_MESSAGE_STYLES.OTHER, validationError: "must be a value in CONST.CHAT_MESSAGE_STYLES"}),
        author: new ForeignDocumentField(BaseUser, {nullable: false, initial: () => game?.user?.id}),
        timestamp: new NumberField({required: true, nullable: false, initial: Date.now}),
        flavor: new HTMLField(),
        content: new HTMLField({textSearch: true}),
        speaker: new SchemaField({
          scene: new ForeignDocumentField(BaseScene, {idOnly: true}),
          actor: new ForeignDocumentField(BaseActor, {idOnly: true}),
          token: new ForeignDocumentField(BaseToken, {idOnly: true}),
          alias: new StringField()
        }),
        whisper: new ArrayField(new ForeignDocumentField(BaseUser, {idOnly: true})),
        blind: new BooleanField(),
        rolls: new ArrayField(new JSONField({validate: BaseChatMessage.#validateRoll})),
        sound: new FilePathField({categories: ["AUDIO"]}),
        emote: new BooleanField(),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      };
    }

    /**
     * Is a user able to create a new chat message?
     */
    static #canCreate(user, doc) {
      if ( user.isGM ) return true;
      if ( user.id !== doc._source.author ) return false; // You cannot impersonate a different user
      return user.hasRole("PLAYER");                      // Any player can create messages
    }

    /**
     * Is a user able to update an existing chat message?
     */
    static #canUpdate(user, doc, data) {
      if ( user.isGM ) return true;                       // GM users can do anything
      if ( user.id !== doc._source.author ) return false; // Otherwise, message authors
      if ( ("author" in data) && (data.author !== user.id) ) return false; // Message author is immutable
      return true;
    }

    /* -------------------------------------------- */

    /**
     * Validate that Rolls belonging to the ChatMessage document are valid
     * @param {string} rollJSON     The serialized Roll data
     */
    static #validateRoll(rollJSON) {
      const roll = JSON.parse(rollJSON);
      if ( !roll.evaluated ) throw new Error(`Roll objects added to ChatMessage documents must be evaluated`);
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    testUserPermission(user, permission, {exact=false}={}) {
      if ( !exact && (user.id === this._source.author) ) return true; // The user who created the chat message
      return super.testUserPermission(user, permission, {exact});
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static migrateData(data) {
      /**
       * V12 migration from user to author
       * @deprecated since v12
       */
      this._addDataFieldMigration(data, "user", "author");
      BaseChatMessage.#migrateTypeToStyle(data);
      return super.migrateData(data);
    }

    /* ---------------------------------------- */

    /**
     * Migrate the type field to the style field in order to allow the type field to be used for system sub-types.
     * @param {Partial<ChatMessageData>} data
     */
    static #migrateTypeToStyle(data) {
      if ( (typeof data.type !== "number") || ("style" in data) ) return;
      // WHISPER, ROLL, and any other invalid style are redirected to OTHER
      data.style = Object.values(CHAT_MESSAGE_STYLES).includes(data.type) ? data.type : 0;
      data.type = BASE_DOCUMENT_TYPE;
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    static shimData(data, options) {
      this._addDataFieldShim(data, "user", "author", {since: 12, until: 14});
      return super.shimData(data, options);
    }

    /* ---------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get user() {
      this.constructor._logDataFieldMigration("user", "author", {since: 12, until: 14});
      return this.author;
    }
  }

  /**
   * @typedef {import("./_types.mjs").CombatData} CombatData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Card Document.
   * Defines the DataSchema and common behaviors for a Combat which are shared between both client and server.
   * @mixes CombatData
   */
  class BaseCombat extends Document {
    /**
     * Construct a Combat document using provided data and context.
     * @param {Partial<CombatData>} data              Initial data from which to construct the Combat
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Combat",
      collection: "combats",
      label: "DOCUMENT.Combat",
      labelPlural: "DOCUMENT.Combats",
      embedded: {
        Combatant: "combatants"
      },
      hasTypeData: true,
      permissions: {
        update: this.#canUpdate
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /* -------------------------------------------- */

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        type: new DocumentTypeField(this, {initial: BASE_DOCUMENT_TYPE}),
        system: new TypeDataField(this),
        scene: new ForeignDocumentField(BaseScene),
        combatants: new EmbeddedCollectionField(BaseCombatant),
        active: new BooleanField(),
        round: new NumberField({required: true, nullable: false, integer: true, min: 0, initial: 0,
          label: "COMBAT.Round"}),
        turn: new NumberField({required: true, integer: true, min: 0, initial: null, label: "COMBAT.Turn"}),
        sort: new IntegerSortField(),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      }
    }

    /* -------------------------------------------- */

    /**
     * Is a user able to update an existing Combat?
     * @protected
     */
    static #canUpdate(user, doc, data) {
      if ( user.isGM ) return true;                             // GM users can do anything
      const turnOnly = ["_id", "round", "turn", "combatants"];  // Players may only modify a subset of fields
      if ( Object.keys(data).some(k => !turnOnly.includes(k)) ) return false;
      if ( ("round" in data) && !doc._canChangeRound(user) ) return false;
      if ( ("turn" in data) && !doc._canChangeTurn(user) ) return false;
      if ( ("combatants" in data) && !doc.#canModifyCombatants(user, data.combatants) ) return false;
      return true;
    }

    /* -------------------------------------------- */

    /**
     * Can a certain User change the Combat round?
     * @param {User} user     The user attempting to change the round
     * @returns {boolean}     Is the user allowed to change the round?
     * @protected
     */
    _canChangeRound(user) {
      return true;
    }

    /* -------------------------------------------- */

    /**
     * Can a certain User change the Combat turn?
     * @param {User} user     The user attempting to change the turn
     * @returns {boolean}     Is the user allowed to change the turn?
     * @protected
     */
    _canChangeTurn(user) {
      return true;
    }

    /* -------------------------------------------- */

    /**
     * Can a certain user make modifications to the array of Combatants?
     * @param {User} user     The user attempting to modify combatants
     * @param {Partial<CombatantData>[]} combatants   Proposed combatant changes
     * @returns {boolean}     Is the user allowed to make this change?
     */
    #canModifyCombatants(user, combatants) {
      for ( const {_id, ...change} of combatants ) {
        const c = this.combatants.get(_id);
        if ( !c ) return false;
        if ( !c.canUserModify(user, "update", change) ) return false;
      }
      return true;
    }

    /* -------------------------------------------- */
    /*  Event Handlers                              */
    /* -------------------------------------------- */

    /** @inheritDoc */
    async _preUpdate(changed, options, user) {
      const allowed = await super._preUpdate(changed, options, user);
      if ( allowed === false ) return false;
      // Don't allow linking to a Scene that doesn't contain all its Combatants
      if ( !("scene" in changed) ) return;
      const sceneId = this.schema.fields.scene.clean(changed.scene);
      if ( (sceneId !== null) && isValidId(sceneId)
        && this.combatants.some(c => c.sceneId && (c.sceneId !== sceneId)) ) {
        throw new Error("You cannot link the Combat to a Scene that doesn't contain all its Combatants.");
      }
    }
  }

  /**
   * @typedef {import("./_types.mjs").CombatantData} CombatantData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Combatant Document.
   * Defines the DataSchema and common behaviors for a Combatant which are shared between both client and server.
   * @mixes CombatantData
   */
  class BaseCombatant extends Document {
    /**
     * Construct a Combatant document using provided data and context.
     * @param {Partial<CombatantData>} data           Initial data from which to construct the Combatant
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Combatant",
      collection: "combatants",
      label: "DOCUMENT.Combatant",
      labelPlural: "DOCUMENT.Combatants",
      isEmbedded: true,
      hasTypeData: true,
      permissions: {
        create: this.#canCreate,
        update: this.#canUpdate
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        type: new DocumentTypeField(this, {initial: BASE_DOCUMENT_TYPE}),
        system: new TypeDataField(this),
        actorId: new ForeignDocumentField(BaseActor, {label: "COMBAT.CombatantActor", idOnly: true}),
        tokenId: new ForeignDocumentField(BaseToken, {label: "COMBAT.CombatantToken", idOnly: true}),
        sceneId: new ForeignDocumentField(BaseScene, {label: "COMBAT.CombatantScene", idOnly: true}),
        name: new StringField({label: "COMBAT.CombatantName", textSearch: true}),
        img: new FilePathField({categories: ["IMAGE"], label: "COMBAT.CombatantImage"}),
        initiative: new NumberField({label: "COMBAT.CombatantInitiative"}),
        hidden: new BooleanField({label: "COMBAT.CombatantHidden"}),
        defeated: new BooleanField({label: "COMBAT.CombatantDefeated"}),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      }
    }

    /**
     * Is a user able to update an existing Combatant?
     * @private
     */
    static #canUpdate(user, doc, data) {
      if ( user.isGM ) return true; // GM users can do anything
      if ( doc.actor && !doc.actor.canUserModify(user, "update", data) ) return false;
      const updateKeys = new Set(Object.keys(data));
      const allowedKeys = new Set(["_id", "initiative", "flags", "defeated"]);
      return updateKeys.isSubset(allowedKeys); // Players may only update initiative scores, flags, and the defeated state
    }

    /**
     * Is a user able to create this Combatant?
     * @private
     */
    static #canCreate(user, doc, data) {
      if ( user.isGM ) return true;
      if ( doc.actor ) return doc.actor.canUserModify(user, "update", data);
      return true;
    }

    /** @override */
    getUserLevel(user) {
      user = user || game.user;
      const {NONE, OWNER} = DOCUMENT_OWNERSHIP_LEVELS;
      if ( user.isGM ) return OWNER;
      return this.actor?.getUserLevel(user) ?? NONE;
    }
  }

  /**
   * @typedef {import("./_types.mjs").DrawingData} DrawingData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Drawing Document.
   * Defines the DataSchema and common behaviors for a Drawing which are shared between both client and server.
   * @mixes DrawingData
   */
  class BaseDrawing extends Document {
    /**
     * Construct a Drawing document using provided data and context.
     * @param {Partial<DrawingData>} data             Initial data from which to construct the Drawing
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* ---------------------------------------- */
    /*  Model Configuration                     */
    /* ---------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Drawing",
      collection: "drawings",
      label: "DOCUMENT.Drawing",
      labelPlural: "DOCUMENT.Drawings",
      isEmbedded: true,
      permissions: {
        create: this.#canCreate,
        update: this.#canUpdate
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /* ---------------------------------------- */

    /** @inheritDoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        author: new ForeignDocumentField(BaseUser, {nullable: false, initial: () => game.user?.id}),
        shape: new EmbeddedDataField(ShapeData),
        x: new NumberField({required: true, nullable: false, initial: 0, label: "XCoord"}),
        y: new NumberField({required: true, nullable: false, initial: 0, label: "YCoord"}),
        elevation: new NumberField({required: true, nullable: false, initial: 0}),
        sort: new NumberField({required: true, integer: true, nullable: false, initial: 0}),
        rotation: new AngleField({label: "DRAWING.Rotation"}),
        bezierFactor: new AlphaField({initial: 0, label: "DRAWING.SmoothingFactor", max: 0.5,
          hint: "DRAWING.SmoothingFactorHint"}),
        fillType: new NumberField({required: true, nullable: false, initial: DRAWING_FILL_TYPES.NONE,
          choices: Object.values(DRAWING_FILL_TYPES), label: "DRAWING.FillTypes",
          validationError: "must be a value in CONST.DRAWING_FILL_TYPES"
        }),
        fillColor: new ColorField({nullable: false, initial: () => game.user?.color.css || "#ffffff", label: "DRAWING.FillColor"}),
        fillAlpha: new AlphaField({initial: 0.5, label: "DRAWING.FillOpacity"}),
        strokeWidth: new NumberField({nullable: false, integer: true, initial: 8, min: 0, label: "DRAWING.LineWidth"}),
        strokeColor: new ColorField({nullable: false, initial: () => game.user?.color.css || "#ffffff", label: "DRAWING.StrokeColor"}),
        strokeAlpha: new AlphaField({initial: 1, label: "DRAWING.LineOpacity"}),
        texture: new FilePathField({categories: ["IMAGE"], label: "DRAWING.FillTexture"}),
        text: new StringField({label: "DRAWING.TextLabel"}),
        fontFamily: new StringField({blank: false, label: "DRAWING.FontFamily",
          initial: () => globalThis.CONFIG?.defaultFontFamily || "Signika"}),
        fontSize: new NumberField({nullable: false, integer: true, min: 8, max: 256, initial: 48, label: "DRAWING.FontSize",
          validationError: "must be an integer between 8 and 256"}),
        textColor: new ColorField({nullable: false, initial: "#ffffff", label: "DRAWING.TextColor"}),
        textAlpha: new AlphaField({label: "DRAWING.TextOpacity"}),
        hidden: new BooleanField(),
        locked: new BooleanField(),
        interface: new BooleanField(),
        flags: new ObjectField()
      }
    }

    /* ---------------------------------------- */

    /**
     * Validate whether the drawing has some visible content (as required by validation).
     * @returns {boolean}
     */
    static #validateVisibleContent(data) {
      const hasText = (data.text !== "") && (data.textAlpha > 0);
      const hasFill = (data.fillType !== DRAWING_FILL_TYPES.NONE) && (data.fillAlpha > 0);
      const hasLine = (data.strokeWidth > 0) && (data.strokeAlpha > 0);
      return hasText || hasFill || hasLine;
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    static validateJoint(data) {
      if ( !BaseDrawing.#validateVisibleContent(data) ) {
        throw new Error(game.i18n.localize("DRAWING.JointValidationError"));
      }
    }

    /* -------------------------------------------- */

    /** @override */
    static canUserCreate(user) {
      return user.hasPermission("DRAWING_CREATE");
    }

    /* ---------------------------------------- */

    /**
     * Is a user able to create a new Drawing?
     * @param {User} user            The user attempting the creation operation.
     * @param {BaseDrawing} doc      The Drawing being created.
     * @returns {boolean}
     */
    static #canCreate(user, doc) {
      if ( !user.isGM && (doc._source.author !== user.id) ) return false;
      return user.hasPermission("DRAWING_CREATE");
    }

    /* ---------------------------------------- */

    /**
     * Is a user able to update the Drawing document?
     */
    static #canUpdate(user, doc, data) {
      if ( !user.isGM && ("author" in data) && (data.author !== user.id) ) return false;
      return doc.testUserPermission(user, "OWNER");
    }

    /* ---------------------------------------- */
    /*  Model Methods                           */
    /* ---------------------------------------- */

    /** @inheritdoc */
    testUserPermission(user, permission, {exact=false}={}) {
      if ( !exact && (user.id === this._source.author) ) return true; // The user who created the drawing
      return super.testUserPermission(user, permission, {exact});
    }

    /* ---------------------------------------- */
    /*  Deprecations and Compatibility          */
    /* ---------------------------------------- */

    /** @inheritdoc */
    static migrateData(data) {
      /**
       * V12 migration to elevation and sort fields
       * @deprecated since v12
       */
      this._addDataFieldMigration(data, "z", "elevation");
      return super.migrateData(data);
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    static shimData(data, options) {
      this._addDataFieldShim(data, "z", "elevation", {since: 12, until: 14});
      return super.shimData(data, options);
    }

    /* ---------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get z() {
      this.constructor._logDataFieldMigration("z", "elevation", {since: 12, until: 14});
      return this.elevation;
    }
  }

  /**
   * @typedef {import("./_types.mjs").FogExplorationData} FogExplorationData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The FogExploration Document.
   * Defines the DataSchema and common behaviors for a FogExploration which are shared between both client and server.
   * @mixes FogExplorationData
   */
  class BaseFogExploration extends Document {
    /**
     * Construct a FogExploration document using provided data and context.
     * @param {Partial<FogExplorationData>} data      Initial data from which to construct the FogExploration
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* ---------------------------------------- */
    /*  Model Configuration                     */
    /* ---------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "FogExploration",
      collection: "fog",
      label: "DOCUMENT.FogExploration",
      labelPlural: "DOCUMENT.FogExplorations",
      isPrimary: true,
      permissions: {
        create: "PLAYER",
        update: this.#canModify,
        delete: this.#canModify
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        scene: new ForeignDocumentField(BaseScene, {initial: () => canvas?.scene?.id}),
        user: new ForeignDocumentField(BaseUser, {initial: () => game?.user?.id}),
        explored: new FilePathField({categories: ["IMAGE"], required: true, base64: true}),
        positions: new ObjectField(),
        timestamp: new NumberField({nullable: false, initial: Date.now}),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      }
    }

    /**
     * Test whether a User can modify a FogExploration document.
     */
    static #canModify(user, doc) {
      return (user.id === doc._source.user) || user.hasRole("ASSISTANT");
    }

    /* ---------------------------------------- */
    /*  Database Event Handlers                 */
    /* ---------------------------------------- */

    /** @inheritDoc */
    async _preUpdate(changed, options, user) {
      const allowed = await super._preUpdate(changed, options, user);
      if ( allowed === false ) return false;
      changed.timestamp = Date.now();
    }
  }

  /**
   * @typedef {import("./_types.mjs").FolderData} FolderData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Folder Document.
   * Defines the DataSchema and common behaviors for a Folder which are shared between both client and server.
   * @mixes FolderData
   */
  class BaseFolder extends Document {
    /**
     * Construct a Folder document using provided data and context.
     * @param {Partial<FolderData>} data              Initial data from which to construct the Folder
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* ---------------------------------------- */
    /*  Model Configuration                     */
    /* ---------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Folder",
      collection: "folders",
      label: "DOCUMENT.Folder",
      labelPlural: "DOCUMENT.Folders",
      coreTypes: FOLDER_DOCUMENT_TYPES,
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, textSearch: true}),
        type: new DocumentTypeField(this),
        description: new HTMLField({textSearch: true}),
        folder: new ForeignDocumentField(BaseFolder),
        sorting: new StringField({required: true, initial: "a", choices: this.SORTING_MODES}),
        sort: new IntegerSortField(),
        color: new ColorField(),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      }
    }

    /** @inheritdoc */
    static validateJoint(data) {
      if ( (data.folder !== null) && (data.folder === data._id) ) {
        throw new Error("A Folder may not contain itself");
      }
    }

    /**
     * Allow folder sorting modes
     * @type {string[]}
     */
    static SORTING_MODES = ["a", "m"];

    /* -------------------------------------------- */

    /** @override */
    static get(documentId, options={}) {
      if ( !documentId ) return null;
      if ( !options.pack ) return super.get(documentId, options);
      const pack = game.packs.get(options.pack);
      if ( !pack ) {
        console.error(`The ${this.name} model references a non-existent pack ${options.pack}.`);
        return null;
      }
      return pack.folders.get(documentId);
    }
  }

  /**
   * @typedef {import("./_types.mjs").ItemData} ItemData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Item Document.
   * Defines the DataSchema and common behaviors for a Item which are shared between both client and server.
   * @mixes ItemData
   */
  class BaseItem extends Document {
    /**
     * Construct a Item document using provided data and context.
     * @param {Partial<ItemData>} data                Initial data from which to construct the Item
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Item",
      collection: "items",
      hasTypeData: true,
      indexed: true,
      compendiumIndexFields: ["_id", "name", "img", "type", "sort", "folder"],
      embedded: {ActiveEffect: "effects"},
      label: "DOCUMENT.Item",
      labelPlural: "DOCUMENT.Items",
      permissions: {create: "ITEM_CREATE"},
      schemaVersion: "12.324"
    }, {inplace: false}));

    /* ---------------------------------------- */

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, textSearch: true}),
        type: new DocumentTypeField(this),
        img: new FilePathField({categories: ["IMAGE"], initial: data => {
          return this.implementation.getDefaultArtwork(data).img;
        }}),
        system: new TypeDataField(this),
        effects: new EmbeddedCollectionField(BaseActiveEffect),
        folder: new ForeignDocumentField(BaseFolder),
        sort: new IntegerSortField(),
        ownership: new DocumentOwnershipField(),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      }
    }

    /* ---------------------------------------- */

    /**
     * The default icon used for newly created Item documents
     * @type {string}
     */
    static DEFAULT_ICON = "icons/svg/item-bag.svg";

    /* -------------------------------------------- */

    /**
     * Determine default artwork based on the provided item data.
     * @param {ItemData} itemData  The source item data.
     * @returns {{img: string}}    Candidate item image.
     */
    static getDefaultArtwork(itemData) {
      return { img: this.DEFAULT_ICON };
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    canUserModify(user, action, data={}) {
      if ( this.isEmbedded ) return this.parent.canUserModify(user, "update");
      return super.canUserModify(user, action, data);
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    testUserPermission(user, permission, {exact=false}={}) {
      if ( this.isEmbedded ) return this.parent.testUserPermission(user, permission, {exact});
      return super.testUserPermission(user, permission, {exact});
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    static migrateData(source) {
      /**
       * Migrate sourceId.
       * @deprecated since v12
       */
      this._addDataFieldMigration(source, "flags.core.sourceId", "_stats.compendiumSource");

      return super.migrateData(source);
    }
  }

  /**
   * @typedef {import("./_types.mjs").JournalEntryData} JournalEntryData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The JournalEntry Document.
   * Defines the DataSchema and common behaviors for a JournalEntry which are shared between both client and server.
   * @mixes JournalEntryData
   */
  class BaseJournalEntry extends Document {
    /**
     * Construct a JournalEntry document using provided data and context.
     * @param {Partial<JournalEntryData>} data        Initial data from which to construct the JournalEntry
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "JournalEntry",
      collection: "journal",
      indexed: true,
      compendiumIndexFields: ["_id", "name", "sort", "folder"],
      embedded: {JournalEntryPage: "pages"},
      label: "DOCUMENT.JournalEntry",
      labelPlural: "DOCUMENT.JournalEntries",
      permissions: {
        create: "JOURNAL_CREATE"
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, textSearch: true}),
        pages: new EmbeddedCollectionField(BaseJournalEntryPage),
        folder: new ForeignDocumentField(BaseFolder),
        sort: new IntegerSortField(),
        ownership: new DocumentOwnershipField(),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      }
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    static migrateData(source) {
      /**
       * Migrate sourceId.
       * @deprecated since v12
       */
      this._addDataFieldMigration(source, "flags.core.sourceId", "_stats.compendiumSource");

      return super.migrateData(source);
    }
  }

  /**
   * @typedef {import("./_types.mjs").JournalEntryPageData} JournalEntryPageData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The JournalEntryPage Document.
   * Defines the DataSchema and common behaviors for a JournalEntryPage which are shared between both client and server.
   * @mixes JournalEntryPageData
   */
  class BaseJournalEntryPage extends Document {
    /**
     * Construct a JournalEntryPage document using provided data and context.
     * @param {Partial<JournalEntryPageData>} data    Initial data from which to construct the JournalEntryPage
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "JournalEntryPage",
      collection: "pages",
      hasTypeData: true,
      indexed: true,
      label: "DOCUMENT.JournalEntryPage",
      labelPlural: "DOCUMENT.JournalEntryPages",
      coreTypes: ["text", "image", "pdf", "video"],
      compendiumIndexFields: ["name", "type", "sort"],
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, label: "JOURNALENTRYPAGE.PageTitle", textSearch: true}),
        type: new DocumentTypeField(this, {initial: "text"}),
        system: new TypeDataField(this),
        title: new SchemaField({
          show: new BooleanField({initial: true}),
          level: new NumberField({required: true, initial: 1, min: 1, max: 6, integer: true, nullable: false})
        }),
        image: new SchemaField({
          caption: new StringField({required: false, initial: undefined})
        }),
        text: new SchemaField({
          content: new HTMLField({required: false, initial: undefined, textSearch: true}),
          markdown: new StringField({required: false, initial: undefined}),
          format: new NumberField({label: "JOURNALENTRYPAGE.Format",
            initial: JOURNAL_ENTRY_PAGE_FORMATS.HTML, choices: Object.values(JOURNAL_ENTRY_PAGE_FORMATS)})
        }),
        video: new SchemaField({
          controls: new BooleanField({initial: true}),
          loop: new BooleanField({required: false, initial: undefined}),
          autoplay: new BooleanField({required: false, initial: undefined}),
          volume: new AlphaField({required: true, step: 0.01, initial: .5}),
          timestamp: new NumberField({required: false, min: 0, initial: undefined}),
          width: new NumberField({required: false, positive: true, integer: true, initial: undefined}),
          height: new NumberField({required: false, positive: true, integer: true, initial: undefined})
        }),
        src: new StringField({required: false, blank: false, nullable: true, initial: null,
          label: "JOURNALENTRYPAGE.Source"}),
        sort: new IntegerSortField(),
        ownership: new DocumentOwnershipField({initial: {default: DOCUMENT_OWNERSHIP_LEVELS.INHERIT}}),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      };
    }

    /** @inheritdoc */
    getUserLevel(user) {
      user = user || game.user;
      const ownership = this.ownership[user.id] ?? this.ownership.default;
      const inherited = ownership === DOCUMENT_OWNERSHIP_LEVELS.INHERIT;
      return inherited ? this.parent.getUserLevel(user) : ownership;
    }
  }

  /**
   * @typedef {import("./_types.mjs").MacroData} MacroData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Macro Document.
   * Defines the DataSchema and common behaviors for a Macro which are shared between both client and server.
   * @mixes MacroData
   */
  class BaseMacro extends Document {
    /**
     * Construct a Macro document using provided data and context.
     * @param {Partial<MacroData>} data               Initial data from which to construct the Macro
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Macro",
      collection: "macros",
      indexed: true,
      compendiumIndexFields: ["_id", "name", "img", "sort", "folder"],
      label: "DOCUMENT.Macro",
      labelPlural: "DOCUMENT.Macros",
      coreTypes: Object.values(MACRO_TYPES),
      permissions: {
        create: this.#canCreate,
        update: this.#canUpdate
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, label: "Name", textSearch: true}),
        type: new DocumentTypeField(this, {initial: MACRO_TYPES.CHAT, label: "Type"}),
        author: new ForeignDocumentField(BaseUser, {initial: () => game?.user?.id}),
        img: new FilePathField({categories: ["IMAGE"], initial: () => this.DEFAULT_ICON, label: "Image"}),
        scope: new StringField({required: true, choices: MACRO_SCOPES, initial: MACRO_SCOPES[0],
          validationError: "must be a value in CONST.MACRO_SCOPES", label: "Scope"}),
        command: new StringField({required: true, blank: true, label: "Command"}),
        folder: new ForeignDocumentField(BaseFolder),
        sort: new IntegerSortField(),
        ownership: new DocumentOwnershipField(),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      }
    }

    /**
     * The default icon used for newly created Macro documents.
     * @type {string}
     */
    static DEFAULT_ICON = "icons/svg/dice-target.svg";

    /* -------------------------------------------- */

    /** @inheritDoc */
    static migrateData(source) {
      /**
       * Migrate sourceId.
       * @deprecated since v12
       */
      this._addDataFieldMigration(source, "flags.core.sourceId", "_stats.compendiumSource");

      return super.migrateData(source);
    }

    /* -------------------------------------------- */
    /*  Model Methods                               */
    /* -------------------------------------------- */

    /** @override */
    static validateJoint(data) {
      if ( data.type !== MACRO_TYPES.SCRIPT ) return;
      const field = new JavaScriptField({ async: true });
      const failure = field.validate(data.command);
      if ( failure ) throw failure.asError();
    }

    /* -------------------------------------------- */

    /** @override */
    static canUserCreate(user) {
      return user.hasRole("PLAYER");
    }

    /* ---------------------------------------- */

    /**
     * Is a user able to create the Macro document?
     */
    static #canCreate(user, doc) {
      if ( !user.isGM && (doc._source.author !== user.id) ) return false;
      if ( (doc._source.type === "script") && !user.hasPermission("MACRO_SCRIPT") ) return false;
      return user.hasRole("PLAYER");
    }

    /* ---------------------------------------- */

    /**
     * Is a user able to update the Macro document?
     */
    static #canUpdate(user, doc, data) {
      if ( !user.isGM && ("author" in data) && (data.author !== user.id) ) return false;
      if ( !user.hasPermission("MACRO_SCRIPT") ) {
        if ( data.type === "script" ) return false;
        if ( (doc._source.type === "script") && ("command" in data) ) return false;
      }
      return doc.testUserPermission(user, "OWNER");
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    testUserPermission(user, permission, {exact=false}={}) {
      if ( !exact && (user.id === this._source.author) ) return true; // Macro authors can edit
      return super.testUserPermission(user, permission, {exact});
    }

    /* -------------------------------------------- */
    /*  Database Event Handlers                     */
    /* -------------------------------------------- */

    /** @inheritDoc */
    async _preCreate(data, options, user) {
      const allowed = await super._preCreate(data, options, user);
      if ( allowed === false ) return false;
      this.updateSource({author: user.id});
    }
  }

  /**
   * @typedef {import("./_types.mjs").MeasuredTemplateData} MeasuredTemplateData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The MeasuredTemplate Document.
   * Defines the DataSchema and common behaviors for a MeasuredTemplate which are shared between both client and server.
   * @mixes MeasuredTemplateData
   */
  class BaseMeasuredTemplate extends Document {
    /**
     * Construct a MeasuredTemplate document using provided data and context.
     * @param {Partial<MeasuredTemplateData>} data    Initial data from which to construct the MeasuredTemplate
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = mergeObject(super.metadata, {
      name: "MeasuredTemplate",
      collection: "templates",
      label: "DOCUMENT.MeasuredTemplate",
      labelPlural: "DOCUMENT.MeasuredTemplates",
      isEmbedded: true,
      permissions: {
        create: this.#canCreate,
        update: this.#canUpdate
      },
      schemaVersion: "12.324"
    }, {inplace: false});

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        author: new ForeignDocumentField(BaseUser, {initial: () => game?.user?.id}),
        t: new StringField({required: true, choices: Object.values(MEASURED_TEMPLATE_TYPES), label: "Type",
          initial: MEASURED_TEMPLATE_TYPES.CIRCLE,
          validationError: "must be a value in CONST.MEASURED_TEMPLATE_TYPES",
        }),
        x: new NumberField({required: true, integer: true, nullable: false, initial: 0, label: "XCoord"}),
        y: new NumberField({required: true, integer: true, nullable: false, initial: 0, label: "YCoord"}),
        elevation: new NumberField({required: true, nullable: false, initial: 0}),
        sort: new NumberField({required: true, integer: true, nullable: false, initial: 0}),
        distance: new NumberField({required: true, nullable: false, initial: 0, min: 0, label: "Distance"}),
        direction: new AngleField({label: "Direction"}),
        angle: new AngleField({normalize: false, label: "Angle"}),
        width: new NumberField({required: true, nullable: false, initial: 0, min: 0, step: 0.01, label: "Width"}),
        borderColor: new ColorField({nullable: false, initial: "#000000"}),
        fillColor: new ColorField({nullable: false, initial: () => game.user?.color.css || "#ffffff"}),
        texture: new FilePathField({categories: ["IMAGE", "VIDEO"]}),
        hidden: new BooleanField({label: "Hidden"}),
        flags: new ObjectField()
      }
    }

    /* ---------------------------------------- */

    /**
     * Is a user able to create a new MeasuredTemplate?
     * @param {User} user                     The user attempting the creation operation.
     * @param {BaseMeasuredTemplate} doc      The MeasuredTemplate being created.
     * @returns {boolean}
     */
    static #canCreate(user, doc) {
      if ( !user.isGM && (doc._source.author !== user.id) ) return false;
      return user.hasPermission("TEMPLATE_CREATE");
    }

    /* ---------------------------------------- */

    /**
     * Is a user able to update the MeasuredTemplate document?
     */
    static #canUpdate(user, doc, data) {
      if ( !user.isGM && ("author" in data) && (data.author !== user.id) ) return false;
      return doc.testUserPermission(user, "OWNER");
    }

    /* -------------------------------------------- */
    /*  Model Methods                               */
    /* -------------------------------------------- */

    /** @inheritdoc */
    testUserPermission(user, permission, {exact=false}={}) {
      if ( !exact && (user.id === this._source.author) ) return true; // The user who created the template
      return super.testUserPermission(user, permission, {exact});
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static migrateData(data) {
      /**
       * V12 migration from user to author
       * @deprecated since v12
       */
      this._addDataFieldMigration(data, "user", "author");
      return super.migrateData(data);
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    static shimData(data, options) {
      this._addDataFieldShim(data, "user", "author", {since: 12, until: 14});
      return super.shimData(data, options);
    }

    /* ---------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get user() {
      this.constructor._logDataFieldMigration("user", "author", {since: 12, until: 14});
      return this.author;
    }
  }

  /**
   * @typedef {import("./_types.mjs").NoteData} NoteData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Note Document.
   * Defines the DataSchema and common behaviors for a Note which are shared between both client and server.
   * @mixes NoteData
   */
  class BaseNote extends Document {
    /**
     * Construct a Note document using provided data and context.
     * @param {Partial<NoteData>} data                Initial data from which to construct the Note
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Note",
      collection: "notes",
      label: "DOCUMENT.Note",
      labelPlural: "DOCUMENT.Notes",
      permissions: {
        create: "NOTE_CREATE"
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        entryId: new ForeignDocumentField(BaseJournalEntry, {idOnly: true}),
        pageId: new ForeignDocumentField(BaseJournalEntryPage, {idOnly: true}),
        x: new NumberField({required: true, integer: true, nullable: false, initial: 0, label: "XCoord"}),
        y: new NumberField({required: true, integer: true, nullable: false, initial: 0, label: "YCoord"}),
        elevation: new NumberField({required: true, nullable: false, initial: 0}),
        sort: new NumberField({required: true, integer: true, nullable: false, initial: 0}),
        texture: new TextureData({}, {categories: ["IMAGE"],
          initial: {src: () => this.DEFAULT_ICON, anchorX: 0.5, anchorY: 0.5, fit: "contain"}, label: "NOTE.EntryIcon"}),
        iconSize: new NumberField({required: true, nullable: false, integer: true, min: 32, initial: 40,
          validationError: "must be an integer greater than 32", label: "NOTE.IconSize"}),
        text: new StringField({label: "NOTE.TextLabel", textSearch: true}),
        fontFamily: new StringField({required: true, label: "NOTE.FontFamily",
          initial: () => globalThis.CONFIG?.defaultFontFamily || "Signika"}),
        fontSize: new NumberField({required: true, integer: true, min: 8, max: 128, initial: 32,
          validationError: "must be an integer between 8 and 128", label: "NOTE.FontSize"}),
        textAnchor: new NumberField({required: true, choices: Object.values(TEXT_ANCHOR_POINTS),
          initial: TEXT_ANCHOR_POINTS.BOTTOM, label: "NOTE.AnchorPoint",
          validationError: "must be a value in CONST.TEXT_ANCHOR_POINTS"}),
        textColor: new ColorField({required: true, nullable: false, initial: "#ffffff", label: "NOTE.TextColor"}),
        global: new BooleanField(),
        flags: new ObjectField()
      }
    }

    /**
     * The default icon used for newly created Note documents.
     * @type {string}
     */
    static DEFAULT_ICON = "icons/svg/book.svg";

    /* -------------------------------------------- */
    /*  Model Methods                               */
    /* -------------------------------------------- */

    /** @inheritdoc */
    testUserPermission(user, permission, {exact=false}={}) {
      if ( user.isGM ) return true;                             // Game-masters always have control
      // Players can create and edit unlinked notes with the appropriate permission.
      if ( !this.entryId ) return user.hasPermission("NOTE_CREATE");
      if ( !this.entry ) return false;                          // Otherwise, permission comes through the JournalEntry
      return this.entry.testUserPermission(user, permission, {exact});
    }
  }

  /**
   * @typedef {import("./_types.mjs").PlaylistData} PlaylistData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Playlist Document.
   * Defines the DataSchema and common behaviors for a Playlist which are shared between both client and server.
   * @mixes PlaylistData
   */
  class BasePlaylist extends Document {
    /**
     * Construct a Playlist document using provided data and context.
     * @param {Partial<PlaylistData>} data            Initial data from which to construct the Playlist
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Playlist",
      collection: "playlists",
      indexed: true,
      compendiumIndexFields: ["_id", "name", "description", "sort", "folder"],
      embedded: {PlaylistSound: "sounds"},
      label: "DOCUMENT.Playlist",
      labelPlural: "DOCUMENT.Playlists",
      permissions: {
        create: "PLAYLIST_CREATE"
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, textSearch: true}),
        description: new StringField({textSearch: true}),
        sounds: new EmbeddedCollectionField(BasePlaylistSound),
        channel: new StringField({choices: AUDIO_CHANNELS, initial: "music", blank: false}),
        mode: new NumberField({required: true, choices: Object.values(PLAYLIST_MODES),
          initial: PLAYLIST_MODES.SEQUENTIAL, validationError: "must be a value in CONST.PLAYLIST_MODES"}),
        playing: new BooleanField(),
        fade: new NumberField({positive: true}),
        folder: new ForeignDocumentField(BaseFolder),
        sorting: new StringField({required: true, choices: Object.values(PLAYLIST_SORT_MODES),
          initial: PLAYLIST_SORT_MODES.ALPHABETICAL,
          validationError: "must be a value in CONST.PLAYLIST_SORTING_MODES"}),
        seed: new NumberField({integer: true, min: 0}),
        sort: new IntegerSortField(),
        ownership: new DocumentOwnershipField(),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      }
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    static migrateData(source) {
      /**
       * Migrate sourceId.
       * @deprecated since v12
       */
      this._addDataFieldMigration(source, "flags.core.sourceId", "_stats.compendiumSource");
      return super.migrateData(source);
    }
  }

  /**
   * @typedef {import("./_types.mjs").PlaylistSoundData} PlaylistSoundData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The PlaylistSound Document.
   * Defines the DataSchema and common behaviors for a PlaylistSound which are shared between both client and server.
   * @mixes PlaylistSoundData
   */
  class BasePlaylistSound extends Document {
    /**
     * Construct a PlaylistSound document using provided data and context.
     * @param {Partial<PlaylistSoundData>} data       Initial data from which to construct the PlaylistSound
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "PlaylistSound",
      collection: "sounds",
      indexed: true,
      label: "DOCUMENT.PlaylistSound",
      labelPlural: "DOCUMENT.PlaylistSounds",
      compendiumIndexFields: ["name", "sort"],
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, textSearch: true}),
        description: new StringField(),
        path: new FilePathField({categories: ["AUDIO"]}),
        channel: new StringField({choices: AUDIO_CHANNELS, initial: "music", blank: true}),
        playing: new BooleanField(),
        pausedTime: new NumberField({min: 0}),
        repeat: new BooleanField(),
        volume: new AlphaField({initial: 0.5, step: 0.01}),
        fade: new NumberField({integer: true, min: 0}),
        sort: new IntegerSortField(),
        flags: new ObjectField(),
      }
    }

    /* -------------------------------------------- */
    /*  Model Methods                               */
    /* -------------------------------------------- */

    /** @inheritdoc */
    testUserPermission(user, permission, {exact = false} = {}) {
      if ( this.isEmbedded ) return this.parent.testUserPermission(user, permission, {exact});
      return super.testUserPermission(user, permission, {exact});
    }
  }

  /**
   * @typedef {import("./_types.mjs").RollTableData} RollTableData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The RollTable Document.
   * Defines the DataSchema and common behaviors for a RollTable which are shared between both client and server.
   * @mixes RollTableData
   */
  class BaseRollTable extends Document {
    /**
     * Construct a RollTable document using provided data and context.
     * @param {Partial<RollTableData>} data           Initial data from which to construct the RollTable
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritDoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "RollTable",
      collection: "tables",
      indexed: true,
      compendiumIndexFields: ["_id", "name", "description", "img", "sort", "folder"],
      embedded: {TableResult: "results"},
      label: "DOCUMENT.RollTable",
      labelPlural: "DOCUMENT.RollTables",
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritDoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, textSearch: true}),
        img: new FilePathField({categories: ["IMAGE"], initial: () => this.DEFAULT_ICON}),
        description: new HTMLField({textSearch: true}),
        results: new EmbeddedCollectionField(BaseTableResult),
        formula: new StringField(),
        replacement: new BooleanField({initial: true}),
        displayRoll: new BooleanField({initial: true}),
        folder: new ForeignDocumentField(BaseFolder),
        sort: new IntegerSortField(),
        ownership: new DocumentOwnershipField(),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      }
    }

    /**
     * The default icon used for newly created Macro documents
     * @type {string}
     */
    static DEFAULT_ICON = "icons/svg/d20-grey.svg";

    /* -------------------------------------------- */

    /** @inheritDoc */
    static migrateData(source) {
      /**
       * Migrate sourceId.
       * @deprecated since v12
       */
      this._addDataFieldMigration(source, "flags.core.sourceId", "_stats.compendiumSource");

      return super.migrateData(source);
    }
  }

  /**
   * @typedef {import("./_types.mjs").SceneData} SceneData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Scene Document.
   * Defines the DataSchema and common behaviors for a Scene which are shared between both client and server.
   * @mixes SceneData
   */
  class BaseScene extends Document {
    /**
     * Construct a Scene document using provided data and context.
     * @param {Partial<SceneData>} data               Initial data from which to construct the Scene
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Scene",
      collection: "scenes",
      indexed: true,
      compendiumIndexFields: ["_id", "name", "thumb", "sort", "folder"],
      embedded: {
        AmbientLight: "lights",
        AmbientSound: "sounds",
        Drawing: "drawings",
        MeasuredTemplate: "templates",
        Note: "notes",
        Region: "regions",
        Tile: "tiles",
        Token: "tokens",
        Wall: "walls"
      },
      label: "DOCUMENT.Scene",
      labelPlural: "DOCUMENT.Scenes",
      preserveOnImport: [...super.metadata.preserveOnImport, "active"],
      schemaVersion: "12.325"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      // Define reusable ambience schema for environment
      const environmentData = defaults => new SchemaField({
        hue: new HueField({required: true, initial: defaults.hue,
          label: "SCENES.ENVIRONMENT.Hue", hint: "SCENES.ENVIRONMENT.HueHint"}),
        intensity: new AlphaField({required: true, nullable: false, initial: defaults.intensity,
          label: "SCENES.ENVIRONMENT.Intensity", hint: "SCENES.ENVIRONMENT.IntensityHint"}),
        luminosity: new NumberField({required: true, nullable: false, initial: defaults.luminosity, min: -1, max: 1,
          label: "SCENES.ENVIRONMENT.Luminosity", hint: "SCENES.ENVIRONMENT.LuminosityHint"}),
        saturation: new NumberField({required: true, nullable: false, initial: defaults.saturation, min: -1, max: 1,
          label: "SCENES.ENVIRONMENT.Saturation", hint: "SCENES.ENVIRONMENT.SaturationHint"}),
        shadows: new NumberField({required: true, nullable: false, initial: defaults.shadows, min: 0, max: 1,
          label: "SCENES.ENVIRONMENT.Shadows", hint: "SCENES.ENVIRONMENT.ShadowsHint"})
      });
      // Reuse parts of the LightData schema for the global light
      const lightDataSchema = foundry.data.LightData.defineSchema();

      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, textSearch: true}),

        // Navigation
        active: new BooleanField(),
        navigation: new BooleanField({initial: true}),
        navOrder: new NumberField({required: true, nullable: false, integer: true, initial: 0}),
        navName: new HTMLField({textSearch: true}),

        // Canvas Dimensions
        background: new TextureData(),
        foreground: new FilePathField({categories: ["IMAGE", "VIDEO"]}),
        foregroundElevation: new NumberField({required: true, positive: true, integer: true}),
        thumb: new FilePathField({categories: ["IMAGE"]}),
        width: new NumberField({integer: true, positive: true, initial: 4000}),
        height: new NumberField({integer: true, positive: true, initial: 3000}),
        padding: new NumberField({required: true, nullable: false, min: 0, max: 0.5, step: 0.05, initial: 0.25}),
        initial: new SchemaField({
          x: new NumberField({integer: true, required: true}),
          y: new NumberField({integer: true, required: true}),
          scale: new NumberField({required: true, max: 3, positive: true, initial: 0.5})
        }),
        backgroundColor: new ColorField({nullable: false, initial: "#999999"}),

        // Grid Configuration
        grid: new SchemaField({
          type: new NumberField({required: true, choices: Object.values(GRID_TYPES),
            initial: () => game.system.grid.type, validationError: "must be a value in CONST.GRID_TYPES"}),
          size: new NumberField({required: true, nullable: false, integer: true, min: GRID_MIN_SIZE,
            initial: 100, validationError: `must be an integer number of pixels, ${GRID_MIN_SIZE} or greater`}),
          style: new StringField({required: true, blank: false, initial: "solidLines"}),
          thickness: new NumberField({required: true, nullable: false, positive: true, integer: true, initial: 1}),
          color: new ColorField({required: true, nullable: false, initial: "#000000"}),
          alpha: new AlphaField({initial: 0.2}),
          distance: new NumberField({required: true, nullable: false, positive: true,
            initial: () => game.system.grid.distance}),
          units: new StringField({required: true, initial: () => game.system.grid.units})
        }),

        // Vision Configuration
        tokenVision: new BooleanField({initial: true}),
        fog: new SchemaField({
          exploration: new BooleanField({initial: true}),
          reset: new NumberField({required: false, initial: undefined}),
          overlay: new FilePathField({categories: ["IMAGE", "VIDEO"]}),
          colors: new SchemaField({
            explored: new ColorField({label: "SCENES.FogExploredColor"}),
            unexplored: new ColorField({label: "SCENES.FogUnexploredColor"})
          })
        }),

        // Environment Configuration
        environment: new SchemaField({
          darknessLevel: new AlphaField({initial: 0}),
          darknessLock: new BooleanField({initial: false}),
          globalLight: new SchemaField({
            enabled: new BooleanField({required: true, initial: false}),
            alpha: lightDataSchema.alpha,
            bright: new BooleanField({required: true, initial: false}),
            color: lightDataSchema.color,
            coloration: lightDataSchema.coloration,
            luminosity: new NumberField({required: true, nullable: false, initial: 0, min: 0, max: 1}),
            saturation: lightDataSchema.saturation,
            contrast: lightDataSchema.contrast,
            shadows: lightDataSchema.shadows,
            darkness: lightDataSchema.darkness
          }),
          cycle: new BooleanField({initial: true}),
          base: environmentData({hue: 0, intensity: 0, luminosity: 0, saturation: 0, shadows: 0}),
          dark: environmentData({hue: 257/360, intensity: 0, luminosity: -0.25, saturation: 0, shadows: 0})
        }),

        // Embedded Collections
        drawings: new EmbeddedCollectionField(BaseDrawing),
        tokens: new EmbeddedCollectionField(BaseToken),
        lights: new EmbeddedCollectionField(BaseAmbientLight),
        notes: new EmbeddedCollectionField(BaseNote),
        sounds: new EmbeddedCollectionField(BaseAmbientSound),
        regions: new EmbeddedCollectionField(BaseRegion),
        templates: new EmbeddedCollectionField(BaseMeasuredTemplate),
        tiles: new EmbeddedCollectionField(BaseTile),
        walls: new EmbeddedCollectionField(BaseWall),

        // Linked Documents
        playlist: new ForeignDocumentField(BasePlaylist),
        playlistSound: new ForeignDocumentField(BasePlaylistSound, {idOnly: true}),
        journal: new ForeignDocumentField(BaseJournalEntry),
        journalEntryPage: new ForeignDocumentField(BaseJournalEntryPage, {idOnly: true}),
        weather: new StringField({required: true}),

        // Permissions
        folder: new ForeignDocumentField(BaseFolder),
        sort: new IntegerSortField(),
        ownership: new DocumentOwnershipField(),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      }
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * Static Initializer Block for deprecated properties.
     * @see [Static Initialization Blocks](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Static_initialization_blocks)
     */
    static {
      const migrations = {
        fogExploration: "fog.exploration",
        fogReset: "fog.reset",
        fogOverlay: "fog.overlay",
        fogExploredColor: "fog.colors.explored",
        fogUnexploredColor: "fog.colors.unexplored",
        globalLight: "environment.globalLight.enabled",
        globalLightThreshold: "environment.globalLight.darkness.max",
        darkness: "environment.darknessLevel"
      };
      Object.defineProperties(this.prototype, Object.fromEntries(
        Object.entries(migrations).map(([o, n]) => [o, {
          get() {
            this.constructor._logDataFieldMigration(o, n, {since: 12, until: 14});
            return foundry.utils.getProperty(this, n);
          },
          set(v) {
            this.constructor._logDataFieldMigration(o, n, {since: 12, until: 14});
            return foundry.utils.setProperty(this, n, v);
          },
          configurable: true
        }])));
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    static migrateData(data) {
      /**
       * Migration to fog schema fields. Can be safely removed in V14+
       * @deprecated since v12
       */
      for ( const [oldKey, newKey] of Object.entries({
        "fogExploration": "fog.exploration",
        "fogReset": "fog.reset",
        "fogOverlay": "fog.overlay",
        "fogExploredColor": "fog.colors.explored",
        "fogUnexploredColor": "fog.colors.unexplored"
      }) ) this._addDataFieldMigration(data, oldKey, newKey);

      /**
       * Migration to global light embedded fields. Can be safely removed in V14+
       * @deprecated since v12
       */
      this._addDataFieldMigration(data, "globalLight", "environment.globalLight.enabled");
      this._addDataFieldMigration(data, "globalLightThreshold", "environment.globalLight.darkness.max",
        d => d.globalLightThreshold ?? 1);

      /**
       * Migration to environment darkness level. Can be safely removed in V14+
       * @deprecated since v12
       */
      this._addDataFieldMigration(data, "darkness", "environment.darknessLevel");

      /**
       * Migrate sourceId.
       * @deprecated since v12
       */
      this._addDataFieldMigration(data, "flags.core.sourceId", "_stats.compendiumSource");

      return super.migrateData(data);
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    static shimData(data, options) {
      /** @deprecated since v12 */
      this._addDataFieldShims(data, {
        fogExploration: "fog.exploration",
        fogReset: "fog.reset",
        fogOverlay: "fog.overlay",
        fogExploredColor: "fog.colors.explored",
        fogUnexploredColor: "fog.colors.unexplored",
        globalLight: "environment.globalLight.enabled",
        globalLightThreshold: "environment.globalLight.darkness.max",
        darkness: "environment.darknessLevel"
      }, {since: 12, until: 14});
      return super.shimData(data, options);
    }
  }

  /**
   * @typedef {import("./_types.mjs").RegionData} RegionData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Region Document.
   * Defines the DataSchema and common behaviors for a Region which are shared between both client and server.
   * @mixes RegionData
   */
  class BaseRegion extends Document {
    /**
     * Construct a Region document using provided data and context.
     * @param {Partial<RegionData>} data         Initial data from which to construct the Region
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Region",
      collection: "regions",
      label: "DOCUMENT.Region",
      labelPlural: "DOCUMENT.Regions",
      isEmbedded: true,
      embedded: {
        RegionBehavior: "behaviors"
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, label: "Name", textSearch: true}),
        color: new ColorField({required: true, nullable: false,
          initial: () => Color$1.fromHSV([Math.random(), 0.8, 0.8]).css,
          label: "REGION.FIELDS.color.label",
          hint: "REGION.FIELDS.color.hint"}),
        shapes: new ArrayField(new TypedSchemaField(BaseShapeData.TYPES),
          {label: "REGION.FIELDS.shapes.label", hint: "REGION.FIELDS.shapes.hint"}),
        elevation: new SchemaField({
          bottom: new NumberField({required: true,
            label: "REGION.FIELDS.elevation.FIELDS.bottom.label",
            hint: "REGION.FIELDS.elevation.FIELDS.bottom.hint"}), // null -> -Infinity
          top: new NumberField({required: true,
            label: "REGION.FIELDS.elevation.FIELDS.top.label",
            hint: "REGION.FIELDS.elevation.FIELDS.top.hint"}) // null -> +Infinity
        }, {
          label: "REGION.FIELDS.elevation.label",
          hint: "REGION.FIELDS.elevation.hint",
          validate: d => (d.bottom ?? -Infinity) <= (d.top ?? Infinity),
          validationError: "elevation.top may not be less than elevation.bottom"
        }),
        behaviors: new EmbeddedCollectionField(BaseRegionBehavior, {label: "REGION.FIELDS.behaviors.label",
          hint: "REGION.FIELDS.behaviors.hint"}),
        visibility: new NumberField({required: true,
          initial: CONST.REGION_VISIBILITY.LAYER,
          choices:  Object.fromEntries(Object.entries(CONST.REGION_VISIBILITY).map(([key, value]) =>
            [value, {label: `REGION.VISIBILITY.${key}.label`}])),
          label: "REGION.FIELDS.visibility.label",
          hint: "REGION.FIELDS.visibility.hint"}),
        locked: new BooleanField(),
        flags: new ObjectField()
      }
    };
  }

  /**
   * @typedef {import("./_types.mjs").RegionBehaviorData} RegionBehaviorData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The RegionBehavior Document.
   * Defines the DataSchema and common behaviors for a RegionBehavior which are shared between both client and server.
   * @mixes SceneRegionData
   */
  class BaseRegionBehavior extends Document {
    /**
     * Construct a RegionBehavior document using provided data and context.
     * @param {Partial<RegionBehaviorData>} data    Initial data from which to construct the RegionBehavior
     * @param {DocumentConstructionContext} context      Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "RegionBehavior",
      collection: "behaviors",
      label: "DOCUMENT.RegionBehavior",
      labelPlural: "DOCUMENT.RegionBehaviors",
      coreTypes: ["adjustDarknessLevel", "displayScrollingText", "executeMacro", "executeScript", "pauseGame", "suppressWeather", "teleportToken", "toggleBehavior"],
      hasTypeData: true,
      isEmbedded: true,
      permissions: {
        create: this.#canCreate,
        update: this.#canUpdate
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: true, label: "Name", textSearch: true}),
        type: new DocumentTypeField(this),
        system: new TypeDataField(this),
        disabled: new BooleanField({label: "BEHAVIOR.FIELDS.disabled.label", hint: "BEHAVIOR.FIELDS.disabled.hint"}),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      };
    }

    /* -------------------------------------------- */

    /** @override */
    static canUserCreate(user) {
      return user.isGM;
    }

    /* ---------------------------------------- */

    /**
     * Is a user able to create the RegionBehavior document?
     */
    static #canCreate(user, doc) {
      if ( (doc._source.type === "executeScript") && !user.hasPermission("MACRO_SCRIPT") ) return false;
      return user.isGM;
    }

    /* ---------------------------------------- */

    /**
     * Is a user able to update the RegionBehavior document?
     */
    static #canUpdate(user, doc, data) {
      if ( (((doc._source.type === "executeScript") && ("system" in data) && ("source" in data.system))
        || (data.type === "executeScript")) && !user.hasPermission("MACRO_SCRIPT") ) return false;
      return user.isGM;
    }
  }

  /**
   * @typedef {import("./_types.mjs").SettingData} SettingData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Setting Document.
   * Defines the DataSchema and common behaviors for a Setting which are shared between both client and server.
   * @mixes SettingData
   */
  class BaseSetting extends Document {
    /**
     * Construct a Setting document using provided data and context.
     * @param {Partial<SettingData>} data             Initial data from which to construct the Setting
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Setting",
      collection: "settings",
      label: "DOCUMENT.Setting",
      labelPlural: "DOCUMENT.Settings",
      permissions: {
        create: this.#canModify,
        update: this.#canModify,
        delete: this.#canModify
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        key: new StringField({required: true, nullable: false, blank: false,
          validate: k => k.split(".").length >= 2,
          validationError: "must have the format {scope}.{field}"}),
        value: new JSONField({required: true, nullable: true, initial: null}),
        _stats: new DocumentStatsField()
      }
    }

    /* -------------------------------------------- */

    /**
     * The settings that only full GMs can modify.
     * @type {string[]}
     */
    static #GAMEMASTER_ONLY_KEYS = ["core.permissions"];

    /* -------------------------------------------- */

    /**
     * The settings that assistant GMs can modify regardless of their permission.
     * @type {string[]}
     */
    static #ALLOWED_ASSISTANT_KEYS = ["core.time", "core.combatTrackerConfig", "core.sheetClasses", "core.scrollingStatusText",
      "core.tokenDragPreview", "core.adventureImports", "core.gridDiagonals", "core.gridTemplates", "core.coneTemplateType"];

    /* -------------------------------------------- */

    /** @override */
    static canUserCreate(user) {
      return user.hasPermission("SETTINGS_MODIFY");
    }

    /* -------------------------------------------- */

    /**
     * Define special rules which allow certain settings to be updated.
     * @protected
     */
    static #canModify(user, doc, data) {
      if ( BaseSetting.#GAMEMASTER_ONLY_KEYS.includes(doc._source.key)
        && (!("key" in data) || BaseSetting.#GAMEMASTER_ONLY_KEYS.includes(data.key)) ) return user.hasRole("GAMEMASTER");
      if ( user.hasPermission("SETTINGS_MODIFY") ) return true;
      if ( !user.isGM ) return false;
      return BaseSetting.#ALLOWED_ASSISTANT_KEYS.includes(doc._source.key)
        && (!("key" in data) || BaseSetting.#ALLOWED_ASSISTANT_KEYS.includes(data.key));
    }
  }

  /**
   * @typedef {import("./_types.mjs").TableResultData} TableResultData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The TableResult Document.
   * Defines the DataSchema and common behaviors for a TableResult which are shared between both client and server.
   * @mixes TableResultData
   */
  class BaseTableResult extends Document {
    /**
     * Construct a TableResult document using provided data and context.
     * @param {Partial<TableResultData>} data         Initial data from which to construct the TableResult
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "TableResult",
      collection: "results",
      label: "DOCUMENT.TableResult",
      labelPlural: "DOCUMENT.TableResults",
      coreTypes: Object.values(TABLE_RESULT_TYPES),
      permissions: {
        update: this.#canUpdate
      },
      compendiumIndexFields: ["type"],
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        type: new DocumentTypeField(this, {initial: TABLE_RESULT_TYPES.TEXT}),
        text: new HTMLField({textSearch: true}),
        img: new FilePathField({categories: ["IMAGE"]}),
        documentCollection: new StringField(),
        documentId: new ForeignDocumentField(Document, {idOnly: true}),
        weight: new NumberField({required: true, integer: true, positive: true, nullable: false, initial: 1}),
        range: new ArrayField(new NumberField({integer: true}), {
          validate: r => (r.length === 2) && (r[1] >= r[0]),
          validationError: "must be a length-2 array of ascending integers"
        }),
        drawn: new BooleanField(),
        flags: new ObjectField()
      }
    }

    /**
     * Is a user able to update an existing TableResult?
     * @private
     */
    static #canUpdate(user, doc, data) {
      if ( user.isGM ) return true;                               // GM users can do anything
      const wasDrawn = new Set(["drawn", "_id"]);                 // Users can update the drawn status of a result
      if ( new Set(Object.keys(data)).equals(wasDrawn) ) return true;
      return doc.parent.canUserModify(user, "update", data);      // Otherwise, go by parent document permission
    }

    /* -------------------------------------------- */
    /*  Model Methods                               */
    /* -------------------------------------------- */

    /** @inheritdoc */
    testUserPermission(user, permission, {exact=false}={}) {
      if ( this.isEmbedded ) return this.parent.testUserPermission(user, permission, {exact});
      return super.testUserPermission(user, permission, {exact});
    }

    /* ---------------------------------------- */
    /*  Deprecations and Compatibility          */
    /* ---------------------------------------- */

    /** @inheritdoc */
    static migrateData(data) {

      /**
       * V12 migration of type from number to string.
       * @deprecated since v12
       */
      if ( typeof data.type === "number" ) {
        switch ( data.type ) {
          case 0: data.type = TABLE_RESULT_TYPES.TEXT; break;
          case 1: data.type = TABLE_RESULT_TYPES.DOCUMENT; break;
          case 2: data.type = TABLE_RESULT_TYPES.COMPENDIUM; break;
        }
      }
      return super.migrateData(data);
    }
  }

  /**
   * @typedef {import("./_types.mjs").TileData} TileData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Tile Document.
   * Defines the DataSchema and common behaviors for a Tile which are shared between both client and server.
   * @mixes TileData
   */
  class BaseTile extends Document {
    /**
     * Construct a Tile document using provided data and context.
     * @param {Partial<TileData>} data                Initial data from which to construct the Tile
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Tile",
      collection: "tiles",
      label: "DOCUMENT.Tile",
      labelPlural: "DOCUMENT.Tiles",
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        texture: new TextureData({}, {initial: {anchorX: 0.5, anchorY: 0.5, alphaThreshold: 0.75}}),
        width: new NumberField({required: true, min: 0, nullable: false, step: 0.1}),
        height: new NumberField({required: true, min: 0, nullable: false, step: 0.1}),
        x: new NumberField({required: true, integer: true, nullable: false, initial: 0, label: "XCoord"}),
        y: new NumberField({required: true, integer: true, nullable: false, initial: 0, label: "YCoord"}),
        elevation: new NumberField({required: true, nullable: false, initial: 0}),
        sort: new NumberField({required: true, integer: true, nullable: false, initial: 0}),
        rotation: new AngleField(),
        alpha: new AlphaField(),
        hidden: new BooleanField(),
        locked: new BooleanField(),
        restrictions: new SchemaField({
          light: new BooleanField(),
          weather: new BooleanField()
        }),
        occlusion: new SchemaField({
          mode: new NumberField({choices: Object.values(OCCLUSION_MODES),
            initial: OCCLUSION_MODES.NONE,
            validationError: "must be a value in CONST.TILE_OCCLUSION_MODES"}),
          alpha: new AlphaField({initial: 0})
        }),
        video: new SchemaField({
          loop: new BooleanField({initial: true}),
          autoplay: new BooleanField({initial: true}),
          volume: new AlphaField({initial: 0, step: 0.01})
        }),
        flags: new ObjectField()
      }
    }


    /* ---------------------------------------- */
    /*  Deprecations and Compatibility          */
    /* ---------------------------------------- */

    /** @inheritdoc */
    static migrateData(data) {
      /**
       * V12 migration to elevation and sort
       * @deprecated since v12
       */
      this._addDataFieldMigration(data, "z", "sort");

      /**
       * V12 migration from roof to restrictions.light and restrictions.weather
       * @deprecated since v12
       */
      if ( foundry.utils.hasProperty(data, "roof") ) {
        const value = foundry.utils.getProperty(data, "roof");
        if ( !foundry.utils.hasProperty(data, "restrictions.light") ) foundry.utils.setProperty(data, "restrictions.light", value);
        if ( !foundry.utils.hasProperty(data, "restrictions.weather") ) foundry.utils.setProperty(data, "restrictions.weather", value);
        delete data["roof"];
      }

      return super.migrateData(data);
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    static shimData(data, options) {
      this._addDataFieldShim(data, "z", "sort", {since: 12, until: 14});
      return super.shimData(data, options);
    }

    /* ---------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    set roof(enabled) {
      this.constructor._logDataFieldMigration("roof", "restrictions.{light|weather}", {since: 12, until: 14});
      this.restrictions.light = enabled;
      this.restrictions.weather = enabled;
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    get roof() {
      this.constructor._logDataFieldMigration("roof", "restrictions.{light|weather}", {since: 12, until: 14});
      return this.restrictions.light && this.restrictions.weather;
    }

    /* ---------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get z() {
      this.constructor._logDataFieldMigration("z", "sort", {since: 12, until: 14});
      return this.sort;
    }

    /* ---------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get overhead() {
      foundry.utils.logCompatibilityWarning(`${this.constructor.name}#overhead is deprecated.`, {since: 12, until: 14});
      return this.elevation >= this.parent?.foregroundElevation;
    }
  }

  /**
   * @typedef {import("./_types.mjs").TokenData} TokenData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Token Document.
   * Defines the DataSchema and common behaviors for a Token which are shared between both client and server.
   * @mixes TokenData
   */
  class BaseToken extends Document {
    /**
     * Construct a Token document using provided data and context.
     * @param {Partial<TokenData>} data               Initial data from which to construct the Token
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Token",
      collection: "tokens",
      label: "DOCUMENT.Token",
      labelPlural: "DOCUMENT.Tokens",
      isEmbedded: true,
      embedded: {
        ActorDelta: "delta"
      },
      permissions: {
        create: "TOKEN_CREATE",
        update: this.#canUpdate,
        delete: "TOKEN_DELETE"
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: true, textSearch: true}),
        displayName: new NumberField({required: true, initial: TOKEN_DISPLAY_MODES.NONE,
          choices: Object.values(TOKEN_DISPLAY_MODES),
          validationError: "must be a value in CONST.TOKEN_DISPLAY_MODES"
        }),
        actorId: new ForeignDocumentField(BaseActor, {idOnly: true}),
        actorLink: new BooleanField(),
        delta: new ActorDeltaField(BaseActorDelta),
        appendNumber: new BooleanField(),
        prependAdjective: new BooleanField(),
        width: new NumberField({nullable: false, positive: true, initial: 1, step: 0.5, label: "Width"}),
        height: new NumberField({nullable: false, positive: true, initial: 1, step: 0.5, label: "Height"}),
        texture: new TextureData({}, {initial: {src: () => this.DEFAULT_ICON, anchorX: 0.5, anchorY: 0.5, fit: "contain",
          alphaThreshold: 0.75}, wildcard: true}),
        hexagonalShape: new NumberField({initial: TOKEN_HEXAGONAL_SHAPES.ELLIPSE_1,
          choices: Object.values(TOKEN_HEXAGONAL_SHAPES)}),
        x: new NumberField({required: true, integer: true, nullable: false, initial: 0, label: "XCoord"}),
        y: new NumberField({required: true, integer: true, nullable: false, initial: 0, label: "YCoord"}),
        elevation: new NumberField({required: true, nullable: false, initial: 0}),
        sort: new NumberField({required: true, integer: true, nullable: false, initial: 0}),
        locked: new BooleanField(),
        lockRotation: new BooleanField(),
        rotation: new AngleField(),
        alpha: new AlphaField(),
        hidden: new BooleanField(),
        disposition: new NumberField({required: true, choices: Object.values(TOKEN_DISPOSITIONS),
          initial: TOKEN_DISPOSITIONS.HOSTILE,
          validationError: "must be a value in CONST.TOKEN_DISPOSITIONS"
        }),
        displayBars: new NumberField({required: true, choices: Object.values(TOKEN_DISPLAY_MODES),
          initial: TOKEN_DISPLAY_MODES.NONE,
          validationError: "must be a value in CONST.TOKEN_DISPLAY_MODES"
        }),
        bar1: new SchemaField({
          attribute: new StringField({required: true, nullable: true, blank: false,
            initial: () => game?.system.primaryTokenAttribute || null})
        }),
        bar2: new SchemaField({
          attribute: new StringField({required: true, nullable: true, blank: false,
            initial: () => game?.system.secondaryTokenAttribute || null})
        }),
        light: new EmbeddedDataField(LightData),
        sight: new SchemaField({
          enabled: new BooleanField({initial: data => Number(data?.sight?.range) > 0}),
          range: new NumberField({required: true, nullable: true, min: 0, step: 0.01, initial: 0}),
          angle: new AngleField({initial: 360, normalize: false}),
          visionMode: new StringField({required: true, blank: false, initial: "basic",
            label: "TOKEN.VisionMode", hint: "TOKEN.VisionModeHint"}),
          color: new ColorField({label: "TOKEN.VisionColor"}),
          attenuation: new AlphaField({initial: 0.1, label: "TOKEN.VisionAttenuation", hint: "TOKEN.VisionAttenuationHint"}),
          brightness: new NumberField({required: true, nullable: false, initial: 0, min: -1, max: 1,
            label: "TOKEN.VisionBrightness", hint: "TOKEN.VisionBrightnessHint"}),
          saturation: new NumberField({required: true, nullable: false, initial: 0, min: -1, max: 1,
            label: "TOKEN.VisionSaturation", hint: "TOKEN.VisionSaturationHint"}),
          contrast: new NumberField({required: true, nullable: false, initial: 0, min: -1, max: 1,
            label: "TOKEN.VisionContrast", hint: "TOKEN.VisionContrastHint"})
        }),
        detectionModes: new ArrayField(new SchemaField({
          id: new StringField(),
          enabled: new BooleanField({initial: true}),
          range: new NumberField({required: true, min: 0, step: 0.01})
        }), {
          validate: BaseToken.#validateDetectionModes
        }),
        occludable: new SchemaField({
          radius: new NumberField({nullable: false, min: 0, step: 0.01, initial: 0})
        }),
        ring: new SchemaField({
          enabled: new BooleanField(),
          colors: new SchemaField({
            ring: new ColorField(),
            background: new ColorField()
          }),
          effects: new NumberField({initial: 1, min: 0, max: 8388607, integer: true}),
          subject: new SchemaField({
            scale: new NumberField({initial: 1, min: 0.5}),
            texture: new FilePathField({categories: ["IMAGE"]})
          })
        }),
        /** @internal */
        _regions: new ArrayField(new ForeignDocumentField(BaseRegion, {idOnly: true})),
        flags: new ObjectField()
      }
    }

    /** @override */
    static LOCALIZATION_PREFIXES = ["TOKEN"];

    /* -------------------------------------------- */

    /**
     * Validate the structure of the detection modes array
     * @param {object[]} modes    Configured detection modes
     * @throws                    An error if the array is invalid
     */
    static #validateDetectionModes(modes) {
      const seen = new Set();
      for ( const mode of modes ) {
        if ( mode.id === "" ) continue;
        if ( seen.has(mode.id) ) {
          throw new Error(`may not have more than one configured detection mode of type "${mode.id}"`);
        }
        seen.add(mode.id);
      }
    }

    /* -------------------------------------------- */

    /**
     * The default icon used for newly created Token documents
     * @type {string}
     */
    static DEFAULT_ICON = DEFAULT_TOKEN;

    /**
     * Is a user able to update an existing Token?
     * @private
     */
    static #canUpdate(user, doc, data) {
      if ( user.isGM ) return true;                     // GM users can do anything
      if ( doc.actor ) {                                // You can update Tokens for Actors you control
        return doc.actor.canUserModify(user, "update", data);
      }
      return !!doc.actorId;                             // It would be good to harden this in the future
    }

    /** @override */
    testUserPermission(user, permission, {exact=false} = {}) {
      if ( this.actor ) return this.actor.testUserPermission(user, permission, {exact});
      else return super.testUserPermission(user, permission, {exact});
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    updateSource(changes={}, options={}) {
      const diff = super.updateSource(changes, options);

      // A copy of the source data is taken for the _backup in updateSource. When this backup is applied as part of a dry-
      // run, if a child singleton embedded document was updated, the reference to its source is broken. We restore it
      // here.
      if ( options.dryRun && ("delta" in changes) ) this._source.delta = this.delta._source;

      return diff;
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    toObject(source=true) {
      const obj = super.toObject(source);
      obj.delta = this.delta ? this.delta.toObject(source) : null;
      return obj;
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /** @inheritDoc */
    static migrateData(data) {

      // Remember that any migrations defined here may also be required for the PrototypeToken model.

      /**
       * Migration of actorData field to ActorDelta document.
       * @deprecated since v11
       */
      if ( ("actorData" in data) && !("delta" in data) ) {
        data.delta = data.actorData;
        if ( "_id" in data ) data.delta._id = data._id;
      }
      return super.migrateData(data);
    }

    /* ----------------------------------------- */

    /** @inheritdoc */
    static shimData(data, options) {

      // Remember that any shims defined here may also be required for the PrototypeToken model.

      this._addDataFieldShim(data, "actorData", "delta", {value: data.delta, since: 11, until: 13});
      this._addDataFieldShim(data, "effects", undefined, {value: [], since: 12, until: 14,
        warning: "TokenDocument#effects is deprecated in favor of using ActiveEffect"
          + " documents on the associated Actor"});
      this._addDataFieldShim(data, "overlayEffect", undefined, {value: "", since: 12, until: 14,
        warning: "TokenDocument#overlayEffect is deprecated in favor of using" +
          " ActiveEffect documents on the associated Actor"});
      return super.shimData(data, options);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get effects() {
      foundry.utils.logCompatibilityWarning("TokenDocument#effects is deprecated in favor of using ActiveEffect"
        + " documents on the associated Actor", {since: 12, until: 14, once: true});
      return [];
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    get overlayEffect() {
      foundry.utils.logCompatibilityWarning("TokenDocument#overlayEffect is deprecated in favor of using" +
        " ActiveEffect documents on the associated Actor", {since: 12, until: 14, once: true});
      return "";
    }
  }

  /* -------------------------------------------- */

  /**
   * A special subclass of EmbeddedDocumentField which allows construction of the ActorDelta to be lazily evaluated.
   */
  class ActorDeltaField extends EmbeddedDocumentField {
    /** @inheritdoc */
    initialize(value, model, options = {}) {
      if ( !value ) return value;
      const descriptor = Object.getOwnPropertyDescriptor(model, this.name);
      if ( (descriptor === undefined) || (!descriptor.get && !descriptor.value) ) {
        return () => {
          const m = new this.model(value, {...options, parent: model, parentCollection: this.name});
          Object.defineProperty(m, "schema", {value: this});
          Object.defineProperty(model, this.name, {
            value: m,
            configurable: true,
            writable: true
          });
          return m;
        };
      }
      else if ( descriptor.get instanceof Function ) return descriptor.get;
      model[this.name]._initialize(options);
      return model[this.name];
    }
  }

  /**
   * @typedef {import("./_types.mjs").UserData} UserData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The User Document.
   * Defines the DataSchema and common behaviors for a User which are shared between both client and server.
   * @mixes UserData
   */
  class BaseUser extends Document {
    /**
     * Construct a User document using provided data and context.
     * @param {Partial<UserData>} data                Initial data from which to construct the User
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "User",
      collection: "users",
      label: "DOCUMENT.User",
      labelPlural: "DOCUMENT.Users",
      permissions: {
        create: this.#canCreate,
        update: this.#canUpdate,
        delete: this.#canDelete
      },
      schemaVersion: "12.324",
    }, {inplace: false}));

    /** @override */
    static LOCALIZATION_PREFIXES = ["USER"];

    /* -------------------------------------------- */

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        name: new StringField({required: true, blank: false, textSearch: true}),
        role: new NumberField({required: true, choices: Object.values(USER_ROLES),
          initial: USER_ROLES.PLAYER, readonly: true}),
        password: new StringField({required: true, blank: true}),
        passwordSalt: new StringField(),
        avatar: new FilePathField({categories: ["IMAGE"]}),
        character: new ForeignDocumentField(BaseActor),
        color: new ColorField({required: true, nullable: false,
          initial: () => Color$1.fromHSV([Math.random(), 0.8, 0.8]).css
        }),
        pronouns: new StringField({required: true}),
        hotbar: new ObjectField({required: true, validate: BaseUser.#validateHotbar,
          validationError: "must be a mapping of slots to macro identifiers"}),
        permissions: new ObjectField({required: true, validate: BaseUser.#validatePermissions,
          validationError: "must be a mapping of permission names to booleans"}),
        flags: new ObjectField(),
        _stats: new DocumentStatsField()
      }
    }

    /* -------------------------------------------- */

    /**
     * Validate the structure of the User hotbar object
     * @param {object} bar      The attempted hotbar data
     * @return {boolean}
     * @private
     */
    static #validateHotbar(bar) {
      if ( typeof bar !== "object" ) return false;
      for ( let [k, v] of Object.entries(bar) ) {
        let slot = parseInt(k);
        if ( !slot || slot < 1 || slot > 50 ) return false;
        if ( !isValidId(v) ) return false;
      }
      return true;
    }

    /* -------------------------------------------- */

    /**
     * Validate the structure of the User permissions object
     * @param {object} perms      The attempted permissions data
     * @return {boolean}
     */
    static #validatePermissions(perms) {
      for ( let [k, v] of Object.entries(perms) ) {
        if ( typeof k !== "string" ) return false;
        if ( k.startsWith("-=") ) {
          if ( v !== null ) return false;
        } else {
          if ( typeof v !== "boolean" ) return false;
        }
      }
      return true;
    }

    /* -------------------------------------------- */
    /*  Model Properties                            */
    /* -------------------------------------------- */

    /**
     * A convenience test for whether this User has the NONE role.
     * @type {boolean}
     */
    get isBanned() {
      return this.role === USER_ROLES.NONE;

    }

    /* -------------------------------------------- */

    /**
     * Test whether the User has a GAMEMASTER or ASSISTANT role in this World?
     * @type {boolean}
     */
    get isGM() {
      return this.hasRole(USER_ROLES.ASSISTANT);
    }

    /* -------------------------------------------- */

    /**
     * Test whether the User is able to perform a certain permission action.
     * The provided permission string may pertain to an explicit permission setting or a named user role.
     *
     * @param {string} action         The action to test
     * @return {boolean}              Does the user have the ability to perform this action?
     */
    can(action) {
      if ( action in USER_PERMISSIONS ) return this.hasPermission(action);
      return this.hasRole(action);
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    getUserLevel(user) {
      return DOCUMENT_OWNERSHIP_LEVELS[user.id === this.id ? "OWNER" : "NONE"];
    }

    /* ---------------------------------------- */

    /**
     * Test whether the User has at least a specific permission
     * @param {string} permission    The permission name from USER_PERMISSIONS to test
     * @return {boolean}             Does the user have at least this permission
     */
    hasPermission(permission) {
      if ( this.isBanned ) return false;

      // CASE 1: The user has the permission set explicitly
      const explicit = this.permissions[permission];
      if (explicit !== undefined) return explicit;

      // CASE 2: Permission defined by the user's role
      const rolePerms = game.permissions[permission];
      return rolePerms ? rolePerms.includes(this.role) : false;
    }

    /* ----------------------------------------- */

    /**
     * Test whether the User has at least the permission level of a certain role
     * @param {string|number} role    The role name from USER_ROLES to test
     * @param {boolean} [exact]       Require the role match to be exact
     * @return {boolean}              Does the user have at this role level (or greater)?
     */
    hasRole(role, {exact = false} = {}) {
      const level = typeof role === "string" ? USER_ROLES[role] : role;
      if (level === undefined) return false;
      return exact ? this.role === level : this.role >= level;
    }

    /* ---------------------------------------- */
    /*  Model Permissions                       */
    /* ---------------------------------------- */

    /**
     * Is a user able to create an existing User?
     * @param {BaseUser} user    The user attempting the creation.
     * @param {BaseUser} doc     The User document being created.
     * @param {object} data      The supplied creation data.
     * @private
     */
    static #canCreate(user, doc, data) {
      if ( !user.isGM ) return false; // Only Assistants and above can create users.
      // Do not allow Assistants to create a new user with special permissions which might be greater than their own.
      if ( !isEmpty$1(doc.permissions) ) return user.hasRole(USER_ROLES.GAMEMASTER);
      return user.hasRole(doc.role);
    }

    /* -------------------------------------------- */

    /**
     * Is a user able to update an existing User?
     * @param {BaseUser} user    The user attempting the update.
     * @param {BaseUser} doc     The User document being updated.
     * @param {object} changes   Proposed changes.
     * @private
     */
    static #canUpdate(user, doc, changes) {
      const roles = USER_ROLES;
      if ( user.role === roles.GAMEMASTER ) return true; // Full GMs can do everything
      if ( user.role === roles.NONE ) return false; // Banned users can do nothing

      // Non-GMs cannot update certain fields.
      const restricted = ["permissions", "passwordSalt"];
      if ( user.role < roles.ASSISTANT ) restricted.push("name", "role");
      if ( doc.role === roles.GAMEMASTER ) restricted.push("password");
      if ( restricted.some(k => k in changes) ) return false;

      // Role changes may not escalate
      if ( ("role" in changes) && !user.hasRole(changes.role) ) return false;

      // Assistant GMs may modify other users. Players may only modify themselves
      return user.isGM || (user.id === doc.id);
    }

    /* -------------------------------------------- */

    /**
     * Is a user able to delete an existing User?
     * Only Assistants and Gamemasters can delete users, and only if the target user has a lesser or equal role.
     * @param {BaseUser} user   The user attempting the deletion.
     * @param {BaseUser} doc    The User document being deleted.
     * @private
     */
    static #canDelete(user, doc) {
      const role = Math.max(USER_ROLES.ASSISTANT, doc.role);
      return user.hasRole(role);
    }
  }

  /**
   * @typedef {import("./_types.mjs").WallData} WallData
   * @typedef {import("../types.mjs").DocumentConstructionContext} DocumentConstructionContext
   */

  /**
   * The Wall Document.
   * Defines the DataSchema and common behaviors for a Wall which are shared between both client and server.
   * @mixes WallData
   */
  class BaseWall extends Document {
    /**
     * Construct a Wall document using provided data and context.
     * @param {Partial<WallData>} data                Initial data from which to construct the Wall
     * @param {DocumentConstructionContext} context   Construction context options
     */
    constructor(data, context) {
      super(data, context);
    }

    /* -------------------------------------------- */
    /*  Model Configuration                         */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static metadata = Object.freeze(mergeObject(super.metadata, {
      name: "Wall",
      collection: "walls",
      label: "DOCUMENT.Wall",
      labelPlural: "DOCUMENT.Walls",
      permissions: {
        update: this.#canUpdate
      },
      schemaVersion: "12.324"
    }, {inplace: false}));

    /** @inheritdoc */
    static defineSchema() {
      return {
        _id: new DocumentIdField(),
        c: new ArrayField(new NumberField({required: true, integer: true, nullable: false}), {
          validate: c => (c.length === 4),
          validationError: "must be a length-4 array of integer coordinates"}),
        light: new NumberField({required: true, choices: Object.values(WALL_SENSE_TYPES),
          initial: WALL_SENSE_TYPES.NORMAL,
          validationError: "must be a value in CONST.WALL_SENSE_TYPES"}),
        move: new NumberField({required: true, choices: Object.values(WALL_MOVEMENT_TYPES),
          initial: WALL_MOVEMENT_TYPES.NORMAL,
          validationError: "must be a value in CONST.WALL_MOVEMENT_TYPES"}),
        sight: new NumberField({required: true, choices: Object.values(WALL_SENSE_TYPES),
          initial: WALL_SENSE_TYPES.NORMAL,
          validationError: "must be a value in CONST.WALL_SENSE_TYPES"}),
        sound: new NumberField({required: true, choices: Object.values(WALL_SENSE_TYPES),
          initial: WALL_SENSE_TYPES.NORMAL,
          validationError: "must be a value in CONST.WALL_SENSE_TYPES"}),
        dir: new NumberField({required: true, choices: Object.values(WALL_DIRECTIONS),
          initial: WALL_DIRECTIONS.BOTH,
          validationError: "must be a value in CONST.WALL_DIRECTIONS"}),
        door: new NumberField({required: true, choices: Object.values(WALL_DOOR_TYPES),
          initial: WALL_DOOR_TYPES.NONE,
          validationError: "must be a value in CONST.WALL_DOOR_TYPES"}),
        ds: new NumberField({required: true, choices: Object.values(WALL_DOOR_STATES),
          initial: WALL_DOOR_STATES.CLOSED,
          validationError: "must be a value in CONST.WALL_DOOR_STATES"}),
        doorSound: new StringField({required: false, blank: true, initial: undefined}),
        threshold: new SchemaField({
          light: new NumberField({required: true, nullable: true, initial: null, positive: true}),
          sight: new NumberField({required: true, nullable: true, initial: null, positive: true}),
          sound: new NumberField({required: true, nullable: true, initial: null, positive: true}),
          attenuation: new BooleanField()
        }),
        flags: new ObjectField()
      };
    }

    /**
     * Is a user able to update an existing Wall?
     * @private
     */
    static #canUpdate(user, doc, data) {
      if ( user.isGM ) return true;                     // GM users can do anything
      const dsOnly = Object.keys(data).every(k => ["_id", "ds"].includes(k));
      if ( dsOnly && (doc.ds !== WALL_DOOR_STATES.LOCKED) && (data.ds !== WALL_DOOR_STATES.LOCKED) ) {
        return user.hasRole("PLAYER");                  // Players may open and close unlocked doors
      }
      return false;
    }
  }

  /** @module foundry.documents */

  var documents = /*#__PURE__*/Object.freeze({
    __proto__: null,
    BaseActiveEffect: BaseActiveEffect,
    BaseActor: BaseActor,
    BaseActorDelta: BaseActorDelta,
    BaseAdventure: BaseAdventure,
    BaseAmbientLight: BaseAmbientLight,
    BaseAmbientSound: BaseAmbientSound,
    BaseCard: BaseCard,
    BaseCards: BaseCards,
    BaseChatMessage: BaseChatMessage,
    BaseCombat: BaseCombat,
    BaseCombatant: BaseCombatant,
    BaseDrawing: BaseDrawing,
    BaseFogExploration: BaseFogExploration,
    BaseFolder: BaseFolder,
    BaseItem: BaseItem,
    BaseJournalEntry: BaseJournalEntry,
    BaseJournalEntryPage: BaseJournalEntryPage,
    BaseMacro: BaseMacro,
    BaseMeasuredTemplate: BaseMeasuredTemplate,
    BaseNote: BaseNote,
    BasePlaylist: BasePlaylist,
    BasePlaylistSound: BasePlaylistSound,
    BaseRegion: BaseRegion,
    BaseRegionBehavior: BaseRegionBehavior,
    BaseRollTable: BaseRollTable,
    BaseScene: BaseScene,
    BaseSetting: BaseSetting,
    BaseTableResult: BaseTableResult,
    BaseTile: BaseTile,
    BaseToken: BaseToken,
    BaseUser: BaseUser,
    BaseWall: BaseWall
  });

  /**
   * A custom SchemaField for defining package compatibility versions.
   * @property {string} minimum     The Package will not function before this version
   * @property {string} verified    Verified compatible up to this version
   * @property {string} maximum     The Package will not function after this version
   */
  class PackageCompatibility extends SchemaField {
    constructor(options) {
      super({
        minimum: new StringField({required: false, blank: false, initial: undefined}),
        verified: new StringField({required: false, blank: false, initial: undefined}),
        maximum: new StringField({required: false, blank: false, initial: undefined})
      }, options);
    }
  }

  /* -------------------------------------------- */

  /**
   * A custom SchemaField for defining package relationships.
   * @property {RelatedPackage[]} systems     Systems that this Package supports
   * @property {RelatedPackage[]} requires    Packages that are required for base functionality
   * @property {RelatedPackage[]} recommends  Packages that are recommended for optimal functionality
   */
  class PackageRelationships extends SchemaField {
    /** @inheritdoc */
    constructor(options) {
      super({
        systems: new PackageRelationshipField(new RelatedPackage({packageType: "system"})),
        requires: new PackageRelationshipField(new RelatedPackage()),
        recommends: new PackageRelationshipField(new RelatedPackage()),
        conflicts: new PackageRelationshipField(new RelatedPackage()),
        flags: new ObjectField()
      }, options);
    }
  }

  /* -------------------------------------------- */

  /**
   * A SetField with custom casting behavior.
   */
  class PackageRelationshipField extends SetField {
    /** @override */
    _cast(value) {
      return value instanceof Array ? value : [value];
    }
  }

  /* -------------------------------------------- */

  /**
   * A custom SchemaField for defining a related Package.
   * It may be required to be a specific type of package, by passing the packageType option to the constructor.
   */
  class RelatedPackage extends SchemaField {
    constructor({packageType, ...options}={}) {
      let typeOptions = {choices: PACKAGE_TYPES, initial:"module"};
      if ( packageType ) typeOptions = {choices: [packageType], initial: packageType};
      super({
        id: new StringField({required: true, blank: false}),
        type: new StringField(typeOptions),
        manifest: new StringField({required: false, blank: false, initial: undefined}),
        compatibility: new PackageCompatibility(),
        reason: new StringField({required: false, blank: false, initial: undefined})
      }, options);
    }
  }

  /* -------------------------------------------- */

  /**
   * A custom SchemaField for defining the folder structure of the included compendium packs.
   */
  class PackageCompendiumFolder extends SchemaField {
    constructor({depth=1, ...options}={}) {
      const schema = {
        name: new StringField({required: true, blank: false}),
        sorting: new StringField({required: false, blank: false, initial: undefined,
          choices: BaseFolder.SORTING_MODES}),
        color: new ColorField(),
        packs: new SetField(new StringField({required: true, blank: false}))
      };
      if ( depth < 4 ) schema.folders = new SetField(new PackageCompendiumFolder(
        {depth: depth+1, options}));
      super(schema, options);
    }
  }

  /* -------------------------------------------- */

  /**
   * A special ObjectField which captures a mapping of USER_ROLES to DOCUMENT_OWNERSHIP_LEVELS.
   */
  class CompendiumOwnershipField extends ObjectField {

    /** @inheritdoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        initial: {PLAYER: "OBSERVER", ASSISTANT: "OWNER"},
        validationError: "is not a mapping of USER_ROLES to DOCUMENT_OWNERSHIP_LEVELS"
      });
    }

    /** @override */
    _validateType(value, options) {
      for ( let [k, v] of Object.entries(value) ) {
        if ( !(k in USER_ROLES) ) throw new Error(`Compendium ownership key "${k}" is not a valid choice in USER_ROLES`);
        if ( !(v in DOCUMENT_OWNERSHIP_LEVELS) ) throw new Error(`Compendium ownership value "${v}" is not a valid 
      choice in DOCUMENT_OWNERSHIP_LEVELS`);
      }
    }
  }

  /* -------------------------------------------- */

  /**
   * A special SetField which provides additional validation and initialization behavior specific to compendium packs.
   */
  class PackageCompendiumPacks extends SetField {

    /** @override */
    _cleanType(value, options) {
      return value.map(v => {
        v = this.element.clean(v, options);
        if ( v.path ) v.path = v.path.replace(/\.db$/, ""); // Strip old NEDB extensions
        else v.path = `packs/${v.name}`; // Auto-populate a default pack path
        return v;
      })
    }

    /* ---------------------------------------- */

    /** @override */
    initialize(value, model, options={}) {
      const packs = new Set();
      const packageName = model._source.id;
      for ( let v of value ) {
        try {
          const pack = this.element.initialize(v, model, options);
          pack.packageType = model.constructor.type;
          pack.packageName = packageName;
          pack.id = `${model.constructor.type === "world" ? "world" : packageName}.${pack.name}`;
          packs.add(pack);
        } catch(err) {
          logger.warn(err.message);
        }
      }
      return packs;
    }

    /* ---------------------------------------- */

    /**
     * Extend the logic for validating the complete set of packs to ensure uniqueness.
     * @inheritDoc
     */
    _validateElements(value, options) {
      const packNames = new Set();
      const duplicateNames = new Set();
      const packPaths = new Set();
      const duplicatePaths = new Set();
      for ( const pack of value ) {
        if ( packNames.has(pack.name) ) duplicateNames.add(pack.name);
        packNames.add(pack.name);
        if ( pack.path ) {
          if ( packPaths.has(pack.path) ) duplicatePaths.add(pack.path);
          packPaths.add(pack.path);
        }
      }
      return super._validateElements(value, {...options, duplicateNames, duplicatePaths});
    }

    /* ---------------------------------------- */

    /**
     * Validate each individual compendium pack, ensuring its name and path are unique.
     * @inheritDoc
     */
    _validateElement(value, {duplicateNames, duplicatePaths, ...options}={}) {
      if ( duplicateNames.has(value.name) ) {
        return new DataModelValidationFailure({
          invalidValue: value.name,
          message: `Duplicate Compendium name "${value.name}" already declared by some other pack`,
          unresolved: true
        });
      }
      if ( duplicatePaths.has(value.path) ) {
        return new DataModelValidationFailure({
          invalidValue: value.path,
          message: `Duplicate Compendium path "${value.path}" already declared by some other pack`,
          unresolved: true
        });
      }
      return this.element.validate(value, options);
    }
  }

  /* -------------------------------------------- */

  /**
   * The data schema used to define a Package manifest.
   * Specific types of packages extend this schema with additional fields.
   */
  class BasePackage extends DataModel {
    /**
     * @param {PackageManifestData} data  Source data for the package
     * @param {object} [options={}]       Options which affect DataModel construction
     */
    constructor(data, options={}) {
      const {availability, locked, exclusive, owned, tags, hasStorage} = data;
      super(data, options);

      /**
       * An availability code in PACKAGE_AVAILABILITY_CODES which defines whether this package can be used.
       * @type {number}
       */
      this.availability = availability ?? this.constructor.testAvailability(this);

      /**
       * A flag which tracks whether this package is currently locked.
       * @type {boolean}
       */
      this.locked = locked ?? false;

      /**
       * A flag which tracks whether this package is a free Exclusive pack
       * @type {boolean}
       */
      this.exclusive = exclusive ?? false;

      /**
       * A flag which tracks whether this package is owned, if it is protected.
       * @type {boolean|null}
       */
      this.owned = owned ?? false;

      /**
       * A set of Tags that indicate what kind of Package this is, provided by the Website
       * @type {string[]}
       */
      this.tags = tags ?? [];

      /**
       * A flag which tracks if this package has files stored in the persistent storage folder
       * @type {boolean}
       */
      this.hasStorage = hasStorage ?? false;
    }

    /**
     * Define the package type in CONST.PACKAGE_TYPES that this class represents.
     * Each BasePackage subclass must define this attribute.
     * @virtual
     * @type {string}
     */
    static type = "package";

    /**
     * The type of this package instance. A value in CONST.PACKAGE_TYPES.
     * @type {string}
     */
    get type() {
      return this.constructor.type;
    }

    /**
     * The canonical identifier for this package
     * @return {string}
     * @deprecated
     */
    get name() {
      logCompatibilityWarning("You are accessing BasePackage#name which is now deprecated in favor of id.",
        {since: 10, until: 13});
      return this.id;
    }

    /**
     * A flag which defines whether this package is unavailable to be used.
     * @type {boolean}
     */
    get unavailable() {
      return this.availability > PACKAGE_AVAILABILITY_CODES.UNVERIFIED_GENERATION;
    }

    /**
     * Is this Package incompatible with the currently installed core Foundry VTT software version?
     * @type {boolean}
     */
    get incompatibleWithCoreVersion() {
      return this.constructor.isIncompatibleWithCoreVersion(this.availability);
    }

    /**
     * Test if a given availability is incompatible with the core version.
     * @param {number} availability  The availability value to test.
     * @returns {boolean}
     */
    static isIncompatibleWithCoreVersion(availability) {
      const codes = CONST.PACKAGE_AVAILABILITY_CODES;
      return (availability >= codes.REQUIRES_CORE_DOWNGRADE) && (availability <= codes.REQUIRES_CORE_UPGRADE_UNSTABLE);
    }

    /**
     * The named collection to which this package type belongs
     * @type {string}
     */
    static get collection() {
      return `${this.type}s`;
    }

    /** @inheritDoc */
    static defineSchema() {
      const optionalString = {required: false, blank: false, initial: undefined};
      return {

        // Package metadata
        id: new StringField({required: true, blank: false, validate: this.validateId}),
        title: new StringField({required: true, blank: false}),
        description: new StringField({required: true}),
        authors: new SetField(new SchemaField({
          name: new StringField({required: true, blank: false}),
          email: new StringField(optionalString),
          url: new StringField(optionalString),
          discord: new StringField(optionalString),
          flags: new ObjectField(),
        })),
        url: new StringField(optionalString),
        license: new StringField(optionalString),
        readme: new StringField(optionalString),
        bugs: new StringField(optionalString),
        changelog: new StringField(optionalString),
        flags: new ObjectField(),
        media: new SetField(new SchemaField({
          type: new StringField(optionalString),
          url: new StringField(optionalString),
          caption: new StringField(optionalString),
          loop: new BooleanField({required: false, blank: false, initial: false}),
          thumbnail: new StringField(optionalString),
          flags: new ObjectField(),
        })),

        // Package versioning
        version: new StringField({required: true, blank: false, initial: "0"}),
        compatibility: new PackageCompatibility(),

        // Included content
        scripts: new SetField(new StringField({required: true, blank: false})),
        esmodules: new SetField(new StringField({required: true, blank: false})),
        styles: new SetField(new StringField({required: true, blank: false})),
        languages: new SetField(new SchemaField({
          lang: new StringField({required: true, blank: false, validate: Intl.getCanonicalLocales,
            validationError: "must be supported by the Intl.getCanonicalLocales function"
          }),
          name: new StringField({required: false}),
          path: new StringField({required: true, blank: false}),
          system: new StringField(optionalString),
          module: new StringField(optionalString),
          flags: new ObjectField(),
        })),
        packs: new PackageCompendiumPacks(new SchemaField({
          name: new StringField({required: true, blank: false, validate: this.validateId}),
          label: new StringField({required: true, blank: false}),
          banner: new StringField({...optionalString, nullable: true}),
          path: new StringField({required: false}),
          type: new StringField({required: true, blank: false, choices: COMPENDIUM_DOCUMENT_TYPES,
            validationError: "must be a value in CONST.COMPENDIUM_DOCUMENT_TYPES"}),
          system: new StringField(optionalString),
          ownership: new CompendiumOwnershipField(),
          flags: new ObjectField(),
        }, {validate: BasePackage.#validatePack})),
        packFolders: new SetField(new PackageCompendiumFolder()),

        // Package relationships
        relationships: new PackageRelationships(),
        socket: new BooleanField(),

        // Package downloading
        manifest: new StringField(),
        download: new StringField({required: false, blank: false, initial: undefined}),
        protected: new BooleanField(),
        exclusive: new BooleanField(),
        persistentStorage: new BooleanField(),
      }
    }

    /* -------------------------------------------- */

    /**
     * Check the given compatibility data against the current installation state and determine its availability.
     * @param {Partial<PackageManifestData>} data  The compatibility data to test.
     * @param {object} [options]
     * @param {ReleaseData} [options.release]      A specific software release for which to test availability.
     *                                             Tests against the current release by default.
     * @returns {number}
     */
    static testAvailability({ compatibility }, { release }={}) {
      release ??= globalThis.release ?? game.release;
      const codes = CONST.PACKAGE_AVAILABILITY_CODES;
      const {minimum, maximum, verified} = compatibility;
      const isGeneration = version => Number.isInteger(Number(version));

      // Require a certain minimum core version.
      if ( minimum && isNewerVersion(minimum, release.version) ) {
        const generation = Number(minimum.split(".").shift());
        const isStable = generation <= release.maxStableGeneration;
        return isStable ? codes.REQUIRES_CORE_UPGRADE_STABLE : codes.REQUIRES_CORE_UPGRADE_UNSTABLE;
      }

      // Require a certain maximum core version.
      if ( maximum ) {
        const compatible = isGeneration(maximum)
          ? release.generation <= Number(maximum)
          : !isNewerVersion(release.version, maximum);
        if ( !compatible ) return codes.REQUIRES_CORE_DOWNGRADE;
      }

      // Require a certain compatible core version.
      if ( verified ) {
        const compatible = isGeneration(verified)
          ? Number(verified) >= release.generation
          : !isNewerVersion(release.version, verified);
        const sameGeneration = release.generation === Number(verified.split(".").shift());
        if ( compatible ) return codes.VERIFIED;
        return sameGeneration ? codes.UNVERIFIED_BUILD : codes.UNVERIFIED_GENERATION;
      }

      // FIXME: Why do we not check if all of this package's dependencies are satisfied?
      // Proposal: Check all relationships.requires and set MISSING_DEPENDENCY if any dependencies are not VERIFIED,
      // UNVERIFIED_BUILD, or UNVERIFIED_GENERATION, or if they do not satisfy the given compatibility range for the
      // relationship.

      // No compatible version is specified.
      return codes.UNKNOWN;
    }

    /* -------------------------------------------- */

    /**
     * Test that the dependencies of a package are satisfied as compatible.
     * This method assumes that all packages in modulesCollection have already had their own availability tested.
     * @param {Collection<string,Module>} modulesCollection   A collection which defines the set of available modules
     * @returns {Promise<boolean>}                            Are all required dependencies satisfied?
     * @internal
     */
    async _testRequiredDependencies(modulesCollection) {
      const requirements = this.relationships.requires;
      for ( const {id, type, manifest, compatibility} of requirements ) {
        if ( type !== "module" ) continue; // Only test modules
        let pkg;

        // If the requirement specifies an explicit remote manifest URL, we need to load it
        if ( manifest ) {
          try {
            pkg = await this.constructor.fromRemoteManifest(manifest, {strict: true});
          } catch(err) {
            return false;
          }
        }

        // Otherwise the dependency must belong to the known modulesCollection
        else pkg = modulesCollection.get(id);
        if ( !pkg ) return false;

        // Ensure that the package matches the required compatibility range
        if ( !this.constructor.testDependencyCompatibility(compatibility, pkg) ) return false;

        // Test compatibility of the dependency
        if ( pkg.unavailable ) return false;
      }
      return true;
    }

    /* -------------------------------------------- */

    /**
     * Test compatibility of a package's supported systems.
     * @param {Collection<string, System>} systemCollection  A collection which defines the set of available systems.
     * @returns {Promise<boolean>}                           True if all supported systems which are currently installed
     *                                                       are compatible or if the package has no supported systems.
     *                                                       Returns false otherwise, or if no supported systems are
     *                                                       installed.
     * @internal
     */
    async _testSupportedSystems(systemCollection) {
      const systems = this.relationships.systems;
      if ( !systems?.size ) return true;
      let supportedSystem = false;
      for ( const { id, compatibility } of systems ) {
        const pkg = systemCollection.get(id);
        if ( !pkg ) continue;
        if ( !this.constructor.testDependencyCompatibility(compatibility, pkg) || pkg.unavailable ) return false;
        supportedSystem = true;
      }
      return supportedSystem;
    }

    /* -------------------------------------------- */

    /**
     * Determine if a dependency is within the given compatibility range.
     * @param {PackageCompatibility} compatibility      The compatibility range declared for the dependency, if any
     * @param {BasePackage} dependency                  The known dependency package
     * @returns {boolean}                               Is the dependency compatible with the required range?
     */
    static testDependencyCompatibility(compatibility, dependency) {
      if ( !compatibility ) return true;
      const {minimum, maximum} = compatibility;
      if ( minimum && isNewerVersion(minimum, dependency.version) ) return false;
      if ( maximum && isNewerVersion(dependency.version, maximum) ) return false;
      return true;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    static cleanData(source={}, { installed, ...options }={}) {

      // Auto-assign language name
      for ( let l of source.languages || [] ) {
        l.name = l.name ?? l.lang;
      }

      // Identify whether this package depends on a single game system
      let systemId = undefined;
      if ( this.type === "system" ) systemId = source.id;
      else if ( this.type === "world" ) systemId = source.system;
      else if ( source.relationships?.systems?.length === 1 ) systemId = source.relationships.systems[0].id;

      // Auto-configure some package data
      for ( const pack of source.packs || [] ) {
        if ( !pack.system && systemId ) pack.system = systemId; // System dependency
        if ( typeof pack.ownership === "string" ) pack.ownership = {PLAYER: pack.ownership};
      }

      /**
       * Clean unsupported non-module dependencies in requires or recommends.
       * @deprecated since v11
       */
      ["requires", "recommends"].forEach(rel => {
        const pkgs = source.relationships?.[rel];
        if ( !Array.isArray(pkgs) ) return;
        const clean = [];
        for ( const pkg of pkgs ) {
          if ( !pkg.type || (pkg.type === "module") ) clean.push(pkg);
        }
        const diff = pkgs.length - clean.length;
        if ( diff ) {
          source.relationships[rel] = clean;
          this._logWarning(
            source.id,
            `The ${this.type} "${source.id}" has a ${rel} relationship on a non-module, which is not supported.`,
            { since: 11, until: 13, stack: false, installed });
        }
      });
      return super.cleanData(source, options);
    }

    /* -------------------------------------------- */

    /**
     * Validate that a Package ID is allowed.
     * @param {string} id     The candidate ID
     * @throws                An error if the candidate ID is invalid
     */
    static validateId(id) {
      const allowed = /^[A-Za-z0-9-_]+$/;
      if ( !allowed.test(id) ) {
        throw new Error("Package and compendium pack IDs may only be alphanumeric with hyphens or underscores.");
      }
      const prohibited = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
      if ( prohibited.test(id) ) throw new Error(`The ID "${id}" uses an operating system prohibited value.`);
    }

    /* -------------------------------------------- */

    /**
     * Validate a single compendium pack object
     * @param {PackageCompendiumData} packData  Candidate compendium packs data
     * @throws                                  An error if the data is invalid
     */
    static #validatePack(packData) {
      if ( SYSTEM_SPECIFIC_COMPENDIUM_TYPES.includes(packData.type) && !packData.system ) {
        throw new Error(`The Compendium pack "${packData.name}" of the "${packData.type}" type must declare the "system"`
        + " upon which it depends.");
      }
    }

    /* -------------------------------------------- */

    /**
     * A wrapper around the default compatibility warning logger which handles some package-specific interactions.
     * @param {string} packageId            The package ID being logged
     * @param {string} message              The warning or error being logged
     * @param {object} options              Logging options passed to foundry.utils.logCompatibilityWarning
     * @param {object} [options.installed]  Is the package installed?
     * @internal
     */
    static _logWarning(packageId, message, { installed, ...options }={}) {
      logCompatibilityWarning(message, options);
      if ( installed ) globalThis.packages?.warnings?.add(packageId, {type: this.type, level: "warning", message});
    }

    /* -------------------------------------------- */

    /**
     * A set of package manifest keys that are migrated.
     * @type {Set<string>}
     */
    static migratedKeys = new Set([
      /** @deprecated since 10 until 13 */
      "name", "dependencies", "minimumCoreVersion", "compatibleCoreVersion"
    ]);

    /* -------------------------------------------- */

    /** @inheritdoc */
    static migrateData(data, { installed }={}) {
      this._migrateNameToId(data, {since: 10, until: 13, stack: false, installed});
      this._migrateDependenciesNameToId(data, {since: 10, until: 13, stack: false, installed});
      this._migrateToRelationships(data, {since: 10, until: 13, stack: false, installed});
      this._migrateCompatibility(data, {since: 10, until: 13, stack: false, installed});
      this._migrateMediaURL(data, {since: 11, until: 13, stack: false, installed});
      this._migrateOwnership(data, {since: 11, until: 13, stack: false, installed});
      this._migratePackIDs(data, {since: 12, until: 14, stack: false, installed});
      this._migratePackEntityToType(data, {since: 9, stack: false, installed});
      return super.migrateData(data);
    }

    /* -------------------------------------------- */

    /** @internal */
    static _migrateNameToId(data, logOptions) {
      if ( data.name && !data.id ) {
        data.id = data.name;
        delete data.name;
        if ( this.type !== "world" ) {
          const warning = `The ${this.type} "${data.id}" is using "name" which is deprecated in favor of "id"`;
          this._logWarning(data.id, warning, logOptions);
        }
      }
    }

    /* -------------------------------------------- */

    /** @internal */
    static _migrateDependenciesNameToId(data, logOptions) {
      if ( data.relationships ) return;
      if ( data.dependencies ) {
        let hasDependencyName = false;
        for ( const dependency of data.dependencies ) {
          if ( dependency.name && !dependency.id ) {
            hasDependencyName = true;
            dependency.id = dependency.name;
            delete dependency.name;
          }
        }
        if ( hasDependencyName ) {
          const msg = `The ${this.type} "${data.id}" contains dependencies using "name" which is deprecated in favor of "id"`;
          this._logWarning(data.id, msg, logOptions);
        }
      }
    }

    /* -------------------------------------------- */

    /** @internal */
    static _migrateToRelationships(data, logOptions) {
      if ( data.relationships ) return;
      data.relationships = {
        requires: [],
        systems: []
      };

      // Dependencies -> Relationships.Requires
      if ( data.dependencies ) {
        for ( const d of data.dependencies ) {
          const relationship = {
            "id": d.id,
            "type": d.type,
            "manifest": d.manifest,
            "compatibility": {
              "compatible": d.version
            }
          };
          d.type === "system" ? data.relationships.systems.push(relationship) : data.relationships.requires.push(relationship);
        }
        const msg = `The ${this.type} "${data.id}" contains "dependencies" which is deprecated in favor of "relationships.requires"`;
        this._logWarning(data.id, msg, logOptions);
        delete data.dependencies;
      }

      // V9: system -> relationships.systems
      else if ( data.system && (this.type === "module") ) {
        data.system = data.system instanceof Array ? data.system : [data.system];
        const newSystems = data.system.map(id => ({id})).filter(s => !data.relationships.systems.find(x => x.id === s.id));
        data.relationships.systems = data.relationships.systems.concat(newSystems);
        const msg = `${this.type} "${data.id}" contains "system" which is deprecated in favor of "relationships.systems"`;
        this._logWarning(data.id, msg, logOptions);
        delete data.system;
      }
    }

    /* -------------------------------------------- */

    /** @internal */
    static _migrateCompatibility(data, logOptions) {
      if ( !data.compatibility && (data.minimumCoreVersion || data.compatibleCoreVersion) ) {
        this._logWarning(data.id, `The ${this.type} "${data.id}" is using the old flat core compatibility fields which `
          + `are deprecated in favor of the new "compatibility" object`,
          logOptions);
        data.compatibility = {
          minimum: data.minimumCoreVersion,
          verified: data.compatibleCoreVersion
        };
        delete data.minimumCoreVersion;
        delete data.compatibleCoreVersion;
      }
    }

    /* -------------------------------------------- */

    /** @internal */
    static _migrateMediaURL(data, logOptions) {
      if ( !data.media ) return;
      let hasMediaLink = false;
      for ( const media of data.media ) {
        if ( "link" in media ) {
          hasMediaLink = true;
          media.url = media.link;
          delete media.link;
        }
      }
      if ( hasMediaLink ) {
        const msg = `${this.type} "${data.id}" declares media.link which is unsupported, media.url should be used`;
        this._logWarning(data.id, msg, logOptions);
      }
    }

    /* -------------------------------------------- */

    /** @internal */
    static _migrateOwnership(data, logOptions) {
      if ( !data.packs ) return;
      let hasPrivatePack = false;
      for ( const pack of data.packs ) {
        if ( pack.private && !("ownership" in pack) ) {
          pack.ownership = {PLAYER: "LIMITED", ASSISTANT: "OWNER"};
          hasPrivatePack = true;
        }
        delete pack.private;
      }
      if ( hasPrivatePack ) {
        const msg = `${this.type} "${data.id}" uses pack.private which has been replaced with pack.ownership`;
        this._logWarning(data.id, msg, logOptions);
      }
      return data;
    }

    /* -------------------------------------------- */

    /** @internal */
    static _migratePackIDs(data, logOptions) {
      if ( !data.packs ) return;
      for ( const pack of data.packs ) {
        const slugified = pack.name.replace(/[^A-Za-z0-9-_]/g, "");
        if ( pack.name !== slugified ) {
          const msg = `The ${this.type} "${data.id}" contains a pack with an invalid name "${pack.name}". `
            + "Pack names containing any character that is non-alphanumeric or an underscore will cease loading in "
            + "version 14 of the software.";
          pack.name = slugified;
          this._logWarning(data.id, msg, logOptions);
        }
      }
    }

    /* -------------------------------------------- */

    /** @internal */
    static _migratePackEntityToType(data, logOptions) {
      if ( !data.packs ) return;
      let hasPackEntity = false;
      for ( const pack of data.packs ) {
        if ( ("entity" in pack) && !("type" in pack) ) {
          pack.type = pack.entity;
          hasPackEntity = true;
        }
        delete pack.entity;
      }
      if ( hasPackEntity ) {
        const msg = `${this.type} "${data.id}" uses pack.entity which has been replaced with pack.type`;
        this._logWarning(data.id, msg, logOptions);
      }
    }

    /* -------------------------------------------- */

    /**
     * Retrieve the latest Package manifest from a provided remote location.
     * @param {string} manifestUrl        A remote manifest URL to load
     * @param {object} options            Additional options which affect package construction
     * @param {boolean} [options.strict=true]   Whether to construct the remote package strictly
     * @return {Promise<ServerPackage>}   A Promise which resolves to a constructed ServerPackage instance
     * @throws                            An error if the retrieved manifest data is invalid
     */
    static async fromRemoteManifest(manifestUrl, {strict=true}={}) {
      throw new Error("Not implemented");
    }
  }

  /**
   * The data schema used to define World manifest files.
   * Extends the basic PackageData schema with some additional world-specific fields.
   * @property {string} system            The game system name which this world relies upon
   * @property {string} coreVersion       The version of the core software for which this world has been migrated
   * @property {string} systemVersion     The version of the game system for which this world has been migrated
   * @property {string} [background]      A web URL or local file path which provides a background banner image
   * @property {string} [nextSession]     An ISO datetime string when the next game session is scheduled to occur
   * @property {boolean} [resetKeys]      Should user access keys be reset as part of the next launch?
   * @property {boolean} [safeMode]       Should the world launch in safe mode?
   * @property {string} [joinTheme]       The theme to use for this world's join page.
   */
  class BaseWorld extends BasePackage {

    /** @inheritDoc */
    static defineSchema() {
      return Object.assign({}, super.defineSchema(), {
        system: new StringField({required: true, blank: false}),
        background: new StringField({required: false, blank: false}),
        joinTheme: new StringField({
          required: false, initial: undefined, nullable: false, blank: false, choices: WORLD_JOIN_THEMES
        }),
        coreVersion: new StringField({required: true, blank: false}),
        systemVersion: new StringField({required: true, blank: false, initial: "0"}),
        lastPlayed: new StringField(),
        playtime: new NumberField({integer: true, min: 0, initial: 0}),
        nextSession: new StringField({blank: false, nullable: true, initial: null}),
        resetKeys: new BooleanField({required: false, initial: undefined}),
        safeMode: new BooleanField({required: false, initial: undefined}),
        version: new StringField({required: true, blank: false, nullable: true, initial: null})
      });
    }

    /** @override */
    static type = "world";

    /**
     * The default icon used for this type of Package.
     * @type {string}
     */
    static icon = "fa-globe-asia";

    /** @inheritDoc */
    static migrateData(data) {
      super.migrateData(data);

      // Legacy compatibility strings
      data.compatibility = data.compatibility || {};
      if ( data.compatibility.maximum === "1.0.0" ) data.compatibility.maximum = undefined;
      if ( data.coreVersion && !data.compatibility.verified ) {
        data.compatibility.minimum = data.compatibility.verified = data.coreVersion;
      }
      return data;
    }

    /* -------------------------------------------- */

    /**
     * Check the given compatibility data against the current installation state and determine its availability.
     * @param {Partial<PackageManifestData>} data  The compatibility data to test.
     * @param {object} [options]
     * @param {ReleaseData} [options.release]      A specific software release for which to test availability.
     *                                             Tests against the current release by default.
     * @param {Collection<string, Module>} [options.modules]  A specific collection of modules to test availability
     *                                                        against. Tests against the currently installed modules by
     *                                                        default.
     * @param {Collection<string, System>} [options.systems]  A specific collection of systems to test availability
     *                                                        against. Tests against the currently installed systems by
     *                                                        default.
     * @param {number} [options.systemAvailabilityThreshold]  Ignore the world's own core software compatibility and
     *                                                        instead defer entirely to the system's core software
     *                                                        compatibility, if the world's availability is less than
     *                                                        this.
     * @returns {number}
     */
    static testAvailability(data, { release, modules, systems, systemAvailabilityThreshold }={}) {
      systems ??= globalThis.packages?.System ?? game.systems;
      modules ??= globalThis.packages?.Module ?? game.modules;
      const { relationships } = data;
      const codes = CONST.PACKAGE_AVAILABILITY_CODES;
      systemAvailabilityThreshold ??= codes.UNKNOWN;

      // If the World itself is incompatible for some reason, report that directly.
      const wa = super.testAvailability(data, { release });
      if ( this.isIncompatibleWithCoreVersion(wa) ) return wa;

      // If the System is missing or incompatible, report that directly.
      const system = data.system instanceof foundry.packages.BaseSystem ? data.system : systems.get(data.system);
      if ( !system ) return codes.MISSING_SYSTEM;
      const sa = system.availability;
      // FIXME: Why do we only check if the system is incompatible with the core version or UNKNOWN?
      // Proposal: If the system is anything but VERIFIED, UNVERIFIED_BUILD, or UNVERIFIED_GENERATION, we should return
      // the system availability.
      if ( system.incompatibleWithCoreVersion || (sa === codes.UNKNOWN) ) return sa;

      // Test the availability of all required modules.
      const checkedModules = new Set();
      // TODO: We do not need to check system requirements here if the above proposal is implemented.
      const requirements = [...relationships.requires.values(), ...system.relationships.requires.values()];
      for ( const r of requirements ) {
        if ( (r.type !== "module") || checkedModules.has(r.id) ) continue;
        const module = modules.get(r.id);
        if ( !module ) return codes.MISSING_DEPENDENCY;
        // FIXME: Why do we only check if the module is incompatible with the core version?
        // Proposal: We should check the actual compatibility information for the relationship to ensure that the module
        // satisfies it.
        if ( module.incompatibleWithCoreVersion ) return codes.REQUIRES_DEPENDENCY_UPDATE;
        checkedModules.add(r.id);
      }

      // Inherit from the System availability in certain cases.
      if ( wa <= systemAvailabilityThreshold ) return sa;
      return wa;
    }
  }

  /**
   * @typedef {Record<string, Record<string, object>>} DocumentTypesConfiguration
   */

  /**
   * A special [ObjectField]{@link ObjectField} available to packages which configures any additional Document subtypes
   * provided by the package.
   */
  class AdditionalTypesField extends ObjectField {

    /** @inheritDoc */
    static get _defaults() {
      return mergeObject(super._defaults, {
        readonly: true,
        validationError: "is not a valid sub-types configuration"
      });
    }

    /* ----------------------------------------- */

    /** @inheritDoc */
    _validateType(value, options={}) {
      super._validateType(value, options);
      for ( const [documentName, subtypes] of Object.entries(value) ) {
        const cls = getDocumentClass(documentName);
        if ( !cls ) throw new Error(`${this.validationError}: '${documentName}' is not a valid Document type`);
        if ( !cls.hasTypeData ) {
          throw new Error(`${this.validationError}: ${documentName} Documents do not support sub-types`);
        }
        if ( getType(subtypes) !== "Object" ) throw new Error(`Malformed ${documentName} documentTypes declaration`);
        for ( const [type, config] of Object.entries(subtypes) ) this.#validateSubtype(cls, type, config);
      }
    }

    /* ----------------------------------------- */

    /**
     * Validate a single defined document subtype.
     * @param {typeof Document} documentClass       The document for which the subtype is being registered
     * @param {string} type                         The requested subtype name
     * @param {object} config                       The provided subtype configuration
     * @throws {Error}                              An error if the subtype is invalid or malformed
     */
    #validateSubtype(documentClass, type, config) {
      const dn = documentClass.documentName;
      if ( documentClass.metadata.coreTypes.includes(type) ) {
        throw new Error(`"${type}" is a reserved core type for the ${dn} document`);
      }
      if ( getType(config) !== "Object" ) {
        throw new Error(`Malformed "${type}" subtype declared for ${dn} documentTypes`);
      }
    }
  }

  /**
   * @typedef {import("./sub-types.mjs").DocumentTypesConfiguration} DocumentTypesConfiguration
   */

  /**
   * The data schema used to define System manifest files.
   * Extends the basic PackageData schema with some additional system-specific fields.
   * @property {DocumentTypesConfiguration} [documentTypes]  Additional document subtypes provided by this system.
   * @property {string} [background]        A web URL or local file path which provides a default background banner for
   *                                        worlds which are created using this system
   * @property {string} [initiative]        A default initiative formula used for this system
   * @property {number} [grid]              The default grid settings to use for Scenes in this system
   * @property {number} [grid.type]         A default grid type to use for Scenes in this system
   * @property {number} [grid.distance]     A default distance measurement to use for Scenes in this system
   * @property {string} [grid.units]        A default unit of measure to use for distance measurement in this system
   * @property {number} [grid.diagonals]    The default rule used by this system for diagonal measurement on square grids
   * @property {string} [primaryTokenAttribute] An Actor data attribute path to use for Token primary resource bars
   * @property {string} [secondaryTokenAttribute] An Actor data attribute path to use for Token secondary resource bars
   */
  class BaseSystem extends BasePackage {

    /** @inheritDoc */
    static defineSchema() {
      return Object.assign({}, super.defineSchema(), {
        documentTypes: new AdditionalTypesField(),
        background: new StringField({required: false, blank: false}),
        initiative: new StringField(),
        grid: new SchemaField({
          type: new NumberField({required: true, choices: Object.values(CONST.GRID_TYPES),
            initial: CONST.GRID_TYPES.SQUARE, validationError: "must be a value in CONST.GRID_TYPES"}),
          distance: new NumberField({required: true, nullable: false, positive: true, initial: 1}),
          units: new StringField({required: true}),
          diagonals: new NumberField({required: true, choices: Object.values(CONST.GRID_DIAGONALS),
            initial: CONST.GRID_DIAGONALS.EQUIDISTANT, validationError: "must be a value in CONST.GRID_DIAGONALS"}),
        }),
        primaryTokenAttribute: new StringField(),
        secondaryTokenAttribute: new StringField()
      });
    }

    /** @inheritdoc */
    static type = "system";

    /**
     * The default icon used for this type of Package.
     * @type {string}
     */
    static icon = "fa-dice";

    /**
     * Does the system template request strict type checking of data compared to template.json inferred types.
     * @type {boolean}
     */
    strictDataCleaning = false;

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * Static initializer block for deprecated properties.
     */
    static {
      /**
       * Shim grid distance and units.
       * @deprecated since v12
       */
      Object.defineProperties(this.prototype, Object.fromEntries(
        Object.entries({
          gridDistance: "grid.distance",
          gridUnits: "grid.units"
        }).map(([o, n]) => [o, {
          get() {
            const msg = `You are accessing BasePackage#${o} which has been migrated to BasePackage#${n}.`;
            foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
            return foundry.utils.getProperty(this, n);
          },
          set(v) {
            const msg = `You are accessing BasePackage#${o} which has been migrated to BasePackage#${n}.`;
            foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
            return foundry.utils.setProperty(this, n, v);
          },
          configurable: true
        }])
      ));
    }

    /* -------------------------------------------- */

    /** @override */
    static migratedKeys = (function() {
      return BasePackage.migratedKeys.union(new Set([
        /** @deprecated since 12 until 14 */
        "gridDistance", "gridUnits"
      ]));
    })();

    /* ---------------------------------------- */

    /** @inheritdoc */
    static migrateData(data, options) {
      /**
       * Migrate grid distance and units.
       * @deprecated since v12
       */
      for ( const [oldKey, [newKey, apply]] of Object.entries({
        gridDistance: ["grid.distance", d => Math.max(d.gridDistance || 0, 1)],
        gridUnits: ["grid.units", d => d.gridUnits || ""]
      })) {
        if ( (oldKey in data) && !foundry.utils.hasProperty(data, newKey) ) {
          foundry.utils.setProperty(data, newKey, apply(data));
          delete data[oldKey];
          const warning = `The ${this.type} "${data.id}" is using "${oldKey}" which is deprecated in favor of "${newKey}".`;
          this._logWarning(data.id, warning, {since: 12, until: 14, stack: false, installed: options.installed});
        }
      }
      return super.migrateData(data, options);
    }

    /* ---------------------------------------- */

    /** @inheritdoc */
    static shimData(data, options) {
      /**
       * Shim grid distance and units.
       * @deprecated since v12
       */
      for ( const [oldKey, newKey] of Object.entries({
        gridDistance: "grid.distance",
        gridUnits: "grid.units"
      })) {
        if ( !data.hasOwnProperty(oldKey) && foundry.utils.hasProperty(data, newKey) ) {
          Object.defineProperty(data, oldKey, {
            get: () => {
              const msg = `You are accessing BasePackage#${oldKey} which has been migrated to BasePackage#${newKey}.`;
              foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
              return foundry.utils.getProperty(data, newKey);
            },
            set: value => foundry.utils.setProperty(data, newKey, value),
            configurable: true
          });
        }
      }
      return super.shimData(data, options);
    }
  }

  /**
   * The data schema used to define Module manifest files.
   * Extends the basic PackageData schema with some additional module-specific fields.
   * @property {boolean} [coreTranslation]         Does this module provide a translation for the core software?
   * @property {boolean} [library]                 A library module provides no user-facing functionality and is solely
   *                                               for use by other modules. Loaded before any system or module scripts.
   * @property {Record<string, string[]>} [documentTypes]  Additional document subtypes provided by this module.
   */
  class BaseModule extends BasePackage {

    /** @inheritDoc */
    static defineSchema() {
      const parentSchema = super.defineSchema();
      return Object.assign({}, parentSchema, {
        coreTranslation: new BooleanField(),
        library: new BooleanField(),
        documentTypes: new AdditionalTypesField()
      });
    }

    /** @override */
    static type = "module";

    /**
     * The default icon used for this type of Package.
     * @type {string}
     */
    static icon = "fa-plug";
  }

  /** @module packages */


  /* ---------------------------------------- */
  /*  Type Definitions                        */
  /* ---------------------------------------- */

  /**
   * @typedef {Object} PackageAuthorData
   * @property {string} name        The author name
   * @property {string} [email]     The author email address
   * @property {string} [url]       A website url for the author
   * @property {string} [discord]   A Discord username for the author
   */

  /**
   * @typedef {Object} PackageCompendiumData
   * @property {string} name        The canonical compendium name. This should contain no spaces or special characters
   * @property {string} label       The human-readable compendium name
   * @property {string} path        The local relative path to the compendium source directory. The filename should match
   *                                the name attribute
   * @property {string} type        The specific document type that is contained within this compendium pack
   * @property {string} [system]    Denote that this compendium pack requires a specific game system to function properly
   */

  /**
   * @typedef {Object} PackageLanguageData
   * @property {string} lang        A string language code which is validated by Intl.getCanonicalLocales
   * @property {string} name        The human-readable language name
   * @property {string} path        The relative path to included JSON translation strings
   * @property {string} [system]    Only apply this set of translations when a specific system is being used
   * @property {string} [module]    Only apply this set of translations when a specific module is active
   */

  /**
   * @typedef {Object} RelatedPackage
   * @property {string} id                              The id of the related package
   * @property {string} type                            The type of the related package
   * @property {string} [manifest]                      An explicit manifest URL, otherwise learned from the Foundry web server
   * @property {PackageCompatibility} [compatibility]   The compatibility data with this related Package
   * @property {string} [reason]                        The reason for this relationship
   */

  /**
   * @typedef {Object} PackageManifestData
   * The data structure of a package manifest. This data structure is extended by BasePackage subclasses to add additional
   * type-specific fields.
   * [[include:full-manifest.md]]
   *
   * @property {string} id              The machine-readable unique package id, should be lower-case with no spaces or special characters
   * @property {string} title           The human-readable package title, containing spaces and special characters
   * @property {string} [description]   An optional package description, may contain HTML
   * @property {PackageAuthorData[]} [authors]  An array of author objects who are co-authors of this package. Preferred to the singular author field.
   * @property {string} [url]           A web url where more details about the package may be found
   * @property {string} [license]       A web url or relative file path where license details may be found
   * @property {string} [readme]        A web url or relative file path where readme instructions may be found
   * @property {string} [bugs]          A web url where bug reports may be submitted and tracked
   * @property {string} [changelog]     A web url where notes detailing package updates are available
   * @property {string} version         The current package version
   * @property {PackageCompatibility} [compatibility]  The compatibility of this version with the core Foundry software
   * @property {string[]} [scripts]     An array of urls or relative file paths for JavaScript files which should be included
   * @property {string[]} [esmodules]   An array of urls or relative file paths for ESModule files which should be included
   * @property {string[]} [styles]      An array of urls or relative file paths for CSS stylesheet files which should be included
   * @property {PackageLanguageData[]} [languages]  An array of language data objects which are included by this package
   * @property {PackageCompendiumData[]} [packs] An array of compendium packs which are included by this package
   * @property {PackageRelationships} [relationships] An organized object of relationships to other Packages
   * @property {boolean} [socket]       Whether to require a package-specific socket namespace for this package
   * @property {string} [manifest]      A publicly accessible web URL which provides the latest available package manifest file. Required in order to support module updates.
   * @property {string} [download]      A publicly accessible web URL where the source files for this package may be downloaded. Required in order to support module installation.
   * @property {boolean} [protected=false] Whether this package uses the protected content access system.
   */

  var packages = /*#__PURE__*/Object.freeze({
    __proto__: null,
    BaseModule: BaseModule,
    BasePackage: BasePackage,
    BaseSystem: BaseSystem,
    BaseWorld: BaseWorld,
    PackageCompatibility: PackageCompatibility,
    RelatedPackage: RelatedPackage
  });

  /** @namespace config */

  /**
   * A data model definition which describes the application configuration options.
   * These options are persisted in the user data Config folder in the options.json file.
   * The server-side software extends this class and provides additional validations and
   * @extends {DataModel}
   * @memberof config
   *
   * @property {string|null} adminPassword        The server administrator password (obscured)
   * @property {string|null} awsConfig            The relative path (to Config) of an AWS configuration file
   * @property {boolean} compressStatic           Whether to compress static files? True by default
   * @property {string} dataPath                  The absolute path of the user data directory (obscured)
   * @property {boolean} fullscreen               Whether the application should automatically start in fullscreen mode?
   * @property {string|null} hostname             A custom hostname applied to internet invitation addresses and URLs
   * @property {string} language                  The default language for the application
   * @property {string|null} localHostname        A custom hostname applied to local invitation addresses
   * @property {string|null} passwordSalt         A custom salt used for hashing user passwords (obscured)
   * @property {number} port                      The port on which the server is listening
   * @property {number} [protocol]                The Internet Protocol version to use, either 4 or 6.
   * @property {number} proxyPort                 An external-facing proxied port used for invitation addresses and URLs
   * @property {boolean} proxySSL                 Is the application running in SSL mode at a reverse-proxy level?
   * @property {string|null} routePrefix          A URL path part which prefixes normal application routing
   * @property {string|null} sslCert              The relative path (to Config) of a used SSL certificate
   * @property {string|null} sslKey               The relative path (to Config) of a used SSL key
   * @property {string} updateChannel             The current application update channel
   * @property {boolean} upnp                     Is UPNP activated?
   * @property {number} upnpLeaseDuration         The duration in seconds of a UPNP lease, if UPNP is active
   * @property {string} world                     A default world name which starts automatically on launch
   */
  class ApplicationConfiguration extends DataModel {
    static defineSchema() {
      return {
        adminPassword: new StringField({required: true, blank: false, nullable: true, initial: null,
          label: "SETUP.AdminPasswordLabel", hint: "SETUP.AdminPasswordHint"}),
        awsConfig: new StringField({label: "SETUP.AWSLabel", hint: "SETUP.AWSHint", blank: false, nullable: true,
          initial: null}),
        compressStatic: new BooleanField({initial: true, label: "SETUP.CompressStaticLabel",
          hint: "SETUP.CompressStaticHint"}),
        compressSocket: new BooleanField({initial: true, label: "SETUP.CompressSocketLabel",
          hint: "SETUP.CompressSocketHint"}),
        cssTheme: new StringField({blank: false, choices: CSS_THEMES, initial: "foundry",
          label: "SETUP.CSSTheme", hint: "SETUP.CSSThemeHint"}),
        dataPath: new StringField({label: "SETUP.DataPathLabel", hint: "SETUP.DataPathHint"}),
        deleteNEDB: new BooleanField({label: "SETUP.DeleteNEDBLabel", hint: "SETUP.DeleteNEDBHint"}),
        fullscreen: new BooleanField({initial: false}),
        hostname: new StringField({required: true, blank: false, nullable: true, initial: null}),
        hotReload: new BooleanField({initial: false, label: "SETUP.HotReloadLabel", hint: "SETUP.HotReloadHint"}),
        language: new StringField({required: true, blank: false, initial: "en.core",
          label: "SETUP.DefaultLanguageLabel", hint: "SETUP.DefaultLanguageHint"}),
        localHostname: new StringField({required: true, blank: false, nullable: true, initial: null}),
        passwordSalt: new StringField({required: true, blank: false, nullable: true, initial: null}),
        port: new NumberField({required: true, nullable: false, integer: true, initial: 30000,
          validate: this._validatePort, label: "SETUP.PortLabel", hint: "SETUP.PortHint"}),
        protocol: new NumberField({integer: true, choices: [4, 6], nullable: true}),
        proxyPort: new NumberField({required: true, nullable: true, integer: true, initial: null}),
        proxySSL: new BooleanField({initial: false}),
        routePrefix: new StringField({required: true, blank: false, nullable: true, initial: null}),
        sslCert: new StringField({label: "SETUP.SSLCertLabel", hint: "SETUP.SSLCertHint", blank: false,
          nullable: true, initial: null}),
        sslKey: new StringField({label: "SETUP.SSLKeyLabel", blank: false, nullable: true, initial: null}),
        telemetry: new BooleanField({required: false, initial: undefined, label: "SETUP.Telemetry",
          hint: "SETUP.TelemetryHint"}),
        updateChannel: new StringField({required: true, choices: SOFTWARE_UPDATE_CHANNELS, initial: "stable"}),
        upnp: new BooleanField({initial: true}),
        upnpLeaseDuration: new NumberField(),
        world: new StringField({required: true, blank: false, nullable: true, initial: null,
          label: "SETUP.WorldLabel", hint: "SETUP.WorldHint"}),
        noBackups: new BooleanField({required: false})
      }
    }

    /* ----------------------------------------- */

    /** @override */
    static migrateData(data) {

      // Backwards compatibility for -v9 update channels
      data.updateChannel = {
        "alpha": "prototype",
        "beta": "testing",
        "release": "stable"
      }[data.updateChannel] || data.updateChannel;

      // Backwards compatibility for awsConfig of true
      if ( data.awsConfig === true ) data.awsConfig = "";
      return data;
    }

    /* ----------------------------------------- */

    /**
     * Validate a port assignment.
     * @param {number} port     The requested port
     * @throws                  An error if the requested port is invalid
     * @private
     */
    static _validatePort(port) {
      if ( !Number.isNumeric(port) || ((port < 1024) && ![80, 443].includes(port)) || (port > 65535) ) {
        throw new Error(`The application port must be an integer, either 80, 443, or between 1024 and 65535`);
      }
    }
  }

  /* ----------------------------------------- */

  /**
   * A data object which represents the details of this Release of Foundry VTT
   * @extends {DataModel}
   * @memberof config
   *
   * @property {number} generation        The major generation of the Release
   * @property {number} [maxGeneration]   The maximum available generation of the software.
   * @property {number} [maxStableGeneration]  The maximum available stable generation of the software.
   * @property {string} channel           The channel the Release belongs to, such as "stable"
   * @property {string} suffix            An optional appended string display for the Release
   * @property {number} build             The internal build number for the Release
   * @property {number} time              When the Release was released
   * @property {number} [node_version]    The minimum required Node.js major version
   * @property {string} [notes]           Release notes for the update version
   * @property {string} [download]        A temporary download URL where this version may be obtained
   */
  class ReleaseData extends DataModel {
    /** @override */
    static defineSchema() {
      return {
        generation: new NumberField({required: true, nullable: false, integer: true, min: 1}),
        maxGeneration: new NumberField({
          required: false, nullable: false, integer: true, min: 1, initial: () => this.generation
        }),
        maxStableGeneration: new NumberField({
          required: false, nullable: false, integer: true, min: 1, initial: () => this.generation
        }),
        channel: new StringField({choices: SOFTWARE_UPDATE_CHANNELS, blank: false}),
        suffix: new StringField(),
        build: new NumberField({required: true, nullable: false, integer: true}),
        time: new NumberField({nullable: false, initial: Date.now}),
        node_version: new NumberField({required: true, nullable: false, integer: true, min: 10}),
        notes: new StringField(),
        download: new StringField()
      }
    }

    /* ----------------------------------------- */

    /**
     * A formatted string for shortened display, such as "Version 9"
     * @return {string}
     */
    get shortDisplay() {
      return `Version ${this.generation} Build ${this.build}`;
    }

    /**
     * A formatted string for general display, such as "V9 Prototype 1" or "Version 9"
     * @return {string}
     */
    get display() {
      return ["Version", this.generation, this.suffix].filterJoin(" ");
    }

    /**
     * A formatted string for Version compatibility checking, such as "9.150"
     * @return {string}
     */
    get version() {
      return `${this.generation}.${this.build}`;
    }

    /* ----------------------------------------- */

    /** @override */
    toString() {
      return this.shortDisplay;
    }

    /* ----------------------------------------- */

    /**
     * Is this ReleaseData object newer than some other version?
     * @param {string|ReleaseData} other        Some other version to compare against
     * @returns {boolean}                       Is this ReleaseData a newer version?
     */
    isNewer(other) {
      const version = other instanceof ReleaseData ? other.version : other;
      return isNewerVersion(this.version, version);
    }

    /* ----------------------------------------- */

    /**
     * Is this ReleaseData object a newer generation than some other version?
     * @param {string|ReleaseData} other        Some other version to compare against
     * @returns {boolean}                       Is this ReleaseData a newer generation?
     */
    isGenerationalChange(other) {
      if ( !other ) return true;
      let generation;
      if ( other instanceof ReleaseData ) generation = other.generation.toString();
      else {
        other = String(other);
        const parts = other.split(".");
        if ( parts[0] === "0" ) parts.shift();
        generation = parts[0];
      }
      return isNewerVersion(this.generation, generation);
    }
  }

  var config = /*#__PURE__*/Object.freeze({
    __proto__: null,
    ApplicationConfiguration: ApplicationConfiguration,
    ReleaseData: ReleaseData
  });

  // ::- Persistent data structure representing an ordered mapping from
  // strings to values, with some convenient update methods.
  function OrderedMap(content) {
    this.content = content;
  }

  OrderedMap.prototype = {
    constructor: OrderedMap,

    find: function(key) {
      for (var i = 0; i < this.content.length; i += 2)
        if (this.content[i] === key) return i
      return -1
    },

    // :: (string) → ?any
    // Retrieve the value stored under `key`, or return undefined when
    // no such key exists.
    get: function(key) {
      var found = this.find(key);
      return found == -1 ? undefined : this.content[found + 1]
    },

    // :: (string, any, ?string) → OrderedMap
    // Create a new map by replacing the value of `key` with a new
    // value, or adding a binding to the end of the map. If `newKey` is
    // given, the key of the binding will be replaced with that key.
    update: function(key, value, newKey) {
      var self = newKey && newKey != key ? this.remove(newKey) : this;
      var found = self.find(key), content = self.content.slice();
      if (found == -1) {
        content.push(newKey || key, value);
      } else {
        content[found + 1] = value;
        if (newKey) content[found] = newKey;
      }
      return new OrderedMap(content)
    },

    // :: (string) → OrderedMap
    // Return a map with the given key removed, if it existed.
    remove: function(key) {
      var found = this.find(key);
      if (found == -1) return this
      var content = this.content.slice();
      content.splice(found, 2);
      return new OrderedMap(content)
    },

    // :: (string, any) → OrderedMap
    // Add a new key to the start of the map.
    addToStart: function(key, value) {
      return new OrderedMap([key, value].concat(this.remove(key).content))
    },

    // :: (string, any) → OrderedMap
    // Add a new key to the end of the map.
    addToEnd: function(key, value) {
      var content = this.remove(key).content.slice();
      content.push(key, value);
      return new OrderedMap(content)
    },

    // :: (string, string, any) → OrderedMap
    // Add a key after the given key. If `place` is not found, the new
    // key is added to the end.
    addBefore: function(place, key, value) {
      var without = this.remove(key), content = without.content.slice();
      var found = without.find(place);
      content.splice(found == -1 ? content.length : found, 0, key, value);
      return new OrderedMap(content)
    },

    // :: ((key: string, value: any))
    // Call the given function for each key/value pair in the map, in
    // order.
    forEach: function(f) {
      for (var i = 0; i < this.content.length; i += 2)
        f(this.content[i], this.content[i + 1]);
    },

    // :: (union<Object, OrderedMap>) → OrderedMap
    // Create a new map by prepending the keys in this map that don't
    // appear in `map` before the keys in `map`.
    prepend: function(map) {
      map = OrderedMap.from(map);
      if (!map.size) return this
      return new OrderedMap(map.content.concat(this.subtract(map).content))
    },

    // :: (union<Object, OrderedMap>) → OrderedMap
    // Create a new map by appending the keys in this map that don't
    // appear in `map` after the keys in `map`.
    append: function(map) {
      map = OrderedMap.from(map);
      if (!map.size) return this
      return new OrderedMap(this.subtract(map).content.concat(map.content))
    },

    // :: (union<Object, OrderedMap>) → OrderedMap
    // Create a map containing all the keys in this map that don't
    // appear in `map`.
    subtract: function(map) {
      var result = this;
      map = OrderedMap.from(map);
      for (var i = 0; i < map.content.length; i += 2)
        result = result.remove(map.content[i]);
      return result
    },

    // :: () → Object
    // Turn ordered map into a plain object.
    toObject: function() {
      var result = {};
      this.forEach(function(key, value) { result[key] = value; });
      return result
    },

    // :: number
    // The amount of keys in this map.
    get size() {
      return this.content.length >> 1
    }
  };

  // :: (?union<Object, OrderedMap>) → OrderedMap
  // Return a map with the given content. If null, create an empty
  // map. If given an ordered map, return that map itself. If given an
  // object, create a map from the object's properties.
  OrderedMap.from = function(value) {
    if (value instanceof OrderedMap) return value
    var content = [];
    if (value) for (var prop in value) content.push(prop, value[prop]);
    return new OrderedMap(content)
  };

  function findDiffStart(a, b, pos) {
      for (let i = 0;; i++) {
          if (i == a.childCount || i == b.childCount)
              return a.childCount == b.childCount ? null : pos;
          let childA = a.child(i), childB = b.child(i);
          if (childA == childB) {
              pos += childA.nodeSize;
              continue;
          }
          if (!childA.sameMarkup(childB))
              return pos;
          if (childA.isText && childA.text != childB.text) {
              for (let j = 0; childA.text[j] == childB.text[j]; j++)
                  pos++;
              return pos;
          }
          if (childA.content.size || childB.content.size) {
              let inner = findDiffStart(childA.content, childB.content, pos + 1);
              if (inner != null)
                  return inner;
          }
          pos += childA.nodeSize;
      }
  }
  function findDiffEnd(a, b, posA, posB) {
      for (let iA = a.childCount, iB = b.childCount;;) {
          if (iA == 0 || iB == 0)
              return iA == iB ? null : { a: posA, b: posB };
          let childA = a.child(--iA), childB = b.child(--iB), size = childA.nodeSize;
          if (childA == childB) {
              posA -= size;
              posB -= size;
              continue;
          }
          if (!childA.sameMarkup(childB))
              return { a: posA, b: posB };
          if (childA.isText && childA.text != childB.text) {
              let same = 0, minSize = Math.min(childA.text.length, childB.text.length);
              while (same < minSize && childA.text[childA.text.length - same - 1] == childB.text[childB.text.length - same - 1]) {
                  same++;
                  posA--;
                  posB--;
              }
              return { a: posA, b: posB };
          }
          if (childA.content.size || childB.content.size) {
              let inner = findDiffEnd(childA.content, childB.content, posA - 1, posB - 1);
              if (inner)
                  return inner;
          }
          posA -= size;
          posB -= size;
      }
  }

  /**
  A fragment represents a node's collection of child nodes.

  Like nodes, fragments are persistent data structures, and you
  should not mutate them or their content. Rather, you create new
  instances whenever needed. The API tries to make this easy.
  */
  class Fragment {
      /**
      @internal
      */
      constructor(
      /**
      @internal
      */
      content, size) {
          this.content = content;
          this.size = size || 0;
          if (size == null)
              for (let i = 0; i < content.length; i++)
                  this.size += content[i].nodeSize;
      }
      /**
      Invoke a callback for all descendant nodes between the given two
      positions (relative to start of this fragment). Doesn't descend
      into a node when the callback returns `false`.
      */
      nodesBetween(from, to, f, nodeStart = 0, parent) {
          for (let i = 0, pos = 0; pos < to; i++) {
              let child = this.content[i], end = pos + child.nodeSize;
              if (end > from && f(child, nodeStart + pos, parent || null, i) !== false && child.content.size) {
                  let start = pos + 1;
                  child.nodesBetween(Math.max(0, from - start), Math.min(child.content.size, to - start), f, nodeStart + start);
              }
              pos = end;
          }
      }
      /**
      Call the given callback for every descendant node. `pos` will be
      relative to the start of the fragment. The callback may return
      `false` to prevent traversal of a given node's children.
      */
      descendants(f) {
          this.nodesBetween(0, this.size, f);
      }
      /**
      Extract the text between `from` and `to`. See the same method on
      [`Node`](https://prosemirror.net/docs/ref/#model.Node.textBetween).
      */
      textBetween(from, to, blockSeparator, leafText) {
          let text = "", first = true;
          this.nodesBetween(from, to, (node, pos) => {
              let nodeText = node.isText ? node.text.slice(Math.max(from, pos) - pos, to - pos)
                  : !node.isLeaf ? ""
                      : leafText ? (typeof leafText === "function" ? leafText(node) : leafText)
                          : node.type.spec.leafText ? node.type.spec.leafText(node)
                              : "";
              if (node.isBlock && (node.isLeaf && nodeText || node.isTextblock) && blockSeparator) {
                  if (first)
                      first = false;
                  else
                      text += blockSeparator;
              }
              text += nodeText;
          }, 0);
          return text;
      }
      /**
      Create a new fragment containing the combined content of this
      fragment and the other.
      */
      append(other) {
          if (!other.size)
              return this;
          if (!this.size)
              return other;
          let last = this.lastChild, first = other.firstChild, content = this.content.slice(), i = 0;
          if (last.isText && last.sameMarkup(first)) {
              content[content.length - 1] = last.withText(last.text + first.text);
              i = 1;
          }
          for (; i < other.content.length; i++)
              content.push(other.content[i]);
          return new Fragment(content, this.size + other.size);
      }
      /**
      Cut out the sub-fragment between the two given positions.
      */
      cut(from, to = this.size) {
          if (from == 0 && to == this.size)
              return this;
          let result = [], size = 0;
          if (to > from)
              for (let i = 0, pos = 0; pos < to; i++) {
                  let child = this.content[i], end = pos + child.nodeSize;
                  if (end > from) {
                      if (pos < from || end > to) {
                          if (child.isText)
                              child = child.cut(Math.max(0, from - pos), Math.min(child.text.length, to - pos));
                          else
                              child = child.cut(Math.max(0, from - pos - 1), Math.min(child.content.size, to - pos - 1));
                      }
                      result.push(child);
                      size += child.nodeSize;
                  }
                  pos = end;
              }
          return new Fragment(result, size);
      }
      /**
      @internal
      */
      cutByIndex(from, to) {
          if (from == to)
              return Fragment.empty;
          if (from == 0 && to == this.content.length)
              return this;
          return new Fragment(this.content.slice(from, to));
      }
      /**
      Create a new fragment in which the node at the given index is
      replaced by the given node.
      */
      replaceChild(index, node) {
          let current = this.content[index];
          if (current == node)
              return this;
          let copy = this.content.slice();
          let size = this.size + node.nodeSize - current.nodeSize;
          copy[index] = node;
          return new Fragment(copy, size);
      }
      /**
      Create a new fragment by prepending the given node to this
      fragment.
      */
      addToStart(node) {
          return new Fragment([node].concat(this.content), this.size + node.nodeSize);
      }
      /**
      Create a new fragment by appending the given node to this
      fragment.
      */
      addToEnd(node) {
          return new Fragment(this.content.concat(node), this.size + node.nodeSize);
      }
      /**
      Compare this fragment to another one.
      */
      eq(other) {
          if (this.content.length != other.content.length)
              return false;
          for (let i = 0; i < this.content.length; i++)
              if (!this.content[i].eq(other.content[i]))
                  return false;
          return true;
      }
      /**
      The first child of the fragment, or `null` if it is empty.
      */
      get firstChild() { return this.content.length ? this.content[0] : null; }
      /**
      The last child of the fragment, or `null` if it is empty.
      */
      get lastChild() { return this.content.length ? this.content[this.content.length - 1] : null; }
      /**
      The number of child nodes in this fragment.
      */
      get childCount() { return this.content.length; }
      /**
      Get the child node at the given index. Raise an error when the
      index is out of range.
      */
      child(index) {
          let found = this.content[index];
          if (!found)
              throw new RangeError("Index " + index + " out of range for " + this);
          return found;
      }
      /**
      Get the child node at the given index, if it exists.
      */
      maybeChild(index) {
          return this.content[index] || null;
      }
      /**
      Call `f` for every child node, passing the node, its offset
      into this parent node, and its index.
      */
      forEach(f) {
          for (let i = 0, p = 0; i < this.content.length; i++) {
              let child = this.content[i];
              f(child, p, i);
              p += child.nodeSize;
          }
      }
      /**
      Find the first position at which this fragment and another
      fragment differ, or `null` if they are the same.
      */
      findDiffStart(other, pos = 0) {
          return findDiffStart(this, other, pos);
      }
      /**
      Find the first position, searching from the end, at which this
      fragment and the given fragment differ, or `null` if they are
      the same. Since this position will not be the same in both
      nodes, an object with two separate positions is returned.
      */
      findDiffEnd(other, pos = this.size, otherPos = other.size) {
          return findDiffEnd(this, other, pos, otherPos);
      }
      /**
      Find the index and inner offset corresponding to a given relative
      position in this fragment. The result object will be reused
      (overwritten) the next time the function is called. (Not public.)
      */
      findIndex(pos, round = -1) {
          if (pos == 0)
              return retIndex(0, pos);
          if (pos == this.size)
              return retIndex(this.content.length, pos);
          if (pos > this.size || pos < 0)
              throw new RangeError(`Position ${pos} outside of fragment (${this})`);
          for (let i = 0, curPos = 0;; i++) {
              let cur = this.child(i), end = curPos + cur.nodeSize;
              if (end >= pos) {
                  if (end == pos || round > 0)
                      return retIndex(i + 1, end);
                  return retIndex(i, curPos);
              }
              curPos = end;
          }
      }
      /**
      Return a debugging string that describes this fragment.
      */
      toString() { return "<" + this.toStringInner() + ">"; }
      /**
      @internal
      */
      toStringInner() { return this.content.join(", "); }
      /**
      Create a JSON-serializeable representation of this fragment.
      */
      toJSON() {
          return this.content.length ? this.content.map(n => n.toJSON()) : null;
      }
      /**
      Deserialize a fragment from its JSON representation.
      */
      static fromJSON(schema, value) {
          if (!value)
              return Fragment.empty;
          if (!Array.isArray(value))
              throw new RangeError("Invalid input for Fragment.fromJSON");
          return new Fragment(value.map(schema.nodeFromJSON));
      }
      /**
      Build a fragment from an array of nodes. Ensures that adjacent
      text nodes with the same marks are joined together.
      */
      static fromArray(array) {
          if (!array.length)
              return Fragment.empty;
          let joined, size = 0;
          for (let i = 0; i < array.length; i++) {
              let node = array[i];
              size += node.nodeSize;
              if (i && node.isText && array[i - 1].sameMarkup(node)) {
                  if (!joined)
                      joined = array.slice(0, i);
                  joined[joined.length - 1] = node
                      .withText(joined[joined.length - 1].text + node.text);
              }
              else if (joined) {
                  joined.push(node);
              }
          }
          return new Fragment(joined || array, size);
      }
      /**
      Create a fragment from something that can be interpreted as a
      set of nodes. For `null`, it returns the empty fragment. For a
      fragment, the fragment itself. For a node or array of nodes, a
      fragment containing those nodes.
      */
      static from(nodes) {
          if (!nodes)
              return Fragment.empty;
          if (nodes instanceof Fragment)
              return nodes;
          if (Array.isArray(nodes))
              return this.fromArray(nodes);
          if (nodes.attrs)
              return new Fragment([nodes], nodes.nodeSize);
          throw new RangeError("Can not convert " + nodes + " to a Fragment" +
              (nodes.nodesBetween ? " (looks like multiple versions of prosemirror-model were loaded)" : ""));
      }
  }
  /**
  An empty fragment. Intended to be reused whenever a node doesn't
  contain anything (rather than allocating a new empty fragment for
  each leaf node).
  */
  Fragment.empty = new Fragment([], 0);
  const found = { index: 0, offset: 0 };
  function retIndex(index, offset) {
      found.index = index;
      found.offset = offset;
      return found;
  }

  function compareDeep(a, b) {
      if (a === b)
          return true;
      if (!(a && typeof a == "object") ||
          !(b && typeof b == "object"))
          return false;
      let array = Array.isArray(a);
      if (Array.isArray(b) != array)
          return false;
      if (array) {
          if (a.length != b.length)
              return false;
          for (let i = 0; i < a.length; i++)
              if (!compareDeep(a[i], b[i]))
                  return false;
      }
      else {
          for (let p in a)
              if (!(p in b) || !compareDeep(a[p], b[p]))
                  return false;
          for (let p in b)
              if (!(p in a))
                  return false;
      }
      return true;
  }

  /**
  A mark is a piece of information that can be attached to a node,
  such as it being emphasized, in code font, or a link. It has a
  type and optionally a set of attributes that provide further
  information (such as the target of the link). Marks are created
  through a `Schema`, which controls which types exist and which
  attributes they have.
  */
  class Mark {
      /**
      @internal
      */
      constructor(
      /**
      The type of this mark.
      */
      type, 
      /**
      The attributes associated with this mark.
      */
      attrs) {
          this.type = type;
          this.attrs = attrs;
      }
      /**
      Given a set of marks, create a new set which contains this one as
      well, in the right position. If this mark is already in the set,
      the set itself is returned. If any marks that are set to be
      [exclusive](https://prosemirror.net/docs/ref/#model.MarkSpec.excludes) with this mark are present,
      those are replaced by this one.
      */
      addToSet(set) {
          let copy, placed = false;
          for (let i = 0; i < set.length; i++) {
              let other = set[i];
              if (this.eq(other))
                  return set;
              if (this.type.excludes(other.type)) {
                  if (!copy)
                      copy = set.slice(0, i);
              }
              else if (other.type.excludes(this.type)) {
                  return set;
              }
              else {
                  if (!placed && other.type.rank > this.type.rank) {
                      if (!copy)
                          copy = set.slice(0, i);
                      copy.push(this);
                      placed = true;
                  }
                  if (copy)
                      copy.push(other);
              }
          }
          if (!copy)
              copy = set.slice();
          if (!placed)
              copy.push(this);
          return copy;
      }
      /**
      Remove this mark from the given set, returning a new set. If this
      mark is not in the set, the set itself is returned.
      */
      removeFromSet(set) {
          for (let i = 0; i < set.length; i++)
              if (this.eq(set[i]))
                  return set.slice(0, i).concat(set.slice(i + 1));
          return set;
      }
      /**
      Test whether this mark is in the given set of marks.
      */
      isInSet(set) {
          for (let i = 0; i < set.length; i++)
              if (this.eq(set[i]))
                  return true;
          return false;
      }
      /**
      Test whether this mark has the same type and attributes as
      another mark.
      */
      eq(other) {
          return this == other ||
              (this.type == other.type && compareDeep(this.attrs, other.attrs));
      }
      /**
      Convert this mark to a JSON-serializeable representation.
      */
      toJSON() {
          let obj = { type: this.type.name };
          for (let _ in this.attrs) {
              obj.attrs = this.attrs;
              break;
          }
          return obj;
      }
      /**
      Deserialize a mark from JSON.
      */
      static fromJSON(schema, json) {
          if (!json)
              throw new RangeError("Invalid input for Mark.fromJSON");
          let type = schema.marks[json.type];
          if (!type)
              throw new RangeError(`There is no mark type ${json.type} in this schema`);
          return type.create(json.attrs);
      }
      /**
      Test whether two sets of marks are identical.
      */
      static sameSet(a, b) {
          if (a == b)
              return true;
          if (a.length != b.length)
              return false;
          for (let i = 0; i < a.length; i++)
              if (!a[i].eq(b[i]))
                  return false;
          return true;
      }
      /**
      Create a properly sorted mark set from null, a single mark, or an
      unsorted array of marks.
      */
      static setFrom(marks) {
          if (!marks || Array.isArray(marks) && marks.length == 0)
              return Mark.none;
          if (marks instanceof Mark)
              return [marks];
          let copy = marks.slice();
          copy.sort((a, b) => a.type.rank - b.type.rank);
          return copy;
      }
  }
  /**
  The empty set of marks.
  */
  Mark.none = [];

  /**
  Error type raised by [`Node.replace`](https://prosemirror.net/docs/ref/#model.Node.replace) when
  given an invalid replacement.
  */
  class ReplaceError extends Error {
  }
  /*
  ReplaceError = function(this: any, message: string) {
    let err = Error.call(this, message)
    ;(err as any).__proto__ = ReplaceError.prototype
    return err
  } as any

  ReplaceError.prototype = Object.create(Error.prototype)
  ReplaceError.prototype.constructor = ReplaceError
  ReplaceError.prototype.name = "ReplaceError"
  */
  /**
  A slice represents a piece cut out of a larger document. It
  stores not only a fragment, but also the depth up to which nodes on
  both side are ‘open’ (cut through).
  */
  class Slice {
      /**
      Create a slice. When specifying a non-zero open depth, you must
      make sure that there are nodes of at least that depth at the
      appropriate side of the fragment—i.e. if the fragment is an
      empty paragraph node, `openStart` and `openEnd` can't be greater
      than 1.
      
      It is not necessary for the content of open nodes to conform to
      the schema's content constraints, though it should be a valid
      start/end/middle for such a node, depending on which sides are
      open.
      */
      constructor(
      /**
      The slice's content.
      */
      content, 
      /**
      The open depth at the start of the fragment.
      */
      openStart, 
      /**
      The open depth at the end.
      */
      openEnd) {
          this.content = content;
          this.openStart = openStart;
          this.openEnd = openEnd;
      }
      /**
      The size this slice would add when inserted into a document.
      */
      get size() {
          return this.content.size - this.openStart - this.openEnd;
      }
      /**
      @internal
      */
      insertAt(pos, fragment) {
          let content = insertInto(this.content, pos + this.openStart, fragment);
          return content && new Slice(content, this.openStart, this.openEnd);
      }
      /**
      @internal
      */
      removeBetween(from, to) {
          return new Slice(removeRange(this.content, from + this.openStart, to + this.openStart), this.openStart, this.openEnd);
      }
      /**
      Tests whether this slice is equal to another slice.
      */
      eq(other) {
          return this.content.eq(other.content) && this.openStart == other.openStart && this.openEnd == other.openEnd;
      }
      /**
      @internal
      */
      toString() {
          return this.content + "(" + this.openStart + "," + this.openEnd + ")";
      }
      /**
      Convert a slice to a JSON-serializable representation.
      */
      toJSON() {
          if (!this.content.size)
              return null;
          let json = { content: this.content.toJSON() };
          if (this.openStart > 0)
              json.openStart = this.openStart;
          if (this.openEnd > 0)
              json.openEnd = this.openEnd;
          return json;
      }
      /**
      Deserialize a slice from its JSON representation.
      */
      static fromJSON(schema, json) {
          if (!json)
              return Slice.empty;
          let openStart = json.openStart || 0, openEnd = json.openEnd || 0;
          if (typeof openStart != "number" || typeof openEnd != "number")
              throw new RangeError("Invalid input for Slice.fromJSON");
          return new Slice(Fragment.fromJSON(schema, json.content), openStart, openEnd);
      }
      /**
      Create a slice from a fragment by taking the maximum possible
      open value on both side of the fragment.
      */
      static maxOpen(fragment, openIsolating = true) {
          let openStart = 0, openEnd = 0;
          for (let n = fragment.firstChild; n && !n.isLeaf && (openIsolating || !n.type.spec.isolating); n = n.firstChild)
              openStart++;
          for (let n = fragment.lastChild; n && !n.isLeaf && (openIsolating || !n.type.spec.isolating); n = n.lastChild)
              openEnd++;
          return new Slice(fragment, openStart, openEnd);
      }
  }
  /**
  The empty slice.
  */
  Slice.empty = new Slice(Fragment.empty, 0, 0);
  function removeRange(content, from, to) {
      let { index, offset } = content.findIndex(from), child = content.maybeChild(index);
      let { index: indexTo, offset: offsetTo } = content.findIndex(to);
      if (offset == from || child.isText) {
          if (offsetTo != to && !content.child(indexTo).isText)
              throw new RangeError("Removing non-flat range");
          return content.cut(0, from).append(content.cut(to));
      }
      if (index != indexTo)
          throw new RangeError("Removing non-flat range");
      return content.replaceChild(index, child.copy(removeRange(child.content, from - offset - 1, to - offset - 1)));
  }
  function insertInto(content, dist, insert, parent) {
      let { index, offset } = content.findIndex(dist), child = content.maybeChild(index);
      if (offset == dist || child.isText) {
          return content.cut(0, dist).append(insert).append(content.cut(dist));
      }
      let inner = insertInto(child.content, dist - offset - 1, insert);
      return inner && content.replaceChild(index, child.copy(inner));
  }
  function replace($from, $to, slice) {
      if (slice.openStart > $from.depth)
          throw new ReplaceError("Inserted content deeper than insertion position");
      if ($from.depth - slice.openStart != $to.depth - slice.openEnd)
          throw new ReplaceError("Inconsistent open depths");
      return replaceOuter($from, $to, slice, 0);
  }
  function replaceOuter($from, $to, slice, depth) {
      let index = $from.index(depth), node = $from.node(depth);
      if (index == $to.index(depth) && depth < $from.depth - slice.openStart) {
          let inner = replaceOuter($from, $to, slice, depth + 1);
          return node.copy(node.content.replaceChild(index, inner));
      }
      else if (!slice.content.size) {
          return close(node, replaceTwoWay($from, $to, depth));
      }
      else if (!slice.openStart && !slice.openEnd && $from.depth == depth && $to.depth == depth) { // Simple, flat case
          let parent = $from.parent, content = parent.content;
          return close(parent, content.cut(0, $from.parentOffset).append(slice.content).append(content.cut($to.parentOffset)));
      }
      else {
          let { start, end } = prepareSliceForReplace(slice, $from);
          return close(node, replaceThreeWay($from, start, end, $to, depth));
      }
  }
  function checkJoin(main, sub) {
      if (!sub.type.compatibleContent(main.type))
          throw new ReplaceError("Cannot join " + sub.type.name + " onto " + main.type.name);
  }
  function joinable$1($before, $after, depth) {
      let node = $before.node(depth);
      checkJoin(node, $after.node(depth));
      return node;
  }
  function addNode(child, target) {
      let last = target.length - 1;
      if (last >= 0 && child.isText && child.sameMarkup(target[last]))
          target[last] = child.withText(target[last].text + child.text);
      else
          target.push(child);
  }
  function addRange($start, $end, depth, target) {
      let node = ($end || $start).node(depth);
      let startIndex = 0, endIndex = $end ? $end.index(depth) : node.childCount;
      if ($start) {
          startIndex = $start.index(depth);
          if ($start.depth > depth) {
              startIndex++;
          }
          else if ($start.textOffset) {
              addNode($start.nodeAfter, target);
              startIndex++;
          }
      }
      for (let i = startIndex; i < endIndex; i++)
          addNode(node.child(i), target);
      if ($end && $end.depth == depth && $end.textOffset)
          addNode($end.nodeBefore, target);
  }
  function close(node, content) {
      node.type.checkContent(content);
      return node.copy(content);
  }
  function replaceThreeWay($from, $start, $end, $to, depth) {
      let openStart = $from.depth > depth && joinable$1($from, $start, depth + 1);
      let openEnd = $to.depth > depth && joinable$1($end, $to, depth + 1);
      let content = [];
      addRange(null, $from, depth, content);
      if (openStart && openEnd && $start.index(depth) == $end.index(depth)) {
          checkJoin(openStart, openEnd);
          addNode(close(openStart, replaceThreeWay($from, $start, $end, $to, depth + 1)), content);
      }
      else {
          if (openStart)
              addNode(close(openStart, replaceTwoWay($from, $start, depth + 1)), content);
          addRange($start, $end, depth, content);
          if (openEnd)
              addNode(close(openEnd, replaceTwoWay($end, $to, depth + 1)), content);
      }
      addRange($to, null, depth, content);
      return new Fragment(content);
  }
  function replaceTwoWay($from, $to, depth) {
      let content = [];
      addRange(null, $from, depth, content);
      if ($from.depth > depth) {
          let type = joinable$1($from, $to, depth + 1);
          addNode(close(type, replaceTwoWay($from, $to, depth + 1)), content);
      }
      addRange($to, null, depth, content);
      return new Fragment(content);
  }
  function prepareSliceForReplace(slice, $along) {
      let extra = $along.depth - slice.openStart, parent = $along.node(extra);
      let node = parent.copy(slice.content);
      for (let i = extra - 1; i >= 0; i--)
          node = $along.node(i).copy(Fragment.from(node));
      return { start: node.resolveNoCache(slice.openStart + extra),
          end: node.resolveNoCache(node.content.size - slice.openEnd - extra) };
  }

  /**
  You can [_resolve_](https://prosemirror.net/docs/ref/#model.Node.resolve) a position to get more
  information about it. Objects of this class represent such a
  resolved position, providing various pieces of context
  information, and some helper methods.

  Throughout this interface, methods that take an optional `depth`
  parameter will interpret undefined as `this.depth` and negative
  numbers as `this.depth + value`.
  */
  class ResolvedPos {
      /**
      @internal
      */
      constructor(
      /**
      The position that was resolved.
      */
      pos, 
      /**
      @internal
      */
      path, 
      /**
      The offset this position has into its parent node.
      */
      parentOffset) {
          this.pos = pos;
          this.path = path;
          this.parentOffset = parentOffset;
          this.depth = path.length / 3 - 1;
      }
      /**
      @internal
      */
      resolveDepth(val) {
          if (val == null)
              return this.depth;
          if (val < 0)
              return this.depth + val;
          return val;
      }
      /**
      The parent node that the position points into. Note that even if
      a position points into a text node, that node is not considered
      the parent—text nodes are ‘flat’ in this model, and have no content.
      */
      get parent() { return this.node(this.depth); }
      /**
      The root node in which the position was resolved.
      */
      get doc() { return this.node(0); }
      /**
      The ancestor node at the given level. `p.node(p.depth)` is the
      same as `p.parent`.
      */
      node(depth) { return this.path[this.resolveDepth(depth) * 3]; }
      /**
      The index into the ancestor at the given level. If this points
      at the 3rd node in the 2nd paragraph on the top level, for
      example, `p.index(0)` is 1 and `p.index(1)` is 2.
      */
      index(depth) { return this.path[this.resolveDepth(depth) * 3 + 1]; }
      /**
      The index pointing after this position into the ancestor at the
      given level.
      */
      indexAfter(depth) {
          depth = this.resolveDepth(depth);
          return this.index(depth) + (depth == this.depth && !this.textOffset ? 0 : 1);
      }
      /**
      The (absolute) position at the start of the node at the given
      level.
      */
      start(depth) {
          depth = this.resolveDepth(depth);
          return depth == 0 ? 0 : this.path[depth * 3 - 1] + 1;
      }
      /**
      The (absolute) position at the end of the node at the given
      level.
      */
      end(depth) {
          depth = this.resolveDepth(depth);
          return this.start(depth) + this.node(depth).content.size;
      }
      /**
      The (absolute) position directly before the wrapping node at the
      given level, or, when `depth` is `this.depth + 1`, the original
      position.
      */
      before(depth) {
          depth = this.resolveDepth(depth);
          if (!depth)
              throw new RangeError("There is no position before the top-level node");
          return depth == this.depth + 1 ? this.pos : this.path[depth * 3 - 1];
      }
      /**
      The (absolute) position directly after the wrapping node at the
      given level, or the original position when `depth` is `this.depth + 1`.
      */
      after(depth) {
          depth = this.resolveDepth(depth);
          if (!depth)
              throw new RangeError("There is no position after the top-level node");
          return depth == this.depth + 1 ? this.pos : this.path[depth * 3 - 1] + this.path[depth * 3].nodeSize;
      }
      /**
      When this position points into a text node, this returns the
      distance between the position and the start of the text node.
      Will be zero for positions that point between nodes.
      */
      get textOffset() { return this.pos - this.path[this.path.length - 1]; }
      /**
      Get the node directly after the position, if any. If the position
      points into a text node, only the part of that node after the
      position is returned.
      */
      get nodeAfter() {
          let parent = this.parent, index = this.index(this.depth);
          if (index == parent.childCount)
              return null;
          let dOff = this.pos - this.path[this.path.length - 1], child = parent.child(index);
          return dOff ? parent.child(index).cut(dOff) : child;
      }
      /**
      Get the node directly before the position, if any. If the
      position points into a text node, only the part of that node
      before the position is returned.
      */
      get nodeBefore() {
          let index = this.index(this.depth);
          let dOff = this.pos - this.path[this.path.length - 1];
          if (dOff)
              return this.parent.child(index).cut(0, dOff);
          return index == 0 ? null : this.parent.child(index - 1);
      }
      /**
      Get the position at the given index in the parent node at the
      given depth (which defaults to `this.depth`).
      */
      posAtIndex(index, depth) {
          depth = this.resolveDepth(depth);
          let node = this.path[depth * 3], pos = depth == 0 ? 0 : this.path[depth * 3 - 1] + 1;
          for (let i = 0; i < index; i++)
              pos += node.child(i).nodeSize;
          return pos;
      }
      /**
      Get the marks at this position, factoring in the surrounding
      marks' [`inclusive`](https://prosemirror.net/docs/ref/#model.MarkSpec.inclusive) property. If the
      position is at the start of a non-empty node, the marks of the
      node after it (if any) are returned.
      */
      marks() {
          let parent = this.parent, index = this.index();
          // In an empty parent, return the empty array
          if (parent.content.size == 0)
              return Mark.none;
          // When inside a text node, just return the text node's marks
          if (this.textOffset)
              return parent.child(index).marks;
          let main = parent.maybeChild(index - 1), other = parent.maybeChild(index);
          // If the `after` flag is true of there is no node before, make
          // the node after this position the main reference.
          if (!main) {
              let tmp = main;
              main = other;
              other = tmp;
          }
          // Use all marks in the main node, except those that have
          // `inclusive` set to false and are not present in the other node.
          let marks = main.marks;
          for (var i = 0; i < marks.length; i++)
              if (marks[i].type.spec.inclusive === false && (!other || !marks[i].isInSet(other.marks)))
                  marks = marks[i--].removeFromSet(marks);
          return marks;
      }
      /**
      Get the marks after the current position, if any, except those
      that are non-inclusive and not present at position `$end`. This
      is mostly useful for getting the set of marks to preserve after a
      deletion. Will return `null` if this position is at the end of
      its parent node or its parent node isn't a textblock (in which
      case no marks should be preserved).
      */
      marksAcross($end) {
          let after = this.parent.maybeChild(this.index());
          if (!after || !after.isInline)
              return null;
          let marks = after.marks, next = $end.parent.maybeChild($end.index());
          for (var i = 0; i < marks.length; i++)
              if (marks[i].type.spec.inclusive === false && (!next || !marks[i].isInSet(next.marks)))
                  marks = marks[i--].removeFromSet(marks);
          return marks;
      }
      /**
      The depth up to which this position and the given (non-resolved)
      position share the same parent nodes.
      */
      sharedDepth(pos) {
          for (let depth = this.depth; depth > 0; depth--)
              if (this.start(depth) <= pos && this.end(depth) >= pos)
                  return depth;
          return 0;
      }
      /**
      Returns a range based on the place where this position and the
      given position diverge around block content. If both point into
      the same textblock, for example, a range around that textblock
      will be returned. If they point into different blocks, the range
      around those blocks in their shared ancestor is returned. You can
      pass in an optional predicate that will be called with a parent
      node to see if a range into that parent is acceptable.
      */
      blockRange(other = this, pred) {
          if (other.pos < this.pos)
              return other.blockRange(this);
          for (let d = this.depth - (this.parent.inlineContent || this.pos == other.pos ? 1 : 0); d >= 0; d--)
              if (other.pos <= this.end(d) && (!pred || pred(this.node(d))))
                  return new NodeRange(this, other, d);
          return null;
      }
      /**
      Query whether the given position shares the same parent node.
      */
      sameParent(other) {
          return this.pos - this.parentOffset == other.pos - other.parentOffset;
      }
      /**
      Return the greater of this and the given position.
      */
      max(other) {
          return other.pos > this.pos ? other : this;
      }
      /**
      Return the smaller of this and the given position.
      */
      min(other) {
          return other.pos < this.pos ? other : this;
      }
      /**
      @internal
      */
      toString() {
          let str = "";
          for (let i = 1; i <= this.depth; i++)
              str += (str ? "/" : "") + this.node(i).type.name + "_" + this.index(i - 1);
          return str + ":" + this.parentOffset;
      }
      /**
      @internal
      */
      static resolve(doc, pos) {
          if (!(pos >= 0 && pos <= doc.content.size))
              throw new RangeError("Position " + pos + " out of range");
          let path = [];
          let start = 0, parentOffset = pos;
          for (let node = doc;;) {
              let { index, offset } = node.content.findIndex(parentOffset);
              let rem = parentOffset - offset;
              path.push(node, index, start + offset);
              if (!rem)
                  break;
              node = node.child(index);
              if (node.isText)
                  break;
              parentOffset = rem - 1;
              start += offset + 1;
          }
          return new ResolvedPos(pos, path, parentOffset);
      }
      /**
      @internal
      */
      static resolveCached(doc, pos) {
          for (let i = 0; i < resolveCache.length; i++) {
              let cached = resolveCache[i];
              if (cached.pos == pos && cached.doc == doc)
                  return cached;
          }
          let result = resolveCache[resolveCachePos] = ResolvedPos.resolve(doc, pos);
          resolveCachePos = (resolveCachePos + 1) % resolveCacheSize;
          return result;
      }
  }
  let resolveCache = [], resolveCachePos = 0, resolveCacheSize = 12;
  /**
  Represents a flat range of content, i.e. one that starts and
  ends in the same node.
  */
  class NodeRange {
      /**
      Construct a node range. `$from` and `$to` should point into the
      same node until at least the given `depth`, since a node range
      denotes an adjacent set of nodes in a single parent node.
      */
      constructor(
      /**
      A resolved position along the start of the content. May have a
      `depth` greater than this object's `depth` property, since
      these are the positions that were used to compute the range,
      not re-resolved positions directly at its boundaries.
      */
      $from, 
      /**
      A position along the end of the content. See
      caveat for [`$from`](https://prosemirror.net/docs/ref/#model.NodeRange.$from).
      */
      $to, 
      /**
      The depth of the node that this range points into.
      */
      depth) {
          this.$from = $from;
          this.$to = $to;
          this.depth = depth;
      }
      /**
      The position at the start of the range.
      */
      get start() { return this.$from.before(this.depth + 1); }
      /**
      The position at the end of the range.
      */
      get end() { return this.$to.after(this.depth + 1); }
      /**
      The parent node that the range points into.
      */
      get parent() { return this.$from.node(this.depth); }
      /**
      The start index of the range in the parent node.
      */
      get startIndex() { return this.$from.index(this.depth); }
      /**
      The end index of the range in the parent node.
      */
      get endIndex() { return this.$to.indexAfter(this.depth); }
  }

  const emptyAttrs = Object.create(null);
  /**
  This class represents a node in the tree that makes up a
  ProseMirror document. So a document is an instance of `Node`, with
  children that are also instances of `Node`.

  Nodes are persistent data structures. Instead of changing them, you
  create new ones with the content you want. Old ones keep pointing
  at the old document shape. This is made cheaper by sharing
  structure between the old and new data as much as possible, which a
  tree shape like this (without back pointers) makes easy.

  **Do not** directly mutate the properties of a `Node` object. See
  [the guide](/docs/guide/#doc) for more information.
  */
  class Node {
      /**
      @internal
      */
      constructor(
      /**
      The type of node that this is.
      */
      type, 
      /**
      An object mapping attribute names to values. The kind of
      attributes allowed and required are
      [determined](https://prosemirror.net/docs/ref/#model.NodeSpec.attrs) by the node type.
      */
      attrs, 
      // A fragment holding the node's children.
      content, 
      /**
      The marks (things like whether it is emphasized or part of a
      link) applied to this node.
      */
      marks = Mark.none) {
          this.type = type;
          this.attrs = attrs;
          this.marks = marks;
          this.content = content || Fragment.empty;
      }
      /**
      The size of this node, as defined by the integer-based [indexing
      scheme](/docs/guide/#doc.indexing). For text nodes, this is the
      amount of characters. For other leaf nodes, it is one. For
      non-leaf nodes, it is the size of the content plus two (the
      start and end token).
      */
      get nodeSize() { return this.isLeaf ? 1 : 2 + this.content.size; }
      /**
      The number of children that the node has.
      */
      get childCount() { return this.content.childCount; }
      /**
      Get the child node at the given index. Raises an error when the
      index is out of range.
      */
      child(index) { return this.content.child(index); }
      /**
      Get the child node at the given index, if it exists.
      */
      maybeChild(index) { return this.content.maybeChild(index); }
      /**
      Call `f` for every child node, passing the node, its offset
      into this parent node, and its index.
      */
      forEach(f) { this.content.forEach(f); }
      /**
      Invoke a callback for all descendant nodes recursively between
      the given two positions that are relative to start of this
      node's content. The callback is invoked with the node, its
      position relative to the original node (method receiver),
      its parent node, and its child index. When the callback returns
      false for a given node, that node's children will not be
      recursed over. The last parameter can be used to specify a
      starting position to count from.
      */
      nodesBetween(from, to, f, startPos = 0) {
          this.content.nodesBetween(from, to, f, startPos, this);
      }
      /**
      Call the given callback for every descendant node. Doesn't
      descend into a node when the callback returns `false`.
      */
      descendants(f) {
          this.nodesBetween(0, this.content.size, f);
      }
      /**
      Concatenates all the text nodes found in this fragment and its
      children.
      */
      get textContent() {
          return (this.isLeaf && this.type.spec.leafText)
              ? this.type.spec.leafText(this)
              : this.textBetween(0, this.content.size, "");
      }
      /**
      Get all text between positions `from` and `to`. When
      `blockSeparator` is given, it will be inserted to separate text
      from different block nodes. If `leafText` is given, it'll be
      inserted for every non-text leaf node encountered, otherwise
      [`leafText`](https://prosemirror.net/docs/ref/#model.NodeSpec^leafText) will be used.
      */
      textBetween(from, to, blockSeparator, leafText) {
          return this.content.textBetween(from, to, blockSeparator, leafText);
      }
      /**
      Returns this node's first child, or `null` if there are no
      children.
      */
      get firstChild() { return this.content.firstChild; }
      /**
      Returns this node's last child, or `null` if there are no
      children.
      */
      get lastChild() { return this.content.lastChild; }
      /**
      Test whether two nodes represent the same piece of document.
      */
      eq(other) {
          return this == other || (this.sameMarkup(other) && this.content.eq(other.content));
      }
      /**
      Compare the markup (type, attributes, and marks) of this node to
      those of another. Returns `true` if both have the same markup.
      */
      sameMarkup(other) {
          return this.hasMarkup(other.type, other.attrs, other.marks);
      }
      /**
      Check whether this node's markup correspond to the given type,
      attributes, and marks.
      */
      hasMarkup(type, attrs, marks) {
          return this.type == type &&
              compareDeep(this.attrs, attrs || type.defaultAttrs || emptyAttrs) &&
              Mark.sameSet(this.marks, marks || Mark.none);
      }
      /**
      Create a new node with the same markup as this node, containing
      the given content (or empty, if no content is given).
      */
      copy(content = null) {
          if (content == this.content)
              return this;
          return new Node(this.type, this.attrs, content, this.marks);
      }
      /**
      Create a copy of this node, with the given set of marks instead
      of the node's own marks.
      */
      mark(marks) {
          return marks == this.marks ? this : new Node(this.type, this.attrs, this.content, marks);
      }
      /**
      Create a copy of this node with only the content between the
      given positions. If `to` is not given, it defaults to the end of
      the node.
      */
      cut(from, to = this.content.size) {
          if (from == 0 && to == this.content.size)
              return this;
          return this.copy(this.content.cut(from, to));
      }
      /**
      Cut out the part of the document between the given positions, and
      return it as a `Slice` object.
      */
      slice(from, to = this.content.size, includeParents = false) {
          if (from == to)
              return Slice.empty;
          let $from = this.resolve(from), $to = this.resolve(to);
          let depth = includeParents ? 0 : $from.sharedDepth(to);
          let start = $from.start(depth), node = $from.node(depth);
          let content = node.content.cut($from.pos - start, $to.pos - start);
          return new Slice(content, $from.depth - depth, $to.depth - depth);
      }
      /**
      Replace the part of the document between the given positions with
      the given slice. The slice must 'fit', meaning its open sides
      must be able to connect to the surrounding content, and its
      content nodes must be valid children for the node they are placed
      into. If any of this is violated, an error of type
      [`ReplaceError`](https://prosemirror.net/docs/ref/#model.ReplaceError) is thrown.
      */
      replace(from, to, slice) {
          return replace(this.resolve(from), this.resolve(to), slice);
      }
      /**
      Find the node directly after the given position.
      */
      nodeAt(pos) {
          for (let node = this;;) {
              let { index, offset } = node.content.findIndex(pos);
              node = node.maybeChild(index);
              if (!node)
                  return null;
              if (offset == pos || node.isText)
                  return node;
              pos -= offset + 1;
          }
      }
      /**
      Find the (direct) child node after the given offset, if any,
      and return it along with its index and offset relative to this
      node.
      */
      childAfter(pos) {
          let { index, offset } = this.content.findIndex(pos);
          return { node: this.content.maybeChild(index), index, offset };
      }
      /**
      Find the (direct) child node before the given offset, if any,
      and return it along with its index and offset relative to this
      node.
      */
      childBefore(pos) {
          if (pos == 0)
              return { node: null, index: 0, offset: 0 };
          let { index, offset } = this.content.findIndex(pos);
          if (offset < pos)
              return { node: this.content.child(index), index, offset };
          let node = this.content.child(index - 1);
          return { node, index: index - 1, offset: offset - node.nodeSize };
      }
      /**
      Resolve the given position in the document, returning an
      [object](https://prosemirror.net/docs/ref/#model.ResolvedPos) with information about its context.
      */
      resolve(pos) { return ResolvedPos.resolveCached(this, pos); }
      /**
      @internal
      */
      resolveNoCache(pos) { return ResolvedPos.resolve(this, pos); }
      /**
      Test whether a given mark or mark type occurs in this document
      between the two given positions.
      */
      rangeHasMark(from, to, type) {
          let found = false;
          if (to > from)
              this.nodesBetween(from, to, node => {
                  if (type.isInSet(node.marks))
                      found = true;
                  return !found;
              });
          return found;
      }
      /**
      True when this is a block (non-inline node)
      */
      get isBlock() { return this.type.isBlock; }
      /**
      True when this is a textblock node, a block node with inline
      content.
      */
      get isTextblock() { return this.type.isTextblock; }
      /**
      True when this node allows inline content.
      */
      get inlineContent() { return this.type.inlineContent; }
      /**
      True when this is an inline node (a text node or a node that can
      appear among text).
      */
      get isInline() { return this.type.isInline; }
      /**
      True when this is a text node.
      */
      get isText() { return this.type.isText; }
      /**
      True when this is a leaf node.
      */
      get isLeaf() { return this.type.isLeaf; }
      /**
      True when this is an atom, i.e. when it does not have directly
      editable content. This is usually the same as `isLeaf`, but can
      be configured with the [`atom` property](https://prosemirror.net/docs/ref/#model.NodeSpec.atom)
      on a node's spec (typically used when the node is displayed as
      an uneditable [node view](https://prosemirror.net/docs/ref/#view.NodeView)).
      */
      get isAtom() { return this.type.isAtom; }
      /**
      Return a string representation of this node for debugging
      purposes.
      */
      toString() {
          if (this.type.spec.toDebugString)
              return this.type.spec.toDebugString(this);
          let name = this.type.name;
          if (this.content.size)
              name += "(" + this.content.toStringInner() + ")";
          return wrapMarks(this.marks, name);
      }
      /**
      Get the content match in this node at the given index.
      */
      contentMatchAt(index) {
          let match = this.type.contentMatch.matchFragment(this.content, 0, index);
          if (!match)
              throw new Error("Called contentMatchAt on a node with invalid content");
          return match;
      }
      /**
      Test whether replacing the range between `from` and `to` (by
      child index) with the given replacement fragment (which defaults
      to the empty fragment) would leave the node's content valid. You
      can optionally pass `start` and `end` indices into the
      replacement fragment.
      */
      canReplace(from, to, replacement = Fragment.empty, start = 0, end = replacement.childCount) {
          let one = this.contentMatchAt(from).matchFragment(replacement, start, end);
          let two = one && one.matchFragment(this.content, to);
          if (!two || !two.validEnd)
              return false;
          for (let i = start; i < end; i++)
              if (!this.type.allowsMarks(replacement.child(i).marks))
                  return false;
          return true;
      }
      /**
      Test whether replacing the range `from` to `to` (by index) with
      a node of the given type would leave the node's content valid.
      */
      canReplaceWith(from, to, type, marks) {
          if (marks && !this.type.allowsMarks(marks))
              return false;
          let start = this.contentMatchAt(from).matchType(type);
          let end = start && start.matchFragment(this.content, to);
          return end ? end.validEnd : false;
      }
      /**
      Test whether the given node's content could be appended to this
      node. If that node is empty, this will only return true if there
      is at least one node type that can appear in both nodes (to avoid
      merging completely incompatible nodes).
      */
      canAppend(other) {
          if (other.content.size)
              return this.canReplace(this.childCount, this.childCount, other.content);
          else
              return this.type.compatibleContent(other.type);
      }
      /**
      Check whether this node and its descendants conform to the
      schema, and raise error when they do not.
      */
      check() {
          this.type.checkContent(this.content);
          let copy = Mark.none;
          for (let i = 0; i < this.marks.length; i++)
              copy = this.marks[i].addToSet(copy);
          if (!Mark.sameSet(copy, this.marks))
              throw new RangeError(`Invalid collection of marks for node ${this.type.name}: ${this.marks.map(m => m.type.name)}`);
          this.content.forEach(node => node.check());
      }
      /**
      Return a JSON-serializeable representation of this node.
      */
      toJSON() {
          let obj = { type: this.type.name };
          for (let _ in this.attrs) {
              obj.attrs = this.attrs;
              break;
          }
          if (this.content.size)
              obj.content = this.content.toJSON();
          if (this.marks.length)
              obj.marks = this.marks.map(n => n.toJSON());
          return obj;
      }
      /**
      Deserialize a node from its JSON representation.
      */
      static fromJSON(schema, json) {
          if (!json)
              throw new RangeError("Invalid input for Node.fromJSON");
          let marks = null;
          if (json.marks) {
              if (!Array.isArray(json.marks))
                  throw new RangeError("Invalid mark data for Node.fromJSON");
              marks = json.marks.map(schema.markFromJSON);
          }
          if (json.type == "text") {
              if (typeof json.text != "string")
                  throw new RangeError("Invalid text node in JSON");
              return schema.text(json.text, marks);
          }
          let content = Fragment.fromJSON(schema, json.content);
          return schema.nodeType(json.type).create(json.attrs, content, marks);
      }
  }
  Node.prototype.text = undefined;
  class TextNode extends Node {
      /**
      @internal
      */
      constructor(type, attrs, content, marks) {
          super(type, attrs, null, marks);
          if (!content)
              throw new RangeError("Empty text nodes are not allowed");
          this.text = content;
      }
      toString() {
          if (this.type.spec.toDebugString)
              return this.type.spec.toDebugString(this);
          return wrapMarks(this.marks, JSON.stringify(this.text));
      }
      get textContent() { return this.text; }
      textBetween(from, to) { return this.text.slice(from, to); }
      get nodeSize() { return this.text.length; }
      mark(marks) {
          return marks == this.marks ? this : new TextNode(this.type, this.attrs, this.text, marks);
      }
      withText(text) {
          if (text == this.text)
              return this;
          return new TextNode(this.type, this.attrs, text, this.marks);
      }
      cut(from = 0, to = this.text.length) {
          if (from == 0 && to == this.text.length)
              return this;
          return this.withText(this.text.slice(from, to));
      }
      eq(other) {
          return this.sameMarkup(other) && this.text == other.text;
      }
      toJSON() {
          let base = super.toJSON();
          base.text = this.text;
          return base;
      }
  }
  function wrapMarks(marks, str) {
      for (let i = marks.length - 1; i >= 0; i--)
          str = marks[i].type.name + "(" + str + ")";
      return str;
  }

  /**
  Instances of this class represent a match state of a node type's
  [content expression](https://prosemirror.net/docs/ref/#model.NodeSpec.content), and can be used to
  find out whether further content matches here, and whether a given
  position is a valid end of the node.
  */
  class ContentMatch {
      /**
      @internal
      */
      constructor(
      /**
      True when this match state represents a valid end of the node.
      */
      validEnd) {
          this.validEnd = validEnd;
          /**
          @internal
          */
          this.next = [];
          /**
          @internal
          */
          this.wrapCache = [];
      }
      /**
      @internal
      */
      static parse(string, nodeTypes) {
          let stream = new TokenStream(string, nodeTypes);
          if (stream.next == null)
              return ContentMatch.empty;
          let expr = parseExpr(stream);
          if (stream.next)
              stream.err("Unexpected trailing text");
          let match = dfa(nfa(expr));
          checkForDeadEnds(match, stream);
          return match;
      }
      /**
      Match a node type, returning a match after that node if
      successful.
      */
      matchType(type) {
          for (let i = 0; i < this.next.length; i++)
              if (this.next[i].type == type)
                  return this.next[i].next;
          return null;
      }
      /**
      Try to match a fragment. Returns the resulting match when
      successful.
      */
      matchFragment(frag, start = 0, end = frag.childCount) {
          let cur = this;
          for (let i = start; cur && i < end; i++)
              cur = cur.matchType(frag.child(i).type);
          return cur;
      }
      /**
      @internal
      */
      get inlineContent() {
          return this.next.length != 0 && this.next[0].type.isInline;
      }
      /**
      Get the first matching node type at this match position that can
      be generated.
      */
      get defaultType() {
          for (let i = 0; i < this.next.length; i++) {
              let { type } = this.next[i];
              if (!(type.isText || type.hasRequiredAttrs()))
                  return type;
          }
          return null;
      }
      /**
      @internal
      */
      compatible(other) {
          for (let i = 0; i < this.next.length; i++)
              for (let j = 0; j < other.next.length; j++)
                  if (this.next[i].type == other.next[j].type)
                      return true;
          return false;
      }
      /**
      Try to match the given fragment, and if that fails, see if it can
      be made to match by inserting nodes in front of it. When
      successful, return a fragment of inserted nodes (which may be
      empty if nothing had to be inserted). When `toEnd` is true, only
      return a fragment if the resulting match goes to the end of the
      content expression.
      */
      fillBefore(after, toEnd = false, startIndex = 0) {
          let seen = [this];
          function search(match, types) {
              let finished = match.matchFragment(after, startIndex);
              if (finished && (!toEnd || finished.validEnd))
                  return Fragment.from(types.map(tp => tp.createAndFill()));
              for (let i = 0; i < match.next.length; i++) {
                  let { type, next } = match.next[i];
                  if (!(type.isText || type.hasRequiredAttrs()) && seen.indexOf(next) == -1) {
                      seen.push(next);
                      let found = search(next, types.concat(type));
                      if (found)
                          return found;
                  }
              }
              return null;
          }
          return search(this, []);
      }
      /**
      Find a set of wrapping node types that would allow a node of the
      given type to appear at this position. The result may be empty
      (when it fits directly) and will be null when no such wrapping
      exists.
      */
      findWrapping(target) {
          for (let i = 0; i < this.wrapCache.length; i += 2)
              if (this.wrapCache[i] == target)
                  return this.wrapCache[i + 1];
          let computed = this.computeWrapping(target);
          this.wrapCache.push(target, computed);
          return computed;
      }
      /**
      @internal
      */
      computeWrapping(target) {
          let seen = Object.create(null), active = [{ match: this, type: null, via: null }];
          while (active.length) {
              let current = active.shift(), match = current.match;
              if (match.matchType(target)) {
                  let result = [];
                  for (let obj = current; obj.type; obj = obj.via)
                      result.push(obj.type);
                  return result.reverse();
              }
              for (let i = 0; i < match.next.length; i++) {
                  let { type, next } = match.next[i];
                  if (!type.isLeaf && !type.hasRequiredAttrs() && !(type.name in seen) && (!current.type || next.validEnd)) {
                      active.push({ match: type.contentMatch, type, via: current });
                      seen[type.name] = true;
                  }
              }
          }
          return null;
      }
      /**
      The number of outgoing edges this node has in the finite
      automaton that describes the content expression.
      */
      get edgeCount() {
          return this.next.length;
      }
      /**
      Get the _n_​th outgoing edge from this node in the finite
      automaton that describes the content expression.
      */
      edge(n) {
          if (n >= this.next.length)
              throw new RangeError(`There's no ${n}th edge in this content match`);
          return this.next[n];
      }
      /**
      @internal
      */
      toString() {
          let seen = [];
          function scan(m) {
              seen.push(m);
              for (let i = 0; i < m.next.length; i++)
                  if (seen.indexOf(m.next[i].next) == -1)
                      scan(m.next[i].next);
          }
          scan(this);
          return seen.map((m, i) => {
              let out = i + (m.validEnd ? "*" : " ") + " ";
              for (let i = 0; i < m.next.length; i++)
                  out += (i ? ", " : "") + m.next[i].type.name + "->" + seen.indexOf(m.next[i].next);
              return out;
          }).join("\n");
      }
  }
  /**
  @internal
  */
  ContentMatch.empty = new ContentMatch(true);
  class TokenStream {
      constructor(string, nodeTypes) {
          this.string = string;
          this.nodeTypes = nodeTypes;
          this.inline = null;
          this.pos = 0;
          this.tokens = string.split(/\s*(?=\b|\W|$)/);
          if (this.tokens[this.tokens.length - 1] == "")
              this.tokens.pop();
          if (this.tokens[0] == "")
              this.tokens.shift();
      }
      get next() { return this.tokens[this.pos]; }
      eat(tok) { return this.next == tok && (this.pos++ || true); }
      err(str) { throw new SyntaxError(str + " (in content expression '" + this.string + "')"); }
  }
  function parseExpr(stream) {
      let exprs = [];
      do {
          exprs.push(parseExprSeq(stream));
      } while (stream.eat("|"));
      return exprs.length == 1 ? exprs[0] : { type: "choice", exprs };
  }
  function parseExprSeq(stream) {
      let exprs = [];
      do {
          exprs.push(parseExprSubscript(stream));
      } while (stream.next && stream.next != ")" && stream.next != "|");
      return exprs.length == 1 ? exprs[0] : { type: "seq", exprs };
  }
  function parseExprSubscript(stream) {
      let expr = parseExprAtom(stream);
      for (;;) {
          if (stream.eat("+"))
              expr = { type: "plus", expr };
          else if (stream.eat("*"))
              expr = { type: "star", expr };
          else if (stream.eat("?"))
              expr = { type: "opt", expr };
          else if (stream.eat("{"))
              expr = parseExprRange(stream, expr);
          else
              break;
      }
      return expr;
  }
  function parseNum(stream) {
      if (/\D/.test(stream.next))
          stream.err("Expected number, got '" + stream.next + "'");
      let result = Number(stream.next);
      stream.pos++;
      return result;
  }
  function parseExprRange(stream, expr) {
      let min = parseNum(stream), max = min;
      if (stream.eat(",")) {
          if (stream.next != "}")
              max = parseNum(stream);
          else
              max = -1;
      }
      if (!stream.eat("}"))
          stream.err("Unclosed braced range");
      return { type: "range", min, max, expr };
  }
  function resolveName(stream, name) {
      let types = stream.nodeTypes, type = types[name];
      if (type)
          return [type];
      let result = [];
      for (let typeName in types) {
          let type = types[typeName];
          if (type.groups.indexOf(name) > -1)
              result.push(type);
      }
      if (result.length == 0)
          stream.err("No node type or group '" + name + "' found");
      return result;
  }
  function parseExprAtom(stream) {
      if (stream.eat("(")) {
          let expr = parseExpr(stream);
          if (!stream.eat(")"))
              stream.err("Missing closing paren");
          return expr;
      }
      else if (!/\W/.test(stream.next)) {
          let exprs = resolveName(stream, stream.next).map(type => {
              if (stream.inline == null)
                  stream.inline = type.isInline;
              else if (stream.inline != type.isInline)
                  stream.err("Mixing inline and block content");
              return { type: "name", value: type };
          });
          stream.pos++;
          return exprs.length == 1 ? exprs[0] : { type: "choice", exprs };
      }
      else {
          stream.err("Unexpected token '" + stream.next + "'");
      }
  }
  /**
  Construct an NFA from an expression as returned by the parser. The
  NFA is represented as an array of states, which are themselves
  arrays of edges, which are `{term, to}` objects. The first state is
  the entry state and the last node is the success state.

  Note that unlike typical NFAs, the edge ordering in this one is
  significant, in that it is used to contruct filler content when
  necessary.
  */
  function nfa(expr) {
      let nfa = [[]];
      connect(compile(expr, 0), node());
      return nfa;
      function node() { return nfa.push([]) - 1; }
      function edge(from, to, term) {
          let edge = { term, to };
          nfa[from].push(edge);
          return edge;
      }
      function connect(edges, to) {
          edges.forEach(edge => edge.to = to);
      }
      function compile(expr, from) {
          if (expr.type == "choice") {
              return expr.exprs.reduce((out, expr) => out.concat(compile(expr, from)), []);
          }
          else if (expr.type == "seq") {
              for (let i = 0;; i++) {
                  let next = compile(expr.exprs[i], from);
                  if (i == expr.exprs.length - 1)
                      return next;
                  connect(next, from = node());
              }
          }
          else if (expr.type == "star") {
              let loop = node();
              edge(from, loop);
              connect(compile(expr.expr, loop), loop);
              return [edge(loop)];
          }
          else if (expr.type == "plus") {
              let loop = node();
              connect(compile(expr.expr, from), loop);
              connect(compile(expr.expr, loop), loop);
              return [edge(loop)];
          }
          else if (expr.type == "opt") {
              return [edge(from)].concat(compile(expr.expr, from));
          }
          else if (expr.type == "range") {
              let cur = from;
              for (let i = 0; i < expr.min; i++) {
                  let next = node();
                  connect(compile(expr.expr, cur), next);
                  cur = next;
              }
              if (expr.max == -1) {
                  connect(compile(expr.expr, cur), cur);
              }
              else {
                  for (let i = expr.min; i < expr.max; i++) {
                      let next = node();
                      edge(cur, next);
                      connect(compile(expr.expr, cur), next);
                      cur = next;
                  }
              }
              return [edge(cur)];
          }
          else if (expr.type == "name") {
              return [edge(from, undefined, expr.value)];
          }
          else {
              throw new Error("Unknown expr type");
          }
      }
  }
  function cmp(a, b) { return b - a; }
  // Get the set of nodes reachable by null edges from `node`. Omit
  // nodes with only a single null-out-edge, since they may lead to
  // needless duplicated nodes.
  function nullFrom(nfa, node) {
      let result = [];
      scan(node);
      return result.sort(cmp);
      function scan(node) {
          let edges = nfa[node];
          if (edges.length == 1 && !edges[0].term)
              return scan(edges[0].to);
          result.push(node);
          for (let i = 0; i < edges.length; i++) {
              let { term, to } = edges[i];
              if (!term && result.indexOf(to) == -1)
                  scan(to);
          }
      }
  }
  // Compiles an NFA as produced by `nfa` into a DFA, modeled as a set
  // of state objects (`ContentMatch` instances) with transitions
  // between them.
  function dfa(nfa) {
      let labeled = Object.create(null);
      return explore(nullFrom(nfa, 0));
      function explore(states) {
          let out = [];
          states.forEach(node => {
              nfa[node].forEach(({ term, to }) => {
                  if (!term)
                      return;
                  let set;
                  for (let i = 0; i < out.length; i++)
                      if (out[i][0] == term)
                          set = out[i][1];
                  nullFrom(nfa, to).forEach(node => {
                      if (!set)
                          out.push([term, set = []]);
                      if (set.indexOf(node) == -1)
                          set.push(node);
                  });
              });
          });
          let state = labeled[states.join(",")] = new ContentMatch(states.indexOf(nfa.length - 1) > -1);
          for (let i = 0; i < out.length; i++) {
              let states = out[i][1].sort(cmp);
              state.next.push({ type: out[i][0], next: labeled[states.join(",")] || explore(states) });
          }
          return state;
      }
  }
  function checkForDeadEnds(match, stream) {
      for (let i = 0, work = [match]; i < work.length; i++) {
          let state = work[i], dead = !state.validEnd, nodes = [];
          for (let j = 0; j < state.next.length; j++) {
              let { type, next } = state.next[j];
              nodes.push(type.name);
              if (dead && !(type.isText || type.hasRequiredAttrs()))
                  dead = false;
              if (work.indexOf(next) == -1)
                  work.push(next);
          }
          if (dead)
              stream.err("Only non-generatable nodes (" + nodes.join(", ") + ") in a required position (see https://prosemirror.net/docs/guide/#generatable)");
      }
  }

  // For node types where all attrs have a default value (or which don't
  // have any attributes), build up a single reusable default attribute
  // object, and use it for all nodes that don't specify specific
  // attributes.
  function defaultAttrs(attrs) {
      let defaults = Object.create(null);
      for (let attrName in attrs) {
          let attr = attrs[attrName];
          if (!attr.hasDefault)
              return null;
          defaults[attrName] = attr.default;
      }
      return defaults;
  }
  function computeAttrs(attrs, value) {
      let built = Object.create(null);
      for (let name in attrs) {
          let given = value && value[name];
          if (given === undefined) {
              let attr = attrs[name];
              if (attr.hasDefault)
                  given = attr.default;
              else
                  throw new RangeError("No value supplied for attribute " + name);
          }
          built[name] = given;
      }
      return built;
  }
  function initAttrs(attrs) {
      let result = Object.create(null);
      if (attrs)
          for (let name in attrs)
              result[name] = new Attribute(attrs[name]);
      return result;
  }
  /**
  Node types are objects allocated once per `Schema` and used to
  [tag](https://prosemirror.net/docs/ref/#model.Node.type) `Node` instances. They contain information
  about the node type, such as its name and what kind of node it
  represents.
  */
  let NodeType$1 = class NodeType {
      /**
      @internal
      */
      constructor(
      /**
      The name the node type has in this schema.
      */
      name, 
      /**
      A link back to the `Schema` the node type belongs to.
      */
      schema, 
      /**
      The spec that this type is based on
      */
      spec) {
          this.name = name;
          this.schema = schema;
          this.spec = spec;
          /**
          The set of marks allowed in this node. `null` means all marks
          are allowed.
          */
          this.markSet = null;
          this.groups = spec.group ? spec.group.split(" ") : [];
          this.attrs = initAttrs(spec.attrs);
          this.defaultAttrs = defaultAttrs(this.attrs);
          this.contentMatch = null;
          this.inlineContent = null;
          this.isBlock = !(spec.inline || name == "text");
          this.isText = name == "text";
      }
      /**
      True if this is an inline type.
      */
      get isInline() { return !this.isBlock; }
      /**
      True if this is a textblock type, a block that contains inline
      content.
      */
      get isTextblock() { return this.isBlock && this.inlineContent; }
      /**
      True for node types that allow no content.
      */
      get isLeaf() { return this.contentMatch == ContentMatch.empty; }
      /**
      True when this node is an atom, i.e. when it does not have
      directly editable content.
      */
      get isAtom() { return this.isLeaf || !!this.spec.atom; }
      /**
      The node type's [whitespace](https://prosemirror.net/docs/ref/#model.NodeSpec.whitespace) option.
      */
      get whitespace() {
          return this.spec.whitespace || (this.spec.code ? "pre" : "normal");
      }
      /**
      Tells you whether this node type has any required attributes.
      */
      hasRequiredAttrs() {
          for (let n in this.attrs)
              if (this.attrs[n].isRequired)
                  return true;
          return false;
      }
      /**
      Indicates whether this node allows some of the same content as
      the given node type.
      */
      compatibleContent(other) {
          return this == other || this.contentMatch.compatible(other.contentMatch);
      }
      /**
      @internal
      */
      computeAttrs(attrs) {
          if (!attrs && this.defaultAttrs)
              return this.defaultAttrs;
          else
              return computeAttrs(this.attrs, attrs);
      }
      /**
      Create a `Node` of this type. The given attributes are
      checked and defaulted (you can pass `null` to use the type's
      defaults entirely, if no required attributes exist). `content`
      may be a `Fragment`, a node, an array of nodes, or
      `null`. Similarly `marks` may be `null` to default to the empty
      set of marks.
      */
      create(attrs = null, content, marks) {
          if (this.isText)
              throw new Error("NodeType.create can't construct text nodes");
          return new Node(this, this.computeAttrs(attrs), Fragment.from(content), Mark.setFrom(marks));
      }
      /**
      Like [`create`](https://prosemirror.net/docs/ref/#model.NodeType.create), but check the given content
      against the node type's content restrictions, and throw an error
      if it doesn't match.
      */
      createChecked(attrs = null, content, marks) {
          content = Fragment.from(content);
          this.checkContent(content);
          return new Node(this, this.computeAttrs(attrs), content, Mark.setFrom(marks));
      }
      /**
      Like [`create`](https://prosemirror.net/docs/ref/#model.NodeType.create), but see if it is
      necessary to add nodes to the start or end of the given fragment
      to make it fit the node. If no fitting wrapping can be found,
      return null. Note that, due to the fact that required nodes can
      always be created, this will always succeed if you pass null or
      `Fragment.empty` as content.
      */
      createAndFill(attrs = null, content, marks) {
          attrs = this.computeAttrs(attrs);
          content = Fragment.from(content);
          if (content.size) {
              let before = this.contentMatch.fillBefore(content);
              if (!before)
                  return null;
              content = before.append(content);
          }
          let matched = this.contentMatch.matchFragment(content);
          let after = matched && matched.fillBefore(Fragment.empty, true);
          if (!after)
              return null;
          return new Node(this, attrs, content.append(after), Mark.setFrom(marks));
      }
      /**
      Returns true if the given fragment is valid content for this node
      type with the given attributes.
      */
      validContent(content) {
          let result = this.contentMatch.matchFragment(content);
          if (!result || !result.validEnd)
              return false;
          for (let i = 0; i < content.childCount; i++)
              if (!this.allowsMarks(content.child(i).marks))
                  return false;
          return true;
      }
      /**
      Throws a RangeError if the given fragment is not valid content for this
      node type.
      @internal
      */
      checkContent(content) {
          if (!this.validContent(content))
              throw new RangeError(`Invalid content for node ${this.name}: ${content.toString().slice(0, 50)}`);
      }
      /**
      Check whether the given mark type is allowed in this node.
      */
      allowsMarkType(markType) {
          return this.markSet == null || this.markSet.indexOf(markType) > -1;
      }
      /**
      Test whether the given set of marks are allowed in this node.
      */
      allowsMarks(marks) {
          if (this.markSet == null)
              return true;
          for (let i = 0; i < marks.length; i++)
              if (!this.allowsMarkType(marks[i].type))
                  return false;
          return true;
      }
      /**
      Removes the marks that are not allowed in this node from the given set.
      */
      allowedMarks(marks) {
          if (this.markSet == null)
              return marks;
          let copy;
          for (let i = 0; i < marks.length; i++) {
              if (!this.allowsMarkType(marks[i].type)) {
                  if (!copy)
                      copy = marks.slice(0, i);
              }
              else if (copy) {
                  copy.push(marks[i]);
              }
          }
          return !copy ? marks : copy.length ? copy : Mark.none;
      }
      /**
      @internal
      */
      static compile(nodes, schema) {
          let result = Object.create(null);
          nodes.forEach((name, spec) => result[name] = new NodeType(name, schema, spec));
          let topType = schema.spec.topNode || "doc";
          if (!result[topType])
              throw new RangeError("Schema is missing its top node type ('" + topType + "')");
          if (!result.text)
              throw new RangeError("Every schema needs a 'text' type");
          for (let _ in result.text.attrs)
              throw new RangeError("The text node type should not have attributes");
          return result;
      }
  };
  // Attribute descriptors
  class Attribute {
      constructor(options) {
          this.hasDefault = Object.prototype.hasOwnProperty.call(options, "default");
          this.default = options.default;
      }
      get isRequired() {
          return !this.hasDefault;
      }
  }
  // Marks
  /**
  Like nodes, marks (which are associated with nodes to signify
  things like emphasis or being part of a link) are
  [tagged](https://prosemirror.net/docs/ref/#model.Mark.type) with type objects, which are
  instantiated once per `Schema`.
  */
  class MarkType {
      /**
      @internal
      */
      constructor(
      /**
      The name of the mark type.
      */
      name, 
      /**
      @internal
      */
      rank, 
      /**
      The schema that this mark type instance is part of.
      */
      schema, 
      /**
      The spec on which the type is based.
      */
      spec) {
          this.name = name;
          this.rank = rank;
          this.schema = schema;
          this.spec = spec;
          this.attrs = initAttrs(spec.attrs);
          this.excluded = null;
          let defaults = defaultAttrs(this.attrs);
          this.instance = defaults ? new Mark(this, defaults) : null;
      }
      /**
      Create a mark of this type. `attrs` may be `null` or an object
      containing only some of the mark's attributes. The others, if
      they have defaults, will be added.
      */
      create(attrs = null) {
          if (!attrs && this.instance)
              return this.instance;
          return new Mark(this, computeAttrs(this.attrs, attrs));
      }
      /**
      @internal
      */
      static compile(marks, schema) {
          let result = Object.create(null), rank = 0;
          marks.forEach((name, spec) => result[name] = new MarkType(name, rank++, schema, spec));
          return result;
      }
      /**
      When there is a mark of this type in the given set, a new set
      without it is returned. Otherwise, the input set is returned.
      */
      removeFromSet(set) {
          for (var i = 0; i < set.length; i++)
              if (set[i].type == this) {
                  set = set.slice(0, i).concat(set.slice(i + 1));
                  i--;
              }
          return set;
      }
      /**
      Tests whether there is a mark of this type in the given set.
      */
      isInSet(set) {
          for (let i = 0; i < set.length; i++)
              if (set[i].type == this)
                  return set[i];
      }
      /**
      Queries whether a given mark type is
      [excluded](https://prosemirror.net/docs/ref/#model.MarkSpec.excludes) by this one.
      */
      excludes(other) {
          return this.excluded.indexOf(other) > -1;
      }
  }
  /**
  A document schema. Holds [node](https://prosemirror.net/docs/ref/#model.NodeType) and [mark
  type](https://prosemirror.net/docs/ref/#model.MarkType) objects for the nodes and marks that may
  occur in conforming documents, and provides functionality for
  creating and deserializing such documents.

  When given, the type parameters provide the names of the nodes and
  marks in this schema.
  */
  class Schema {
      /**
      Construct a schema from a schema [specification](https://prosemirror.net/docs/ref/#model.SchemaSpec).
      */
      constructor(spec) {
          /**
          The [linebreak
          replacement](https://prosemirror.net/docs/ref/#model.NodeSpec.linebreakReplacement) node defined
          in this schema, if any.
          */
          this.linebreakReplacement = null;
          /**
          An object for storing whatever values modules may want to
          compute and cache per schema. (If you want to store something
          in it, try to use property names unlikely to clash.)
          */
          this.cached = Object.create(null);
          let instanceSpec = this.spec = {};
          for (let prop in spec)
              instanceSpec[prop] = spec[prop];
          instanceSpec.nodes = OrderedMap.from(spec.nodes),
              instanceSpec.marks = OrderedMap.from(spec.marks || {}),
              this.nodes = NodeType$1.compile(this.spec.nodes, this);
          this.marks = MarkType.compile(this.spec.marks, this);
          let contentExprCache = Object.create(null);
          for (let prop in this.nodes) {
              if (prop in this.marks)
                  throw new RangeError(prop + " can not be both a node and a mark");
              let type = this.nodes[prop], contentExpr = type.spec.content || "", markExpr = type.spec.marks;
              type.contentMatch = contentExprCache[contentExpr] ||
                  (contentExprCache[contentExpr] = ContentMatch.parse(contentExpr, this.nodes));
              type.inlineContent = type.contentMatch.inlineContent;
              if (type.spec.linebreakReplacement) {
                  if (this.linebreakReplacement)
                      throw new RangeError("Multiple linebreak nodes defined");
                  if (!type.isInline || !type.isLeaf)
                      throw new RangeError("Linebreak replacement nodes must be inline leaf nodes");
                  this.linebreakReplacement = type;
              }
              type.markSet = markExpr == "_" ? null :
                  markExpr ? gatherMarks(this, markExpr.split(" ")) :
                      markExpr == "" || !type.inlineContent ? [] : null;
          }
          for (let prop in this.marks) {
              let type = this.marks[prop], excl = type.spec.excludes;
              type.excluded = excl == null ? [type] : excl == "" ? [] : gatherMarks(this, excl.split(" "));
          }
          this.nodeFromJSON = this.nodeFromJSON.bind(this);
          this.markFromJSON = this.markFromJSON.bind(this);
          this.topNodeType = this.nodes[this.spec.topNode || "doc"];
          this.cached.wrappings = Object.create(null);
      }
      /**
      Create a node in this schema. The `type` may be a string or a
      `NodeType` instance. Attributes will be extended with defaults,
      `content` may be a `Fragment`, `null`, a `Node`, or an array of
      nodes.
      */
      node(type, attrs = null, content, marks) {
          if (typeof type == "string")
              type = this.nodeType(type);
          else if (!(type instanceof NodeType$1))
              throw new RangeError("Invalid node type: " + type);
          else if (type.schema != this)
              throw new RangeError("Node type from different schema used (" + type.name + ")");
          return type.createChecked(attrs, content, marks);
      }
      /**
      Create a text node in the schema. Empty text nodes are not
      allowed.
      */
      text(text, marks) {
          let type = this.nodes.text;
          return new TextNode(type, type.defaultAttrs, text, Mark.setFrom(marks));
      }
      /**
      Create a mark with the given type and attributes.
      */
      mark(type, attrs) {
          if (typeof type == "string")
              type = this.marks[type];
          return type.create(attrs);
      }
      /**
      Deserialize a node from its JSON representation. This method is
      bound.
      */
      nodeFromJSON(json) {
          return Node.fromJSON(this, json);
      }
      /**
      Deserialize a mark from its JSON representation. This method is
      bound.
      */
      markFromJSON(json) {
          return Mark.fromJSON(this, json);
      }
      /**
      @internal
      */
      nodeType(name) {
          let found = this.nodes[name];
          if (!found)
              throw new RangeError("Unknown node type: " + name);
          return found;
      }
  }
  function gatherMarks(schema, marks) {
      let found = [];
      for (let i = 0; i < marks.length; i++) {
          let name = marks[i], mark = schema.marks[name], ok = mark;
          if (mark) {
              found.push(mark);
          }
          else {
              for (let prop in schema.marks) {
                  let mark = schema.marks[prop];
                  if (name == "_" || (mark.spec.group && mark.spec.group.split(" ").indexOf(name) > -1))
                      found.push(ok = mark);
              }
          }
          if (!ok)
              throw new SyntaxError("Unknown mark type: '" + marks[i] + "'");
      }
      return found;
  }

  function isTagRule(rule) { return rule.tag != null; }
  function isStyleRule(rule) { return rule.style != null; }
  /**
  A DOM parser represents a strategy for parsing DOM content into a
  ProseMirror document conforming to a given schema. Its behavior is
  defined by an array of [rules](https://prosemirror.net/docs/ref/#model.ParseRule).
  */
  let DOMParser$1 = class DOMParser {
      /**
      Create a parser that targets the given schema, using the given
      parsing rules.
      */
      constructor(
      /**
      The schema into which the parser parses.
      */
      schema, 
      /**
      The set of [parse rules](https://prosemirror.net/docs/ref/#model.ParseRule) that the parser
      uses, in order of precedence.
      */
      rules) {
          this.schema = schema;
          this.rules = rules;
          /**
          @internal
          */
          this.tags = [];
          /**
          @internal
          */
          this.styles = [];
          rules.forEach(rule => {
              if (isTagRule(rule))
                  this.tags.push(rule);
              else if (isStyleRule(rule))
                  this.styles.push(rule);
          });
          // Only normalize list elements when lists in the schema can't directly contain themselves
          this.normalizeLists = !this.tags.some(r => {
              if (!/^(ul|ol)\b/.test(r.tag) || !r.node)
                  return false;
              let node = schema.nodes[r.node];
              return node.contentMatch.matchType(node);
          });
      }
      /**
      Parse a document from the content of a DOM node.
      */
      parse(dom, options = {}) {
          let context = new ParseContext(this, options, false);
          context.addAll(dom, options.from, options.to);
          return context.finish();
      }
      /**
      Parses the content of the given DOM node, like
      [`parse`](https://prosemirror.net/docs/ref/#model.DOMParser.parse), and takes the same set of
      options. But unlike that method, which produces a whole node,
      this one returns a slice that is open at the sides, meaning that
      the schema constraints aren't applied to the start of nodes to
      the left of the input and the end of nodes at the end.
      */
      parseSlice(dom, options = {}) {
          let context = new ParseContext(this, options, true);
          context.addAll(dom, options.from, options.to);
          return Slice.maxOpen(context.finish());
      }
      /**
      @internal
      */
      matchTag(dom, context, after) {
          for (let i = after ? this.tags.indexOf(after) + 1 : 0; i < this.tags.length; i++) {
              let rule = this.tags[i];
              if (matches(dom, rule.tag) &&
                  (rule.namespace === undefined || dom.namespaceURI == rule.namespace) &&
                  (!rule.context || context.matchesContext(rule.context))) {
                  if (rule.getAttrs) {
                      let result = rule.getAttrs(dom);
                      if (result === false)
                          continue;
                      rule.attrs = result || undefined;
                  }
                  return rule;
              }
          }
      }
      /**
      @internal
      */
      matchStyle(prop, value, context, after) {
          for (let i = after ? this.styles.indexOf(after) + 1 : 0; i < this.styles.length; i++) {
              let rule = this.styles[i], style = rule.style;
              if (style.indexOf(prop) != 0 ||
                  rule.context && !context.matchesContext(rule.context) ||
                  // Test that the style string either precisely matches the prop,
                  // or has an '=' sign after the prop, followed by the given
                  // value.
                  style.length > prop.length &&
                      (style.charCodeAt(prop.length) != 61 || style.slice(prop.length + 1) != value))
                  continue;
              if (rule.getAttrs) {
                  let result = rule.getAttrs(value);
                  if (result === false)
                      continue;
                  rule.attrs = result || undefined;
              }
              return rule;
          }
      }
      /**
      @internal
      */
      static schemaRules(schema) {
          let result = [];
          function insert(rule) {
              let priority = rule.priority == null ? 50 : rule.priority, i = 0;
              for (; i < result.length; i++) {
                  let next = result[i], nextPriority = next.priority == null ? 50 : next.priority;
                  if (nextPriority < priority)
                      break;
              }
              result.splice(i, 0, rule);
          }
          for (let name in schema.marks) {
              let rules = schema.marks[name].spec.parseDOM;
              if (rules)
                  rules.forEach(rule => {
                      insert(rule = copy(rule));
                      if (!(rule.mark || rule.ignore || rule.clearMark))
                          rule.mark = name;
                  });
          }
          for (let name in schema.nodes) {
              let rules = schema.nodes[name].spec.parseDOM;
              if (rules)
                  rules.forEach(rule => {
                      insert(rule = copy(rule));
                      if (!(rule.node || rule.ignore || rule.mark))
                          rule.node = name;
                  });
          }
          return result;
      }
      /**
      Construct a DOM parser using the parsing rules listed in a
      schema's [node specs](https://prosemirror.net/docs/ref/#model.NodeSpec.parseDOM), reordered by
      [priority](https://prosemirror.net/docs/ref/#model.ParseRule.priority).
      */
      static fromSchema(schema) {
          return schema.cached.domParser ||
              (schema.cached.domParser = new DOMParser(schema, DOMParser.schemaRules(schema)));
      }
  };
  const blockTags = {
      address: true, article: true, aside: true, blockquote: true, canvas: true,
      dd: true, div: true, dl: true, fieldset: true, figcaption: true, figure: true,
      footer: true, form: true, h1: true, h2: true, h3: true, h4: true, h5: true,
      h6: true, header: true, hgroup: true, hr: true, li: true, noscript: true, ol: true,
      output: true, p: true, pre: true, section: true, table: true, tfoot: true, ul: true
  };
  const ignoreTags = {
      head: true, noscript: true, object: true, script: true, style: true, title: true
  };
  const listTags = { ol: true, ul: true };
  // Using a bitfield for node context options
  const OPT_PRESERVE_WS = 1, OPT_PRESERVE_WS_FULL = 2, OPT_OPEN_LEFT = 4;
  function wsOptionsFor(type, preserveWhitespace, base) {
      if (preserveWhitespace != null)
          return (preserveWhitespace ? OPT_PRESERVE_WS : 0) |
              (preserveWhitespace === "full" ? OPT_PRESERVE_WS_FULL : 0);
      return type && type.whitespace == "pre" ? OPT_PRESERVE_WS | OPT_PRESERVE_WS_FULL : base & ~OPT_OPEN_LEFT;
  }
  class NodeContext {
      constructor(type, attrs, 
      // Marks applied to this node itself
      marks, 
      // Marks that can't apply here, but will be used in children if possible
      pendingMarks, solid, match, options) {
          this.type = type;
          this.attrs = attrs;
          this.marks = marks;
          this.pendingMarks = pendingMarks;
          this.solid = solid;
          this.options = options;
          this.content = [];
          // Marks applied to the node's children
          this.activeMarks = Mark.none;
          // Nested Marks with same type
          this.stashMarks = [];
          this.match = match || (options & OPT_OPEN_LEFT ? null : type.contentMatch);
      }
      findWrapping(node) {
          if (!this.match) {
              if (!this.type)
                  return [];
              let fill = this.type.contentMatch.fillBefore(Fragment.from(node));
              if (fill) {
                  this.match = this.type.contentMatch.matchFragment(fill);
              }
              else {
                  let start = this.type.contentMatch, wrap;
                  if (wrap = start.findWrapping(node.type)) {
                      this.match = start;
                      return wrap;
                  }
                  else {
                      return null;
                  }
              }
          }
          return this.match.findWrapping(node.type);
      }
      finish(openEnd) {
          if (!(this.options & OPT_PRESERVE_WS)) { // Strip trailing whitespace
              let last = this.content[this.content.length - 1], m;
              if (last && last.isText && (m = /[ \t\r\n\u000c]+$/.exec(last.text))) {
                  let text = last;
                  if (last.text.length == m[0].length)
                      this.content.pop();
                  else
                      this.content[this.content.length - 1] = text.withText(text.text.slice(0, text.text.length - m[0].length));
              }
          }
          let content = Fragment.from(this.content);
          if (!openEnd && this.match)
              content = content.append(this.match.fillBefore(Fragment.empty, true));
          return this.type ? this.type.create(this.attrs, content, this.marks) : content;
      }
      popFromStashMark(mark) {
          for (let i = this.stashMarks.length - 1; i >= 0; i--)
              if (mark.eq(this.stashMarks[i]))
                  return this.stashMarks.splice(i, 1)[0];
      }
      applyPending(nextType) {
          for (let i = 0, pending = this.pendingMarks; i < pending.length; i++) {
              let mark = pending[i];
              if ((this.type ? this.type.allowsMarkType(mark.type) : markMayApply(mark.type, nextType)) &&
                  !mark.isInSet(this.activeMarks)) {
                  this.activeMarks = mark.addToSet(this.activeMarks);
                  this.pendingMarks = mark.removeFromSet(this.pendingMarks);
              }
          }
      }
      inlineContext(node) {
          if (this.type)
              return this.type.inlineContent;
          if (this.content.length)
              return this.content[0].isInline;
          return node.parentNode && !blockTags.hasOwnProperty(node.parentNode.nodeName.toLowerCase());
      }
  }
  class ParseContext {
      constructor(
      // The parser we are using.
      parser, 
      // The options passed to this parse.
      options, isOpen) {
          this.parser = parser;
          this.options = options;
          this.isOpen = isOpen;
          this.open = 0;
          let topNode = options.topNode, topContext;
          let topOptions = wsOptionsFor(null, options.preserveWhitespace, 0) | (isOpen ? OPT_OPEN_LEFT : 0);
          if (topNode)
              topContext = new NodeContext(topNode.type, topNode.attrs, Mark.none, Mark.none, true, options.topMatch || topNode.type.contentMatch, topOptions);
          else if (isOpen)
              topContext = new NodeContext(null, null, Mark.none, Mark.none, true, null, topOptions);
          else
              topContext = new NodeContext(parser.schema.topNodeType, null, Mark.none, Mark.none, true, null, topOptions);
          this.nodes = [topContext];
          this.find = options.findPositions;
          this.needsBlock = false;
      }
      get top() {
          return this.nodes[this.open];
      }
      // Add a DOM node to the content. Text is inserted as text node,
      // otherwise, the node is passed to `addElement` or, if it has a
      // `style` attribute, `addElementWithStyles`.
      addDOM(dom) {
          if (dom.nodeType == 3)
              this.addTextNode(dom);
          else if (dom.nodeType == 1)
              this.addElement(dom);
      }
      withStyleRules(dom, f) {
          let style = dom.getAttribute("style");
          if (!style)
              return f();
          let marks = this.readStyles(parseStyles(style));
          if (!marks)
              return; // A style with ignore: true
          let [addMarks, removeMarks] = marks, top = this.top;
          for (let i = 0; i < removeMarks.length; i++)
              this.removePendingMark(removeMarks[i], top);
          for (let i = 0; i < addMarks.length; i++)
              this.addPendingMark(addMarks[i]);
          f();
          for (let i = 0; i < addMarks.length; i++)
              this.removePendingMark(addMarks[i], top);
          for (let i = 0; i < removeMarks.length; i++)
              this.addPendingMark(removeMarks[i]);
      }
      addTextNode(dom) {
          let value = dom.nodeValue;
          let top = this.top;
          if (top.options & OPT_PRESERVE_WS_FULL ||
              top.inlineContext(dom) ||
              /[^ \t\r\n\u000c]/.test(value)) {
              if (!(top.options & OPT_PRESERVE_WS)) {
                  value = value.replace(/[ \t\r\n\u000c]+/g, " ");
                  // If this starts with whitespace, and there is no node before it, or
                  // a hard break, or a text node that ends with whitespace, strip the
                  // leading space.
                  if (/^[ \t\r\n\u000c]/.test(value) && this.open == this.nodes.length - 1) {
                      let nodeBefore = top.content[top.content.length - 1];
                      let domNodeBefore = dom.previousSibling;
                      if (!nodeBefore ||
                          (domNodeBefore && domNodeBefore.nodeName == 'BR') ||
                          (nodeBefore.isText && /[ \t\r\n\u000c]$/.test(nodeBefore.text)))
                          value = value.slice(1);
                  }
              }
              else if (!(top.options & OPT_PRESERVE_WS_FULL)) {
                  value = value.replace(/\r?\n|\r/g, " ");
              }
              else {
                  value = value.replace(/\r\n?/g, "\n");
              }
              if (value)
                  this.insertNode(this.parser.schema.text(value));
              this.findInText(dom);
          }
          else {
              this.findInside(dom);
          }
      }
      // Try to find a handler for the given tag and use that to parse. If
      // none is found, the element's content nodes are added directly.
      addElement(dom, matchAfter) {
          let name = dom.nodeName.toLowerCase(), ruleID;
          if (listTags.hasOwnProperty(name) && this.parser.normalizeLists)
              normalizeList(dom);
          let rule = (this.options.ruleFromNode && this.options.ruleFromNode(dom)) ||
              (ruleID = this.parser.matchTag(dom, this, matchAfter));
          if (rule ? rule.ignore : ignoreTags.hasOwnProperty(name)) {
              this.findInside(dom);
              this.ignoreFallback(dom);
          }
          else if (!rule || rule.skip || rule.closeParent) {
              if (rule && rule.closeParent)
                  this.open = Math.max(0, this.open - 1);
              else if (rule && rule.skip.nodeType)
                  dom = rule.skip;
              let sync, top = this.top, oldNeedsBlock = this.needsBlock;
              if (blockTags.hasOwnProperty(name)) {
                  if (top.content.length && top.content[0].isInline && this.open) {
                      this.open--;
                      top = this.top;
                  }
                  sync = true;
                  if (!top.type)
                      this.needsBlock = true;
              }
              else if (!dom.firstChild) {
                  this.leafFallback(dom);
                  return;
              }
              if (rule && rule.skip)
                  this.addAll(dom);
              else
                  this.withStyleRules(dom, () => this.addAll(dom));
              if (sync)
                  this.sync(top);
              this.needsBlock = oldNeedsBlock;
          }
          else {
              this.withStyleRules(dom, () => {
                  this.addElementByRule(dom, rule, rule.consuming === false ? ruleID : undefined);
              });
          }
      }
      // Called for leaf DOM nodes that would otherwise be ignored
      leafFallback(dom) {
          if (dom.nodeName == "BR" && this.top.type && this.top.type.inlineContent)
              this.addTextNode(dom.ownerDocument.createTextNode("\n"));
      }
      // Called for ignored nodes
      ignoreFallback(dom) {
          // Ignored BR nodes should at least create an inline context
          if (dom.nodeName == "BR" && (!this.top.type || !this.top.type.inlineContent))
              this.findPlace(this.parser.schema.text("-"));
      }
      // Run any style parser associated with the node's styles. Either
      // return an array of marks, or null to indicate some of the styles
      // had a rule with `ignore` set.
      readStyles(styles) {
          let add = Mark.none, remove = Mark.none;
          for (let i = 0; i < styles.length; i += 2) {
              for (let after = undefined;;) {
                  let rule = this.parser.matchStyle(styles[i], styles[i + 1], this, after);
                  if (!rule)
                      break;
                  if (rule.ignore)
                      return null;
                  if (rule.clearMark) {
                      this.top.pendingMarks.concat(this.top.activeMarks).forEach(m => {
                          if (rule.clearMark(m))
                              remove = m.addToSet(remove);
                      });
                  }
                  else {
                      add = this.parser.schema.marks[rule.mark].create(rule.attrs).addToSet(add);
                  }
                  if (rule.consuming === false)
                      after = rule;
                  else
                      break;
              }
          }
          return [add, remove];
      }
      // Look up a handler for the given node. If none are found, return
      // false. Otherwise, apply it, use its return value to drive the way
      // the node's content is wrapped, and return true.
      addElementByRule(dom, rule, continueAfter) {
          let sync, nodeType, mark;
          if (rule.node) {
              nodeType = this.parser.schema.nodes[rule.node];
              if (!nodeType.isLeaf) {
                  sync = this.enter(nodeType, rule.attrs || null, rule.preserveWhitespace);
              }
              else if (!this.insertNode(nodeType.create(rule.attrs))) {
                  this.leafFallback(dom);
              }
          }
          else {
              let markType = this.parser.schema.marks[rule.mark];
              mark = markType.create(rule.attrs);
              this.addPendingMark(mark);
          }
          let startIn = this.top;
          if (nodeType && nodeType.isLeaf) {
              this.findInside(dom);
          }
          else if (continueAfter) {
              this.addElement(dom, continueAfter);
          }
          else if (rule.getContent) {
              this.findInside(dom);
              rule.getContent(dom, this.parser.schema).forEach(node => this.insertNode(node));
          }
          else {
              let contentDOM = dom;
              if (typeof rule.contentElement == "string")
                  contentDOM = dom.querySelector(rule.contentElement);
              else if (typeof rule.contentElement == "function")
                  contentDOM = rule.contentElement(dom);
              else if (rule.contentElement)
                  contentDOM = rule.contentElement;
              this.findAround(dom, contentDOM, true);
              this.addAll(contentDOM);
          }
          if (sync && this.sync(startIn))
              this.open--;
          if (mark)
              this.removePendingMark(mark, startIn);
      }
      // Add all child nodes between `startIndex` and `endIndex` (or the
      // whole node, if not given). If `sync` is passed, use it to
      // synchronize after every block element.
      addAll(parent, startIndex, endIndex) {
          let index = startIndex || 0;
          for (let dom = startIndex ? parent.childNodes[startIndex] : parent.firstChild, end = endIndex == null ? null : parent.childNodes[endIndex]; dom != end; dom = dom.nextSibling, ++index) {
              this.findAtPoint(parent, index);
              this.addDOM(dom);
          }
          this.findAtPoint(parent, index);
      }
      // Try to find a way to fit the given node type into the current
      // context. May add intermediate wrappers and/or leave non-solid
      // nodes that we're in.
      findPlace(node) {
          let route, sync;
          for (let depth = this.open; depth >= 0; depth--) {
              let cx = this.nodes[depth];
              let found = cx.findWrapping(node);
              if (found && (!route || route.length > found.length)) {
                  route = found;
                  sync = cx;
                  if (!found.length)
                      break;
              }
              if (cx.solid)
                  break;
          }
          if (!route)
              return false;
          this.sync(sync);
          for (let i = 0; i < route.length; i++)
              this.enterInner(route[i], null, false);
          return true;
      }
      // Try to insert the given node, adjusting the context when needed.
      insertNode(node) {
          if (node.isInline && this.needsBlock && !this.top.type) {
              let block = this.textblockFromContext();
              if (block)
                  this.enterInner(block);
          }
          if (this.findPlace(node)) {
              this.closeExtra();
              let top = this.top;
              top.applyPending(node.type);
              if (top.match)
                  top.match = top.match.matchType(node.type);
              let marks = top.activeMarks;
              for (let i = 0; i < node.marks.length; i++)
                  if (!top.type || top.type.allowsMarkType(node.marks[i].type))
                      marks = node.marks[i].addToSet(marks);
              top.content.push(node.mark(marks));
              return true;
          }
          return false;
      }
      // Try to start a node of the given type, adjusting the context when
      // necessary.
      enter(type, attrs, preserveWS) {
          let ok = this.findPlace(type.create(attrs));
          if (ok)
              this.enterInner(type, attrs, true, preserveWS);
          return ok;
      }
      // Open a node of the given type
      enterInner(type, attrs = null, solid = false, preserveWS) {
          this.closeExtra();
          let top = this.top;
          top.applyPending(type);
          top.match = top.match && top.match.matchType(type);
          let options = wsOptionsFor(type, preserveWS, top.options);
          if ((top.options & OPT_OPEN_LEFT) && top.content.length == 0)
              options |= OPT_OPEN_LEFT;
          this.nodes.push(new NodeContext(type, attrs, top.activeMarks, top.pendingMarks, solid, null, options));
          this.open++;
      }
      // Make sure all nodes above this.open are finished and added to
      // their parents
      closeExtra(openEnd = false) {
          let i = this.nodes.length - 1;
          if (i > this.open) {
              for (; i > this.open; i--)
                  this.nodes[i - 1].content.push(this.nodes[i].finish(openEnd));
              this.nodes.length = this.open + 1;
          }
      }
      finish() {
          this.open = 0;
          this.closeExtra(this.isOpen);
          return this.nodes[0].finish(this.isOpen || this.options.topOpen);
      }
      sync(to) {
          for (let i = this.open; i >= 0; i--)
              if (this.nodes[i] == to) {
                  this.open = i;
                  return true;
              }
          return false;
      }
      get currentPos() {
          this.closeExtra();
          let pos = 0;
          for (let i = this.open; i >= 0; i--) {
              let content = this.nodes[i].content;
              for (let j = content.length - 1; j >= 0; j--)
                  pos += content[j].nodeSize;
              if (i)
                  pos++;
          }
          return pos;
      }
      findAtPoint(parent, offset) {
          if (this.find)
              for (let i = 0; i < this.find.length; i++) {
                  if (this.find[i].node == parent && this.find[i].offset == offset)
                      this.find[i].pos = this.currentPos;
              }
      }
      findInside(parent) {
          if (this.find)
              for (let i = 0; i < this.find.length; i++) {
                  if (this.find[i].pos == null && parent.nodeType == 1 && parent.contains(this.find[i].node))
                      this.find[i].pos = this.currentPos;
              }
      }
      findAround(parent, content, before) {
          if (parent != content && this.find)
              for (let i = 0; i < this.find.length; i++) {
                  if (this.find[i].pos == null && parent.nodeType == 1 && parent.contains(this.find[i].node)) {
                      let pos = content.compareDocumentPosition(this.find[i].node);
                      if (pos & (before ? 2 : 4))
                          this.find[i].pos = this.currentPos;
                  }
              }
      }
      findInText(textNode) {
          if (this.find)
              for (let i = 0; i < this.find.length; i++) {
                  if (this.find[i].node == textNode)
                      this.find[i].pos = this.currentPos - (textNode.nodeValue.length - this.find[i].offset);
              }
      }
      // Determines whether the given context string matches this context.
      matchesContext(context) {
          if (context.indexOf("|") > -1)
              return context.split(/\s*\|\s*/).some(this.matchesContext, this);
          let parts = context.split("/");
          let option = this.options.context;
          let useRoot = !this.isOpen && (!option || option.parent.type == this.nodes[0].type);
          let minDepth = -(option ? option.depth + 1 : 0) + (useRoot ? 0 : 1);
          let match = (i, depth) => {
              for (; i >= 0; i--) {
                  let part = parts[i];
                  if (part == "") {
                      if (i == parts.length - 1 || i == 0)
                          continue;
                      for (; depth >= minDepth; depth--)
                          if (match(i - 1, depth))
                              return true;
                      return false;
                  }
                  else {
                      let next = depth > 0 || (depth == 0 && useRoot) ? this.nodes[depth].type
                          : option && depth >= minDepth ? option.node(depth - minDepth).type
                              : null;
                      if (!next || (next.name != part && next.groups.indexOf(part) == -1))
                          return false;
                      depth--;
                  }
              }
              return true;
          };
          return match(parts.length - 1, this.open);
      }
      textblockFromContext() {
          let $context = this.options.context;
          if ($context)
              for (let d = $context.depth; d >= 0; d--) {
                  let deflt = $context.node(d).contentMatchAt($context.indexAfter(d)).defaultType;
                  if (deflt && deflt.isTextblock && deflt.defaultAttrs)
                      return deflt;
              }
          for (let name in this.parser.schema.nodes) {
              let type = this.parser.schema.nodes[name];
              if (type.isTextblock && type.defaultAttrs)
                  return type;
          }
      }
      addPendingMark(mark) {
          let found = findSameMarkInSet(mark, this.top.pendingMarks);
          if (found)
              this.top.stashMarks.push(found);
          this.top.pendingMarks = mark.addToSet(this.top.pendingMarks);
      }
      removePendingMark(mark, upto) {
          for (let depth = this.open; depth >= 0; depth--) {
              let level = this.nodes[depth];
              let found = level.pendingMarks.lastIndexOf(mark);
              if (found > -1) {
                  level.pendingMarks = mark.removeFromSet(level.pendingMarks);
              }
              else {
                  level.activeMarks = mark.removeFromSet(level.activeMarks);
                  let stashMark = level.popFromStashMark(mark);
                  if (stashMark && level.type && level.type.allowsMarkType(stashMark.type))
                      level.activeMarks = stashMark.addToSet(level.activeMarks);
              }
              if (level == upto)
                  break;
          }
      }
  }
  // Kludge to work around directly nested list nodes produced by some
  // tools and allowed by browsers to mean that the nested list is
  // actually part of the list item above it.
  function normalizeList(dom) {
      for (let child = dom.firstChild, prevItem = null; child; child = child.nextSibling) {
          let name = child.nodeType == 1 ? child.nodeName.toLowerCase() : null;
          if (name && listTags.hasOwnProperty(name) && prevItem) {
              prevItem.appendChild(child);
              child = prevItem;
          }
          else if (name == "li") {
              prevItem = child;
          }
          else if (name) {
              prevItem = null;
          }
      }
  }
  // Apply a CSS selector.
  function matches(dom, selector) {
      return (dom.matches || dom.msMatchesSelector || dom.webkitMatchesSelector || dom.mozMatchesSelector).call(dom, selector);
  }
  // Tokenize a style attribute into property/value pairs.
  function parseStyles(style) {
      let re = /\s*([\w-]+)\s*:\s*([^;]+)/g, m, result = [];
      while (m = re.exec(style))
          result.push(m[1], m[2].trim());
      return result;
  }
  function copy(obj) {
      let copy = {};
      for (let prop in obj)
          copy[prop] = obj[prop];
      return copy;
  }
  // Used when finding a mark at the top level of a fragment parse.
  // Checks whether it would be reasonable to apply a given mark type to
  // a given node, by looking at the way the mark occurs in the schema.
  function markMayApply(markType, nodeType) {
      let nodes = nodeType.schema.nodes;
      for (let name in nodes) {
          let parent = nodes[name];
          if (!parent.allowsMarkType(markType))
              continue;
          let seen = [], scan = (match) => {
              seen.push(match);
              for (let i = 0; i < match.edgeCount; i++) {
                  let { type, next } = match.edge(i);
                  if (type == nodeType)
                      return true;
                  if (seen.indexOf(next) < 0 && scan(next))
                      return true;
              }
          };
          if (scan(parent.contentMatch))
              return true;
      }
  }
  function findSameMarkInSet(mark, set) {
      for (let i = 0; i < set.length; i++) {
          if (mark.eq(set[i]))
              return set[i];
      }
  }

  /**
  A DOM serializer knows how to convert ProseMirror nodes and
  marks of various types to DOM nodes.
  */
  class DOMSerializer {
      /**
      Create a serializer. `nodes` should map node names to functions
      that take a node and return a description of the corresponding
      DOM. `marks` does the same for mark names, but also gets an
      argument that tells it whether the mark's content is block or
      inline content (for typical use, it'll always be inline). A mark
      serializer may be `null` to indicate that marks of that type
      should not be serialized.
      */
      constructor(
      /**
      The node serialization functions.
      */
      nodes, 
      /**
      The mark serialization functions.
      */
      marks) {
          this.nodes = nodes;
          this.marks = marks;
      }
      /**
      Serialize the content of this fragment to a DOM fragment. When
      not in the browser, the `document` option, containing a DOM
      document, should be passed so that the serializer can create
      nodes.
      */
      serializeFragment(fragment, options = {}, target) {
          if (!target)
              target = doc$2(options).createDocumentFragment();
          let top = target, active = [];
          fragment.forEach(node => {
              if (active.length || node.marks.length) {
                  let keep = 0, rendered = 0;
                  while (keep < active.length && rendered < node.marks.length) {
                      let next = node.marks[rendered];
                      if (!this.marks[next.type.name]) {
                          rendered++;
                          continue;
                      }
                      if (!next.eq(active[keep][0]) || next.type.spec.spanning === false)
                          break;
                      keep++;
                      rendered++;
                  }
                  while (keep < active.length)
                      top = active.pop()[1];
                  while (rendered < node.marks.length) {
                      let add = node.marks[rendered++];
                      let markDOM = this.serializeMark(add, node.isInline, options);
                      if (markDOM) {
                          active.push([add, top]);
                          top.appendChild(markDOM.dom);
                          top = markDOM.contentDOM || markDOM.dom;
                      }
                  }
              }
              top.appendChild(this.serializeNodeInner(node, options));
          });
          return target;
      }
      /**
      @internal
      */
      serializeNodeInner(node, options) {
          let { dom, contentDOM } = DOMSerializer.renderSpec(doc$2(options), this.nodes[node.type.name](node));
          if (contentDOM) {
              if (node.isLeaf)
                  throw new RangeError("Content hole not allowed in a leaf node spec");
              this.serializeFragment(node.content, options, contentDOM);
          }
          return dom;
      }
      /**
      Serialize this node to a DOM node. This can be useful when you
      need to serialize a part of a document, as opposed to the whole
      document. To serialize a whole document, use
      [`serializeFragment`](https://prosemirror.net/docs/ref/#model.DOMSerializer.serializeFragment) on
      its [content](https://prosemirror.net/docs/ref/#model.Node.content).
      */
      serializeNode(node, options = {}) {
          let dom = this.serializeNodeInner(node, options);
          for (let i = node.marks.length - 1; i >= 0; i--) {
              let wrap = this.serializeMark(node.marks[i], node.isInline, options);
              if (wrap) {
                  (wrap.contentDOM || wrap.dom).appendChild(dom);
                  dom = wrap.dom;
              }
          }
          return dom;
      }
      /**
      @internal
      */
      serializeMark(mark, inline, options = {}) {
          let toDOM = this.marks[mark.type.name];
          return toDOM && DOMSerializer.renderSpec(doc$2(options), toDOM(mark, inline));
      }
      /**
      Render an [output spec](https://prosemirror.net/docs/ref/#model.DOMOutputSpec) to a DOM node. If
      the spec has a hole (zero) in it, `contentDOM` will point at the
      node with the hole.
      */
      static renderSpec(doc, structure, xmlNS = null) {
          if (typeof structure == "string")
              return { dom: doc.createTextNode(structure) };
          if (structure.nodeType != null)
              return { dom: structure };
          if (structure.dom && structure.dom.nodeType != null)
              return structure;
          let tagName = structure[0], space = tagName.indexOf(" ");
          if (space > 0) {
              xmlNS = tagName.slice(0, space);
              tagName = tagName.slice(space + 1);
          }
          let contentDOM;
          let dom = (xmlNS ? doc.createElementNS(xmlNS, tagName) : doc.createElement(tagName));
          let attrs = structure[1], start = 1;
          if (attrs && typeof attrs == "object" && attrs.nodeType == null && !Array.isArray(attrs)) {
              start = 2;
              for (let name in attrs)
                  if (attrs[name] != null) {
                      let space = name.indexOf(" ");
                      if (space > 0)
                          dom.setAttributeNS(name.slice(0, space), name.slice(space + 1), attrs[name]);
                      else
                          dom.setAttribute(name, attrs[name]);
                  }
          }
          for (let i = start; i < structure.length; i++) {
              let child = structure[i];
              if (child === 0) {
                  if (i < structure.length - 1 || i > start)
                      throw new RangeError("Content hole must be the only child of its parent node");
                  return { dom, contentDOM: dom };
              }
              else {
                  let { dom: inner, contentDOM: innerContent } = DOMSerializer.renderSpec(doc, child, xmlNS);
                  dom.appendChild(inner);
                  if (innerContent) {
                      if (contentDOM)
                          throw new RangeError("Multiple content holes");
                      contentDOM = innerContent;
                  }
              }
          }
          return { dom, contentDOM };
      }
      /**
      Build a serializer using the [`toDOM`](https://prosemirror.net/docs/ref/#model.NodeSpec.toDOM)
      properties in a schema's node and mark specs.
      */
      static fromSchema(schema) {
          return schema.cached.domSerializer ||
              (schema.cached.domSerializer = new DOMSerializer(this.nodesFromSchema(schema), this.marksFromSchema(schema)));
      }
      /**
      Gather the serializers in a schema's node specs into an object.
      This can be useful as a base to build a custom serializer from.
      */
      static nodesFromSchema(schema) {
          let result = gatherToDOM(schema.nodes);
          if (!result.text)
              result.text = node => node.text;
          return result;
      }
      /**
      Gather the serializers in a schema's mark specs into an object.
      */
      static marksFromSchema(schema) {
          return gatherToDOM(schema.marks);
      }
  }
  function gatherToDOM(obj) {
      let result = {};
      for (let name in obj) {
          let toDOM = obj[name].spec.toDOM;
          if (toDOM)
              result[name] = toDOM;
      }
      return result;
  }
  function doc$2(options) {
      return options.document || window.document;
  }

  // Recovery values encode a range index and an offset. They are
  // represented as numbers, because tons of them will be created when
  // mapping, for example, a large number of decorations. The number's
  // lower 16 bits provide the index, the remaining bits the offset.
  //
  // Note: We intentionally don't use bit shift operators to en- and
  // decode these, since those clip to 32 bits, which we might in rare
  // cases want to overflow. A 64-bit float can represent 48-bit
  // integers precisely.
  const lower16 = 0xffff;
  const factor16 = Math.pow(2, 16);
  function makeRecover(index, offset) { return index + offset * factor16; }
  function recoverIndex(value) { return value & lower16; }
  function recoverOffset(value) { return (value - (value & lower16)) / factor16; }
  const DEL_BEFORE = 1, DEL_AFTER = 2, DEL_ACROSS = 4, DEL_SIDE = 8;
  /**
  An object representing a mapped position with extra
  information.
  */
  class MapResult {
      /**
      @internal
      */
      constructor(
      /**
      The mapped version of the position.
      */
      pos, 
      /**
      @internal
      */
      delInfo, 
      /**
      @internal
      */
      recover) {
          this.pos = pos;
          this.delInfo = delInfo;
          this.recover = recover;
      }
      /**
      Tells you whether the position was deleted, that is, whether the
      step removed the token on the side queried (via the `assoc`)
      argument from the document.
      */
      get deleted() { return (this.delInfo & DEL_SIDE) > 0; }
      /**
      Tells you whether the token before the mapped position was deleted.
      */
      get deletedBefore() { return (this.delInfo & (DEL_BEFORE | DEL_ACROSS)) > 0; }
      /**
      True when the token after the mapped position was deleted.
      */
      get deletedAfter() { return (this.delInfo & (DEL_AFTER | DEL_ACROSS)) > 0; }
      /**
      Tells whether any of the steps mapped through deletes across the
      position (including both the token before and after the
      position).
      */
      get deletedAcross() { return (this.delInfo & DEL_ACROSS) > 0; }
  }
  /**
  A map describing the deletions and insertions made by a step, which
  can be used to find the correspondence between positions in the
  pre-step version of a document and the same position in the
  post-step version.
  */
  class StepMap {
      /**
      Create a position map. The modifications to the document are
      represented as an array of numbers, in which each group of three
      represents a modified chunk as `[start, oldSize, newSize]`.
      */
      constructor(
      /**
      @internal
      */
      ranges, 
      /**
      @internal
      */
      inverted = false) {
          this.ranges = ranges;
          this.inverted = inverted;
          if (!ranges.length && StepMap.empty)
              return StepMap.empty;
      }
      /**
      @internal
      */
      recover(value) {
          let diff = 0, index = recoverIndex(value);
          if (!this.inverted)
              for (let i = 0; i < index; i++)
                  diff += this.ranges[i * 3 + 2] - this.ranges[i * 3 + 1];
          return this.ranges[index * 3] + diff + recoverOffset(value);
      }
      mapResult(pos, assoc = 1) { return this._map(pos, assoc, false); }
      map(pos, assoc = 1) { return this._map(pos, assoc, true); }
      /**
      @internal
      */
      _map(pos, assoc, simple) {
          let diff = 0, oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2;
          for (let i = 0; i < this.ranges.length; i += 3) {
              let start = this.ranges[i] - (this.inverted ? diff : 0);
              if (start > pos)
                  break;
              let oldSize = this.ranges[i + oldIndex], newSize = this.ranges[i + newIndex], end = start + oldSize;
              if (pos <= end) {
                  let side = !oldSize ? assoc : pos == start ? -1 : pos == end ? 1 : assoc;
                  let result = start + diff + (side < 0 ? 0 : newSize);
                  if (simple)
                      return result;
                  let recover = pos == (assoc < 0 ? start : end) ? null : makeRecover(i / 3, pos - start);
                  let del = pos == start ? DEL_AFTER : pos == end ? DEL_BEFORE : DEL_ACROSS;
                  if (assoc < 0 ? pos != start : pos != end)
                      del |= DEL_SIDE;
                  return new MapResult(result, del, recover);
              }
              diff += newSize - oldSize;
          }
          return simple ? pos + diff : new MapResult(pos + diff, 0, null);
      }
      /**
      @internal
      */
      touches(pos, recover) {
          let diff = 0, index = recoverIndex(recover);
          let oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2;
          for (let i = 0; i < this.ranges.length; i += 3) {
              let start = this.ranges[i] - (this.inverted ? diff : 0);
              if (start > pos)
                  break;
              let oldSize = this.ranges[i + oldIndex], end = start + oldSize;
              if (pos <= end && i == index * 3)
                  return true;
              diff += this.ranges[i + newIndex] - oldSize;
          }
          return false;
      }
      /**
      Calls the given function on each of the changed ranges included in
      this map.
      */
      forEach(f) {
          let oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2;
          for (let i = 0, diff = 0; i < this.ranges.length; i += 3) {
              let start = this.ranges[i], oldStart = start - (this.inverted ? diff : 0), newStart = start + (this.inverted ? 0 : diff);
              let oldSize = this.ranges[i + oldIndex], newSize = this.ranges[i + newIndex];
              f(oldStart, oldStart + oldSize, newStart, newStart + newSize);
              diff += newSize - oldSize;
          }
      }
      /**
      Create an inverted version of this map. The result can be used to
      map positions in the post-step document to the pre-step document.
      */
      invert() {
          return new StepMap(this.ranges, !this.inverted);
      }
      /**
      @internal
      */
      toString() {
          return (this.inverted ? "-" : "") + JSON.stringify(this.ranges);
      }
      /**
      Create a map that moves all positions by offset `n` (which may be
      negative). This can be useful when applying steps meant for a
      sub-document to a larger document, or vice-versa.
      */
      static offset(n) {
          return n == 0 ? StepMap.empty : new StepMap(n < 0 ? [0, -n, 0] : [0, 0, n]);
      }
  }
  /**
  A StepMap that contains no changed ranges.
  */
  StepMap.empty = new StepMap([]);
  /**
  A mapping represents a pipeline of zero or more [step
  maps](https://prosemirror.net/docs/ref/#transform.StepMap). It has special provisions for losslessly
  handling mapping positions through a series of steps in which some
  steps are inverted versions of earlier steps. (This comes up when
  ‘[rebasing](/docs/guide/#transform.rebasing)’ steps for
  collaboration or history management.)
  */
  class Mapping {
      /**
      Create a new mapping with the given position maps.
      */
      constructor(
      /**
      The step maps in this mapping.
      */
      maps = [], 
      /**
      @internal
      */
      mirror, 
      /**
      The starting position in the `maps` array, used when `map` or
      `mapResult` is called.
      */
      from = 0, 
      /**
      The end position in the `maps` array.
      */
      to = maps.length) {
          this.maps = maps;
          this.mirror = mirror;
          this.from = from;
          this.to = to;
      }
      /**
      Create a mapping that maps only through a part of this one.
      */
      slice(from = 0, to = this.maps.length) {
          return new Mapping(this.maps, this.mirror, from, to);
      }
      /**
      @internal
      */
      copy() {
          return new Mapping(this.maps.slice(), this.mirror && this.mirror.slice(), this.from, this.to);
      }
      /**
      Add a step map to the end of this mapping. If `mirrors` is
      given, it should be the index of the step map that is the mirror
      image of this one.
      */
      appendMap(map, mirrors) {
          this.to = this.maps.push(map);
          if (mirrors != null)
              this.setMirror(this.maps.length - 1, mirrors);
      }
      /**
      Add all the step maps in a given mapping to this one (preserving
      mirroring information).
      */
      appendMapping(mapping) {
          for (let i = 0, startSize = this.maps.length; i < mapping.maps.length; i++) {
              let mirr = mapping.getMirror(i);
              this.appendMap(mapping.maps[i], mirr != null && mirr < i ? startSize + mirr : undefined);
          }
      }
      /**
      Finds the offset of the step map that mirrors the map at the
      given offset, in this mapping (as per the second argument to
      `appendMap`).
      */
      getMirror(n) {
          if (this.mirror)
              for (let i = 0; i < this.mirror.length; i++)
                  if (this.mirror[i] == n)
                      return this.mirror[i + (i % 2 ? -1 : 1)];
      }
      /**
      @internal
      */
      setMirror(n, m) {
          if (!this.mirror)
              this.mirror = [];
          this.mirror.push(n, m);
      }
      /**
      Append the inverse of the given mapping to this one.
      */
      appendMappingInverted(mapping) {
          for (let i = mapping.maps.length - 1, totalSize = this.maps.length + mapping.maps.length; i >= 0; i--) {
              let mirr = mapping.getMirror(i);
              this.appendMap(mapping.maps[i].invert(), mirr != null && mirr > i ? totalSize - mirr - 1 : undefined);
          }
      }
      /**
      Create an inverted version of this mapping.
      */
      invert() {
          let inverse = new Mapping;
          inverse.appendMappingInverted(this);
          return inverse;
      }
      /**
      Map a position through this mapping.
      */
      map(pos, assoc = 1) {
          if (this.mirror)
              return this._map(pos, assoc, true);
          for (let i = this.from; i < this.to; i++)
              pos = this.maps[i].map(pos, assoc);
          return pos;
      }
      /**
      Map a position through this mapping, returning a mapping
      result.
      */
      mapResult(pos, assoc = 1) { return this._map(pos, assoc, false); }
      /**
      @internal
      */
      _map(pos, assoc, simple) {
          let delInfo = 0;
          for (let i = this.from; i < this.to; i++) {
              let map = this.maps[i], result = map.mapResult(pos, assoc);
              if (result.recover != null) {
                  let corr = this.getMirror(i);
                  if (corr != null && corr > i && corr < this.to) {
                      i = corr;
                      pos = this.maps[corr].recover(result.recover);
                      continue;
                  }
              }
              delInfo |= result.delInfo;
              pos = result.pos;
          }
          return simple ? pos : new MapResult(pos, delInfo, null);
      }
  }

  const stepsByID = Object.create(null);
  /**
  A step object represents an atomic change. It generally applies
  only to the document it was created for, since the positions
  stored in it will only make sense for that document.

  New steps are defined by creating classes that extend `Step`,
  overriding the `apply`, `invert`, `map`, `getMap` and `fromJSON`
  methods, and registering your class with a unique
  JSON-serialization identifier using
  [`Step.jsonID`](https://prosemirror.net/docs/ref/#transform.Step^jsonID).
  */
  class Step {
      /**
      Get the step map that represents the changes made by this step,
      and which can be used to transform between positions in the old
      and the new document.
      */
      getMap() { return StepMap.empty; }
      /**
      Try to merge this step with another one, to be applied directly
      after it. Returns the merged step when possible, null if the
      steps can't be merged.
      */
      merge(other) { return null; }
      /**
      Deserialize a step from its JSON representation. Will call
      through to the step class' own implementation of this method.
      */
      static fromJSON(schema, json) {
          if (!json || !json.stepType)
              throw new RangeError("Invalid input for Step.fromJSON");
          let type = stepsByID[json.stepType];
          if (!type)
              throw new RangeError(`No step type ${json.stepType} defined`);
          return type.fromJSON(schema, json);
      }
      /**
      To be able to serialize steps to JSON, each step needs a string
      ID to attach to its JSON representation. Use this method to
      register an ID for your step classes. Try to pick something
      that's unlikely to clash with steps from other modules.
      */
      static jsonID(id, stepClass) {
          if (id in stepsByID)
              throw new RangeError("Duplicate use of step JSON ID " + id);
          stepsByID[id] = stepClass;
          stepClass.prototype.jsonID = id;
          return stepClass;
      }
  }
  /**
  The result of [applying](https://prosemirror.net/docs/ref/#transform.Step.apply) a step. Contains either a
  new document or a failure value.
  */
  class StepResult {
      /**
      @internal
      */
      constructor(
      /**
      The transformed document, if successful.
      */
      doc, 
      /**
      The failure message, if unsuccessful.
      */
      failed) {
          this.doc = doc;
          this.failed = failed;
      }
      /**
      Create a successful step result.
      */
      static ok(doc) { return new StepResult(doc, null); }
      /**
      Create a failed step result.
      */
      static fail(message) { return new StepResult(null, message); }
      /**
      Call [`Node.replace`](https://prosemirror.net/docs/ref/#model.Node.replace) with the given
      arguments. Create a successful result if it succeeds, and a
      failed one if it throws a `ReplaceError`.
      */
      static fromReplace(doc, from, to, slice) {
          try {
              return StepResult.ok(doc.replace(from, to, slice));
          }
          catch (e) {
              if (e instanceof ReplaceError)
                  return StepResult.fail(e.message);
              throw e;
          }
      }
  }

  function mapFragment(fragment, f, parent) {
      let mapped = [];
      for (let i = 0; i < fragment.childCount; i++) {
          let child = fragment.child(i);
          if (child.content.size)
              child = child.copy(mapFragment(child.content, f, child));
          if (child.isInline)
              child = f(child, parent, i);
          mapped.push(child);
      }
      return Fragment.fromArray(mapped);
  }
  /**
  Add a mark to all inline content between two positions.
  */
  class AddMarkStep extends Step {
      /**
      Create a mark step.
      */
      constructor(
      /**
      The start of the marked range.
      */
      from, 
      /**
      The end of the marked range.
      */
      to, 
      /**
      The mark to add.
      */
      mark) {
          super();
          this.from = from;
          this.to = to;
          this.mark = mark;
      }
      apply(doc) {
          let oldSlice = doc.slice(this.from, this.to), $from = doc.resolve(this.from);
          let parent = $from.node($from.sharedDepth(this.to));
          let slice = new Slice(mapFragment(oldSlice.content, (node, parent) => {
              if (!node.isAtom || !parent.type.allowsMarkType(this.mark.type))
                  return node;
              return node.mark(this.mark.addToSet(node.marks));
          }, parent), oldSlice.openStart, oldSlice.openEnd);
          return StepResult.fromReplace(doc, this.from, this.to, slice);
      }
      invert() {
          return new RemoveMarkStep(this.from, this.to, this.mark);
      }
      map(mapping) {
          let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1);
          if (from.deleted && to.deleted || from.pos >= to.pos)
              return null;
          return new AddMarkStep(from.pos, to.pos, this.mark);
      }
      merge(other) {
          if (other instanceof AddMarkStep &&
              other.mark.eq(this.mark) &&
              this.from <= other.to && this.to >= other.from)
              return new AddMarkStep(Math.min(this.from, other.from), Math.max(this.to, other.to), this.mark);
          return null;
      }
      toJSON() {
          return { stepType: "addMark", mark: this.mark.toJSON(),
              from: this.from, to: this.to };
      }
      /**
      @internal
      */
      static fromJSON(schema, json) {
          if (typeof json.from != "number" || typeof json.to != "number")
              throw new RangeError("Invalid input for AddMarkStep.fromJSON");
          return new AddMarkStep(json.from, json.to, schema.markFromJSON(json.mark));
      }
  }
  Step.jsonID("addMark", AddMarkStep);
  /**
  Remove a mark from all inline content between two positions.
  */
  class RemoveMarkStep extends Step {
      /**
      Create a mark-removing step.
      */
      constructor(
      /**
      The start of the unmarked range.
      */
      from, 
      /**
      The end of the unmarked range.
      */
      to, 
      /**
      The mark to remove.
      */
      mark) {
          super();
          this.from = from;
          this.to = to;
          this.mark = mark;
      }
      apply(doc) {
          let oldSlice = doc.slice(this.from, this.to);
          let slice = new Slice(mapFragment(oldSlice.content, node => {
              return node.mark(this.mark.removeFromSet(node.marks));
          }, doc), oldSlice.openStart, oldSlice.openEnd);
          return StepResult.fromReplace(doc, this.from, this.to, slice);
      }
      invert() {
          return new AddMarkStep(this.from, this.to, this.mark);
      }
      map(mapping) {
          let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1);
          if (from.deleted && to.deleted || from.pos >= to.pos)
              return null;
          return new RemoveMarkStep(from.pos, to.pos, this.mark);
      }
      merge(other) {
          if (other instanceof RemoveMarkStep &&
              other.mark.eq(this.mark) &&
              this.from <= other.to && this.to >= other.from)
              return new RemoveMarkStep(Math.min(this.from, other.from), Math.max(this.to, other.to), this.mark);
          return null;
      }
      toJSON() {
          return { stepType: "removeMark", mark: this.mark.toJSON(),
              from: this.from, to: this.to };
      }
      /**
      @internal
      */
      static fromJSON(schema, json) {
          if (typeof json.from != "number" || typeof json.to != "number")
              throw new RangeError("Invalid input for RemoveMarkStep.fromJSON");
          return new RemoveMarkStep(json.from, json.to, schema.markFromJSON(json.mark));
      }
  }
  Step.jsonID("removeMark", RemoveMarkStep);
  /**
  Add a mark to a specific node.
  */
  class AddNodeMarkStep extends Step {
      /**
      Create a node mark step.
      */
      constructor(
      /**
      The position of the target node.
      */
      pos, 
      /**
      The mark to add.
      */
      mark) {
          super();
          this.pos = pos;
          this.mark = mark;
      }
      apply(doc) {
          let node = doc.nodeAt(this.pos);
          if (!node)
              return StepResult.fail("No node at mark step's position");
          let updated = node.type.create(node.attrs, null, this.mark.addToSet(node.marks));
          return StepResult.fromReplace(doc, this.pos, this.pos + 1, new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1));
      }
      invert(doc) {
          let node = doc.nodeAt(this.pos);
          if (node) {
              let newSet = this.mark.addToSet(node.marks);
              if (newSet.length == node.marks.length) {
                  for (let i = 0; i < node.marks.length; i++)
                      if (!node.marks[i].isInSet(newSet))
                          return new AddNodeMarkStep(this.pos, node.marks[i]);
                  return new AddNodeMarkStep(this.pos, this.mark);
              }
          }
          return new RemoveNodeMarkStep(this.pos, this.mark);
      }
      map(mapping) {
          let pos = mapping.mapResult(this.pos, 1);
          return pos.deletedAfter ? null : new AddNodeMarkStep(pos.pos, this.mark);
      }
      toJSON() {
          return { stepType: "addNodeMark", pos: this.pos, mark: this.mark.toJSON() };
      }
      /**
      @internal
      */
      static fromJSON(schema, json) {
          if (typeof json.pos != "number")
              throw new RangeError("Invalid input for AddNodeMarkStep.fromJSON");
          return new AddNodeMarkStep(json.pos, schema.markFromJSON(json.mark));
      }
  }
  Step.jsonID("addNodeMark", AddNodeMarkStep);
  /**
  Remove a mark from a specific node.
  */
  class RemoveNodeMarkStep extends Step {
      /**
      Create a mark-removing step.
      */
      constructor(
      /**
      The position of the target node.
      */
      pos, 
      /**
      The mark to remove.
      */
      mark) {
          super();
          this.pos = pos;
          this.mark = mark;
      }
      apply(doc) {
          let node = doc.nodeAt(this.pos);
          if (!node)
              return StepResult.fail("No node at mark step's position");
          let updated = node.type.create(node.attrs, null, this.mark.removeFromSet(node.marks));
          return StepResult.fromReplace(doc, this.pos, this.pos + 1, new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1));
      }
      invert(doc) {
          let node = doc.nodeAt(this.pos);
          if (!node || !this.mark.isInSet(node.marks))
              return this;
          return new AddNodeMarkStep(this.pos, this.mark);
      }
      map(mapping) {
          let pos = mapping.mapResult(this.pos, 1);
          return pos.deletedAfter ? null : new RemoveNodeMarkStep(pos.pos, this.mark);
      }
      toJSON() {
          return { stepType: "removeNodeMark", pos: this.pos, mark: this.mark.toJSON() };
      }
      /**
      @internal
      */
      static fromJSON(schema, json) {
          if (typeof json.pos != "number")
              throw new RangeError("Invalid input for RemoveNodeMarkStep.fromJSON");
          return new RemoveNodeMarkStep(json.pos, schema.markFromJSON(json.mark));
      }
  }
  Step.jsonID("removeNodeMark", RemoveNodeMarkStep);

  /**
  Replace a part of the document with a slice of new content.
  */
  class ReplaceStep extends Step {
      /**
      The given `slice` should fit the 'gap' between `from` and
      `to`—the depths must line up, and the surrounding nodes must be
      able to be joined with the open sides of the slice. When
      `structure` is true, the step will fail if the content between
      from and to is not just a sequence of closing and then opening
      tokens (this is to guard against rebased replace steps
      overwriting something they weren't supposed to).
      */
      constructor(
      /**
      The start position of the replaced range.
      */
      from, 
      /**
      The end position of the replaced range.
      */
      to, 
      /**
      The slice to insert.
      */
      slice, 
      /**
      @internal
      */
      structure = false) {
          super();
          this.from = from;
          this.to = to;
          this.slice = slice;
          this.structure = structure;
      }
      apply(doc) {
          if (this.structure && contentBetween(doc, this.from, this.to))
              return StepResult.fail("Structure replace would overwrite content");
          return StepResult.fromReplace(doc, this.from, this.to, this.slice);
      }
      getMap() {
          return new StepMap([this.from, this.to - this.from, this.slice.size]);
      }
      invert(doc) {
          return new ReplaceStep(this.from, this.from + this.slice.size, doc.slice(this.from, this.to));
      }
      map(mapping) {
          let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1);
          if (from.deletedAcross && to.deletedAcross)
              return null;
          return new ReplaceStep(from.pos, Math.max(from.pos, to.pos), this.slice);
      }
      merge(other) {
          if (!(other instanceof ReplaceStep) || other.structure || this.structure)
              return null;
          if (this.from + this.slice.size == other.from && !this.slice.openEnd && !other.slice.openStart) {
              let slice = this.slice.size + other.slice.size == 0 ? Slice.empty
                  : new Slice(this.slice.content.append(other.slice.content), this.slice.openStart, other.slice.openEnd);
              return new ReplaceStep(this.from, this.to + (other.to - other.from), slice, this.structure);
          }
          else if (other.to == this.from && !this.slice.openStart && !other.slice.openEnd) {
              let slice = this.slice.size + other.slice.size == 0 ? Slice.empty
                  : new Slice(other.slice.content.append(this.slice.content), other.slice.openStart, this.slice.openEnd);
              return new ReplaceStep(other.from, this.to, slice, this.structure);
          }
          else {
              return null;
          }
      }
      toJSON() {
          let json = { stepType: "replace", from: this.from, to: this.to };
          if (this.slice.size)
              json.slice = this.slice.toJSON();
          if (this.structure)
              json.structure = true;
          return json;
      }
      /**
      @internal
      */
      static fromJSON(schema, json) {
          if (typeof json.from != "number" || typeof json.to != "number")
              throw new RangeError("Invalid input for ReplaceStep.fromJSON");
          return new ReplaceStep(json.from, json.to, Slice.fromJSON(schema, json.slice), !!json.structure);
      }
  }
  Step.jsonID("replace", ReplaceStep);
  /**
  Replace a part of the document with a slice of content, but
  preserve a range of the replaced content by moving it into the
  slice.
  */
  class ReplaceAroundStep extends Step {
      /**
      Create a replace-around step with the given range and gap.
      `insert` should be the point in the slice into which the content
      of the gap should be moved. `structure` has the same meaning as
      it has in the [`ReplaceStep`](https://prosemirror.net/docs/ref/#transform.ReplaceStep) class.
      */
      constructor(
      /**
      The start position of the replaced range.
      */
      from, 
      /**
      The end position of the replaced range.
      */
      to, 
      /**
      The start of preserved range.
      */
      gapFrom, 
      /**
      The end of preserved range.
      */
      gapTo, 
      /**
      The slice to insert.
      */
      slice, 
      /**
      The position in the slice where the preserved range should be
      inserted.
      */
      insert, 
      /**
      @internal
      */
      structure = false) {
          super();
          this.from = from;
          this.to = to;
          this.gapFrom = gapFrom;
          this.gapTo = gapTo;
          this.slice = slice;
          this.insert = insert;
          this.structure = structure;
      }
      apply(doc) {
          if (this.structure && (contentBetween(doc, this.from, this.gapFrom) ||
              contentBetween(doc, this.gapTo, this.to)))
              return StepResult.fail("Structure gap-replace would overwrite content");
          let gap = doc.slice(this.gapFrom, this.gapTo);
          if (gap.openStart || gap.openEnd)
              return StepResult.fail("Gap is not a flat range");
          let inserted = this.slice.insertAt(this.insert, gap.content);
          if (!inserted)
              return StepResult.fail("Content does not fit in gap");
          return StepResult.fromReplace(doc, this.from, this.to, inserted);
      }
      getMap() {
          return new StepMap([this.from, this.gapFrom - this.from, this.insert,
              this.gapTo, this.to - this.gapTo, this.slice.size - this.insert]);
      }
      invert(doc) {
          let gap = this.gapTo - this.gapFrom;
          return new ReplaceAroundStep(this.from, this.from + this.slice.size + gap, this.from + this.insert, this.from + this.insert + gap, doc.slice(this.from, this.to).removeBetween(this.gapFrom - this.from, this.gapTo - this.from), this.gapFrom - this.from, this.structure);
      }
      map(mapping) {
          let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1);
          let gapFrom = this.from == this.gapFrom ? from.pos : mapping.map(this.gapFrom, -1);
          let gapTo = this.to == this.gapTo ? to.pos : mapping.map(this.gapTo, 1);
          if ((from.deletedAcross && to.deletedAcross) || gapFrom < from.pos || gapTo > to.pos)
              return null;
          return new ReplaceAroundStep(from.pos, to.pos, gapFrom, gapTo, this.slice, this.insert, this.structure);
      }
      toJSON() {
          let json = { stepType: "replaceAround", from: this.from, to: this.to,
              gapFrom: this.gapFrom, gapTo: this.gapTo, insert: this.insert };
          if (this.slice.size)
              json.slice = this.slice.toJSON();
          if (this.structure)
              json.structure = true;
          return json;
      }
      /**
      @internal
      */
      static fromJSON(schema, json) {
          if (typeof json.from != "number" || typeof json.to != "number" ||
              typeof json.gapFrom != "number" || typeof json.gapTo != "number" || typeof json.insert != "number")
              throw new RangeError("Invalid input for ReplaceAroundStep.fromJSON");
          return new ReplaceAroundStep(json.from, json.to, json.gapFrom, json.gapTo, Slice.fromJSON(schema, json.slice), json.insert, !!json.structure);
      }
  }
  Step.jsonID("replaceAround", ReplaceAroundStep);
  function contentBetween(doc, from, to) {
      let $from = doc.resolve(from), dist = to - from, depth = $from.depth;
      while (dist > 0 && depth > 0 && $from.indexAfter(depth) == $from.node(depth).childCount) {
          depth--;
          dist--;
      }
      if (dist > 0) {
          let next = $from.node(depth).maybeChild($from.indexAfter(depth));
          while (dist > 0) {
              if (!next || next.isLeaf)
                  return true;
              next = next.firstChild;
              dist--;
          }
      }
      return false;
  }

  function addMark(tr, from, to, mark) {
      let removed = [], added = [];
      let removing, adding;
      tr.doc.nodesBetween(from, to, (node, pos, parent) => {
          if (!node.isInline)
              return;
          let marks = node.marks;
          if (!mark.isInSet(marks) && parent.type.allowsMarkType(mark.type)) {
              let start = Math.max(pos, from), end = Math.min(pos + node.nodeSize, to);
              let newSet = mark.addToSet(marks);
              for (let i = 0; i < marks.length; i++) {
                  if (!marks[i].isInSet(newSet)) {
                      if (removing && removing.to == start && removing.mark.eq(marks[i]))
                          removing.to = end;
                      else
                          removed.push(removing = new RemoveMarkStep(start, end, marks[i]));
                  }
              }
              if (adding && adding.to == start)
                  adding.to = end;
              else
                  added.push(adding = new AddMarkStep(start, end, mark));
          }
      });
      removed.forEach(s => tr.step(s));
      added.forEach(s => tr.step(s));
  }
  function removeMark(tr, from, to, mark) {
      let matched = [], step = 0;
      tr.doc.nodesBetween(from, to, (node, pos) => {
          if (!node.isInline)
              return;
          step++;
          let toRemove = null;
          if (mark instanceof MarkType) {
              let set = node.marks, found;
              while (found = mark.isInSet(set)) {
                  (toRemove || (toRemove = [])).push(found);
                  set = found.removeFromSet(set);
              }
          }
          else if (mark) {
              if (mark.isInSet(node.marks))
                  toRemove = [mark];
          }
          else {
              toRemove = node.marks;
          }
          if (toRemove && toRemove.length) {
              let end = Math.min(pos + node.nodeSize, to);
              for (let i = 0; i < toRemove.length; i++) {
                  let style = toRemove[i], found;
                  for (let j = 0; j < matched.length; j++) {
                      let m = matched[j];
                      if (m.step == step - 1 && style.eq(matched[j].style))
                          found = m;
                  }
                  if (found) {
                      found.to = end;
                      found.step = step;
                  }
                  else {
                      matched.push({ style, from: Math.max(pos, from), to: end, step });
                  }
              }
          }
      });
      matched.forEach(m => tr.step(new RemoveMarkStep(m.from, m.to, m.style)));
  }
  function clearIncompatible(tr, pos, parentType, match = parentType.contentMatch, clearNewlines = true) {
      let node = tr.doc.nodeAt(pos);
      let replSteps = [], cur = pos + 1;
      for (let i = 0; i < node.childCount; i++) {
          let child = node.child(i), end = cur + child.nodeSize;
          let allowed = match.matchType(child.type);
          if (!allowed) {
              replSteps.push(new ReplaceStep(cur, end, Slice.empty));
          }
          else {
              match = allowed;
              for (let j = 0; j < child.marks.length; j++)
                  if (!parentType.allowsMarkType(child.marks[j].type))
                      tr.step(new RemoveMarkStep(cur, end, child.marks[j]));
              if (clearNewlines && child.isText && parentType.whitespace != "pre") {
                  let m, newline = /\r?\n|\r/g, slice;
                  while (m = newline.exec(child.text)) {
                      if (!slice)
                          slice = new Slice(Fragment.from(parentType.schema.text(" ", parentType.allowedMarks(child.marks))), 0, 0);
                      replSteps.push(new ReplaceStep(cur + m.index, cur + m.index + m[0].length, slice));
                  }
              }
          }
          cur = end;
      }
      if (!match.validEnd) {
          let fill = match.fillBefore(Fragment.empty, true);
          tr.replace(cur, cur, new Slice(fill, 0, 0));
      }
      for (let i = replSteps.length - 1; i >= 0; i--)
          tr.step(replSteps[i]);
  }

  function canCut(node, start, end) {
      return (start == 0 || node.canReplace(start, node.childCount)) &&
          (end == node.childCount || node.canReplace(0, end));
  }
  /**
  Try to find a target depth to which the content in the given range
  can be lifted. Will not go across
  [isolating](https://prosemirror.net/docs/ref/#model.NodeSpec.isolating) parent nodes.
  */
  function liftTarget(range) {
      let parent = range.parent;
      let content = parent.content.cutByIndex(range.startIndex, range.endIndex);
      for (let depth = range.depth;; --depth) {
          let node = range.$from.node(depth);
          let index = range.$from.index(depth), endIndex = range.$to.indexAfter(depth);
          if (depth < range.depth && node.canReplace(index, endIndex, content))
              return depth;
          if (depth == 0 || node.type.spec.isolating || !canCut(node, index, endIndex))
              break;
      }
      return null;
  }
  function lift$1(tr, range, target) {
      let { $from, $to, depth } = range;
      let gapStart = $from.before(depth + 1), gapEnd = $to.after(depth + 1);
      let start = gapStart, end = gapEnd;
      let before = Fragment.empty, openStart = 0;
      for (let d = depth, splitting = false; d > target; d--)
          if (splitting || $from.index(d) > 0) {
              splitting = true;
              before = Fragment.from($from.node(d).copy(before));
              openStart++;
          }
          else {
              start--;
          }
      let after = Fragment.empty, openEnd = 0;
      for (let d = depth, splitting = false; d > target; d--)
          if (splitting || $to.after(d + 1) < $to.end(d)) {
              splitting = true;
              after = Fragment.from($to.node(d).copy(after));
              openEnd++;
          }
          else {
              end++;
          }
      tr.step(new ReplaceAroundStep(start, end, gapStart, gapEnd, new Slice(before.append(after), openStart, openEnd), before.size - openStart, true));
  }
  /**
  Try to find a valid way to wrap the content in the given range in a
  node of the given type. May introduce extra nodes around and inside
  the wrapper node, if necessary. Returns null if no valid wrapping
  could be found. When `innerRange` is given, that range's content is
  used as the content to fit into the wrapping, instead of the
  content of `range`.
  */
  function findWrapping(range, nodeType, attrs = null, innerRange = range) {
      let around = findWrappingOutside(range, nodeType);
      let inner = around && findWrappingInside(innerRange, nodeType);
      if (!inner)
          return null;
      return around.map(withAttrs)
          .concat({ type: nodeType, attrs }).concat(inner.map(withAttrs));
  }
  function withAttrs(type) { return { type, attrs: null }; }
  function findWrappingOutside(range, type) {
      let { parent, startIndex, endIndex } = range;
      let around = parent.contentMatchAt(startIndex).findWrapping(type);
      if (!around)
          return null;
      let outer = around.length ? around[0] : type;
      return parent.canReplaceWith(startIndex, endIndex, outer) ? around : null;
  }
  function findWrappingInside(range, type) {
      let { parent, startIndex, endIndex } = range;
      let inner = parent.child(startIndex);
      let inside = type.contentMatch.findWrapping(inner.type);
      if (!inside)
          return null;
      let lastType = inside.length ? inside[inside.length - 1] : type;
      let innerMatch = lastType.contentMatch;
      for (let i = startIndex; innerMatch && i < endIndex; i++)
          innerMatch = innerMatch.matchType(parent.child(i).type);
      if (!innerMatch || !innerMatch.validEnd)
          return null;
      return inside;
  }
  function wrap(tr, range, wrappers) {
      let content = Fragment.empty;
      for (let i = wrappers.length - 1; i >= 0; i--) {
          if (content.size) {
              let match = wrappers[i].type.contentMatch.matchFragment(content);
              if (!match || !match.validEnd)
                  throw new RangeError("Wrapper type given to Transform.wrap does not form valid content of its parent wrapper");
          }
          content = Fragment.from(wrappers[i].type.create(wrappers[i].attrs, content));
      }
      let start = range.start, end = range.end;
      tr.step(new ReplaceAroundStep(start, end, start, end, new Slice(content, 0, 0), wrappers.length, true));
  }
  function setBlockType$1(tr, from, to, type, attrs) {
      if (!type.isTextblock)
          throw new RangeError("Type given to setBlockType should be a textblock");
      let mapFrom = tr.steps.length;
      tr.doc.nodesBetween(from, to, (node, pos) => {
          if (node.isTextblock && !node.hasMarkup(type, attrs) && canChangeType(tr.doc, tr.mapping.slice(mapFrom).map(pos), type)) {
              let convertNewlines = null;
              if (type.schema.linebreakReplacement) {
                  let pre = type.whitespace == "pre", supportLinebreak = !!type.contentMatch.matchType(type.schema.linebreakReplacement);
                  if (pre && !supportLinebreak)
                      convertNewlines = false;
                  else if (!pre && supportLinebreak)
                      convertNewlines = true;
              }
              // Ensure all markup that isn't allowed in the new node type is cleared
              if (convertNewlines === false)
                  replaceLinebreaks(tr, node, pos, mapFrom);
              clearIncompatible(tr, tr.mapping.slice(mapFrom).map(pos, 1), type, undefined, convertNewlines === null);
              let mapping = tr.mapping.slice(mapFrom);
              let startM = mapping.map(pos, 1), endM = mapping.map(pos + node.nodeSize, 1);
              tr.step(new ReplaceAroundStep(startM, endM, startM + 1, endM - 1, new Slice(Fragment.from(type.create(attrs, null, node.marks)), 0, 0), 1, true));
              if (convertNewlines === true)
                  replaceNewlines(tr, node, pos, mapFrom);
              return false;
          }
      });
  }
  function replaceNewlines(tr, node, pos, mapFrom) {
      node.forEach((child, offset) => {
          if (child.isText) {
              let m, newline = /\r?\n|\r/g;
              while (m = newline.exec(child.text)) {
                  let start = tr.mapping.slice(mapFrom).map(pos + 1 + offset + m.index);
                  tr.replaceWith(start, start + 1, node.type.schema.linebreakReplacement.create());
              }
          }
      });
  }
  function replaceLinebreaks(tr, node, pos, mapFrom) {
      node.forEach((child, offset) => {
          if (child.type == child.type.schema.linebreakReplacement) {
              let start = tr.mapping.slice(mapFrom).map(pos + 1 + offset);
              tr.replaceWith(start, start + 1, node.type.schema.text("\n"));
          }
      });
  }
  function canChangeType(doc, pos, type) {
      let $pos = doc.resolve(pos), index = $pos.index();
      return $pos.parent.canReplaceWith(index, index + 1, type);
  }
  /**
  Change the type, attributes, and/or marks of the node at `pos`.
  When `type` isn't given, the existing node type is preserved,
  */
  function setNodeMarkup(tr, pos, type, attrs, marks) {
      let node = tr.doc.nodeAt(pos);
      if (!node)
          throw new RangeError("No node at given position");
      if (!type)
          type = node.type;
      let newNode = type.create(attrs, null, marks || node.marks);
      if (node.isLeaf)
          return tr.replaceWith(pos, pos + node.nodeSize, newNode);
      if (!type.validContent(node.content))
          throw new RangeError("Invalid content for node type " + type.name);
      tr.step(new ReplaceAroundStep(pos, pos + node.nodeSize, pos + 1, pos + node.nodeSize - 1, new Slice(Fragment.from(newNode), 0, 0), 1, true));
  }
  /**
  Check whether splitting at the given position is allowed.
  */
  function canSplit(doc, pos, depth = 1, typesAfter) {
      let $pos = doc.resolve(pos), base = $pos.depth - depth;
      let innerType = (typesAfter && typesAfter[typesAfter.length - 1]) || $pos.parent;
      if (base < 0 || $pos.parent.type.spec.isolating ||
          !$pos.parent.canReplace($pos.index(), $pos.parent.childCount) ||
          !innerType.type.validContent($pos.parent.content.cutByIndex($pos.index(), $pos.parent.childCount)))
          return false;
      for (let d = $pos.depth - 1, i = depth - 2; d > base; d--, i--) {
          let node = $pos.node(d), index = $pos.index(d);
          if (node.type.spec.isolating)
              return false;
          let rest = node.content.cutByIndex(index, node.childCount);
          let overrideChild = typesAfter && typesAfter[i + 1];
          if (overrideChild)
              rest = rest.replaceChild(0, overrideChild.type.create(overrideChild.attrs));
          let after = (typesAfter && typesAfter[i]) || node;
          if (!node.canReplace(index + 1, node.childCount) || !after.type.validContent(rest))
              return false;
      }
      let index = $pos.indexAfter(base);
      let baseType = typesAfter && typesAfter[0];
      return $pos.node(base).canReplaceWith(index, index, baseType ? baseType.type : $pos.node(base + 1).type);
  }
  function split(tr, pos, depth = 1, typesAfter) {
      let $pos = tr.doc.resolve(pos), before = Fragment.empty, after = Fragment.empty;
      for (let d = $pos.depth, e = $pos.depth - depth, i = depth - 1; d > e; d--, i--) {
          before = Fragment.from($pos.node(d).copy(before));
          let typeAfter = typesAfter && typesAfter[i];
          after = Fragment.from(typeAfter ? typeAfter.type.create(typeAfter.attrs, after) : $pos.node(d).copy(after));
      }
      tr.step(new ReplaceStep(pos, pos, new Slice(before.append(after), depth, depth), true));
  }
  /**
  Test whether the blocks before and after a given position can be
  joined.
  */
  function canJoin(doc, pos) {
      let $pos = doc.resolve(pos), index = $pos.index();
      return joinable($pos.nodeBefore, $pos.nodeAfter) &&
          $pos.parent.canReplace(index, index + 1);
  }
  function joinable(a, b) {
      return !!(a && b && !a.isLeaf && a.canAppend(b));
  }
  /**
  Find an ancestor of the given position that can be joined to the
  block before (or after if `dir` is positive). Returns the joinable
  point, if any.
  */
  function joinPoint(doc, pos, dir = -1) {
      let $pos = doc.resolve(pos);
      for (let d = $pos.depth;; d--) {
          let before, after, index = $pos.index(d);
          if (d == $pos.depth) {
              before = $pos.nodeBefore;
              after = $pos.nodeAfter;
          }
          else if (dir > 0) {
              before = $pos.node(d + 1);
              index++;
              after = $pos.node(d).maybeChild(index);
          }
          else {
              before = $pos.node(d).maybeChild(index - 1);
              after = $pos.node(d + 1);
          }
          if (before && !before.isTextblock && joinable(before, after) &&
              $pos.node(d).canReplace(index, index + 1))
              return pos;
          if (d == 0)
              break;
          pos = dir < 0 ? $pos.before(d) : $pos.after(d);
      }
  }
  function join(tr, pos, depth) {
      let step = new ReplaceStep(pos - depth, pos + depth, Slice.empty, true);
      tr.step(step);
  }
  /**
  Try to find a point where a node of the given type can be inserted
  near `pos`, by searching up the node hierarchy when `pos` itself
  isn't a valid place but is at the start or end of a node. Return
  null if no position was found.
  */
  function insertPoint(doc, pos, nodeType) {
      let $pos = doc.resolve(pos);
      if ($pos.parent.canReplaceWith($pos.index(), $pos.index(), nodeType))
          return pos;
      if ($pos.parentOffset == 0)
          for (let d = $pos.depth - 1; d >= 0; d--) {
              let index = $pos.index(d);
              if ($pos.node(d).canReplaceWith(index, index, nodeType))
                  return $pos.before(d + 1);
              if (index > 0)
                  return null;
          }
      if ($pos.parentOffset == $pos.parent.content.size)
          for (let d = $pos.depth - 1; d >= 0; d--) {
              let index = $pos.indexAfter(d);
              if ($pos.node(d).canReplaceWith(index, index, nodeType))
                  return $pos.after(d + 1);
              if (index < $pos.node(d).childCount)
                  return null;
          }
      return null;
  }
  /**
  Finds a position at or around the given position where the given
  slice can be inserted. Will look at parent nodes' nearest boundary
  and try there, even if the original position wasn't directly at the
  start or end of that node. Returns null when no position was found.
  */
  function dropPoint(doc, pos, slice) {
      let $pos = doc.resolve(pos);
      if (!slice.content.size)
          return pos;
      let content = slice.content;
      for (let i = 0; i < slice.openStart; i++)
          content = content.firstChild.content;
      for (let pass = 1; pass <= (slice.openStart == 0 && slice.size ? 2 : 1); pass++) {
          for (let d = $pos.depth; d >= 0; d--) {
              let bias = d == $pos.depth ? 0 : $pos.pos <= ($pos.start(d + 1) + $pos.end(d + 1)) / 2 ? -1 : 1;
              let insertPos = $pos.index(d) + (bias > 0 ? 1 : 0);
              let parent = $pos.node(d), fits = false;
              if (pass == 1) {
                  fits = parent.canReplace(insertPos, insertPos, content);
              }
              else {
                  let wrapping = parent.contentMatchAt(insertPos).findWrapping(content.firstChild.type);
                  fits = wrapping && parent.canReplaceWith(insertPos, insertPos, wrapping[0]);
              }
              if (fits)
                  return bias == 0 ? $pos.pos : bias < 0 ? $pos.before(d + 1) : $pos.after(d + 1);
          }
      }
      return null;
  }

  /**
  ‘Fit’ a slice into a given position in the document, producing a
  [step](https://prosemirror.net/docs/ref/#transform.Step) that inserts it. Will return null if
  there's no meaningful way to insert the slice here, or inserting it
  would be a no-op (an empty slice over an empty range).
  */
  function replaceStep(doc, from, to = from, slice = Slice.empty) {
      if (from == to && !slice.size)
          return null;
      let $from = doc.resolve(from), $to = doc.resolve(to);
      // Optimization -- avoid work if it's obvious that it's not needed.
      if (fitsTrivially($from, $to, slice))
          return new ReplaceStep(from, to, slice);
      return new Fitter($from, $to, slice).fit();
  }
  function fitsTrivially($from, $to, slice) {
      return !slice.openStart && !slice.openEnd && $from.start() == $to.start() &&
          $from.parent.canReplace($from.index(), $to.index(), slice.content);
  }
  // Algorithm for 'placing' the elements of a slice into a gap:
  //
  // We consider the content of each node that is open to the left to be
  // independently placeable. I.e. in <p("foo"), p("bar")>, when the
  // paragraph on the left is open, "foo" can be placed (somewhere on
  // the left side of the replacement gap) independently from p("bar").
  //
  // This class tracks the state of the placement progress in the
  // following properties:
  //
  //  - `frontier` holds a stack of `{type, match}` objects that
  //    represent the open side of the replacement. It starts at
  //    `$from`, then moves forward as content is placed, and is finally
  //    reconciled with `$to`.
  //
  //  - `unplaced` is a slice that represents the content that hasn't
  //    been placed yet.
  //
  //  - `placed` is a fragment of placed content. Its open-start value
  //    is implicit in `$from`, and its open-end value in `frontier`.
  class Fitter {
      constructor($from, $to, unplaced) {
          this.$from = $from;
          this.$to = $to;
          this.unplaced = unplaced;
          this.frontier = [];
          this.placed = Fragment.empty;
          for (let i = 0; i <= $from.depth; i++) {
              let node = $from.node(i);
              this.frontier.push({
                  type: node.type,
                  match: node.contentMatchAt($from.indexAfter(i))
              });
          }
          for (let i = $from.depth; i > 0; i--)
              this.placed = Fragment.from($from.node(i).copy(this.placed));
      }
      get depth() { return this.frontier.length - 1; }
      fit() {
          // As long as there's unplaced content, try to place some of it.
          // If that fails, either increase the open score of the unplaced
          // slice, or drop nodes from it, and then try again.
          while (this.unplaced.size) {
              let fit = this.findFittable();
              if (fit)
                  this.placeNodes(fit);
              else
                  this.openMore() || this.dropNode();
          }
          // When there's inline content directly after the frontier _and_
          // directly after `this.$to`, we must generate a `ReplaceAround`
          // step that pulls that content into the node after the frontier.
          // That means the fitting must be done to the end of the textblock
          // node after `this.$to`, not `this.$to` itself.
          let moveInline = this.mustMoveInline(), placedSize = this.placed.size - this.depth - this.$from.depth;
          let $from = this.$from, $to = this.close(moveInline < 0 ? this.$to : $from.doc.resolve(moveInline));
          if (!$to)
              return null;
          // If closing to `$to` succeeded, create a step
          let content = this.placed, openStart = $from.depth, openEnd = $to.depth;
          while (openStart && openEnd && content.childCount == 1) { // Normalize by dropping open parent nodes
              content = content.firstChild.content;
              openStart--;
              openEnd--;
          }
          let slice = new Slice(content, openStart, openEnd);
          if (moveInline > -1)
              return new ReplaceAroundStep($from.pos, moveInline, this.$to.pos, this.$to.end(), slice, placedSize);
          if (slice.size || $from.pos != this.$to.pos) // Don't generate no-op steps
              return new ReplaceStep($from.pos, $to.pos, slice);
          return null;
      }
      // Find a position on the start spine of `this.unplaced` that has
      // content that can be moved somewhere on the frontier. Returns two
      // depths, one for the slice and one for the frontier.
      findFittable() {
          let startDepth = this.unplaced.openStart;
          for (let cur = this.unplaced.content, d = 0, openEnd = this.unplaced.openEnd; d < startDepth; d++) {
              let node = cur.firstChild;
              if (cur.childCount > 1)
                  openEnd = 0;
              if (node.type.spec.isolating && openEnd <= d) {
                  startDepth = d;
                  break;
              }
              cur = node.content;
          }
          // Only try wrapping nodes (pass 2) after finding a place without
          // wrapping failed.
          for (let pass = 1; pass <= 2; pass++) {
              for (let sliceDepth = pass == 1 ? startDepth : this.unplaced.openStart; sliceDepth >= 0; sliceDepth--) {
                  let fragment, parent = null;
                  if (sliceDepth) {
                      parent = contentAt(this.unplaced.content, sliceDepth - 1).firstChild;
                      fragment = parent.content;
                  }
                  else {
                      fragment = this.unplaced.content;
                  }
                  let first = fragment.firstChild;
                  for (let frontierDepth = this.depth; frontierDepth >= 0; frontierDepth--) {
                      let { type, match } = this.frontier[frontierDepth], wrap, inject = null;
                      // In pass 1, if the next node matches, or there is no next
                      // node but the parents look compatible, we've found a
                      // place.
                      if (pass == 1 && (first ? match.matchType(first.type) || (inject = match.fillBefore(Fragment.from(first), false))
                          : parent && type.compatibleContent(parent.type)))
                          return { sliceDepth, frontierDepth, parent, inject };
                      // In pass 2, look for a set of wrapping nodes that make
                      // `first` fit here.
                      else if (pass == 2 && first && (wrap = match.findWrapping(first.type)))
                          return { sliceDepth, frontierDepth, parent, wrap };
                      // Don't continue looking further up if the parent node
                      // would fit here.
                      if (parent && match.matchType(parent.type))
                          break;
                  }
              }
          }
      }
      openMore() {
          let { content, openStart, openEnd } = this.unplaced;
          let inner = contentAt(content, openStart);
          if (!inner.childCount || inner.firstChild.isLeaf)
              return false;
          this.unplaced = new Slice(content, openStart + 1, Math.max(openEnd, inner.size + openStart >= content.size - openEnd ? openStart + 1 : 0));
          return true;
      }
      dropNode() {
          let { content, openStart, openEnd } = this.unplaced;
          let inner = contentAt(content, openStart);
          if (inner.childCount <= 1 && openStart > 0) {
              let openAtEnd = content.size - openStart <= openStart + inner.size;
              this.unplaced = new Slice(dropFromFragment(content, openStart - 1, 1), openStart - 1, openAtEnd ? openStart - 1 : openEnd);
          }
          else {
              this.unplaced = new Slice(dropFromFragment(content, openStart, 1), openStart, openEnd);
          }
      }
      // Move content from the unplaced slice at `sliceDepth` to the
      // frontier node at `frontierDepth`. Close that frontier node when
      // applicable.
      placeNodes({ sliceDepth, frontierDepth, parent, inject, wrap }) {
          while (this.depth > frontierDepth)
              this.closeFrontierNode();
          if (wrap)
              for (let i = 0; i < wrap.length; i++)
                  this.openFrontierNode(wrap[i]);
          let slice = this.unplaced, fragment = parent ? parent.content : slice.content;
          let openStart = slice.openStart - sliceDepth;
          let taken = 0, add = [];
          let { match, type } = this.frontier[frontierDepth];
          if (inject) {
              for (let i = 0; i < inject.childCount; i++)
                  add.push(inject.child(i));
              match = match.matchFragment(inject);
          }
          // Computes the amount of (end) open nodes at the end of the
          // fragment. When 0, the parent is open, but no more. When
          // negative, nothing is open.
          let openEndCount = (fragment.size + sliceDepth) - (slice.content.size - slice.openEnd);
          // Scan over the fragment, fitting as many child nodes as
          // possible.
          while (taken < fragment.childCount) {
              let next = fragment.child(taken), matches = match.matchType(next.type);
              if (!matches)
                  break;
              taken++;
              if (taken > 1 || openStart == 0 || next.content.size) { // Drop empty open nodes
                  match = matches;
                  add.push(closeNodeStart(next.mark(type.allowedMarks(next.marks)), taken == 1 ? openStart : 0, taken == fragment.childCount ? openEndCount : -1));
              }
          }
          let toEnd = taken == fragment.childCount;
          if (!toEnd)
              openEndCount = -1;
          this.placed = addToFragment(this.placed, frontierDepth, Fragment.from(add));
          this.frontier[frontierDepth].match = match;
          // If the parent types match, and the entire node was moved, and
          // it's not open, close this frontier node right away.
          if (toEnd && openEndCount < 0 && parent && parent.type == this.frontier[this.depth].type && this.frontier.length > 1)
              this.closeFrontierNode();
          // Add new frontier nodes for any open nodes at the end.
          for (let i = 0, cur = fragment; i < openEndCount; i++) {
              let node = cur.lastChild;
              this.frontier.push({ type: node.type, match: node.contentMatchAt(node.childCount) });
              cur = node.content;
          }
          // Update `this.unplaced`. Drop the entire node from which we
          // placed it we got to its end, otherwise just drop the placed
          // nodes.
          this.unplaced = !toEnd ? new Slice(dropFromFragment(slice.content, sliceDepth, taken), slice.openStart, slice.openEnd)
              : sliceDepth == 0 ? Slice.empty
                  : new Slice(dropFromFragment(slice.content, sliceDepth - 1, 1), sliceDepth - 1, openEndCount < 0 ? slice.openEnd : sliceDepth - 1);
      }
      mustMoveInline() {
          if (!this.$to.parent.isTextblock)
              return -1;
          let top = this.frontier[this.depth], level;
          if (!top.type.isTextblock || !contentAfterFits(this.$to, this.$to.depth, top.type, top.match, false) ||
              (this.$to.depth == this.depth && (level = this.findCloseLevel(this.$to)) && level.depth == this.depth))
              return -1;
          let { depth } = this.$to, after = this.$to.after(depth);
          while (depth > 1 && after == this.$to.end(--depth))
              ++after;
          return after;
      }
      findCloseLevel($to) {
          scan: for (let i = Math.min(this.depth, $to.depth); i >= 0; i--) {
              let { match, type } = this.frontier[i];
              let dropInner = i < $to.depth && $to.end(i + 1) == $to.pos + ($to.depth - (i + 1));
              let fit = contentAfterFits($to, i, type, match, dropInner);
              if (!fit)
                  continue;
              for (let d = i - 1; d >= 0; d--) {
                  let { match, type } = this.frontier[d];
                  let matches = contentAfterFits($to, d, type, match, true);
                  if (!matches || matches.childCount)
                      continue scan;
              }
              return { depth: i, fit, move: dropInner ? $to.doc.resolve($to.after(i + 1)) : $to };
          }
      }
      close($to) {
          let close = this.findCloseLevel($to);
          if (!close)
              return null;
          while (this.depth > close.depth)
              this.closeFrontierNode();
          if (close.fit.childCount)
              this.placed = addToFragment(this.placed, close.depth, close.fit);
          $to = close.move;
          for (let d = close.depth + 1; d <= $to.depth; d++) {
              let node = $to.node(d), add = node.type.contentMatch.fillBefore(node.content, true, $to.index(d));
              this.openFrontierNode(node.type, node.attrs, add);
          }
          return $to;
      }
      openFrontierNode(type, attrs = null, content) {
          let top = this.frontier[this.depth];
          top.match = top.match.matchType(type);
          this.placed = addToFragment(this.placed, this.depth, Fragment.from(type.create(attrs, content)));
          this.frontier.push({ type, match: type.contentMatch });
      }
      closeFrontierNode() {
          let open = this.frontier.pop();
          let add = open.match.fillBefore(Fragment.empty, true);
          if (add.childCount)
              this.placed = addToFragment(this.placed, this.frontier.length, add);
      }
  }
  function dropFromFragment(fragment, depth, count) {
      if (depth == 0)
          return fragment.cutByIndex(count, fragment.childCount);
      return fragment.replaceChild(0, fragment.firstChild.copy(dropFromFragment(fragment.firstChild.content, depth - 1, count)));
  }
  function addToFragment(fragment, depth, content) {
      if (depth == 0)
          return fragment.append(content);
      return fragment.replaceChild(fragment.childCount - 1, fragment.lastChild.copy(addToFragment(fragment.lastChild.content, depth - 1, content)));
  }
  function contentAt(fragment, depth) {
      for (let i = 0; i < depth; i++)
          fragment = fragment.firstChild.content;
      return fragment;
  }
  function closeNodeStart(node, openStart, openEnd) {
      if (openStart <= 0)
          return node;
      let frag = node.content;
      if (openStart > 1)
          frag = frag.replaceChild(0, closeNodeStart(frag.firstChild, openStart - 1, frag.childCount == 1 ? openEnd - 1 : 0));
      if (openStart > 0) {
          frag = node.type.contentMatch.fillBefore(frag).append(frag);
          if (openEnd <= 0)
              frag = frag.append(node.type.contentMatch.matchFragment(frag).fillBefore(Fragment.empty, true));
      }
      return node.copy(frag);
  }
  function contentAfterFits($to, depth, type, match, open) {
      let node = $to.node(depth), index = open ? $to.indexAfter(depth) : $to.index(depth);
      if (index == node.childCount && !type.compatibleContent(node.type))
          return null;
      let fit = match.fillBefore(node.content, true, index);
      return fit && !invalidMarks(type, node.content, index) ? fit : null;
  }
  function invalidMarks(type, fragment, start) {
      for (let i = start; i < fragment.childCount; i++)
          if (!type.allowsMarks(fragment.child(i).marks))
              return true;
      return false;
  }
  function definesContent(type) {
      return type.spec.defining || type.spec.definingForContent;
  }
  function replaceRange(tr, from, to, slice) {
      if (!slice.size)
          return tr.deleteRange(from, to);
      let $from = tr.doc.resolve(from), $to = tr.doc.resolve(to);
      if (fitsTrivially($from, $to, slice))
          return tr.step(new ReplaceStep(from, to, slice));
      let targetDepths = coveredDepths($from, tr.doc.resolve(to));
      // Can't replace the whole document, so remove 0 if it's present
      if (targetDepths[targetDepths.length - 1] == 0)
          targetDepths.pop();
      // Negative numbers represent not expansion over the whole node at
      // that depth, but replacing from $from.before(-D) to $to.pos.
      let preferredTarget = -($from.depth + 1);
      targetDepths.unshift(preferredTarget);
      // This loop picks a preferred target depth, if one of the covering
      // depths is not outside of a defining node, and adds negative
      // depths for any depth that has $from at its start and does not
      // cross a defining node.
      for (let d = $from.depth, pos = $from.pos - 1; d > 0; d--, pos--) {
          let spec = $from.node(d).type.spec;
          if (spec.defining || spec.definingAsContext || spec.isolating)
              break;
          if (targetDepths.indexOf(d) > -1)
              preferredTarget = d;
          else if ($from.before(d) == pos)
              targetDepths.splice(1, 0, -d);
      }
      // Try to fit each possible depth of the slice into each possible
      // target depth, starting with the preferred depths.
      let preferredTargetIndex = targetDepths.indexOf(preferredTarget);
      let leftNodes = [], preferredDepth = slice.openStart;
      for (let content = slice.content, i = 0;; i++) {
          let node = content.firstChild;
          leftNodes.push(node);
          if (i == slice.openStart)
              break;
          content = node.content;
      }
      // Back up preferredDepth to cover defining textblocks directly
      // above it, possibly skipping a non-defining textblock.
      for (let d = preferredDepth - 1; d >= 0; d--) {
          let leftNode = leftNodes[d], def = definesContent(leftNode.type);
          if (def && !leftNode.sameMarkup($from.node(Math.abs(preferredTarget) - 1)))
              preferredDepth = d;
          else if (def || !leftNode.type.isTextblock)
              break;
      }
      for (let j = slice.openStart; j >= 0; j--) {
          let openDepth = (j + preferredDepth + 1) % (slice.openStart + 1);
          let insert = leftNodes[openDepth];
          if (!insert)
              continue;
          for (let i = 0; i < targetDepths.length; i++) {
              // Loop over possible expansion levels, starting with the
              // preferred one
              let targetDepth = targetDepths[(i + preferredTargetIndex) % targetDepths.length], expand = true;
              if (targetDepth < 0) {
                  expand = false;
                  targetDepth = -targetDepth;
              }
              let parent = $from.node(targetDepth - 1), index = $from.index(targetDepth - 1);
              if (parent.canReplaceWith(index, index, insert.type, insert.marks))
                  return tr.replace($from.before(targetDepth), expand ? $to.after(targetDepth) : to, new Slice(closeFragment(slice.content, 0, slice.openStart, openDepth), openDepth, slice.openEnd));
          }
      }
      let startSteps = tr.steps.length;
      for (let i = targetDepths.length - 1; i >= 0; i--) {
          tr.replace(from, to, slice);
          if (tr.steps.length > startSteps)
              break;
          let depth = targetDepths[i];
          if (depth < 0)
              continue;
          from = $from.before(depth);
          to = $to.after(depth);
      }
  }
  function closeFragment(fragment, depth, oldOpen, newOpen, parent) {
      if (depth < oldOpen) {
          let first = fragment.firstChild;
          fragment = fragment.replaceChild(0, first.copy(closeFragment(first.content, depth + 1, oldOpen, newOpen, first)));
      }
      if (depth > newOpen) {
          let match = parent.contentMatchAt(0);
          let start = match.fillBefore(fragment).append(fragment);
          fragment = start.append(match.matchFragment(start).fillBefore(Fragment.empty, true));
      }
      return fragment;
  }
  function replaceRangeWith(tr, from, to, node) {
      if (!node.isInline && from == to && tr.doc.resolve(from).parent.content.size) {
          let point = insertPoint(tr.doc, from, node.type);
          if (point != null)
              from = to = point;
      }
      tr.replaceRange(from, to, new Slice(Fragment.from(node), 0, 0));
  }
  function deleteRange(tr, from, to) {
      let $from = tr.doc.resolve(from), $to = tr.doc.resolve(to);
      let covered = coveredDepths($from, $to);
      for (let i = 0; i < covered.length; i++) {
          let depth = covered[i], last = i == covered.length - 1;
          if ((last && depth == 0) || $from.node(depth).type.contentMatch.validEnd)
              return tr.delete($from.start(depth), $to.end(depth));
          if (depth > 0 && (last || $from.node(depth - 1).canReplace($from.index(depth - 1), $to.indexAfter(depth - 1))))
              return tr.delete($from.before(depth), $to.after(depth));
      }
      for (let d = 1; d <= $from.depth && d <= $to.depth; d++) {
          if (from - $from.start(d) == $from.depth - d && to > $from.end(d) && $to.end(d) - to != $to.depth - d)
              return tr.delete($from.before(d), to);
      }
      tr.delete(from, to);
  }
  // Returns an array of all depths for which $from - $to spans the
  // whole content of the nodes at that depth.
  function coveredDepths($from, $to) {
      let result = [], minDepth = Math.min($from.depth, $to.depth);
      for (let d = minDepth; d >= 0; d--) {
          let start = $from.start(d);
          if (start < $from.pos - ($from.depth - d) ||
              $to.end(d) > $to.pos + ($to.depth - d) ||
              $from.node(d).type.spec.isolating ||
              $to.node(d).type.spec.isolating)
              break;
          if (start == $to.start(d) ||
              (d == $from.depth && d == $to.depth && $from.parent.inlineContent && $to.parent.inlineContent &&
                  d && $to.start(d - 1) == start - 1))
              result.push(d);
      }
      return result;
  }

  /**
  Update an attribute in a specific node.
  */
  class AttrStep extends Step {
      /**
      Construct an attribute step.
      */
      constructor(
      /**
      The position of the target node.
      */
      pos, 
      /**
      The attribute to set.
      */
      attr, 
      // The attribute's new value.
      value) {
          super();
          this.pos = pos;
          this.attr = attr;
          this.value = value;
      }
      apply(doc) {
          let node = doc.nodeAt(this.pos);
          if (!node)
              return StepResult.fail("No node at attribute step's position");
          let attrs = Object.create(null);
          for (let name in node.attrs)
              attrs[name] = node.attrs[name];
          attrs[this.attr] = this.value;
          let updated = node.type.create(attrs, null, node.marks);
          return StepResult.fromReplace(doc, this.pos, this.pos + 1, new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1));
      }
      getMap() {
          return StepMap.empty;
      }
      invert(doc) {
          return new AttrStep(this.pos, this.attr, doc.nodeAt(this.pos).attrs[this.attr]);
      }
      map(mapping) {
          let pos = mapping.mapResult(this.pos, 1);
          return pos.deletedAfter ? null : new AttrStep(pos.pos, this.attr, this.value);
      }
      toJSON() {
          return { stepType: "attr", pos: this.pos, attr: this.attr, value: this.value };
      }
      static fromJSON(schema, json) {
          if (typeof json.pos != "number" || typeof json.attr != "string")
              throw new RangeError("Invalid input for AttrStep.fromJSON");
          return new AttrStep(json.pos, json.attr, json.value);
      }
  }
  Step.jsonID("attr", AttrStep);
  /**
  Update an attribute in the doc node.
  */
  class DocAttrStep extends Step {
      /**
      Construct an attribute step.
      */
      constructor(
      /**
      The attribute to set.
      */
      attr, 
      // The attribute's new value.
      value) {
          super();
          this.attr = attr;
          this.value = value;
      }
      apply(doc) {
          let attrs = Object.create(null);
          for (let name in doc.attrs)
              attrs[name] = doc.attrs[name];
          attrs[this.attr] = this.value;
          let updated = doc.type.create(attrs, doc.content, doc.marks);
          return StepResult.ok(updated);
      }
      getMap() {
          return StepMap.empty;
      }
      invert(doc) {
          return new DocAttrStep(this.attr, doc.attrs[this.attr]);
      }
      map(mapping) {
          return this;
      }
      toJSON() {
          return { stepType: "docAttr", attr: this.attr, value: this.value };
      }
      static fromJSON(schema, json) {
          if (typeof json.attr != "string")
              throw new RangeError("Invalid input for DocAttrStep.fromJSON");
          return new DocAttrStep(json.attr, json.value);
      }
  }
  Step.jsonID("docAttr", DocAttrStep);

  /**
  @internal
  */
  let TransformError = class extends Error {
  };
  TransformError = function TransformError(message) {
      let err = Error.call(this, message);
      err.__proto__ = TransformError.prototype;
      return err;
  };
  TransformError.prototype = Object.create(Error.prototype);
  TransformError.prototype.constructor = TransformError;
  TransformError.prototype.name = "TransformError";
  /**
  Abstraction to build up and track an array of
  [steps](https://prosemirror.net/docs/ref/#transform.Step) representing a document transformation.

  Most transforming methods return the `Transform` object itself, so
  that they can be chained.
  */
  class Transform {
      /**
      Create a transform that starts with the given document.
      */
      constructor(
      /**
      The current document (the result of applying the steps in the
      transform).
      */
      doc) {
          this.doc = doc;
          /**
          The steps in this transform.
          */
          this.steps = [];
          /**
          The documents before each of the steps.
          */
          this.docs = [];
          /**
          A mapping with the maps for each of the steps in this transform.
          */
          this.mapping = new Mapping;
      }
      /**
      The starting document.
      */
      get before() { return this.docs.length ? this.docs[0] : this.doc; }
      /**
      Apply a new step in this transform, saving the result. Throws an
      error when the step fails.
      */
      step(step) {
          let result = this.maybeStep(step);
          if (result.failed)
              throw new TransformError(result.failed);
          return this;
      }
      /**
      Try to apply a step in this transformation, ignoring it if it
      fails. Returns the step result.
      */
      maybeStep(step) {
          let result = step.apply(this.doc);
          if (!result.failed)
              this.addStep(step, result.doc);
          return result;
      }
      /**
      True when the document has been changed (when there are any
      steps).
      */
      get docChanged() {
          return this.steps.length > 0;
      }
      /**
      @internal
      */
      addStep(step, doc) {
          this.docs.push(this.doc);
          this.steps.push(step);
          this.mapping.appendMap(step.getMap());
          this.doc = doc;
      }
      /**
      Replace the part of the document between `from` and `to` with the
      given `slice`.
      */
      replace(from, to = from, slice = Slice.empty) {
          let step = replaceStep(this.doc, from, to, slice);
          if (step)
              this.step(step);
          return this;
      }
      /**
      Replace the given range with the given content, which may be a
      fragment, node, or array of nodes.
      */
      replaceWith(from, to, content) {
          return this.replace(from, to, new Slice(Fragment.from(content), 0, 0));
      }
      /**
      Delete the content between the given positions.
      */
      delete(from, to) {
          return this.replace(from, to, Slice.empty);
      }
      /**
      Insert the given content at the given position.
      */
      insert(pos, content) {
          return this.replaceWith(pos, pos, content);
      }
      /**
      Replace a range of the document with a given slice, using
      `from`, `to`, and the slice's
      [`openStart`](https://prosemirror.net/docs/ref/#model.Slice.openStart) property as hints, rather
      than fixed start and end points. This method may grow the
      replaced area or close open nodes in the slice in order to get a
      fit that is more in line with WYSIWYG expectations, by dropping
      fully covered parent nodes of the replaced region when they are
      marked [non-defining as
      context](https://prosemirror.net/docs/ref/#model.NodeSpec.definingAsContext), or including an
      open parent node from the slice that _is_ marked as [defining
      its content](https://prosemirror.net/docs/ref/#model.NodeSpec.definingForContent).
      
      This is the method, for example, to handle paste. The similar
      [`replace`](https://prosemirror.net/docs/ref/#transform.Transform.replace) method is a more
      primitive tool which will _not_ move the start and end of its given
      range, and is useful in situations where you need more precise
      control over what happens.
      */
      replaceRange(from, to, slice) {
          replaceRange(this, from, to, slice);
          return this;
      }
      /**
      Replace the given range with a node, but use `from` and `to` as
      hints, rather than precise positions. When from and to are the same
      and are at the start or end of a parent node in which the given
      node doesn't fit, this method may _move_ them out towards a parent
      that does allow the given node to be placed. When the given range
      completely covers a parent node, this method may completely replace
      that parent node.
      */
      replaceRangeWith(from, to, node) {
          replaceRangeWith(this, from, to, node);
          return this;
      }
      /**
      Delete the given range, expanding it to cover fully covered
      parent nodes until a valid replace is found.
      */
      deleteRange(from, to) {
          deleteRange(this, from, to);
          return this;
      }
      /**
      Split the content in the given range off from its parent, if there
      is sibling content before or after it, and move it up the tree to
      the depth specified by `target`. You'll probably want to use
      [`liftTarget`](https://prosemirror.net/docs/ref/#transform.liftTarget) to compute `target`, to make
      sure the lift is valid.
      */
      lift(range, target) {
          lift$1(this, range, target);
          return this;
      }
      /**
      Join the blocks around the given position. If depth is 2, their
      last and first siblings are also joined, and so on.
      */
      join(pos, depth = 1) {
          join(this, pos, depth);
          return this;
      }
      /**
      Wrap the given [range](https://prosemirror.net/docs/ref/#model.NodeRange) in the given set of wrappers.
      The wrappers are assumed to be valid in this position, and should
      probably be computed with [`findWrapping`](https://prosemirror.net/docs/ref/#transform.findWrapping).
      */
      wrap(range, wrappers) {
          wrap(this, range, wrappers);
          return this;
      }
      /**
      Set the type of all textblocks (partly) between `from` and `to` to
      the given node type with the given attributes.
      */
      setBlockType(from, to = from, type, attrs = null) {
          setBlockType$1(this, from, to, type, attrs);
          return this;
      }
      /**
      Change the type, attributes, and/or marks of the node at `pos`.
      When `type` isn't given, the existing node type is preserved,
      */
      setNodeMarkup(pos, type, attrs = null, marks) {
          setNodeMarkup(this, pos, type, attrs, marks);
          return this;
      }
      /**
      Set a single attribute on a given node to a new value.
      The `pos` addresses the document content. Use `setDocAttribute`
      to set attributes on the document itself.
      */
      setNodeAttribute(pos, attr, value) {
          this.step(new AttrStep(pos, attr, value));
          return this;
      }
      /**
      Set a single attribute on the document to a new value.
      */
      setDocAttribute(attr, value) {
          this.step(new DocAttrStep(attr, value));
          return this;
      }
      /**
      Add a mark to the node at position `pos`.
      */
      addNodeMark(pos, mark) {
          this.step(new AddNodeMarkStep(pos, mark));
          return this;
      }
      /**
      Remove a mark (or a mark of the given type) from the node at
      position `pos`.
      */
      removeNodeMark(pos, mark) {
          if (!(mark instanceof Mark)) {
              let node = this.doc.nodeAt(pos);
              if (!node)
                  throw new RangeError("No node at position " + pos);
              mark = mark.isInSet(node.marks);
              if (!mark)
                  return this;
          }
          this.step(new RemoveNodeMarkStep(pos, mark));
          return this;
      }
      /**
      Split the node at the given position, and optionally, if `depth` is
      greater than one, any number of nodes above that. By default, the
      parts split off will inherit the node type of the original node.
      This can be changed by passing an array of types and attributes to
      use after the split.
      */
      split(pos, depth = 1, typesAfter) {
          split(this, pos, depth, typesAfter);
          return this;
      }
      /**
      Add the given mark to the inline content between `from` and `to`.
      */
      addMark(from, to, mark) {
          addMark(this, from, to, mark);
          return this;
      }
      /**
      Remove marks from inline nodes between `from` and `to`. When
      `mark` is a single mark, remove precisely that mark. When it is
      a mark type, remove all marks of that type. When it is null,
      remove all marks of any type.
      */
      removeMark(from, to, mark) {
          removeMark(this, from, to, mark);
          return this;
      }
      /**
      Removes all marks and nodes from the content of the node at
      `pos` that don't match the given new parent node type. Accepts
      an optional starting [content match](https://prosemirror.net/docs/ref/#model.ContentMatch) as
      third argument.
      */
      clearIncompatible(pos, parentType, match) {
          clearIncompatible(this, pos, parentType, match);
          return this;
      }
  }

  var index$6 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    AddMarkStep: AddMarkStep,
    AddNodeMarkStep: AddNodeMarkStep,
    AttrStep: AttrStep,
    DocAttrStep: DocAttrStep,
    MapResult: MapResult,
    Mapping: Mapping,
    RemoveMarkStep: RemoveMarkStep,
    RemoveNodeMarkStep: RemoveNodeMarkStep,
    ReplaceAroundStep: ReplaceAroundStep,
    ReplaceStep: ReplaceStep,
    Step: Step,
    StepMap: StepMap,
    StepResult: StepResult,
    Transform: Transform,
    get TransformError () { return TransformError; },
    canJoin: canJoin,
    canSplit: canSplit,
    dropPoint: dropPoint,
    findWrapping: findWrapping,
    insertPoint: insertPoint,
    joinPoint: joinPoint,
    liftTarget: liftTarget,
    replaceStep: replaceStep
  });

  const classesById = Object.create(null);
  /**
  Superclass for editor selections. Every selection type should
  extend this. Should not be instantiated directly.
  */
  class Selection {
      /**
      Initialize a selection with the head and anchor and ranges. If no
      ranges are given, constructs a single range across `$anchor` and
      `$head`.
      */
      constructor(
      /**
      The resolved anchor of the selection (the side that stays in
      place when the selection is modified).
      */
      $anchor, 
      /**
      The resolved head of the selection (the side that moves when
      the selection is modified).
      */
      $head, ranges) {
          this.$anchor = $anchor;
          this.$head = $head;
          this.ranges = ranges || [new SelectionRange($anchor.min($head), $anchor.max($head))];
      }
      /**
      The selection's anchor, as an unresolved position.
      */
      get anchor() { return this.$anchor.pos; }
      /**
      The selection's head.
      */
      get head() { return this.$head.pos; }
      /**
      The lower bound of the selection's main range.
      */
      get from() { return this.$from.pos; }
      /**
      The upper bound of the selection's main range.
      */
      get to() { return this.$to.pos; }
      /**
      The resolved lower  bound of the selection's main range.
      */
      get $from() {
          return this.ranges[0].$from;
      }
      /**
      The resolved upper bound of the selection's main range.
      */
      get $to() {
          return this.ranges[0].$to;
      }
      /**
      Indicates whether the selection contains any content.
      */
      get empty() {
          let ranges = this.ranges;
          for (let i = 0; i < ranges.length; i++)
              if (ranges[i].$from.pos != ranges[i].$to.pos)
                  return false;
          return true;
      }
      /**
      Get the content of this selection as a slice.
      */
      content() {
          return this.$from.doc.slice(this.from, this.to, true);
      }
      /**
      Replace the selection with a slice or, if no slice is given,
      delete the selection. Will append to the given transaction.
      */
      replace(tr, content = Slice.empty) {
          // Put the new selection at the position after the inserted
          // content. When that ended in an inline node, search backwards,
          // to get the position after that node. If not, search forward.
          let lastNode = content.content.lastChild, lastParent = null;
          for (let i = 0; i < content.openEnd; i++) {
              lastParent = lastNode;
              lastNode = lastNode.lastChild;
          }
          let mapFrom = tr.steps.length, ranges = this.ranges;
          for (let i = 0; i < ranges.length; i++) {
              let { $from, $to } = ranges[i], mapping = tr.mapping.slice(mapFrom);
              tr.replaceRange(mapping.map($from.pos), mapping.map($to.pos), i ? Slice.empty : content);
              if (i == 0)
                  selectionToInsertionEnd(tr, mapFrom, (lastNode ? lastNode.isInline : lastParent && lastParent.isTextblock) ? -1 : 1);
          }
      }
      /**
      Replace the selection with the given node, appending the changes
      to the given transaction.
      */
      replaceWith(tr, node) {
          let mapFrom = tr.steps.length, ranges = this.ranges;
          for (let i = 0; i < ranges.length; i++) {
              let { $from, $to } = ranges[i], mapping = tr.mapping.slice(mapFrom);
              let from = mapping.map($from.pos), to = mapping.map($to.pos);
              if (i) {
                  tr.deleteRange(from, to);
              }
              else {
                  tr.replaceRangeWith(from, to, node);
                  selectionToInsertionEnd(tr, mapFrom, node.isInline ? -1 : 1);
              }
          }
      }
      /**
      Find a valid cursor or leaf node selection starting at the given
      position and searching back if `dir` is negative, and forward if
      positive. When `textOnly` is true, only consider cursor
      selections. Will return null when no valid selection position is
      found.
      */
      static findFrom($pos, dir, textOnly = false) {
          let inner = $pos.parent.inlineContent ? new TextSelection($pos)
              : findSelectionIn($pos.node(0), $pos.parent, $pos.pos, $pos.index(), dir, textOnly);
          if (inner)
              return inner;
          for (let depth = $pos.depth - 1; depth >= 0; depth--) {
              let found = dir < 0
                  ? findSelectionIn($pos.node(0), $pos.node(depth), $pos.before(depth + 1), $pos.index(depth), dir, textOnly)
                  : findSelectionIn($pos.node(0), $pos.node(depth), $pos.after(depth + 1), $pos.index(depth) + 1, dir, textOnly);
              if (found)
                  return found;
          }
          return null;
      }
      /**
      Find a valid cursor or leaf node selection near the given
      position. Searches forward first by default, but if `bias` is
      negative, it will search backwards first.
      */
      static near($pos, bias = 1) {
          return this.findFrom($pos, bias) || this.findFrom($pos, -bias) || new AllSelection($pos.node(0));
      }
      /**
      Find the cursor or leaf node selection closest to the start of
      the given document. Will return an
      [`AllSelection`](https://prosemirror.net/docs/ref/#state.AllSelection) if no valid position
      exists.
      */
      static atStart(doc) {
          return findSelectionIn(doc, doc, 0, 0, 1) || new AllSelection(doc);
      }
      /**
      Find the cursor or leaf node selection closest to the end of the
      given document.
      */
      static atEnd(doc) {
          return findSelectionIn(doc, doc, doc.content.size, doc.childCount, -1) || new AllSelection(doc);
      }
      /**
      Deserialize the JSON representation of a selection. Must be
      implemented for custom classes (as a static class method).
      */
      static fromJSON(doc, json) {
          if (!json || !json.type)
              throw new RangeError("Invalid input for Selection.fromJSON");
          let cls = classesById[json.type];
          if (!cls)
              throw new RangeError(`No selection type ${json.type} defined`);
          return cls.fromJSON(doc, json);
      }
      /**
      To be able to deserialize selections from JSON, custom selection
      classes must register themselves with an ID string, so that they
      can be disambiguated. Try to pick something that's unlikely to
      clash with classes from other modules.
      */
      static jsonID(id, selectionClass) {
          if (id in classesById)
              throw new RangeError("Duplicate use of selection JSON ID " + id);
          classesById[id] = selectionClass;
          selectionClass.prototype.jsonID = id;
          return selectionClass;
      }
      /**
      Get a [bookmark](https://prosemirror.net/docs/ref/#state.SelectionBookmark) for this selection,
      which is a value that can be mapped without having access to a
      current document, and later resolved to a real selection for a
      given document again. (This is used mostly by the history to
      track and restore old selections.) The default implementation of
      this method just converts the selection to a text selection and
      returns the bookmark for that.
      */
      getBookmark() {
          return TextSelection.between(this.$anchor, this.$head).getBookmark();
      }
  }
  Selection.prototype.visible = true;
  /**
  Represents a selected range in a document.
  */
  class SelectionRange {
      /**
      Create a range.
      */
      constructor(
      /**
      The lower bound of the range.
      */
      $from, 
      /**
      The upper bound of the range.
      */
      $to) {
          this.$from = $from;
          this.$to = $to;
      }
  }
  let warnedAboutTextSelection = false;
  function checkTextSelection($pos) {
      if (!warnedAboutTextSelection && !$pos.parent.inlineContent) {
          warnedAboutTextSelection = true;
          console["warn"]("TextSelection endpoint not pointing into a node with inline content (" + $pos.parent.type.name + ")");
      }
  }
  /**
  A text selection represents a classical editor selection, with a
  head (the moving side) and anchor (immobile side), both of which
  point into textblock nodes. It can be empty (a regular cursor
  position).
  */
  class TextSelection extends Selection {
      /**
      Construct a text selection between the given points.
      */
      constructor($anchor, $head = $anchor) {
          checkTextSelection($anchor);
          checkTextSelection($head);
          super($anchor, $head);
      }
      /**
      Returns a resolved position if this is a cursor selection (an
      empty text selection), and null otherwise.
      */
      get $cursor() { return this.$anchor.pos == this.$head.pos ? this.$head : null; }
      map(doc, mapping) {
          let $head = doc.resolve(mapping.map(this.head));
          if (!$head.parent.inlineContent)
              return Selection.near($head);
          let $anchor = doc.resolve(mapping.map(this.anchor));
          return new TextSelection($anchor.parent.inlineContent ? $anchor : $head, $head);
      }
      replace(tr, content = Slice.empty) {
          super.replace(tr, content);
          if (content == Slice.empty) {
              let marks = this.$from.marksAcross(this.$to);
              if (marks)
                  tr.ensureMarks(marks);
          }
      }
      eq(other) {
          return other instanceof TextSelection && other.anchor == this.anchor && other.head == this.head;
      }
      getBookmark() {
          return new TextBookmark(this.anchor, this.head);
      }
      toJSON() {
          return { type: "text", anchor: this.anchor, head: this.head };
      }
      /**
      @internal
      */
      static fromJSON(doc, json) {
          if (typeof json.anchor != "number" || typeof json.head != "number")
              throw new RangeError("Invalid input for TextSelection.fromJSON");
          return new TextSelection(doc.resolve(json.anchor), doc.resolve(json.head));
      }
      /**
      Create a text selection from non-resolved positions.
      */
      static create(doc, anchor, head = anchor) {
          let $anchor = doc.resolve(anchor);
          return new this($anchor, head == anchor ? $anchor : doc.resolve(head));
      }
      /**
      Return a text selection that spans the given positions or, if
      they aren't text positions, find a text selection near them.
      `bias` determines whether the method searches forward (default)
      or backwards (negative number) first. Will fall back to calling
      [`Selection.near`](https://prosemirror.net/docs/ref/#state.Selection^near) when the document
      doesn't contain a valid text position.
      */
      static between($anchor, $head, bias) {
          let dPos = $anchor.pos - $head.pos;
          if (!bias || dPos)
              bias = dPos >= 0 ? 1 : -1;
          if (!$head.parent.inlineContent) {
              let found = Selection.findFrom($head, bias, true) || Selection.findFrom($head, -bias, true);
              if (found)
                  $head = found.$head;
              else
                  return Selection.near($head, bias);
          }
          if (!$anchor.parent.inlineContent) {
              if (dPos == 0) {
                  $anchor = $head;
              }
              else {
                  $anchor = (Selection.findFrom($anchor, -bias, true) || Selection.findFrom($anchor, bias, true)).$anchor;
                  if (($anchor.pos < $head.pos) != (dPos < 0))
                      $anchor = $head;
              }
          }
          return new TextSelection($anchor, $head);
      }
  }
  Selection.jsonID("text", TextSelection);
  class TextBookmark {
      constructor(anchor, head) {
          this.anchor = anchor;
          this.head = head;
      }
      map(mapping) {
          return new TextBookmark(mapping.map(this.anchor), mapping.map(this.head));
      }
      resolve(doc) {
          return TextSelection.between(doc.resolve(this.anchor), doc.resolve(this.head));
      }
  }
  /**
  A node selection is a selection that points at a single node. All
  nodes marked [selectable](https://prosemirror.net/docs/ref/#model.NodeSpec.selectable) can be the
  target of a node selection. In such a selection, `from` and `to`
  point directly before and after the selected node, `anchor` equals
  `from`, and `head` equals `to`..
  */
  class NodeSelection extends Selection {
      /**
      Create a node selection. Does not verify the validity of its
      argument.
      */
      constructor($pos) {
          let node = $pos.nodeAfter;
          let $end = $pos.node(0).resolve($pos.pos + node.nodeSize);
          super($pos, $end);
          this.node = node;
      }
      map(doc, mapping) {
          let { deleted, pos } = mapping.mapResult(this.anchor);
          let $pos = doc.resolve(pos);
          if (deleted)
              return Selection.near($pos);
          return new NodeSelection($pos);
      }
      content() {
          return new Slice(Fragment.from(this.node), 0, 0);
      }
      eq(other) {
          return other instanceof NodeSelection && other.anchor == this.anchor;
      }
      toJSON() {
          return { type: "node", anchor: this.anchor };
      }
      getBookmark() { return new NodeBookmark(this.anchor); }
      /**
      @internal
      */
      static fromJSON(doc, json) {
          if (typeof json.anchor != "number")
              throw new RangeError("Invalid input for NodeSelection.fromJSON");
          return new NodeSelection(doc.resolve(json.anchor));
      }
      /**
      Create a node selection from non-resolved positions.
      */
      static create(doc, from) {
          return new NodeSelection(doc.resolve(from));
      }
      /**
      Determines whether the given node may be selected as a node
      selection.
      */
      static isSelectable(node) {
          return !node.isText && node.type.spec.selectable !== false;
      }
  }
  NodeSelection.prototype.visible = false;
  Selection.jsonID("node", NodeSelection);
  class NodeBookmark {
      constructor(anchor) {
          this.anchor = anchor;
      }
      map(mapping) {
          let { deleted, pos } = mapping.mapResult(this.anchor);
          return deleted ? new TextBookmark(pos, pos) : new NodeBookmark(pos);
      }
      resolve(doc) {
          let $pos = doc.resolve(this.anchor), node = $pos.nodeAfter;
          if (node && NodeSelection.isSelectable(node))
              return new NodeSelection($pos);
          return Selection.near($pos);
      }
  }
  /**
  A selection type that represents selecting the whole document
  (which can not necessarily be expressed with a text selection, when
  there are for example leaf block nodes at the start or end of the
  document).
  */
  class AllSelection extends Selection {
      /**
      Create an all-selection over the given document.
      */
      constructor(doc) {
          super(doc.resolve(0), doc.resolve(doc.content.size));
      }
      replace(tr, content = Slice.empty) {
          if (content == Slice.empty) {
              tr.delete(0, tr.doc.content.size);
              let sel = Selection.atStart(tr.doc);
              if (!sel.eq(tr.selection))
                  tr.setSelection(sel);
          }
          else {
              super.replace(tr, content);
          }
      }
      toJSON() { return { type: "all" }; }
      /**
      @internal
      */
      static fromJSON(doc) { return new AllSelection(doc); }
      map(doc) { return new AllSelection(doc); }
      eq(other) { return other instanceof AllSelection; }
      getBookmark() { return AllBookmark; }
  }
  Selection.jsonID("all", AllSelection);
  const AllBookmark = {
      map() { return this; },
      resolve(doc) { return new AllSelection(doc); }
  };
  // FIXME we'll need some awareness of text direction when scanning for selections
  // Try to find a selection inside the given node. `pos` points at the
  // position where the search starts. When `text` is true, only return
  // text selections.
  function findSelectionIn(doc, node, pos, index, dir, text = false) {
      if (node.inlineContent)
          return TextSelection.create(doc, pos);
      for (let i = index - (dir > 0 ? 0 : 1); dir > 0 ? i < node.childCount : i >= 0; i += dir) {
          let child = node.child(i);
          if (!child.isAtom) {
              let inner = findSelectionIn(doc, child, pos + dir, dir < 0 ? child.childCount : 0, dir, text);
              if (inner)
                  return inner;
          }
          else if (!text && NodeSelection.isSelectable(child)) {
              return NodeSelection.create(doc, pos - (dir < 0 ? child.nodeSize : 0));
          }
          pos += child.nodeSize * dir;
      }
      return null;
  }
  function selectionToInsertionEnd(tr, startLen, bias) {
      let last = tr.steps.length - 1;
      if (last < startLen)
          return;
      let step = tr.steps[last];
      if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep))
          return;
      let map = tr.mapping.maps[last], end;
      map.forEach((_from, _to, _newFrom, newTo) => { if (end == null)
          end = newTo; });
      tr.setSelection(Selection.near(tr.doc.resolve(end), bias));
  }

  const UPDATED_SEL = 1, UPDATED_MARKS = 2, UPDATED_SCROLL = 4;
  /**
  An editor state transaction, which can be applied to a state to
  create an updated state. Use
  [`EditorState.tr`](https://prosemirror.net/docs/ref/#state.EditorState.tr) to create an instance.

  Transactions track changes to the document (they are a subclass of
  [`Transform`](https://prosemirror.net/docs/ref/#transform.Transform)), but also other state changes,
  like selection updates and adjustments of the set of [stored
  marks](https://prosemirror.net/docs/ref/#state.EditorState.storedMarks). In addition, you can store
  metadata properties in a transaction, which are extra pieces of
  information that client code or plugins can use to describe what a
  transaction represents, so that they can update their [own
  state](https://prosemirror.net/docs/ref/#state.StateField) accordingly.

  The [editor view](https://prosemirror.net/docs/ref/#view.EditorView) uses a few metadata
  properties: it will attach a property `"pointer"` with the value
  `true` to selection transactions directly caused by mouse or touch
  input, a `"composition"` property holding an ID identifying the
  composition that caused it to transactions caused by composed DOM
  input, and a `"uiEvent"` property of that may be `"paste"`,
  `"cut"`, or `"drop"`.
  */
  class Transaction extends Transform {
      /**
      @internal
      */
      constructor(state) {
          super(state.doc);
          // The step count for which the current selection is valid.
          this.curSelectionFor = 0;
          // Bitfield to track which aspects of the state were updated by
          // this transaction.
          this.updated = 0;
          // Object used to store metadata properties for the transaction.
          this.meta = Object.create(null);
          this.time = Date.now();
          this.curSelection = state.selection;
          this.storedMarks = state.storedMarks;
      }
      /**
      The transaction's current selection. This defaults to the editor
      selection [mapped](https://prosemirror.net/docs/ref/#state.Selection.map) through the steps in the
      transaction, but can be overwritten with
      [`setSelection`](https://prosemirror.net/docs/ref/#state.Transaction.setSelection).
      */
      get selection() {
          if (this.curSelectionFor < this.steps.length) {
              this.curSelection = this.curSelection.map(this.doc, this.mapping.slice(this.curSelectionFor));
              this.curSelectionFor = this.steps.length;
          }
          return this.curSelection;
      }
      /**
      Update the transaction's current selection. Will determine the
      selection that the editor gets when the transaction is applied.
      */
      setSelection(selection) {
          if (selection.$from.doc != this.doc)
              throw new RangeError("Selection passed to setSelection must point at the current document");
          this.curSelection = selection;
          this.curSelectionFor = this.steps.length;
          this.updated = (this.updated | UPDATED_SEL) & ~UPDATED_MARKS;
          this.storedMarks = null;
          return this;
      }
      /**
      Whether the selection was explicitly updated by this transaction.
      */
      get selectionSet() {
          return (this.updated & UPDATED_SEL) > 0;
      }
      /**
      Set the current stored marks.
      */
      setStoredMarks(marks) {
          this.storedMarks = marks;
          this.updated |= UPDATED_MARKS;
          return this;
      }
      /**
      Make sure the current stored marks or, if that is null, the marks
      at the selection, match the given set of marks. Does nothing if
      this is already the case.
      */
      ensureMarks(marks) {
          if (!Mark.sameSet(this.storedMarks || this.selection.$from.marks(), marks))
              this.setStoredMarks(marks);
          return this;
      }
      /**
      Add a mark to the set of stored marks.
      */
      addStoredMark(mark) {
          return this.ensureMarks(mark.addToSet(this.storedMarks || this.selection.$head.marks()));
      }
      /**
      Remove a mark or mark type from the set of stored marks.
      */
      removeStoredMark(mark) {
          return this.ensureMarks(mark.removeFromSet(this.storedMarks || this.selection.$head.marks()));
      }
      /**
      Whether the stored marks were explicitly set for this transaction.
      */
      get storedMarksSet() {
          return (this.updated & UPDATED_MARKS) > 0;
      }
      /**
      @internal
      */
      addStep(step, doc) {
          super.addStep(step, doc);
          this.updated = this.updated & ~UPDATED_MARKS;
          this.storedMarks = null;
      }
      /**
      Update the timestamp for the transaction.
      */
      setTime(time) {
          this.time = time;
          return this;
      }
      /**
      Replace the current selection with the given slice.
      */
      replaceSelection(slice) {
          this.selection.replace(this, slice);
          return this;
      }
      /**
      Replace the selection with the given node. When `inheritMarks` is
      true and the content is inline, it inherits the marks from the
      place where it is inserted.
      */
      replaceSelectionWith(node, inheritMarks = true) {
          let selection = this.selection;
          if (inheritMarks)
              node = node.mark(this.storedMarks || (selection.empty ? selection.$from.marks() : (selection.$from.marksAcross(selection.$to) || Mark.none)));
          selection.replaceWith(this, node);
          return this;
      }
      /**
      Delete the selection.
      */
      deleteSelection() {
          this.selection.replace(this);
          return this;
      }
      /**
      Replace the given range, or the selection if no range is given,
      with a text node containing the given string.
      */
      insertText(text, from, to) {
          let schema = this.doc.type.schema;
          if (from == null) {
              if (!text)
                  return this.deleteSelection();
              return this.replaceSelectionWith(schema.text(text), true);
          }
          else {
              if (to == null)
                  to = from;
              to = to == null ? from : to;
              if (!text)
                  return this.deleteRange(from, to);
              let marks = this.storedMarks;
              if (!marks) {
                  let $from = this.doc.resolve(from);
                  marks = to == from ? $from.marks() : $from.marksAcross(this.doc.resolve(to));
              }
              this.replaceRangeWith(from, to, schema.text(text, marks));
              if (!this.selection.empty)
                  this.setSelection(Selection.near(this.selection.$to));
              return this;
          }
      }
      /**
      Store a metadata property in this transaction, keyed either by
      name or by plugin.
      */
      setMeta(key, value) {
          this.meta[typeof key == "string" ? key : key.key] = value;
          return this;
      }
      /**
      Retrieve a metadata property for a given name or plugin.
      */
      getMeta(key) {
          return this.meta[typeof key == "string" ? key : key.key];
      }
      /**
      Returns true if this transaction doesn't contain any metadata,
      and can thus safely be extended.
      */
      get isGeneric() {
          for (let _ in this.meta)
              return false;
          return true;
      }
      /**
      Indicate that the editor should scroll the selection into view
      when updated to the state produced by this transaction.
      */
      scrollIntoView() {
          this.updated |= UPDATED_SCROLL;
          return this;
      }
      /**
      True when this transaction has had `scrollIntoView` called on it.
      */
      get scrolledIntoView() {
          return (this.updated & UPDATED_SCROLL) > 0;
      }
  }

  function bind(f, self) {
      return !self || !f ? f : f.bind(self);
  }
  class FieldDesc {
      constructor(name, desc, self) {
          this.name = name;
          this.init = bind(desc.init, self);
          this.apply = bind(desc.apply, self);
      }
  }
  const baseFields = [
      new FieldDesc("doc", {
          init(config) { return config.doc || config.schema.topNodeType.createAndFill(); },
          apply(tr) { return tr.doc; }
      }),
      new FieldDesc("selection", {
          init(config, instance) { return config.selection || Selection.atStart(instance.doc); },
          apply(tr) { return tr.selection; }
      }),
      new FieldDesc("storedMarks", {
          init(config) { return config.storedMarks || null; },
          apply(tr, _marks, _old, state) { return state.selection.$cursor ? tr.storedMarks : null; }
      }),
      new FieldDesc("scrollToSelection", {
          init() { return 0; },
          apply(tr, prev) { return tr.scrolledIntoView ? prev + 1 : prev; }
      })
  ];
  // Object wrapping the part of a state object that stays the same
  // across transactions. Stored in the state's `config` property.
  class Configuration {
      constructor(schema, plugins) {
          this.schema = schema;
          this.plugins = [];
          this.pluginsByKey = Object.create(null);
          this.fields = baseFields.slice();
          if (plugins)
              plugins.forEach(plugin => {
                  if (this.pluginsByKey[plugin.key])
                      throw new RangeError("Adding different instances of a keyed plugin (" + plugin.key + ")");
                  this.plugins.push(plugin);
                  this.pluginsByKey[plugin.key] = plugin;
                  if (plugin.spec.state)
                      this.fields.push(new FieldDesc(plugin.key, plugin.spec.state, plugin));
              });
      }
  }
  /**
  The state of a ProseMirror editor is represented by an object of
  this type. A state is a persistent data structure—it isn't
  updated, but rather a new state value is computed from an old one
  using the [`apply`](https://prosemirror.net/docs/ref/#state.EditorState.apply) method.

  A state holds a number of built-in fields, and plugins can
  [define](https://prosemirror.net/docs/ref/#state.PluginSpec.state) additional fields.
  */
  class EditorState {
      /**
      @internal
      */
      constructor(
      /**
      @internal
      */
      config) {
          this.config = config;
      }
      /**
      The schema of the state's document.
      */
      get schema() {
          return this.config.schema;
      }
      /**
      The plugins that are active in this state.
      */
      get plugins() {
          return this.config.plugins;
      }
      /**
      Apply the given transaction to produce a new state.
      */
      apply(tr) {
          return this.applyTransaction(tr).state;
      }
      /**
      @internal
      */
      filterTransaction(tr, ignore = -1) {
          for (let i = 0; i < this.config.plugins.length; i++)
              if (i != ignore) {
                  let plugin = this.config.plugins[i];
                  if (plugin.spec.filterTransaction && !plugin.spec.filterTransaction.call(plugin, tr, this))
                      return false;
              }
          return true;
      }
      /**
      Verbose variant of [`apply`](https://prosemirror.net/docs/ref/#state.EditorState.apply) that
      returns the precise transactions that were applied (which might
      be influenced by the [transaction
      hooks](https://prosemirror.net/docs/ref/#state.PluginSpec.filterTransaction) of
      plugins) along with the new state.
      */
      applyTransaction(rootTr) {
          if (!this.filterTransaction(rootTr))
              return { state: this, transactions: [] };
          let trs = [rootTr], newState = this.applyInner(rootTr), seen = null;
          // This loop repeatedly gives plugins a chance to respond to
          // transactions as new transactions are added, making sure to only
          // pass the transactions the plugin did not see before.
          for (;;) {
              let haveNew = false;
              for (let i = 0; i < this.config.plugins.length; i++) {
                  let plugin = this.config.plugins[i];
                  if (plugin.spec.appendTransaction) {
                      let n = seen ? seen[i].n : 0, oldState = seen ? seen[i].state : this;
                      let tr = n < trs.length &&
                          plugin.spec.appendTransaction.call(plugin, n ? trs.slice(n) : trs, oldState, newState);
                      if (tr && newState.filterTransaction(tr, i)) {
                          tr.setMeta("appendedTransaction", rootTr);
                          if (!seen) {
                              seen = [];
                              for (let j = 0; j < this.config.plugins.length; j++)
                                  seen.push(j < i ? { state: newState, n: trs.length } : { state: this, n: 0 });
                          }
                          trs.push(tr);
                          newState = newState.applyInner(tr);
                          haveNew = true;
                      }
                      if (seen)
                          seen[i] = { state: newState, n: trs.length };
                  }
              }
              if (!haveNew)
                  return { state: newState, transactions: trs };
          }
      }
      /**
      @internal
      */
      applyInner(tr) {
          if (!tr.before.eq(this.doc))
              throw new RangeError("Applying a mismatched transaction");
          let newInstance = new EditorState(this.config), fields = this.config.fields;
          for (let i = 0; i < fields.length; i++) {
              let field = fields[i];
              newInstance[field.name] = field.apply(tr, this[field.name], this, newInstance);
          }
          return newInstance;
      }
      /**
      Start a [transaction](https://prosemirror.net/docs/ref/#state.Transaction) from this state.
      */
      get tr() { return new Transaction(this); }
      /**
      Create a new state.
      */
      static create(config) {
          let $config = new Configuration(config.doc ? config.doc.type.schema : config.schema, config.plugins);
          let instance = new EditorState($config);
          for (let i = 0; i < $config.fields.length; i++)
              instance[$config.fields[i].name] = $config.fields[i].init(config, instance);
          return instance;
      }
      /**
      Create a new state based on this one, but with an adjusted set
      of active plugins. State fields that exist in both sets of
      plugins are kept unchanged. Those that no longer exist are
      dropped, and those that are new are initialized using their
      [`init`](https://prosemirror.net/docs/ref/#state.StateField.init) method, passing in the new
      configuration object..
      */
      reconfigure(config) {
          let $config = new Configuration(this.schema, config.plugins);
          let fields = $config.fields, instance = new EditorState($config);
          for (let i = 0; i < fields.length; i++) {
              let name = fields[i].name;
              instance[name] = this.hasOwnProperty(name) ? this[name] : fields[i].init(config, instance);
          }
          return instance;
      }
      /**
      Serialize this state to JSON. If you want to serialize the state
      of plugins, pass an object mapping property names to use in the
      resulting JSON object to plugin objects. The argument may also be
      a string or number, in which case it is ignored, to support the
      way `JSON.stringify` calls `toString` methods.
      */
      toJSON(pluginFields) {
          let result = { doc: this.doc.toJSON(), selection: this.selection.toJSON() };
          if (this.storedMarks)
              result.storedMarks = this.storedMarks.map(m => m.toJSON());
          if (pluginFields && typeof pluginFields == 'object')
              for (let prop in pluginFields) {
                  if (prop == "doc" || prop == "selection")
                      throw new RangeError("The JSON fields `doc` and `selection` are reserved");
                  let plugin = pluginFields[prop], state = plugin.spec.state;
                  if (state && state.toJSON)
                      result[prop] = state.toJSON.call(plugin, this[plugin.key]);
              }
          return result;
      }
      /**
      Deserialize a JSON representation of a state. `config` should
      have at least a `schema` field, and should contain array of
      plugins to initialize the state with. `pluginFields` can be used
      to deserialize the state of plugins, by associating plugin
      instances with the property names they use in the JSON object.
      */
      static fromJSON(config, json, pluginFields) {
          if (!json)
              throw new RangeError("Invalid input for EditorState.fromJSON");
          if (!config.schema)
              throw new RangeError("Required config field 'schema' missing");
          let $config = new Configuration(config.schema, config.plugins);
          let instance = new EditorState($config);
          $config.fields.forEach(field => {
              if (field.name == "doc") {
                  instance.doc = Node.fromJSON(config.schema, json.doc);
              }
              else if (field.name == "selection") {
                  instance.selection = Selection.fromJSON(instance.doc, json.selection);
              }
              else if (field.name == "storedMarks") {
                  if (json.storedMarks)
                      instance.storedMarks = json.storedMarks.map(config.schema.markFromJSON);
              }
              else {
                  if (pluginFields)
                      for (let prop in pluginFields) {
                          let plugin = pluginFields[prop], state = plugin.spec.state;
                          if (plugin.key == field.name && state && state.fromJSON &&
                              Object.prototype.hasOwnProperty.call(json, prop)) {
                              instance[field.name] = state.fromJSON.call(plugin, config, json[prop], instance);
                              return;
                          }
                      }
                  instance[field.name] = field.init(config, instance);
              }
          });
          return instance;
      }
  }

  function bindProps(obj, self, target) {
      for (let prop in obj) {
          let val = obj[prop];
          if (val instanceof Function)
              val = val.bind(self);
          else if (prop == "handleDOMEvents")
              val = bindProps(val, self, {});
          target[prop] = val;
      }
      return target;
  }
  /**
  Plugins bundle functionality that can be added to an editor.
  They are part of the [editor state](https://prosemirror.net/docs/ref/#state.EditorState) and
  may influence that state and the view that contains it.
  */
  class Plugin {
      /**
      Create a plugin.
      */
      constructor(
      /**
      The plugin's [spec object](https://prosemirror.net/docs/ref/#state.PluginSpec).
      */
      spec) {
          this.spec = spec;
          /**
          The [props](https://prosemirror.net/docs/ref/#view.EditorProps) exported by this plugin.
          */
          this.props = {};
          if (spec.props)
              bindProps(spec.props, this, this.props);
          this.key = spec.key ? spec.key.key : createKey("plugin");
      }
      /**
      Extract the plugin's state field from an editor state.
      */
      getState(state) { return state[this.key]; }
  }
  const keys = Object.create(null);
  function createKey(name) {
      if (name in keys)
          return name + "$" + ++keys[name];
      keys[name] = 0;
      return name + "$";
  }
  /**
  A key is used to [tag](https://prosemirror.net/docs/ref/#state.PluginSpec.key) plugins in a way
  that makes it possible to find them, given an editor state.
  Assigning a key does mean only one plugin of that type can be
  active in a state.
  */
  class PluginKey {
      /**
      Create a plugin key.
      */
      constructor(name = "key") { this.key = createKey(name); }
      /**
      Get the active plugin with this key, if any, from an editor
      state.
      */
      get(state) { return state.config.pluginsByKey[this.key]; }
      /**
      Get the plugin's state from an editor state.
      */
      getState(state) { return state[this.key]; }
  }

  var index$5 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    AllSelection: AllSelection,
    EditorState: EditorState,
    NodeSelection: NodeSelection,
    Plugin: Plugin,
    PluginKey: PluginKey,
    Selection: Selection,
    SelectionRange: SelectionRange,
    TextSelection: TextSelection,
    Transaction: Transaction
  });

  const domIndex = function (node) {
      for (var index = 0;; index++) {
          node = node.previousSibling;
          if (!node)
              return index;
      }
  };
  const parentNode = function (node) {
      let parent = node.assignedSlot || node.parentNode;
      return parent && parent.nodeType == 11 ? parent.host : parent;
  };
  let reusedRange = null;
  // Note that this will always return the same range, because DOM range
  // objects are every expensive, and keep slowing down subsequent DOM
  // updates, for some reason.
  const textRange = function (node, from, to) {
      let range = reusedRange || (reusedRange = document.createRange());
      range.setEnd(node, to == null ? node.nodeValue.length : to);
      range.setStart(node, from || 0);
      return range;
  };
  const clearReusedRange = function () {
      reusedRange = null;
  };
  // Scans forward and backward through DOM positions equivalent to the
  // given one to see if the two are in the same place (i.e. after a
  // text node vs at the end of that text node)
  const isEquivalentPosition = function (node, off, targetNode, targetOff) {
      return targetNode && (scanFor(node, off, targetNode, targetOff, -1) ||
          scanFor(node, off, targetNode, targetOff, 1));
  };
  const atomElements = /^(img|br|input|textarea|hr)$/i;
  function scanFor(node, off, targetNode, targetOff, dir) {
      for (;;) {
          if (node == targetNode && off == targetOff)
              return true;
          if (off == (dir < 0 ? 0 : nodeSize(node))) {
              let parent = node.parentNode;
              if (!parent || parent.nodeType != 1 || hasBlockDesc(node) || atomElements.test(node.nodeName) ||
                  node.contentEditable == "false")
                  return false;
              off = domIndex(node) + (dir < 0 ? 0 : 1);
              node = parent;
          }
          else if (node.nodeType == 1) {
              node = node.childNodes[off + (dir < 0 ? -1 : 0)];
              if (node.contentEditable == "false")
                  return false;
              off = dir < 0 ? nodeSize(node) : 0;
          }
          else {
              return false;
          }
      }
  }
  function nodeSize(node) {
      return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length;
  }
  function textNodeBefore$1(node, offset) {
      for (;;) {
          if (node.nodeType == 3 && offset)
              return node;
          if (node.nodeType == 1 && offset > 0) {
              if (node.contentEditable == "false")
                  return null;
              node = node.childNodes[offset - 1];
              offset = nodeSize(node);
          }
          else if (node.parentNode && !hasBlockDesc(node)) {
              offset = domIndex(node);
              node = node.parentNode;
          }
          else {
              return null;
          }
      }
  }
  function textNodeAfter$1(node, offset) {
      for (;;) {
          if (node.nodeType == 3 && offset < node.nodeValue.length)
              return node;
          if (node.nodeType == 1 && offset < node.childNodes.length) {
              if (node.contentEditable == "false")
                  return null;
              node = node.childNodes[offset];
              offset = 0;
          }
          else if (node.parentNode && !hasBlockDesc(node)) {
              offset = domIndex(node) + 1;
              node = node.parentNode;
          }
          else {
              return null;
          }
      }
  }
  function isOnEdge(node, offset, parent) {
      for (let atStart = offset == 0, atEnd = offset == nodeSize(node); atStart || atEnd;) {
          if (node == parent)
              return true;
          let index = domIndex(node);
          node = node.parentNode;
          if (!node)
              return false;
          atStart = atStart && index == 0;
          atEnd = atEnd && index == nodeSize(node);
      }
  }
  function hasBlockDesc(dom) {
      let desc;
      for (let cur = dom; cur; cur = cur.parentNode)
          if (desc = cur.pmViewDesc)
              break;
      return desc && desc.node && desc.node.isBlock && (desc.dom == dom || desc.contentDOM == dom);
  }
  // Work around Chrome issue https://bugs.chromium.org/p/chromium/issues/detail?id=447523
  // (isCollapsed inappropriately returns true in shadow dom)
  const selectionCollapsed = function (domSel) {
      return domSel.focusNode && isEquivalentPosition(domSel.focusNode, domSel.focusOffset, domSel.anchorNode, domSel.anchorOffset);
  };
  function keyEvent(keyCode, key) {
      let event = document.createEvent("Event");
      event.initEvent("keydown", true, true);
      event.keyCode = keyCode;
      event.key = event.code = key;
      return event;
  }
  function deepActiveElement(doc) {
      let elt = doc.activeElement;
      while (elt && elt.shadowRoot)
          elt = elt.shadowRoot.activeElement;
      return elt;
  }
  function caretFromPoint(doc, x, y) {
      if (doc.caretPositionFromPoint) {
          try { // Firefox throws for this call in hard-to-predict circumstances (#994)
              let pos = doc.caretPositionFromPoint(x, y);
              if (pos)
                  return { node: pos.offsetNode, offset: pos.offset };
          }
          catch (_) { }
      }
      if (doc.caretRangeFromPoint) {
          let range = doc.caretRangeFromPoint(x, y);
          if (range)
              return { node: range.startContainer, offset: range.startOffset };
      }
  }

  const nav = typeof navigator != "undefined" ? navigator : null;
  const doc$1 = typeof document != "undefined" ? document : null;
  const agent = (nav && nav.userAgent) || "";
  const ie_edge = /Edge\/(\d+)/.exec(agent);
  const ie_upto10 = /MSIE \d/.exec(agent);
  const ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(agent);
  const ie$1 = !!(ie_upto10 || ie_11up || ie_edge);
  const ie_version = ie_upto10 ? document.documentMode : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : 0;
  const gecko = !ie$1 && /gecko\/(\d+)/i.test(agent);
  gecko && +(/Firefox\/(\d+)/.exec(agent) || [0, 0])[1];
  const _chrome = !ie$1 && /Chrome\/(\d+)/.exec(agent);
  const chrome = !!_chrome;
  const chrome_version = _chrome ? +_chrome[1] : 0;
  const safari = !ie$1 && !!nav && /Apple Computer/.test(nav.vendor);
  // Is true for both iOS and iPadOS for convenience
  const ios = safari && (/Mobile\/\w+/.test(agent) || !!nav && nav.maxTouchPoints > 2);
  const mac$3 = ios || (nav ? /Mac/.test(nav.platform) : false);
  const windows = nav ? /Win/.test(nav.platform) : false;
  const android = /Android \d/.test(agent);
  const webkit = !!doc$1 && "webkitFontSmoothing" in doc$1.documentElement.style;
  const webkit_version = webkit ? +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1] : 0;

  function windowRect(doc) {
      let vp = doc.defaultView && doc.defaultView.visualViewport;
      if (vp)
          return {
              left: 0, right: vp.width,
              top: 0, bottom: vp.height
          };
      return { left: 0, right: doc.documentElement.clientWidth,
          top: 0, bottom: doc.documentElement.clientHeight };
  }
  function getSide(value, side) {
      return typeof value == "number" ? value : value[side];
  }
  function clientRect(node) {
      let rect = node.getBoundingClientRect();
      // Adjust for elements with style "transform: scale()"
      let scaleX = (rect.width / node.offsetWidth) || 1;
      let scaleY = (rect.height / node.offsetHeight) || 1;
      // Make sure scrollbar width isn't included in the rectangle
      return { left: rect.left, right: rect.left + node.clientWidth * scaleX,
          top: rect.top, bottom: rect.top + node.clientHeight * scaleY };
  }
  function scrollRectIntoView(view, rect, startDOM) {
      let scrollThreshold = view.someProp("scrollThreshold") || 0, scrollMargin = view.someProp("scrollMargin") || 5;
      let doc = view.dom.ownerDocument;
      for (let parent = startDOM || view.dom;; parent = parentNode(parent)) {
          if (!parent)
              break;
          if (parent.nodeType != 1)
              continue;
          let elt = parent;
          let atTop = elt == doc.body;
          let bounding = atTop ? windowRect(doc) : clientRect(elt);
          let moveX = 0, moveY = 0;
          if (rect.top < bounding.top + getSide(scrollThreshold, "top"))
              moveY = -(bounding.top - rect.top + getSide(scrollMargin, "top"));
          else if (rect.bottom > bounding.bottom - getSide(scrollThreshold, "bottom"))
              moveY = rect.bottom - rect.top > bounding.bottom - bounding.top
                  ? rect.top + getSide(scrollMargin, "top") - bounding.top
                  : rect.bottom - bounding.bottom + getSide(scrollMargin, "bottom");
          if (rect.left < bounding.left + getSide(scrollThreshold, "left"))
              moveX = -(bounding.left - rect.left + getSide(scrollMargin, "left"));
          else if (rect.right > bounding.right - getSide(scrollThreshold, "right"))
              moveX = rect.right - bounding.right + getSide(scrollMargin, "right");
          if (moveX || moveY) {
              if (atTop) {
                  doc.defaultView.scrollBy(moveX, moveY);
              }
              else {
                  let startX = elt.scrollLeft, startY = elt.scrollTop;
                  if (moveY)
                      elt.scrollTop += moveY;
                  if (moveX)
                      elt.scrollLeft += moveX;
                  let dX = elt.scrollLeft - startX, dY = elt.scrollTop - startY;
                  rect = { left: rect.left - dX, top: rect.top - dY, right: rect.right - dX, bottom: rect.bottom - dY };
              }
          }
          if (atTop || /^(fixed|sticky)$/.test(getComputedStyle(parent).position))
              break;
      }
  }
  // Store the scroll position of the editor's parent nodes, along with
  // the top position of an element near the top of the editor, which
  // will be used to make sure the visible viewport remains stable even
  // when the size of the content above changes.
  function storeScrollPos(view) {
      let rect = view.dom.getBoundingClientRect(), startY = Math.max(0, rect.top);
      let refDOM, refTop;
      for (let x = (rect.left + rect.right) / 2, y = startY + 1; y < Math.min(innerHeight, rect.bottom); y += 5) {
          let dom = view.root.elementFromPoint(x, y);
          if (!dom || dom == view.dom || !view.dom.contains(dom))
              continue;
          let localRect = dom.getBoundingClientRect();
          if (localRect.top >= startY - 20) {
              refDOM = dom;
              refTop = localRect.top;
              break;
          }
      }
      return { refDOM: refDOM, refTop: refTop, stack: scrollStack(view.dom) };
  }
  function scrollStack(dom) {
      let stack = [], doc = dom.ownerDocument;
      for (let cur = dom; cur; cur = parentNode(cur)) {
          stack.push({ dom: cur, top: cur.scrollTop, left: cur.scrollLeft });
          if (dom == doc)
              break;
      }
      return stack;
  }
  // Reset the scroll position of the editor's parent nodes to that what
  // it was before, when storeScrollPos was called.
  function resetScrollPos({ refDOM, refTop, stack }) {
      let newRefTop = refDOM ? refDOM.getBoundingClientRect().top : 0;
      restoreScrollStack(stack, newRefTop == 0 ? 0 : newRefTop - refTop);
  }
  function restoreScrollStack(stack, dTop) {
      for (let i = 0; i < stack.length; i++) {
          let { dom, top, left } = stack[i];
          if (dom.scrollTop != top + dTop)
              dom.scrollTop = top + dTop;
          if (dom.scrollLeft != left)
              dom.scrollLeft = left;
      }
  }
  let preventScrollSupported = null;
  // Feature-detects support for .focus({preventScroll: true}), and uses
  // a fallback kludge when not supported.
  function focusPreventScroll(dom) {
      if (dom.setActive)
          return dom.setActive(); // in IE
      if (preventScrollSupported)
          return dom.focus(preventScrollSupported);
      let stored = scrollStack(dom);
      dom.focus(preventScrollSupported == null ? {
          get preventScroll() {
              preventScrollSupported = { preventScroll: true };
              return true;
          }
      } : undefined);
      if (!preventScrollSupported) {
          preventScrollSupported = false;
          restoreScrollStack(stored, 0);
      }
  }
  function findOffsetInNode(node, coords) {
      let closest, dxClosest = 2e8, coordsClosest, offset = 0;
      let rowBot = coords.top, rowTop = coords.top;
      let firstBelow, coordsBelow;
      for (let child = node.firstChild, childIndex = 0; child; child = child.nextSibling, childIndex++) {
          let rects;
          if (child.nodeType == 1)
              rects = child.getClientRects();
          else if (child.nodeType == 3)
              rects = textRange(child).getClientRects();
          else
              continue;
          for (let i = 0; i < rects.length; i++) {
              let rect = rects[i];
              if (rect.top <= rowBot && rect.bottom >= rowTop) {
                  rowBot = Math.max(rect.bottom, rowBot);
                  rowTop = Math.min(rect.top, rowTop);
                  let dx = rect.left > coords.left ? rect.left - coords.left
                      : rect.right < coords.left ? coords.left - rect.right : 0;
                  if (dx < dxClosest) {
                      closest = child;
                      dxClosest = dx;
                      coordsClosest = dx && closest.nodeType == 3 ? {
                          left: rect.right < coords.left ? rect.right : rect.left,
                          top: coords.top
                      } : coords;
                      if (child.nodeType == 1 && dx)
                          offset = childIndex + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0);
                      continue;
                  }
              }
              else if (rect.top > coords.top && !firstBelow && rect.left <= coords.left && rect.right >= coords.left) {
                  firstBelow = child;
                  coordsBelow = { left: Math.max(rect.left, Math.min(rect.right, coords.left)), top: rect.top };
              }
              if (!closest && (coords.left >= rect.right && coords.top >= rect.top ||
                  coords.left >= rect.left && coords.top >= rect.bottom))
                  offset = childIndex + 1;
          }
      }
      if (!closest && firstBelow) {
          closest = firstBelow;
          coordsClosest = coordsBelow;
          dxClosest = 0;
      }
      if (closest && closest.nodeType == 3)
          return findOffsetInText(closest, coordsClosest);
      if (!closest || (dxClosest && closest.nodeType == 1))
          return { node, offset };
      return findOffsetInNode(closest, coordsClosest);
  }
  function findOffsetInText(node, coords) {
      let len = node.nodeValue.length;
      let range = document.createRange();
      for (let i = 0; i < len; i++) {
          range.setEnd(node, i + 1);
          range.setStart(node, i);
          let rect = singleRect(range, 1);
          if (rect.top == rect.bottom)
              continue;
          if (inRect(coords, rect))
              return { node, offset: i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0) };
      }
      return { node, offset: 0 };
  }
  function inRect(coords, rect) {
      return coords.left >= rect.left - 1 && coords.left <= rect.right + 1 &&
          coords.top >= rect.top - 1 && coords.top <= rect.bottom + 1;
  }
  function targetKludge(dom, coords) {
      let parent = dom.parentNode;
      if (parent && /^li$/i.test(parent.nodeName) && coords.left < dom.getBoundingClientRect().left)
          return parent;
      return dom;
  }
  function posFromElement(view, elt, coords) {
      let { node, offset } = findOffsetInNode(elt, coords), bias = -1;
      if (node.nodeType == 1 && !node.firstChild) {
          let rect = node.getBoundingClientRect();
          bias = rect.left != rect.right && coords.left > (rect.left + rect.right) / 2 ? 1 : -1;
      }
      return view.docView.posFromDOM(node, offset, bias);
  }
  function posFromCaret(view, node, offset, coords) {
      // Browser (in caretPosition/RangeFromPoint) will agressively
      // normalize towards nearby inline nodes. Since we are interested in
      // positions between block nodes too, we first walk up the hierarchy
      // of nodes to see if there are block nodes that the coordinates
      // fall outside of. If so, we take the position before/after that
      // block. If not, we call `posFromDOM` on the raw node/offset.
      let outsideBlock = -1;
      for (let cur = node, sawBlock = false;;) {
          if (cur == view.dom)
              break;
          let desc = view.docView.nearestDesc(cur, true);
          if (!desc)
              return null;
          if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent && !sawBlock || !desc.contentDOM)) {
              let rect = desc.dom.getBoundingClientRect();
              if (desc.node.isBlock && desc.parent && !sawBlock) {
                  sawBlock = true;
                  if (rect.left > coords.left || rect.top > coords.top)
                      outsideBlock = desc.posBefore;
                  else if (rect.right < coords.left || rect.bottom < coords.top)
                      outsideBlock = desc.posAfter;
              }
              if (!desc.contentDOM && outsideBlock < 0 && !desc.node.isText) {
                  // If we are inside a leaf, return the side of the leaf closer to the coords
                  let before = desc.node.isBlock ? coords.top < (rect.top + rect.bottom) / 2
                      : coords.left < (rect.left + rect.right) / 2;
                  return before ? desc.posBefore : desc.posAfter;
              }
          }
          cur = desc.dom.parentNode;
      }
      return outsideBlock > -1 ? outsideBlock : view.docView.posFromDOM(node, offset, -1);
  }
  function elementFromPoint(element, coords, box) {
      let len = element.childNodes.length;
      if (len && box.top < box.bottom) {
          for (let startI = Math.max(0, Math.min(len - 1, Math.floor(len * (coords.top - box.top) / (box.bottom - box.top)) - 2)), i = startI;;) {
              let child = element.childNodes[i];
              if (child.nodeType == 1) {
                  let rects = child.getClientRects();
                  for (let j = 0; j < rects.length; j++) {
                      let rect = rects[j];
                      if (inRect(coords, rect))
                          return elementFromPoint(child, coords, rect);
                  }
              }
              if ((i = (i + 1) % len) == startI)
                  break;
          }
      }
      return element;
  }
  // Given an x,y position on the editor, get the position in the document.
  function posAtCoords(view, coords) {
      let doc = view.dom.ownerDocument, node, offset = 0;
      let caret = caretFromPoint(doc, coords.left, coords.top);
      if (caret)
          ({ node, offset } = caret);
      let elt = (view.root.elementFromPoint ? view.root : doc)
          .elementFromPoint(coords.left, coords.top);
      let pos;
      if (!elt || !view.dom.contains(elt.nodeType != 1 ? elt.parentNode : elt)) {
          let box = view.dom.getBoundingClientRect();
          if (!inRect(coords, box))
              return null;
          elt = elementFromPoint(view.dom, coords, box);
          if (!elt)
              return null;
      }
      // Safari's caretRangeFromPoint returns nonsense when on a draggable element
      if (safari) {
          for (let p = elt; node && p; p = parentNode(p))
              if (p.draggable)
                  node = undefined;
      }
      elt = targetKludge(elt, coords);
      if (node) {
          if (gecko && node.nodeType == 1) {
              // Firefox will sometimes return offsets into <input> nodes, which
              // have no actual children, from caretPositionFromPoint (#953)
              offset = Math.min(offset, node.childNodes.length);
              // It'll also move the returned position before image nodes,
              // even if those are behind it.
              if (offset < node.childNodes.length) {
                  let next = node.childNodes[offset], box;
                  if (next.nodeName == "IMG" && (box = next.getBoundingClientRect()).right <= coords.left &&
                      box.bottom > coords.top)
                      offset++;
              }
          }
          let prev;
          // When clicking above the right side of an uneditable node, Chrome will report a cursor position after that node.
          if (webkit && offset && node.nodeType == 1 && (prev = node.childNodes[offset - 1]).nodeType == 1 &&
              prev.contentEditable == "false" && prev.getBoundingClientRect().top >= coords.top)
              offset--;
          // Suspiciously specific kludge to work around caret*FromPoint
          // never returning a position at the end of the document
          if (node == view.dom && offset == node.childNodes.length - 1 && node.lastChild.nodeType == 1 &&
              coords.top > node.lastChild.getBoundingClientRect().bottom)
              pos = view.state.doc.content.size;
          // Ignore positions directly after a BR, since caret*FromPoint
          // 'round up' positions that would be more accurately placed
          // before the BR node.
          else if (offset == 0 || node.nodeType != 1 || node.childNodes[offset - 1].nodeName != "BR")
              pos = posFromCaret(view, node, offset, coords);
      }
      if (pos == null)
          pos = posFromElement(view, elt, coords);
      let desc = view.docView.nearestDesc(elt, true);
      return { pos, inside: desc ? desc.posAtStart - desc.border : -1 };
  }
  function nonZero(rect) {
      return rect.top < rect.bottom || rect.left < rect.right;
  }
  function singleRect(target, bias) {
      let rects = target.getClientRects();
      if (rects.length) {
          let first = rects[bias < 0 ? 0 : rects.length - 1];
          if (nonZero(first))
              return first;
      }
      return Array.prototype.find.call(rects, nonZero) || target.getBoundingClientRect();
  }
  const BIDI = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/;
  // Given a position in the document model, get a bounding box of the
  // character at that position, relative to the window.
  function coordsAtPos(view, pos, side) {
      let { node, offset, atom } = view.docView.domFromPos(pos, side < 0 ? -1 : 1);
      let supportEmptyRange = webkit || gecko;
      if (node.nodeType == 3) {
          // These browsers support querying empty text ranges. Prefer that in
          // bidi context or when at the end of a node.
          if (supportEmptyRange && (BIDI.test(node.nodeValue) || (side < 0 ? !offset : offset == node.nodeValue.length))) {
              let rect = singleRect(textRange(node, offset, offset), side);
              // Firefox returns bad results (the position before the space)
              // when querying a position directly after line-broken
              // whitespace. Detect this situation and and kludge around it
              if (gecko && offset && /\s/.test(node.nodeValue[offset - 1]) && offset < node.nodeValue.length) {
                  let rectBefore = singleRect(textRange(node, offset - 1, offset - 1), -1);
                  if (rectBefore.top == rect.top) {
                      let rectAfter = singleRect(textRange(node, offset, offset + 1), -1);
                      if (rectAfter.top != rect.top)
                          return flattenV(rectAfter, rectAfter.left < rectBefore.left);
                  }
              }
              return rect;
          }
          else {
              let from = offset, to = offset, takeSide = side < 0 ? 1 : -1;
              if (side < 0 && !offset) {
                  to++;
                  takeSide = -1;
              }
              else if (side >= 0 && offset == node.nodeValue.length) {
                  from--;
                  takeSide = 1;
              }
              else if (side < 0) {
                  from--;
              }
              else {
                  to++;
              }
              return flattenV(singleRect(textRange(node, from, to), takeSide), takeSide < 0);
          }
      }
      let $dom = view.state.doc.resolve(pos - (atom || 0));
      // Return a horizontal line in block context
      if (!$dom.parent.inlineContent) {
          if (atom == null && offset && (side < 0 || offset == nodeSize(node))) {
              let before = node.childNodes[offset - 1];
              if (before.nodeType == 1)
                  return flattenH(before.getBoundingClientRect(), false);
          }
          if (atom == null && offset < nodeSize(node)) {
              let after = node.childNodes[offset];
              if (after.nodeType == 1)
                  return flattenH(after.getBoundingClientRect(), true);
          }
          return flattenH(node.getBoundingClientRect(), side >= 0);
      }
      // Inline, not in text node (this is not Bidi-safe)
      if (atom == null && offset && (side < 0 || offset == nodeSize(node))) {
          let before = node.childNodes[offset - 1];
          let target = before.nodeType == 3 ? textRange(before, nodeSize(before) - (supportEmptyRange ? 0 : 1))
              // BR nodes tend to only return the rectangle before them.
              // Only use them if they are the last element in their parent
              : before.nodeType == 1 && (before.nodeName != "BR" || !before.nextSibling) ? before : null;
          if (target)
              return flattenV(singleRect(target, 1), false);
      }
      if (atom == null && offset < nodeSize(node)) {
          let after = node.childNodes[offset];
          while (after.pmViewDesc && after.pmViewDesc.ignoreForCoords)
              after = after.nextSibling;
          let target = !after ? null : after.nodeType == 3 ? textRange(after, 0, (supportEmptyRange ? 0 : 1))
              : after.nodeType == 1 ? after : null;
          if (target)
              return flattenV(singleRect(target, -1), true);
      }
      // All else failed, just try to get a rectangle for the target node
      return flattenV(singleRect(node.nodeType == 3 ? textRange(node) : node, -side), side >= 0);
  }
  function flattenV(rect, left) {
      if (rect.width == 0)
          return rect;
      let x = left ? rect.left : rect.right;
      return { top: rect.top, bottom: rect.bottom, left: x, right: x };
  }
  function flattenH(rect, top) {
      if (rect.height == 0)
          return rect;
      let y = top ? rect.top : rect.bottom;
      return { top: y, bottom: y, left: rect.left, right: rect.right };
  }
  function withFlushedState(view, state, f) {
      let viewState = view.state, active = view.root.activeElement;
      if (viewState != state)
          view.updateState(state);
      if (active != view.dom)
          view.focus();
      try {
          return f();
      }
      finally {
          if (viewState != state)
              view.updateState(viewState);
          if (active != view.dom && active)
              active.focus();
      }
  }
  // Whether vertical position motion in a given direction
  // from a position would leave a text block.
  function endOfTextblockVertical(view, state, dir) {
      let sel = state.selection;
      let $pos = dir == "up" ? sel.$from : sel.$to;
      return withFlushedState(view, state, () => {
          let { node: dom } = view.docView.domFromPos($pos.pos, dir == "up" ? -1 : 1);
          for (;;) {
              let nearest = view.docView.nearestDesc(dom, true);
              if (!nearest)
                  break;
              if (nearest.node.isBlock) {
                  dom = nearest.contentDOM || nearest.dom;
                  break;
              }
              dom = nearest.dom.parentNode;
          }
          let coords = coordsAtPos(view, $pos.pos, 1);
          for (let child = dom.firstChild; child; child = child.nextSibling) {
              let boxes;
              if (child.nodeType == 1)
                  boxes = child.getClientRects();
              else if (child.nodeType == 3)
                  boxes = textRange(child, 0, child.nodeValue.length).getClientRects();
              else
                  continue;
              for (let i = 0; i < boxes.length; i++) {
                  let box = boxes[i];
                  if (box.bottom > box.top + 1 &&
                      (dir == "up" ? coords.top - box.top > (box.bottom - coords.top) * 2
                          : box.bottom - coords.bottom > (coords.bottom - box.top) * 2))
                      return false;
              }
          }
          return true;
      });
  }
  const maybeRTL = /[\u0590-\u08ac]/;
  function endOfTextblockHorizontal(view, state, dir) {
      let { $head } = state.selection;
      if (!$head.parent.isTextblock)
          return false;
      let offset = $head.parentOffset, atStart = !offset, atEnd = offset == $head.parent.content.size;
      let sel = view.domSelection();
      // If the textblock is all LTR, or the browser doesn't support
      // Selection.modify (Edge), fall back to a primitive approach
      if (!maybeRTL.test($head.parent.textContent) || !sel.modify)
          return dir == "left" || dir == "backward" ? atStart : atEnd;
      return withFlushedState(view, state, () => {
          // This is a huge hack, but appears to be the best we can
          // currently do: use `Selection.modify` to move the selection by
          // one character, and see if that moves the cursor out of the
          // textblock (or doesn't move it at all, when at the start/end of
          // the document).
          let { focusNode: oldNode, focusOffset: oldOff, anchorNode, anchorOffset } = view.domSelectionRange();
          let oldBidiLevel = sel.caretBidiLevel // Only for Firefox
          ;
          sel.modify("move", dir, "character");
          let parentDOM = $head.depth ? view.docView.domAfterPos($head.before()) : view.dom;
          let { focusNode: newNode, focusOffset: newOff } = view.domSelectionRange();
          let result = newNode && !parentDOM.contains(newNode.nodeType == 1 ? newNode : newNode.parentNode) ||
              (oldNode == newNode && oldOff == newOff);
          // Restore the previous selection
          try {
              sel.collapse(anchorNode, anchorOffset);
              if (oldNode && (oldNode != anchorNode || oldOff != anchorOffset) && sel.extend)
                  sel.extend(oldNode, oldOff);
          }
          catch (_) { }
          if (oldBidiLevel != null)
              sel.caretBidiLevel = oldBidiLevel;
          return result;
      });
  }
  let cachedState = null;
  let cachedDir = null;
  let cachedResult = false;
  function endOfTextblock(view, state, dir) {
      if (cachedState == state && cachedDir == dir)
          return cachedResult;
      cachedState = state;
      cachedDir = dir;
      return cachedResult = dir == "up" || dir == "down"
          ? endOfTextblockVertical(view, state, dir)
          : endOfTextblockHorizontal(view, state, dir);
  }

  // View descriptions are data structures that describe the DOM that is
  // used to represent the editor's content. They are used for:
  //
  // - Incremental redrawing when the document changes
  //
  // - Figuring out what part of the document a given DOM position
  //   corresponds to
  //
  // - Wiring in custom implementations of the editing interface for a
  //   given node
  //
  // They form a doubly-linked mutable tree, starting at `view.docView`.
  const NOT_DIRTY = 0, CHILD_DIRTY = 1, CONTENT_DIRTY = 2, NODE_DIRTY = 3;
  // Superclass for the various kinds of descriptions. Defines their
  // basic structure and shared methods.
  class ViewDesc {
      constructor(parent, children, dom, 
      // This is the node that holds the child views. It may be null for
      // descs that don't have children.
      contentDOM) {
          this.parent = parent;
          this.children = children;
          this.dom = dom;
          this.contentDOM = contentDOM;
          this.dirty = NOT_DIRTY;
          // An expando property on the DOM node provides a link back to its
          // description.
          dom.pmViewDesc = this;
      }
      // Used to check whether a given description corresponds to a
      // widget/mark/node.
      matchesWidget(widget) { return false; }
      matchesMark(mark) { return false; }
      matchesNode(node, outerDeco, innerDeco) { return false; }
      matchesHack(nodeName) { return false; }
      // When parsing in-editor content (in domchange.js), we allow
      // descriptions to determine the parse rules that should be used to
      // parse them.
      parseRule() { return null; }
      // Used by the editor's event handler to ignore events that come
      // from certain descs.
      stopEvent(event) { return false; }
      // The size of the content represented by this desc.
      get size() {
          let size = 0;
          for (let i = 0; i < this.children.length; i++)
              size += this.children[i].size;
          return size;
      }
      // For block nodes, this represents the space taken up by their
      // start/end tokens.
      get border() { return 0; }
      destroy() {
          this.parent = undefined;
          if (this.dom.pmViewDesc == this)
              this.dom.pmViewDesc = undefined;
          for (let i = 0; i < this.children.length; i++)
              this.children[i].destroy();
      }
      posBeforeChild(child) {
          for (let i = 0, pos = this.posAtStart;; i++) {
              let cur = this.children[i];
              if (cur == child)
                  return pos;
              pos += cur.size;
          }
      }
      get posBefore() {
          return this.parent.posBeforeChild(this);
      }
      get posAtStart() {
          return this.parent ? this.parent.posBeforeChild(this) + this.border : 0;
      }
      get posAfter() {
          return this.posBefore + this.size;
      }
      get posAtEnd() {
          return this.posAtStart + this.size - 2 * this.border;
      }
      localPosFromDOM(dom, offset, bias) {
          // If the DOM position is in the content, use the child desc after
          // it to figure out a position.
          if (this.contentDOM && this.contentDOM.contains(dom.nodeType == 1 ? dom : dom.parentNode)) {
              if (bias < 0) {
                  let domBefore, desc;
                  if (dom == this.contentDOM) {
                      domBefore = dom.childNodes[offset - 1];
                  }
                  else {
                      while (dom.parentNode != this.contentDOM)
                          dom = dom.parentNode;
                      domBefore = dom.previousSibling;
                  }
                  while (domBefore && !((desc = domBefore.pmViewDesc) && desc.parent == this))
                      domBefore = domBefore.previousSibling;
                  return domBefore ? this.posBeforeChild(desc) + desc.size : this.posAtStart;
              }
              else {
                  let domAfter, desc;
                  if (dom == this.contentDOM) {
                      domAfter = dom.childNodes[offset];
                  }
                  else {
                      while (dom.parentNode != this.contentDOM)
                          dom = dom.parentNode;
                      domAfter = dom.nextSibling;
                  }
                  while (domAfter && !((desc = domAfter.pmViewDesc) && desc.parent == this))
                      domAfter = domAfter.nextSibling;
                  return domAfter ? this.posBeforeChild(desc) : this.posAtEnd;
              }
          }
          // Otherwise, use various heuristics, falling back on the bias
          // parameter, to determine whether to return the position at the
          // start or at the end of this view desc.
          let atEnd;
          if (dom == this.dom && this.contentDOM) {
              atEnd = offset > domIndex(this.contentDOM);
          }
          else if (this.contentDOM && this.contentDOM != this.dom && this.dom.contains(this.contentDOM)) {
              atEnd = dom.compareDocumentPosition(this.contentDOM) & 2;
          }
          else if (this.dom.firstChild) {
              if (offset == 0)
                  for (let search = dom;; search = search.parentNode) {
                      if (search == this.dom) {
                          atEnd = false;
                          break;
                      }
                      if (search.previousSibling)
                          break;
                  }
              if (atEnd == null && offset == dom.childNodes.length)
                  for (let search = dom;; search = search.parentNode) {
                      if (search == this.dom) {
                          atEnd = true;
                          break;
                      }
                      if (search.nextSibling)
                          break;
                  }
          }
          return (atEnd == null ? bias > 0 : atEnd) ? this.posAtEnd : this.posAtStart;
      }
      nearestDesc(dom, onlyNodes = false) {
          for (let first = true, cur = dom; cur; cur = cur.parentNode) {
              let desc = this.getDesc(cur), nodeDOM;
              if (desc && (!onlyNodes || desc.node)) {
                  // If dom is outside of this desc's nodeDOM, don't count it.
                  if (first && (nodeDOM = desc.nodeDOM) &&
                      !(nodeDOM.nodeType == 1 ? nodeDOM.contains(dom.nodeType == 1 ? dom : dom.parentNode) : nodeDOM == dom))
                      first = false;
                  else
                      return desc;
              }
          }
      }
      getDesc(dom) {
          let desc = dom.pmViewDesc;
          for (let cur = desc; cur; cur = cur.parent)
              if (cur == this)
                  return desc;
      }
      posFromDOM(dom, offset, bias) {
          for (let scan = dom; scan; scan = scan.parentNode) {
              let desc = this.getDesc(scan);
              if (desc)
                  return desc.localPosFromDOM(dom, offset, bias);
          }
          return -1;
      }
      // Find the desc for the node after the given pos, if any. (When a
      // parent node overrode rendering, there might not be one.)
      descAt(pos) {
          for (let i = 0, offset = 0; i < this.children.length; i++) {
              let child = this.children[i], end = offset + child.size;
              if (offset == pos && end != offset) {
                  while (!child.border && child.children.length)
                      child = child.children[0];
                  return child;
              }
              if (pos < end)
                  return child.descAt(pos - offset - child.border);
              offset = end;
          }
      }
      domFromPos(pos, side) {
          if (!this.contentDOM)
              return { node: this.dom, offset: 0, atom: pos + 1 };
          // First find the position in the child array
          let i = 0, offset = 0;
          for (let curPos = 0; i < this.children.length; i++) {
              let child = this.children[i], end = curPos + child.size;
              if (end > pos || child instanceof TrailingHackViewDesc) {
                  offset = pos - curPos;
                  break;
              }
              curPos = end;
          }
          // If this points into the middle of a child, call through
          if (offset)
              return this.children[i].domFromPos(offset - this.children[i].border, side);
          // Go back if there were any zero-length widgets with side >= 0 before this point
          for (let prev; i && !(prev = this.children[i - 1]).size && prev instanceof WidgetViewDesc && prev.side >= 0; i--) { }
          // Scan towards the first useable node
          if (side <= 0) {
              let prev, enter = true;
              for (;; i--, enter = false) {
                  prev = i ? this.children[i - 1] : null;
                  if (!prev || prev.dom.parentNode == this.contentDOM)
                      break;
              }
              if (prev && side && enter && !prev.border && !prev.domAtom)
                  return prev.domFromPos(prev.size, side);
              return { node: this.contentDOM, offset: prev ? domIndex(prev.dom) + 1 : 0 };
          }
          else {
              let next, enter = true;
              for (;; i++, enter = false) {
                  next = i < this.children.length ? this.children[i] : null;
                  if (!next || next.dom.parentNode == this.contentDOM)
                      break;
              }
              if (next && enter && !next.border && !next.domAtom)
                  return next.domFromPos(0, side);
              return { node: this.contentDOM, offset: next ? domIndex(next.dom) : this.contentDOM.childNodes.length };
          }
      }
      // Used to find a DOM range in a single parent for a given changed
      // range.
      parseRange(from, to, base = 0) {
          if (this.children.length == 0)
              return { node: this.contentDOM, from, to, fromOffset: 0, toOffset: this.contentDOM.childNodes.length };
          let fromOffset = -1, toOffset = -1;
          for (let offset = base, i = 0;; i++) {
              let child = this.children[i], end = offset + child.size;
              if (fromOffset == -1 && from <= end) {
                  let childBase = offset + child.border;
                  // FIXME maybe descend mark views to parse a narrower range?
                  if (from >= childBase && to <= end - child.border && child.node &&
                      child.contentDOM && this.contentDOM.contains(child.contentDOM))
                      return child.parseRange(from, to, childBase);
                  from = offset;
                  for (let j = i; j > 0; j--) {
                      let prev = this.children[j - 1];
                      if (prev.size && prev.dom.parentNode == this.contentDOM && !prev.emptyChildAt(1)) {
                          fromOffset = domIndex(prev.dom) + 1;
                          break;
                      }
                      from -= prev.size;
                  }
                  if (fromOffset == -1)
                      fromOffset = 0;
              }
              if (fromOffset > -1 && (end > to || i == this.children.length - 1)) {
                  to = end;
                  for (let j = i + 1; j < this.children.length; j++) {
                      let next = this.children[j];
                      if (next.size && next.dom.parentNode == this.contentDOM && !next.emptyChildAt(-1)) {
                          toOffset = domIndex(next.dom);
                          break;
                      }
                      to += next.size;
                  }
                  if (toOffset == -1)
                      toOffset = this.contentDOM.childNodes.length;
                  break;
              }
              offset = end;
          }
          return { node: this.contentDOM, from, to, fromOffset, toOffset };
      }
      emptyChildAt(side) {
          if (this.border || !this.contentDOM || !this.children.length)
              return false;
          let child = this.children[side < 0 ? 0 : this.children.length - 1];
          return child.size == 0 || child.emptyChildAt(side);
      }
      domAfterPos(pos) {
          let { node, offset } = this.domFromPos(pos, 0);
          if (node.nodeType != 1 || offset == node.childNodes.length)
              throw new RangeError("No node after pos " + pos);
          return node.childNodes[offset];
      }
      // View descs are responsible for setting any selection that falls
      // entirely inside of them, so that custom implementations can do
      // custom things with the selection. Note that this falls apart when
      // a selection starts in such a node and ends in another, in which
      // case we just use whatever domFromPos produces as a best effort.
      setSelection(anchor, head, root, force = false) {
          // If the selection falls entirely in a child, give it to that child
          let from = Math.min(anchor, head), to = Math.max(anchor, head);
          for (let i = 0, offset = 0; i < this.children.length; i++) {
              let child = this.children[i], end = offset + child.size;
              if (from > offset && to < end)
                  return child.setSelection(anchor - offset - child.border, head - offset - child.border, root, force);
              offset = end;
          }
          let anchorDOM = this.domFromPos(anchor, anchor ? -1 : 1);
          let headDOM = head == anchor ? anchorDOM : this.domFromPos(head, head ? -1 : 1);
          let domSel = root.getSelection();
          let brKludge = false;
          // On Firefox, using Selection.collapse to put the cursor after a
          // BR node for some reason doesn't always work (#1073). On Safari,
          // the cursor sometimes inexplicable visually lags behind its
          // reported position in such situations (#1092).
          if ((gecko || safari) && anchor == head) {
              let { node, offset } = anchorDOM;
              if (node.nodeType == 3) {
                  brKludge = !!(offset && node.nodeValue[offset - 1] == "\n");
                  // Issue #1128
                  if (brKludge && offset == node.nodeValue.length) {
                      for (let scan = node, after; scan; scan = scan.parentNode) {
                          if (after = scan.nextSibling) {
                              if (after.nodeName == "BR")
                                  anchorDOM = headDOM = { node: after.parentNode, offset: domIndex(after) + 1 };
                              break;
                          }
                          let desc = scan.pmViewDesc;
                          if (desc && desc.node && desc.node.isBlock)
                              break;
                      }
                  }
              }
              else {
                  let prev = node.childNodes[offset - 1];
                  brKludge = prev && (prev.nodeName == "BR" || prev.contentEditable == "false");
              }
          }
          // Firefox can act strangely when the selection is in front of an
          // uneditable node. See #1163 and https://bugzilla.mozilla.org/show_bug.cgi?id=1709536
          if (gecko && domSel.focusNode && domSel.focusNode != headDOM.node && domSel.focusNode.nodeType == 1) {
              let after = domSel.focusNode.childNodes[domSel.focusOffset];
              if (after && after.contentEditable == "false")
                  force = true;
          }
          if (!(force || brKludge && safari) &&
              isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset) &&
              isEquivalentPosition(headDOM.node, headDOM.offset, domSel.focusNode, domSel.focusOffset))
              return;
          // Selection.extend can be used to create an 'inverted' selection
          // (one where the focus is before the anchor), but not all
          // browsers support it yet.
          let domSelExtended = false;
          if ((domSel.extend || anchor == head) && !brKludge) {
              domSel.collapse(anchorDOM.node, anchorDOM.offset);
              try {
                  if (anchor != head)
                      domSel.extend(headDOM.node, headDOM.offset);
                  domSelExtended = true;
              }
              catch (_) {
                  // In some cases with Chrome the selection is empty after calling
                  // collapse, even when it should be valid. This appears to be a bug, but
                  // it is difficult to isolate. If this happens fallback to the old path
                  // without using extend.
                  // Similarly, this could crash on Safari if the editor is hidden, and
                  // there was no selection.
              }
          }
          if (!domSelExtended) {
              if (anchor > head) {
                  let tmp = anchorDOM;
                  anchorDOM = headDOM;
                  headDOM = tmp;
              }
              let range = document.createRange();
              range.setEnd(headDOM.node, headDOM.offset);
              range.setStart(anchorDOM.node, anchorDOM.offset);
              domSel.removeAllRanges();
              domSel.addRange(range);
          }
      }
      ignoreMutation(mutation) {
          return !this.contentDOM && mutation.type != "selection";
      }
      get contentLost() {
          return this.contentDOM && this.contentDOM != this.dom && !this.dom.contains(this.contentDOM);
      }
      // Remove a subtree of the element tree that has been touched
      // by a DOM change, so that the next update will redraw it.
      markDirty(from, to) {
          for (let offset = 0, i = 0; i < this.children.length; i++) {
              let child = this.children[i], end = offset + child.size;
              if (offset == end ? from <= end && to >= offset : from < end && to > offset) {
                  let startInside = offset + child.border, endInside = end - child.border;
                  if (from >= startInside && to <= endInside) {
                      this.dirty = from == offset || to == end ? CONTENT_DIRTY : CHILD_DIRTY;
                      if (from == startInside && to == endInside &&
                          (child.contentLost || child.dom.parentNode != this.contentDOM))
                          child.dirty = NODE_DIRTY;
                      else
                          child.markDirty(from - startInside, to - startInside);
                      return;
                  }
                  else {
                      child.dirty = child.dom == child.contentDOM && child.dom.parentNode == this.contentDOM && !child.children.length
                          ? CONTENT_DIRTY : NODE_DIRTY;
                  }
              }
              offset = end;
          }
          this.dirty = CONTENT_DIRTY;
      }
      markParentsDirty() {
          let level = 1;
          for (let node = this.parent; node; node = node.parent, level++) {
              let dirty = level == 1 ? CONTENT_DIRTY : CHILD_DIRTY;
              if (node.dirty < dirty)
                  node.dirty = dirty;
          }
      }
      get domAtom() { return false; }
      get ignoreForCoords() { return false; }
      isText(text) { return false; }
  }
  // A widget desc represents a widget decoration, which is a DOM node
  // drawn between the document nodes.
  class WidgetViewDesc extends ViewDesc {
      constructor(parent, widget, view, pos) {
          let self, dom = widget.type.toDOM;
          if (typeof dom == "function")
              dom = dom(view, () => {
                  if (!self)
                      return pos;
                  if (self.parent)
                      return self.parent.posBeforeChild(self);
              });
          if (!widget.type.spec.raw) {
              if (dom.nodeType != 1) {
                  let wrap = document.createElement("span");
                  wrap.appendChild(dom);
                  dom = wrap;
              }
              dom.contentEditable = "false";
              dom.classList.add("ProseMirror-widget");
          }
          super(parent, [], dom, null);
          this.widget = widget;
          this.widget = widget;
          self = this;
      }
      matchesWidget(widget) {
          return this.dirty == NOT_DIRTY && widget.type.eq(this.widget.type);
      }
      parseRule() { return { ignore: true }; }
      stopEvent(event) {
          let stop = this.widget.spec.stopEvent;
          return stop ? stop(event) : false;
      }
      ignoreMutation(mutation) {
          return mutation.type != "selection" || this.widget.spec.ignoreSelection;
      }
      destroy() {
          this.widget.type.destroy(this.dom);
          super.destroy();
      }
      get domAtom() { return true; }
      get side() { return this.widget.type.side; }
  }
  class CompositionViewDesc extends ViewDesc {
      constructor(parent, dom, textDOM, text) {
          super(parent, [], dom, null);
          this.textDOM = textDOM;
          this.text = text;
      }
      get size() { return this.text.length; }
      localPosFromDOM(dom, offset) {
          if (dom != this.textDOM)
              return this.posAtStart + (offset ? this.size : 0);
          return this.posAtStart + offset;
      }
      domFromPos(pos) {
          return { node: this.textDOM, offset: pos };
      }
      ignoreMutation(mut) {
          return mut.type === 'characterData' && mut.target.nodeValue == mut.oldValue;
      }
  }
  // A mark desc represents a mark. May have multiple children,
  // depending on how the mark is split. Note that marks are drawn using
  // a fixed nesting order, for simplicity and predictability, so in
  // some cases they will be split more often than would appear
  // necessary.
  class MarkViewDesc extends ViewDesc {
      constructor(parent, mark, dom, contentDOM) {
          super(parent, [], dom, contentDOM);
          this.mark = mark;
      }
      static create(parent, mark, inline, view) {
          let custom = view.nodeViews[mark.type.name];
          let spec = custom && custom(mark, view, inline);
          if (!spec || !spec.dom)
              spec = DOMSerializer.renderSpec(document, mark.type.spec.toDOM(mark, inline));
          return new MarkViewDesc(parent, mark, spec.dom, spec.contentDOM || spec.dom);
      }
      parseRule() {
          if ((this.dirty & NODE_DIRTY) || this.mark.type.spec.reparseInView)
              return null;
          return { mark: this.mark.type.name, attrs: this.mark.attrs, contentElement: this.contentDOM };
      }
      matchesMark(mark) { return this.dirty != NODE_DIRTY && this.mark.eq(mark); }
      markDirty(from, to) {
          super.markDirty(from, to);
          // Move dirty info to nearest node view
          if (this.dirty != NOT_DIRTY) {
              let parent = this.parent;
              while (!parent.node)
                  parent = parent.parent;
              if (parent.dirty < this.dirty)
                  parent.dirty = this.dirty;
              this.dirty = NOT_DIRTY;
          }
      }
      slice(from, to, view) {
          let copy = MarkViewDesc.create(this.parent, this.mark, true, view);
          let nodes = this.children, size = this.size;
          if (to < size)
              nodes = replaceNodes(nodes, to, size, view);
          if (from > 0)
              nodes = replaceNodes(nodes, 0, from, view);
          for (let i = 0; i < nodes.length; i++)
              nodes[i].parent = copy;
          copy.children = nodes;
          return copy;
      }
  }
  // Node view descs are the main, most common type of view desc, and
  // correspond to an actual node in the document. Unlike mark descs,
  // they populate their child array themselves.
  class NodeViewDesc extends ViewDesc {
      constructor(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, view, pos) {
          super(parent, [], dom, contentDOM);
          this.node = node;
          this.outerDeco = outerDeco;
          this.innerDeco = innerDeco;
          this.nodeDOM = nodeDOM;
      }
      // By default, a node is rendered using the `toDOM` method from the
      // node type spec. But client code can use the `nodeViews` spec to
      // supply a custom node view, which can influence various aspects of
      // the way the node works.
      //
      // (Using subclassing for this was intentionally decided against,
      // since it'd require exposing a whole slew of finicky
      // implementation details to the user code that they probably will
      // never need.)
      static create(parent, node, outerDeco, innerDeco, view, pos) {
          let custom = view.nodeViews[node.type.name], descObj;
          let spec = custom && custom(node, view, () => {
              // (This is a function that allows the custom view to find its
              // own position)
              if (!descObj)
                  return pos;
              if (descObj.parent)
                  return descObj.parent.posBeforeChild(descObj);
          }, outerDeco, innerDeco);
          let dom = spec && spec.dom, contentDOM = spec && spec.contentDOM;
          if (node.isText) {
              if (!dom)
                  dom = document.createTextNode(node.text);
              else if (dom.nodeType != 3)
                  throw new RangeError("Text must be rendered as a DOM text node");
          }
          else if (!dom) {
              ({ dom, contentDOM } = DOMSerializer.renderSpec(document, node.type.spec.toDOM(node)));
          }
          if (!contentDOM && !node.isText && dom.nodeName != "BR") { // Chrome gets confused by <br contenteditable=false>
              if (!dom.hasAttribute("contenteditable"))
                  dom.contentEditable = "false";
              if (node.type.spec.draggable)
                  dom.draggable = true;
          }
          let nodeDOM = dom;
          dom = applyOuterDeco(dom, outerDeco, node);
          if (spec)
              return descObj = new CustomNodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM, spec, view, pos + 1);
          else if (node.isText)
              return new TextViewDesc(parent, node, outerDeco, innerDeco, dom, nodeDOM, view);
          else
              return new NodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM, view, pos + 1);
      }
      parseRule() {
          // Experimental kludge to allow opt-in re-parsing of nodes
          if (this.node.type.spec.reparseInView)
              return null;
          // FIXME the assumption that this can always return the current
          // attrs means that if the user somehow manages to change the
          // attrs in the dom, that won't be picked up. Not entirely sure
          // whether this is a problem
          let rule = { node: this.node.type.name, attrs: this.node.attrs };
          if (this.node.type.whitespace == "pre")
              rule.preserveWhitespace = "full";
          if (!this.contentDOM) {
              rule.getContent = () => this.node.content;
          }
          else if (!this.contentLost) {
              rule.contentElement = this.contentDOM;
          }
          else {
              // Chrome likes to randomly recreate parent nodes when
              // backspacing things. When that happens, this tries to find the
              // new parent.
              for (let i = this.children.length - 1; i >= 0; i--) {
                  let child = this.children[i];
                  if (this.dom.contains(child.dom.parentNode)) {
                      rule.contentElement = child.dom.parentNode;
                      break;
                  }
              }
              if (!rule.contentElement)
                  rule.getContent = () => Fragment.empty;
          }
          return rule;
      }
      matchesNode(node, outerDeco, innerDeco) {
          return this.dirty == NOT_DIRTY && node.eq(this.node) &&
              sameOuterDeco(outerDeco, this.outerDeco) && innerDeco.eq(this.innerDeco);
      }
      get size() { return this.node.nodeSize; }
      get border() { return this.node.isLeaf ? 0 : 1; }
      // Syncs `this.children` to match `this.node.content` and the local
      // decorations, possibly introducing nesting for marks. Then, in a
      // separate step, syncs the DOM inside `this.contentDOM` to
      // `this.children`.
      updateChildren(view, pos) {
          let inline = this.node.inlineContent, off = pos;
          let composition = view.composing ? this.localCompositionInfo(view, pos) : null;
          let localComposition = composition && composition.pos > -1 ? composition : null;
          let compositionInChild = composition && composition.pos < 0;
          let updater = new ViewTreeUpdater(this, localComposition && localComposition.node, view);
          iterDeco(this.node, this.innerDeco, (widget, i, insideNode) => {
              if (widget.spec.marks)
                  updater.syncToMarks(widget.spec.marks, inline, view);
              else if (widget.type.side >= 0 && !insideNode)
                  updater.syncToMarks(i == this.node.childCount ? Mark.none : this.node.child(i).marks, inline, view);
              // If the next node is a desc matching this widget, reuse it,
              // otherwise insert the widget as a new view desc.
              updater.placeWidget(widget, view, off);
          }, (child, outerDeco, innerDeco, i) => {
              // Make sure the wrapping mark descs match the node's marks.
              updater.syncToMarks(child.marks, inline, view);
              // Try several strategies for drawing this node
              let compIndex;
              if (updater.findNodeMatch(child, outerDeco, innerDeco, i)) ;
              else if (compositionInChild && view.state.selection.from > off &&
                  view.state.selection.to < off + child.nodeSize &&
                  (compIndex = updater.findIndexWithChild(composition.node)) > -1 &&
                  updater.updateNodeAt(child, outerDeco, innerDeco, compIndex, view)) ;
              else if (updater.updateNextNode(child, outerDeco, innerDeco, view, i, off)) ;
              else {
                  // Add it as a new view
                  updater.addNode(child, outerDeco, innerDeco, view, off);
              }
              off += child.nodeSize;
          });
          // Drop all remaining descs after the current position.
          updater.syncToMarks([], inline, view);
          if (this.node.isTextblock)
              updater.addTextblockHacks();
          updater.destroyRest();
          // Sync the DOM if anything changed
          if (updater.changed || this.dirty == CONTENT_DIRTY) {
              // May have to protect focused DOM from being changed if a composition is active
              if (localComposition)
                  this.protectLocalComposition(view, localComposition);
              renderDescs(this.contentDOM, this.children, view);
              if (ios)
                  iosHacks(this.dom);
          }
      }
      localCompositionInfo(view, pos) {
          // Only do something if both the selection and a focused text node
          // are inside of this node
          let { from, to } = view.state.selection;
          if (!(view.state.selection instanceof TextSelection) || from < pos || to > pos + this.node.content.size)
              return null;
          let textNode = view.input.compositionNode;
          if (!textNode || !this.dom.contains(textNode.parentNode))
              return null;
          if (this.node.inlineContent) {
              // Find the text in the focused node in the node, stop if it's not
              // there (may have been modified through other means, in which
              // case it should overwritten)
              let text = textNode.nodeValue;
              let textPos = findTextInFragment(this.node.content, text, from - pos, to - pos);
              return textPos < 0 ? null : { node: textNode, pos: textPos, text };
          }
          else {
              return { node: textNode, pos: -1, text: "" };
          }
      }
      protectLocalComposition(view, { node, pos, text }) {
          // The node is already part of a local view desc, leave it there
          if (this.getDesc(node))
              return;
          // Create a composition view for the orphaned nodes
          let topNode = node;
          for (;; topNode = topNode.parentNode) {
              if (topNode.parentNode == this.contentDOM)
                  break;
              while (topNode.previousSibling)
                  topNode.parentNode.removeChild(topNode.previousSibling);
              while (topNode.nextSibling)
                  topNode.parentNode.removeChild(topNode.nextSibling);
              if (topNode.pmViewDesc)
                  topNode.pmViewDesc = undefined;
          }
          let desc = new CompositionViewDesc(this, topNode, node, text);
          view.input.compositionNodes.push(desc);
          // Patch up this.children to contain the composition view
          this.children = replaceNodes(this.children, pos, pos + text.length, view, desc);
      }
      // If this desc must be updated to match the given node decoration,
      // do so and return true.
      update(node, outerDeco, innerDeco, view) {
          if (this.dirty == NODE_DIRTY ||
              !node.sameMarkup(this.node))
              return false;
          this.updateInner(node, outerDeco, innerDeco, view);
          return true;
      }
      updateInner(node, outerDeco, innerDeco, view) {
          this.updateOuterDeco(outerDeco);
          this.node = node;
          this.innerDeco = innerDeco;
          if (this.contentDOM)
              this.updateChildren(view, this.posAtStart);
          this.dirty = NOT_DIRTY;
      }
      updateOuterDeco(outerDeco) {
          if (sameOuterDeco(outerDeco, this.outerDeco))
              return;
          let needsWrap = this.nodeDOM.nodeType != 1;
          let oldDOM = this.dom;
          this.dom = patchOuterDeco(this.dom, this.nodeDOM, computeOuterDeco(this.outerDeco, this.node, needsWrap), computeOuterDeco(outerDeco, this.node, needsWrap));
          if (this.dom != oldDOM) {
              oldDOM.pmViewDesc = undefined;
              this.dom.pmViewDesc = this;
          }
          this.outerDeco = outerDeco;
      }
      // Mark this node as being the selected node.
      selectNode() {
          if (this.nodeDOM.nodeType == 1)
              this.nodeDOM.classList.add("ProseMirror-selectednode");
          if (this.contentDOM || !this.node.type.spec.draggable)
              this.dom.draggable = true;
      }
      // Remove selected node marking from this node.
      deselectNode() {
          if (this.nodeDOM.nodeType == 1)
              this.nodeDOM.classList.remove("ProseMirror-selectednode");
          if (this.contentDOM || !this.node.type.spec.draggable)
              this.dom.removeAttribute("draggable");
      }
      get domAtom() { return this.node.isAtom; }
  }
  // Create a view desc for the top-level document node, to be exported
  // and used by the view class.
  function docViewDesc(doc, outerDeco, innerDeco, dom, view) {
      applyOuterDeco(dom, outerDeco, doc);
      let docView = new NodeViewDesc(undefined, doc, outerDeco, innerDeco, dom, dom, dom, view, 0);
      if (docView.contentDOM)
          docView.updateChildren(view, 0);
      return docView;
  }
  class TextViewDesc extends NodeViewDesc {
      constructor(parent, node, outerDeco, innerDeco, dom, nodeDOM, view) {
          super(parent, node, outerDeco, innerDeco, dom, null, nodeDOM, view, 0);
      }
      parseRule() {
          let skip = this.nodeDOM.parentNode;
          while (skip && skip != this.dom && !skip.pmIsDeco)
              skip = skip.parentNode;
          return { skip: (skip || true) };
      }
      update(node, outerDeco, innerDeco, view) {
          if (this.dirty == NODE_DIRTY || (this.dirty != NOT_DIRTY && !this.inParent()) ||
              !node.sameMarkup(this.node))
              return false;
          this.updateOuterDeco(outerDeco);
          if ((this.dirty != NOT_DIRTY || node.text != this.node.text) && node.text != this.nodeDOM.nodeValue) {
              this.nodeDOM.nodeValue = node.text;
              if (view.trackWrites == this.nodeDOM)
                  view.trackWrites = null;
          }
          this.node = node;
          this.dirty = NOT_DIRTY;
          return true;
      }
      inParent() {
          let parentDOM = this.parent.contentDOM;
          for (let n = this.nodeDOM; n; n = n.parentNode)
              if (n == parentDOM)
                  return true;
          return false;
      }
      domFromPos(pos) {
          return { node: this.nodeDOM, offset: pos };
      }
      localPosFromDOM(dom, offset, bias) {
          if (dom == this.nodeDOM)
              return this.posAtStart + Math.min(offset, this.node.text.length);
          return super.localPosFromDOM(dom, offset, bias);
      }
      ignoreMutation(mutation) {
          return mutation.type != "characterData" && mutation.type != "selection";
      }
      slice(from, to, view) {
          let node = this.node.cut(from, to), dom = document.createTextNode(node.text);
          return new TextViewDesc(this.parent, node, this.outerDeco, this.innerDeco, dom, dom, view);
      }
      markDirty(from, to) {
          super.markDirty(from, to);
          if (this.dom != this.nodeDOM && (from == 0 || to == this.nodeDOM.nodeValue.length))
              this.dirty = NODE_DIRTY;
      }
      get domAtom() { return false; }
      isText(text) { return this.node.text == text; }
  }
  // A dummy desc used to tag trailing BR or IMG nodes created to work
  // around contentEditable terribleness.
  class TrailingHackViewDesc extends ViewDesc {
      parseRule() { return { ignore: true }; }
      matchesHack(nodeName) { return this.dirty == NOT_DIRTY && this.dom.nodeName == nodeName; }
      get domAtom() { return true; }
      get ignoreForCoords() { return this.dom.nodeName == "IMG"; }
  }
  // A separate subclass is used for customized node views, so that the
  // extra checks only have to be made for nodes that are actually
  // customized.
  class CustomNodeViewDesc extends NodeViewDesc {
      constructor(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, spec, view, pos) {
          super(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, view, pos);
          this.spec = spec;
      }
      // A custom `update` method gets to decide whether the update goes
      // through. If it does, and there's a `contentDOM` node, our logic
      // updates the children.
      update(node, outerDeco, innerDeco, view) {
          if (this.dirty == NODE_DIRTY)
              return false;
          if (this.spec.update) {
              let result = this.spec.update(node, outerDeco, innerDeco);
              if (result)
                  this.updateInner(node, outerDeco, innerDeco, view);
              return result;
          }
          else if (!this.contentDOM && !node.isLeaf) {
              return false;
          }
          else {
              return super.update(node, outerDeco, innerDeco, view);
          }
      }
      selectNode() {
          this.spec.selectNode ? this.spec.selectNode() : super.selectNode();
      }
      deselectNode() {
          this.spec.deselectNode ? this.spec.deselectNode() : super.deselectNode();
      }
      setSelection(anchor, head, root, force) {
          this.spec.setSelection ? this.spec.setSelection(anchor, head, root)
              : super.setSelection(anchor, head, root, force);
      }
      destroy() {
          if (this.spec.destroy)
              this.spec.destroy();
          super.destroy();
      }
      stopEvent(event) {
          return this.spec.stopEvent ? this.spec.stopEvent(event) : false;
      }
      ignoreMutation(mutation) {
          return this.spec.ignoreMutation ? this.spec.ignoreMutation(mutation) : super.ignoreMutation(mutation);
      }
  }
  // Sync the content of the given DOM node with the nodes associated
  // with the given array of view descs, recursing into mark descs
  // because this should sync the subtree for a whole node at a time.
  function renderDescs(parentDOM, descs, view) {
      let dom = parentDOM.firstChild, written = false;
      for (let i = 0; i < descs.length; i++) {
          let desc = descs[i], childDOM = desc.dom;
          if (childDOM.parentNode == parentDOM) {
              while (childDOM != dom) {
                  dom = rm(dom);
                  written = true;
              }
              dom = dom.nextSibling;
          }
          else {
              written = true;
              parentDOM.insertBefore(childDOM, dom);
          }
          if (desc instanceof MarkViewDesc) {
              let pos = dom ? dom.previousSibling : parentDOM.lastChild;
              renderDescs(desc.contentDOM, desc.children, view);
              dom = pos ? pos.nextSibling : parentDOM.firstChild;
          }
      }
      while (dom) {
          dom = rm(dom);
          written = true;
      }
      if (written && view.trackWrites == parentDOM)
          view.trackWrites = null;
  }
  const OuterDecoLevel = function (nodeName) {
      if (nodeName)
          this.nodeName = nodeName;
  };
  OuterDecoLevel.prototype = Object.create(null);
  const noDeco = [new OuterDecoLevel];
  function computeOuterDeco(outerDeco, node, needsWrap) {
      if (outerDeco.length == 0)
          return noDeco;
      let top = needsWrap ? noDeco[0] : new OuterDecoLevel, result = [top];
      for (let i = 0; i < outerDeco.length; i++) {
          let attrs = outerDeco[i].type.attrs;
          if (!attrs)
              continue;
          if (attrs.nodeName)
              result.push(top = new OuterDecoLevel(attrs.nodeName));
          for (let name in attrs) {
              let val = attrs[name];
              if (val == null)
                  continue;
              if (needsWrap && result.length == 1)
                  result.push(top = new OuterDecoLevel(node.isInline ? "span" : "div"));
              if (name == "class")
                  top.class = (top.class ? top.class + " " : "") + val;
              else if (name == "style")
                  top.style = (top.style ? top.style + ";" : "") + val;
              else if (name != "nodeName")
                  top[name] = val;
          }
      }
      return result;
  }
  function patchOuterDeco(outerDOM, nodeDOM, prevComputed, curComputed) {
      // Shortcut for trivial case
      if (prevComputed == noDeco && curComputed == noDeco)
          return nodeDOM;
      let curDOM = nodeDOM;
      for (let i = 0; i < curComputed.length; i++) {
          let deco = curComputed[i], prev = prevComputed[i];
          if (i) {
              let parent;
              if (prev && prev.nodeName == deco.nodeName && curDOM != outerDOM &&
                  (parent = curDOM.parentNode) && parent.nodeName.toLowerCase() == deco.nodeName) {
                  curDOM = parent;
              }
              else {
                  parent = document.createElement(deco.nodeName);
                  parent.pmIsDeco = true;
                  parent.appendChild(curDOM);
                  prev = noDeco[0];
                  curDOM = parent;
              }
          }
          patchAttributes(curDOM, prev || noDeco[0], deco);
      }
      return curDOM;
  }
  function patchAttributes(dom, prev, cur) {
      for (let name in prev)
          if (name != "class" && name != "style" && name != "nodeName" && !(name in cur))
              dom.removeAttribute(name);
      for (let name in cur)
          if (name != "class" && name != "style" && name != "nodeName" && cur[name] != prev[name])
              dom.setAttribute(name, cur[name]);
      if (prev.class != cur.class) {
          let prevList = prev.class ? prev.class.split(" ").filter(Boolean) : [];
          let curList = cur.class ? cur.class.split(" ").filter(Boolean) : [];
          for (let i = 0; i < prevList.length; i++)
              if (curList.indexOf(prevList[i]) == -1)
                  dom.classList.remove(prevList[i]);
          for (let i = 0; i < curList.length; i++)
              if (prevList.indexOf(curList[i]) == -1)
                  dom.classList.add(curList[i]);
          if (dom.classList.length == 0)
              dom.removeAttribute("class");
      }
      if (prev.style != cur.style) {
          if (prev.style) {
              let prop = /\s*([\w\-\xa1-\uffff]+)\s*:(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\(.*?\)|[^;])*/g, m;
              while (m = prop.exec(prev.style))
                  dom.style.removeProperty(m[1]);
          }
          if (cur.style)
              dom.style.cssText += cur.style;
      }
  }
  function applyOuterDeco(dom, deco, node) {
      return patchOuterDeco(dom, dom, noDeco, computeOuterDeco(deco, node, dom.nodeType != 1));
  }
  function sameOuterDeco(a, b) {
      if (a.length != b.length)
          return false;
      for (let i = 0; i < a.length; i++)
          if (!a[i].type.eq(b[i].type))
              return false;
      return true;
  }
  // Remove a DOM node and return its next sibling.
  function rm(dom) {
      let next = dom.nextSibling;
      dom.parentNode.removeChild(dom);
      return next;
  }
  // Helper class for incrementally updating a tree of mark descs and
  // the widget and node descs inside of them.
  class ViewTreeUpdater {
      constructor(top, lock, view) {
          this.lock = lock;
          this.view = view;
          // Index into `this.top`'s child array, represents the current
          // update position.
          this.index = 0;
          // When entering a mark, the current top and index are pushed
          // onto this.
          this.stack = [];
          // Tracks whether anything was changed
          this.changed = false;
          this.top = top;
          this.preMatch = preMatch(top.node.content, top);
      }
      // Destroy and remove the children between the given indices in
      // `this.top`.
      destroyBetween(start, end) {
          if (start == end)
              return;
          for (let i = start; i < end; i++)
              this.top.children[i].destroy();
          this.top.children.splice(start, end - start);
          this.changed = true;
      }
      // Destroy all remaining children in `this.top`.
      destroyRest() {
          this.destroyBetween(this.index, this.top.children.length);
      }
      // Sync the current stack of mark descs with the given array of
      // marks, reusing existing mark descs when possible.
      syncToMarks(marks, inline, view) {
          let keep = 0, depth = this.stack.length >> 1;
          let maxKeep = Math.min(depth, marks.length);
          while (keep < maxKeep &&
              (keep == depth - 1 ? this.top : this.stack[(keep + 1) << 1])
                  .matchesMark(marks[keep]) && marks[keep].type.spec.spanning !== false)
              keep++;
          while (keep < depth) {
              this.destroyRest();
              this.top.dirty = NOT_DIRTY;
              this.index = this.stack.pop();
              this.top = this.stack.pop();
              depth--;
          }
          while (depth < marks.length) {
              this.stack.push(this.top, this.index + 1);
              let found = -1;
              for (let i = this.index; i < Math.min(this.index + 3, this.top.children.length); i++) {
                  let next = this.top.children[i];
                  if (next.matchesMark(marks[depth]) && !this.isLocked(next.dom)) {
                      found = i;
                      break;
                  }
              }
              if (found > -1) {
                  if (found > this.index) {
                      this.changed = true;
                      this.destroyBetween(this.index, found);
                  }
                  this.top = this.top.children[this.index];
              }
              else {
                  let markDesc = MarkViewDesc.create(this.top, marks[depth], inline, view);
                  this.top.children.splice(this.index, 0, markDesc);
                  this.top = markDesc;
                  this.changed = true;
              }
              this.index = 0;
              depth++;
          }
      }
      // Try to find a node desc matching the given data. Skip over it and
      // return true when successful.
      findNodeMatch(node, outerDeco, innerDeco, index) {
          let found = -1, targetDesc;
          if (index >= this.preMatch.index &&
              (targetDesc = this.preMatch.matches[index - this.preMatch.index]).parent == this.top &&
              targetDesc.matchesNode(node, outerDeco, innerDeco)) {
              found = this.top.children.indexOf(targetDesc, this.index);
          }
          else {
              for (let i = this.index, e = Math.min(this.top.children.length, i + 5); i < e; i++) {
                  let child = this.top.children[i];
                  if (child.matchesNode(node, outerDeco, innerDeco) && !this.preMatch.matched.has(child)) {
                      found = i;
                      break;
                  }
              }
          }
          if (found < 0)
              return false;
          this.destroyBetween(this.index, found);
          this.index++;
          return true;
      }
      updateNodeAt(node, outerDeco, innerDeco, index, view) {
          let child = this.top.children[index];
          if (child.dirty == NODE_DIRTY && child.dom == child.contentDOM)
              child.dirty = CONTENT_DIRTY;
          if (!child.update(node, outerDeco, innerDeco, view))
              return false;
          this.destroyBetween(this.index, index);
          this.index++;
          return true;
      }
      findIndexWithChild(domNode) {
          for (;;) {
              let parent = domNode.parentNode;
              if (!parent)
                  return -1;
              if (parent == this.top.contentDOM) {
                  let desc = domNode.pmViewDesc;
                  if (desc)
                      for (let i = this.index; i < this.top.children.length; i++) {
                          if (this.top.children[i] == desc)
                              return i;
                      }
                  return -1;
              }
              domNode = parent;
          }
      }
      // Try to update the next node, if any, to the given data. Checks
      // pre-matches to avoid overwriting nodes that could still be used.
      updateNextNode(node, outerDeco, innerDeco, view, index, pos) {
          for (let i = this.index; i < this.top.children.length; i++) {
              let next = this.top.children[i];
              if (next instanceof NodeViewDesc) {
                  let preMatch = this.preMatch.matched.get(next);
                  if (preMatch != null && preMatch != index)
                      return false;
                  let nextDOM = next.dom, updated;
                  // Can't update if nextDOM is or contains this.lock, except if
                  // it's a text node whose content already matches the new text
                  // and whose decorations match the new ones.
                  let locked = this.isLocked(nextDOM) &&
                      !(node.isText && next.node && next.node.isText && next.nodeDOM.nodeValue == node.text &&
                          next.dirty != NODE_DIRTY && sameOuterDeco(outerDeco, next.outerDeco));
                  if (!locked && next.update(node, outerDeco, innerDeco, view)) {
                      this.destroyBetween(this.index, i);
                      if (next.dom != nextDOM)
                          this.changed = true;
                      this.index++;
                      return true;
                  }
                  else if (!locked && (updated = this.recreateWrapper(next, node, outerDeco, innerDeco, view, pos))) {
                      this.top.children[this.index] = updated;
                      if (updated.contentDOM) {
                          updated.dirty = CONTENT_DIRTY;
                          updated.updateChildren(view, pos + 1);
                          updated.dirty = NOT_DIRTY;
                      }
                      this.changed = true;
                      this.index++;
                      return true;
                  }
                  break;
              }
          }
          return false;
      }
      // When a node with content is replaced by a different node with
      // identical content, move over its children.
      recreateWrapper(next, node, outerDeco, innerDeco, view, pos) {
          if (next.dirty || node.isAtom || !next.children.length ||
              !next.node.content.eq(node.content))
              return null;
          let wrapper = NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos);
          if (wrapper.contentDOM) {
              wrapper.children = next.children;
              next.children = [];
              for (let ch of wrapper.children)
                  ch.parent = wrapper;
          }
          next.destroy();
          return wrapper;
      }
      // Insert the node as a newly created node desc.
      addNode(node, outerDeco, innerDeco, view, pos) {
          let desc = NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos);
          if (desc.contentDOM)
              desc.updateChildren(view, pos + 1);
          this.top.children.splice(this.index++, 0, desc);
          this.changed = true;
      }
      placeWidget(widget, view, pos) {
          let next = this.index < this.top.children.length ? this.top.children[this.index] : null;
          if (next && next.matchesWidget(widget) &&
              (widget == next.widget || !next.widget.type.toDOM.parentNode)) {
              this.index++;
          }
          else {
              let desc = new WidgetViewDesc(this.top, widget, view, pos);
              this.top.children.splice(this.index++, 0, desc);
              this.changed = true;
          }
      }
      // Make sure a textblock looks and behaves correctly in
      // contentEditable.
      addTextblockHacks() {
          let lastChild = this.top.children[this.index - 1], parent = this.top;
          while (lastChild instanceof MarkViewDesc) {
              parent = lastChild;
              lastChild = parent.children[parent.children.length - 1];
          }
          if (!lastChild || // Empty textblock
              !(lastChild instanceof TextViewDesc) ||
              /\n$/.test(lastChild.node.text) ||
              (this.view.requiresGeckoHackNode && /\s$/.test(lastChild.node.text))) {
              // Avoid bugs in Safari's cursor drawing (#1165) and Chrome's mouse selection (#1152)
              if ((safari || chrome) && lastChild && lastChild.dom.contentEditable == "false")
                  this.addHackNode("IMG", parent);
              this.addHackNode("BR", this.top);
          }
      }
      addHackNode(nodeName, parent) {
          if (parent == this.top && this.index < parent.children.length && parent.children[this.index].matchesHack(nodeName)) {
              this.index++;
          }
          else {
              let dom = document.createElement(nodeName);
              if (nodeName == "IMG") {
                  dom.className = "ProseMirror-separator";
                  dom.alt = "";
              }
              if (nodeName == "BR")
                  dom.className = "ProseMirror-trailingBreak";
              let hack = new TrailingHackViewDesc(this.top, [], dom, null);
              if (parent != this.top)
                  parent.children.push(hack);
              else
                  parent.children.splice(this.index++, 0, hack);
              this.changed = true;
          }
      }
      isLocked(node) {
          return this.lock && (node == this.lock || node.nodeType == 1 && node.contains(this.lock.parentNode));
      }
  }
  // Iterate from the end of the fragment and array of descs to find
  // directly matching ones, in order to avoid overeagerly reusing those
  // for other nodes. Returns the fragment index of the first node that
  // is part of the sequence of matched nodes at the end of the
  // fragment.
  function preMatch(frag, parentDesc) {
      let curDesc = parentDesc, descI = curDesc.children.length;
      let fI = frag.childCount, matched = new Map, matches = [];
      outer: while (fI > 0) {
          let desc;
          for (;;) {
              if (descI) {
                  let next = curDesc.children[descI - 1];
                  if (next instanceof MarkViewDesc) {
                      curDesc = next;
                      descI = next.children.length;
                  }
                  else {
                      desc = next;
                      descI--;
                      break;
                  }
              }
              else if (curDesc == parentDesc) {
                  break outer;
              }
              else {
                  // FIXME
                  descI = curDesc.parent.children.indexOf(curDesc);
                  curDesc = curDesc.parent;
              }
          }
          let node = desc.node;
          if (!node)
              continue;
          if (node != frag.child(fI - 1))
              break;
          --fI;
          matched.set(desc, fI);
          matches.push(desc);
      }
      return { index: fI, matched, matches: matches.reverse() };
  }
  function compareSide(a, b) {
      return a.type.side - b.type.side;
  }
  // This function abstracts iterating over the nodes and decorations in
  // a fragment. Calls `onNode` for each node, with its local and child
  // decorations. Splits text nodes when there is a decoration starting
  // or ending inside of them. Calls `onWidget` for each widget.
  function iterDeco(parent, deco, onWidget, onNode) {
      let locals = deco.locals(parent), offset = 0;
      // Simple, cheap variant for when there are no local decorations
      if (locals.length == 0) {
          for (let i = 0; i < parent.childCount; i++) {
              let child = parent.child(i);
              onNode(child, locals, deco.forChild(offset, child), i);
              offset += child.nodeSize;
          }
          return;
      }
      let decoIndex = 0, active = [], restNode = null;
      for (let parentIndex = 0;;) {
          let widget, widgets;
          while (decoIndex < locals.length && locals[decoIndex].to == offset) {
              let next = locals[decoIndex++];
              if (next.widget) {
                  if (!widget)
                      widget = next;
                  else
                      (widgets || (widgets = [widget])).push(next);
              }
          }
          if (widget) {
              if (widgets) {
                  widgets.sort(compareSide);
                  for (let i = 0; i < widgets.length; i++)
                      onWidget(widgets[i], parentIndex, !!restNode);
              }
              else {
                  onWidget(widget, parentIndex, !!restNode);
              }
          }
          let child, index;
          if (restNode) {
              index = -1;
              child = restNode;
              restNode = null;
          }
          else if (parentIndex < parent.childCount) {
              index = parentIndex;
              child = parent.child(parentIndex++);
          }
          else {
              break;
          }
          for (let i = 0; i < active.length; i++)
              if (active[i].to <= offset)
                  active.splice(i--, 1);
          while (decoIndex < locals.length && locals[decoIndex].from <= offset && locals[decoIndex].to > offset)
              active.push(locals[decoIndex++]);
          let end = offset + child.nodeSize;
          if (child.isText) {
              let cutAt = end;
              if (decoIndex < locals.length && locals[decoIndex].from < cutAt)
                  cutAt = locals[decoIndex].from;
              for (let i = 0; i < active.length; i++)
                  if (active[i].to < cutAt)
                      cutAt = active[i].to;
              if (cutAt < end) {
                  restNode = child.cut(cutAt - offset);
                  child = child.cut(0, cutAt - offset);
                  end = cutAt;
                  index = -1;
              }
          }
          else {
              while (decoIndex < locals.length && locals[decoIndex].to < end)
                  decoIndex++;
          }
          let outerDeco = child.isInline && !child.isLeaf ? active.filter(d => !d.inline) : active.slice();
          onNode(child, outerDeco, deco.forChild(offset, child), index);
          offset = end;
      }
  }
  // List markers in Mobile Safari will mysteriously disappear
  // sometimes. This works around that.
  function iosHacks(dom) {
      if (dom.nodeName == "UL" || dom.nodeName == "OL") {
          let oldCSS = dom.style.cssText;
          dom.style.cssText = oldCSS + "; list-style: square !important";
          window.getComputedStyle(dom).listStyle;
          dom.style.cssText = oldCSS;
      }
  }
  // Find a piece of text in an inline fragment, overlapping from-to
  function findTextInFragment(frag, text, from, to) {
      for (let i = 0, pos = 0; i < frag.childCount && pos <= to;) {
          let child = frag.child(i++), childStart = pos;
          pos += child.nodeSize;
          if (!child.isText)
              continue;
          let str = child.text;
          while (i < frag.childCount) {
              let next = frag.child(i++);
              pos += next.nodeSize;
              if (!next.isText)
                  break;
              str += next.text;
          }
          if (pos >= from) {
              if (pos >= to && str.slice(to - text.length - childStart, to - childStart) == text)
                  return to - text.length;
              let found = childStart < to ? str.lastIndexOf(text, to - childStart - 1) : -1;
              if (found >= 0 && found + text.length + childStart >= from)
                  return childStart + found;
              if (from == to && str.length >= (to + text.length) - childStart &&
                  str.slice(to - childStart, to - childStart + text.length) == text)
                  return to;
          }
      }
      return -1;
  }
  // Replace range from-to in an array of view descs with replacement
  // (may be null to just delete). This goes very much against the grain
  // of the rest of this code, which tends to create nodes with the
  // right shape in one go, rather than messing with them after
  // creation, but is necessary in the composition hack.
  function replaceNodes(nodes, from, to, view, replacement) {
      let result = [];
      for (let i = 0, off = 0; i < nodes.length; i++) {
          let child = nodes[i], start = off, end = off += child.size;
          if (start >= to || end <= from) {
              result.push(child);
          }
          else {
              if (start < from)
                  result.push(child.slice(0, from - start, view));
              if (replacement) {
                  result.push(replacement);
                  replacement = undefined;
              }
              if (end > to)
                  result.push(child.slice(to - start, child.size, view));
          }
      }
      return result;
  }

  function selectionFromDOM(view, origin = null) {
      let domSel = view.domSelectionRange(), doc = view.state.doc;
      if (!domSel.focusNode)
          return null;
      let nearestDesc = view.docView.nearestDesc(domSel.focusNode), inWidget = nearestDesc && nearestDesc.size == 0;
      let head = view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset, 1);
      if (head < 0)
          return null;
      let $head = doc.resolve(head), $anchor, selection;
      if (selectionCollapsed(domSel)) {
          $anchor = $head;
          while (nearestDesc && !nearestDesc.node)
              nearestDesc = nearestDesc.parent;
          let nearestDescNode = nearestDesc.node;
          if (nearestDesc && nearestDescNode.isAtom && NodeSelection.isSelectable(nearestDescNode) && nearestDesc.parent
              && !(nearestDescNode.isInline && isOnEdge(domSel.focusNode, domSel.focusOffset, nearestDesc.dom))) {
              let pos = nearestDesc.posBefore;
              selection = new NodeSelection(head == pos ? $head : doc.resolve(pos));
          }
      }
      else {
          let anchor = view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset, 1);
          if (anchor < 0)
              return null;
          $anchor = doc.resolve(anchor);
      }
      if (!selection) {
          let bias = origin == "pointer" || (view.state.selection.head < $head.pos && !inWidget) ? 1 : -1;
          selection = selectionBetween(view, $anchor, $head, bias);
      }
      return selection;
  }
  function editorOwnsSelection(view) {
      return view.editable ? view.hasFocus() :
          hasSelection(view) && document.activeElement && document.activeElement.contains(view.dom);
  }
  function selectionToDOM(view, force = false) {
      let sel = view.state.selection;
      syncNodeSelection(view, sel);
      if (!editorOwnsSelection(view))
          return;
      // The delayed drag selection causes issues with Cell Selections
      // in Safari. And the drag selection delay is to workarond issues
      // which only present in Chrome.
      if (!force && view.input.mouseDown && view.input.mouseDown.allowDefault && chrome) {
          let domSel = view.domSelectionRange(), curSel = view.domObserver.currentSelection;
          if (domSel.anchorNode && curSel.anchorNode &&
              isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset, curSel.anchorNode, curSel.anchorOffset)) {
              view.input.mouseDown.delayedSelectionSync = true;
              view.domObserver.setCurSelection();
              return;
          }
      }
      view.domObserver.disconnectSelection();
      if (view.cursorWrapper) {
          selectCursorWrapper(view);
      }
      else {
          let { anchor, head } = sel, resetEditableFrom, resetEditableTo;
          if (brokenSelectBetweenUneditable && !(sel instanceof TextSelection)) {
              if (!sel.$from.parent.inlineContent)
                  resetEditableFrom = temporarilyEditableNear(view, sel.from);
              if (!sel.empty && !sel.$from.parent.inlineContent)
                  resetEditableTo = temporarilyEditableNear(view, sel.to);
          }
          view.docView.setSelection(anchor, head, view.root, force);
          if (brokenSelectBetweenUneditable) {
              if (resetEditableFrom)
                  resetEditable(resetEditableFrom);
              if (resetEditableTo)
                  resetEditable(resetEditableTo);
          }
          if (sel.visible) {
              view.dom.classList.remove("ProseMirror-hideselection");
          }
          else {
              view.dom.classList.add("ProseMirror-hideselection");
              if ("onselectionchange" in document)
                  removeClassOnSelectionChange(view);
          }
      }
      view.domObserver.setCurSelection();
      view.domObserver.connectSelection();
  }
  // Kludge to work around Webkit not allowing a selection to start/end
  // between non-editable block nodes. We briefly make something
  // editable, set the selection, then set it uneditable again.
  const brokenSelectBetweenUneditable = safari || chrome && chrome_version < 63;
  function temporarilyEditableNear(view, pos) {
      let { node, offset } = view.docView.domFromPos(pos, 0);
      let after = offset < node.childNodes.length ? node.childNodes[offset] : null;
      let before = offset ? node.childNodes[offset - 1] : null;
      if (safari && after && after.contentEditable == "false")
          return setEditable(after);
      if ((!after || after.contentEditable == "false") &&
          (!before || before.contentEditable == "false")) {
          if (after)
              return setEditable(after);
          else if (before)
              return setEditable(before);
      }
  }
  function setEditable(element) {
      element.contentEditable = "true";
      if (safari && element.draggable) {
          element.draggable = false;
          element.wasDraggable = true;
      }
      return element;
  }
  function resetEditable(element) {
      element.contentEditable = "false";
      if (element.wasDraggable) {
          element.draggable = true;
          element.wasDraggable = null;
      }
  }
  function removeClassOnSelectionChange(view) {
      let doc = view.dom.ownerDocument;
      doc.removeEventListener("selectionchange", view.input.hideSelectionGuard);
      let domSel = view.domSelectionRange();
      let node = domSel.anchorNode, offset = domSel.anchorOffset;
      doc.addEventListener("selectionchange", view.input.hideSelectionGuard = () => {
          if (domSel.anchorNode != node || domSel.anchorOffset != offset) {
              doc.removeEventListener("selectionchange", view.input.hideSelectionGuard);
              setTimeout(() => {
                  if (!editorOwnsSelection(view) || view.state.selection.visible)
                      view.dom.classList.remove("ProseMirror-hideselection");
              }, 20);
          }
      });
  }
  function selectCursorWrapper(view) {
      let domSel = view.domSelection(), range = document.createRange();
      let node = view.cursorWrapper.dom, img = node.nodeName == "IMG";
      if (img)
          range.setEnd(node.parentNode, domIndex(node) + 1);
      else
          range.setEnd(node, 0);
      range.collapse(false);
      domSel.removeAllRanges();
      domSel.addRange(range);
      // Kludge to kill 'control selection' in IE11 when selecting an
      // invisible cursor wrapper, since that would result in those weird
      // resize handles and a selection that considers the absolutely
      // positioned wrapper, rather than the root editable node, the
      // focused element.
      if (!img && !view.state.selection.visible && ie$1 && ie_version <= 11) {
          node.disabled = true;
          node.disabled = false;
      }
  }
  function syncNodeSelection(view, sel) {
      if (sel instanceof NodeSelection) {
          let desc = view.docView.descAt(sel.from);
          if (desc != view.lastSelectedViewDesc) {
              clearNodeSelection(view);
              if (desc)
                  desc.selectNode();
              view.lastSelectedViewDesc = desc;
          }
      }
      else {
          clearNodeSelection(view);
      }
  }
  // Clear all DOM statefulness of the last node selection.
  function clearNodeSelection(view) {
      if (view.lastSelectedViewDesc) {
          if (view.lastSelectedViewDesc.parent)
              view.lastSelectedViewDesc.deselectNode();
          view.lastSelectedViewDesc = undefined;
      }
  }
  function selectionBetween(view, $anchor, $head, bias) {
      return view.someProp("createSelectionBetween", f => f(view, $anchor, $head))
          || TextSelection.between($anchor, $head, bias);
  }
  function hasFocusAndSelection(view) {
      if (view.editable && !view.hasFocus())
          return false;
      return hasSelection(view);
  }
  function hasSelection(view) {
      let sel = view.domSelectionRange();
      if (!sel.anchorNode)
          return false;
      try {
          // Firefox will raise 'permission denied' errors when accessing
          // properties of `sel.anchorNode` when it's in a generated CSS
          // element.
          return view.dom.contains(sel.anchorNode.nodeType == 3 ? sel.anchorNode.parentNode : sel.anchorNode) &&
              (view.editable || view.dom.contains(sel.focusNode.nodeType == 3 ? sel.focusNode.parentNode : sel.focusNode));
      }
      catch (_) {
          return false;
      }
  }
  function anchorInRightPlace(view) {
      let anchorDOM = view.docView.domFromPos(view.state.selection.anchor, 0);
      let domSel = view.domSelectionRange();
      return isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode, domSel.anchorOffset);
  }

  function moveSelectionBlock(state, dir) {
      let { $anchor, $head } = state.selection;
      let $side = dir > 0 ? $anchor.max($head) : $anchor.min($head);
      let $start = !$side.parent.inlineContent ? $side : $side.depth ? state.doc.resolve(dir > 0 ? $side.after() : $side.before()) : null;
      return $start && Selection.findFrom($start, dir);
  }
  function apply(view, sel) {
      view.dispatch(view.state.tr.setSelection(sel).scrollIntoView());
      return true;
  }
  function selectHorizontally(view, dir, mods) {
      let sel = view.state.selection;
      if (sel instanceof TextSelection) {
          if (mods.indexOf("s") > -1) {
              let { $head } = sel, node = $head.textOffset ? null : dir < 0 ? $head.nodeBefore : $head.nodeAfter;
              if (!node || node.isText || !node.isLeaf)
                  return false;
              let $newHead = view.state.doc.resolve($head.pos + node.nodeSize * (dir < 0 ? -1 : 1));
              return apply(view, new TextSelection(sel.$anchor, $newHead));
          }
          else if (!sel.empty) {
              return false;
          }
          else if (view.endOfTextblock(dir > 0 ? "forward" : "backward")) {
              let next = moveSelectionBlock(view.state, dir);
              if (next && (next instanceof NodeSelection))
                  return apply(view, next);
              return false;
          }
          else if (!(mac$3 && mods.indexOf("m") > -1)) {
              let $head = sel.$head, node = $head.textOffset ? null : dir < 0 ? $head.nodeBefore : $head.nodeAfter, desc;
              if (!node || node.isText)
                  return false;
              let nodePos = dir < 0 ? $head.pos - node.nodeSize : $head.pos;
              if (!(node.isAtom || (desc = view.docView.descAt(nodePos)) && !desc.contentDOM))
                  return false;
              if (NodeSelection.isSelectable(node)) {
                  return apply(view, new NodeSelection(dir < 0 ? view.state.doc.resolve($head.pos - node.nodeSize) : $head));
              }
              else if (webkit) {
                  // Chrome and Safari will introduce extra pointless cursor
                  // positions around inline uneditable nodes, so we have to
                  // take over and move the cursor past them (#937)
                  return apply(view, new TextSelection(view.state.doc.resolve(dir < 0 ? nodePos : nodePos + node.nodeSize)));
              }
              else {
                  return false;
              }
          }
      }
      else if (sel instanceof NodeSelection && sel.node.isInline) {
          return apply(view, new TextSelection(dir > 0 ? sel.$to : sel.$from));
      }
      else {
          let next = moveSelectionBlock(view.state, dir);
          if (next)
              return apply(view, next);
          return false;
      }
  }
  function nodeLen(node) {
      return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length;
  }
  function isIgnorable(dom, dir) {
      let desc = dom.pmViewDesc;
      return desc && desc.size == 0 && (dir < 0 || dom.nextSibling || dom.nodeName != "BR");
  }
  function skipIgnoredNodes(view, dir) {
      return dir < 0 ? skipIgnoredNodesBefore(view) : skipIgnoredNodesAfter(view);
  }
  // Make sure the cursor isn't directly after one or more ignored
  // nodes, which will confuse the browser's cursor motion logic.
  function skipIgnoredNodesBefore(view) {
      let sel = view.domSelectionRange();
      let node = sel.focusNode, offset = sel.focusOffset;
      if (!node)
          return;
      let moveNode, moveOffset, force = false;
      // Gecko will do odd things when the selection is directly in front
      // of a non-editable node, so in that case, move it into the next
      // node if possible. Issue prosemirror/prosemirror#832.
      if (gecko && node.nodeType == 1 && offset < nodeLen(node) && isIgnorable(node.childNodes[offset], -1))
          force = true;
      for (;;) {
          if (offset > 0) {
              if (node.nodeType != 1) {
                  break;
              }
              else {
                  let before = node.childNodes[offset - 1];
                  if (isIgnorable(before, -1)) {
                      moveNode = node;
                      moveOffset = --offset;
                  }
                  else if (before.nodeType == 3) {
                      node = before;
                      offset = node.nodeValue.length;
                  }
                  else
                      break;
              }
          }
          else if (isBlockNode(node)) {
              break;
          }
          else {
              let prev = node.previousSibling;
              while (prev && isIgnorable(prev, -1)) {
                  moveNode = node.parentNode;
                  moveOffset = domIndex(prev);
                  prev = prev.previousSibling;
              }
              if (!prev) {
                  node = node.parentNode;
                  if (node == view.dom)
                      break;
                  offset = 0;
              }
              else {
                  node = prev;
                  offset = nodeLen(node);
              }
          }
      }
      if (force)
          setSelFocus(view, node, offset);
      else if (moveNode)
          setSelFocus(view, moveNode, moveOffset);
  }
  // Make sure the cursor isn't directly before one or more ignored
  // nodes.
  function skipIgnoredNodesAfter(view) {
      let sel = view.domSelectionRange();
      let node = sel.focusNode, offset = sel.focusOffset;
      if (!node)
          return;
      let len = nodeLen(node);
      let moveNode, moveOffset;
      for (;;) {
          if (offset < len) {
              if (node.nodeType != 1)
                  break;
              let after = node.childNodes[offset];
              if (isIgnorable(after, 1)) {
                  moveNode = node;
                  moveOffset = ++offset;
              }
              else
                  break;
          }
          else if (isBlockNode(node)) {
              break;
          }
          else {
              let next = node.nextSibling;
              while (next && isIgnorable(next, 1)) {
                  moveNode = next.parentNode;
                  moveOffset = domIndex(next) + 1;
                  next = next.nextSibling;
              }
              if (!next) {
                  node = node.parentNode;
                  if (node == view.dom)
                      break;
                  offset = len = 0;
              }
              else {
                  node = next;
                  offset = 0;
                  len = nodeLen(node);
              }
          }
      }
      if (moveNode)
          setSelFocus(view, moveNode, moveOffset);
  }
  function isBlockNode(dom) {
      let desc = dom.pmViewDesc;
      return desc && desc.node && desc.node.isBlock;
  }
  function textNodeAfter(node, offset) {
      while (node && offset == node.childNodes.length && !hasBlockDesc(node)) {
          offset = domIndex(node) + 1;
          node = node.parentNode;
      }
      while (node && offset < node.childNodes.length) {
          let next = node.childNodes[offset];
          if (next.nodeType == 3)
              return next;
          if (next.nodeType == 1 && next.contentEditable == "false")
              break;
          node = next;
          offset = 0;
      }
  }
  function textNodeBefore(node, offset) {
      while (node && !offset && !hasBlockDesc(node)) {
          offset = domIndex(node);
          node = node.parentNode;
      }
      while (node && offset) {
          let next = node.childNodes[offset - 1];
          if (next.nodeType == 3)
              return next;
          if (next.nodeType == 1 && next.contentEditable == "false")
              break;
          node = next;
          offset = node.childNodes.length;
      }
  }
  function setSelFocus(view, node, offset) {
      if (node.nodeType != 3) {
          let before, after;
          if (after = textNodeAfter(node, offset)) {
              node = after;
              offset = 0;
          }
          else if (before = textNodeBefore(node, offset)) {
              node = before;
              offset = before.nodeValue.length;
          }
      }
      let sel = view.domSelection();
      if (selectionCollapsed(sel)) {
          let range = document.createRange();
          range.setEnd(node, offset);
          range.setStart(node, offset);
          sel.removeAllRanges();
          sel.addRange(range);
      }
      else if (sel.extend) {
          sel.extend(node, offset);
      }
      view.domObserver.setCurSelection();
      let { state } = view;
      // If no state update ends up happening, reset the selection.
      setTimeout(() => {
          if (view.state == state)
              selectionToDOM(view);
      }, 50);
  }
  function findDirection(view, pos) {
      let $pos = view.state.doc.resolve(pos);
      if (!(chrome || windows) && $pos.parent.inlineContent) {
          let coords = view.coordsAtPos(pos);
          if (pos > $pos.start()) {
              let before = view.coordsAtPos(pos - 1);
              let mid = (before.top + before.bottom) / 2;
              if (mid > coords.top && mid < coords.bottom && Math.abs(before.left - coords.left) > 1)
                  return before.left < coords.left ? "ltr" : "rtl";
          }
          if (pos < $pos.end()) {
              let after = view.coordsAtPos(pos + 1);
              let mid = (after.top + after.bottom) / 2;
              if (mid > coords.top && mid < coords.bottom && Math.abs(after.left - coords.left) > 1)
                  return after.left > coords.left ? "ltr" : "rtl";
          }
      }
      let computed = getComputedStyle(view.dom).direction;
      return computed == "rtl" ? "rtl" : "ltr";
  }
  // Check whether vertical selection motion would involve node
  // selections. If so, apply it (if not, the result is left to the
  // browser)
  function selectVertically(view, dir, mods) {
      let sel = view.state.selection;
      if (sel instanceof TextSelection && !sel.empty || mods.indexOf("s") > -1)
          return false;
      if (mac$3 && mods.indexOf("m") > -1)
          return false;
      let { $from, $to } = sel;
      if (!$from.parent.inlineContent || view.endOfTextblock(dir < 0 ? "up" : "down")) {
          let next = moveSelectionBlock(view.state, dir);
          if (next && (next instanceof NodeSelection))
              return apply(view, next);
      }
      if (!$from.parent.inlineContent) {
          let side = dir < 0 ? $from : $to;
          let beyond = sel instanceof AllSelection ? Selection.near(side, dir) : Selection.findFrom(side, dir);
          return beyond ? apply(view, beyond) : false;
      }
      return false;
  }
  function stopNativeHorizontalDelete(view, dir) {
      if (!(view.state.selection instanceof TextSelection))
          return true;
      let { $head, $anchor, empty } = view.state.selection;
      if (!$head.sameParent($anchor))
          return true;
      if (!empty)
          return false;
      if (view.endOfTextblock(dir > 0 ? "forward" : "backward"))
          return true;
      let nextNode = !$head.textOffset && (dir < 0 ? $head.nodeBefore : $head.nodeAfter);
      if (nextNode && !nextNode.isText) {
          let tr = view.state.tr;
          if (dir < 0)
              tr.delete($head.pos - nextNode.nodeSize, $head.pos);
          else
              tr.delete($head.pos, $head.pos + nextNode.nodeSize);
          view.dispatch(tr);
          return true;
      }
      return false;
  }
  function switchEditable(view, node, state) {
      view.domObserver.stop();
      node.contentEditable = state;
      view.domObserver.start();
  }
  // Issue #867 / #1090 / https://bugs.chromium.org/p/chromium/issues/detail?id=903821
  // In which Safari (and at some point in the past, Chrome) does really
  // wrong things when the down arrow is pressed when the cursor is
  // directly at the start of a textblock and has an uneditable node
  // after it
  function safariDownArrowBug(view) {
      if (!safari || view.state.selection.$head.parentOffset > 0)
          return false;
      let { focusNode, focusOffset } = view.domSelectionRange();
      if (focusNode && focusNode.nodeType == 1 && focusOffset == 0 &&
          focusNode.firstChild && focusNode.firstChild.contentEditable == "false") {
          let child = focusNode.firstChild;
          switchEditable(view, child, "true");
          setTimeout(() => switchEditable(view, child, "false"), 20);
      }
      return false;
  }
  // A backdrop key mapping used to make sure we always suppress keys
  // that have a dangerous default effect, even if the commands they are
  // bound to return false, and to make sure that cursor-motion keys
  // find a cursor (as opposed to a node selection) when pressed. For
  // cursor-motion keys, the code in the handlers also takes care of
  // block selections.
  function getMods(event) {
      let result = "";
      if (event.ctrlKey)
          result += "c";
      if (event.metaKey)
          result += "m";
      if (event.altKey)
          result += "a";
      if (event.shiftKey)
          result += "s";
      return result;
  }
  function captureKeyDown(view, event) {
      let code = event.keyCode, mods = getMods(event);
      if (code == 8 || (mac$3 && code == 72 && mods == "c")) { // Backspace, Ctrl-h on Mac
          return stopNativeHorizontalDelete(view, -1) || skipIgnoredNodes(view, -1);
      }
      else if ((code == 46 && !event.shiftKey) || (mac$3 && code == 68 && mods == "c")) { // Delete, Ctrl-d on Mac
          return stopNativeHorizontalDelete(view, 1) || skipIgnoredNodes(view, 1);
      }
      else if (code == 13 || code == 27) { // Enter, Esc
          return true;
      }
      else if (code == 37 || (mac$3 && code == 66 && mods == "c")) { // Left arrow, Ctrl-b on Mac
          let dir = code == 37 ? (findDirection(view, view.state.selection.from) == "ltr" ? -1 : 1) : -1;
          return selectHorizontally(view, dir, mods) || skipIgnoredNodes(view, dir);
      }
      else if (code == 39 || (mac$3 && code == 70 && mods == "c")) { // Right arrow, Ctrl-f on Mac
          let dir = code == 39 ? (findDirection(view, view.state.selection.from) == "ltr" ? 1 : -1) : 1;
          return selectHorizontally(view, dir, mods) || skipIgnoredNodes(view, dir);
      }
      else if (code == 38 || (mac$3 && code == 80 && mods == "c")) { // Up arrow, Ctrl-p on Mac
          return selectVertically(view, -1, mods) || skipIgnoredNodes(view, -1);
      }
      else if (code == 40 || (mac$3 && code == 78 && mods == "c")) { // Down arrow, Ctrl-n on Mac
          return safariDownArrowBug(view) || selectVertically(view, 1, mods) || skipIgnoredNodes(view, 1);
      }
      else if (mods == (mac$3 ? "m" : "c") &&
          (code == 66 || code == 73 || code == 89 || code == 90)) { // Mod-[biyz]
          return true;
      }
      return false;
  }

  function serializeForClipboard(view, slice) {
      view.someProp("transformCopied", f => { slice = f(slice, view); });
      let context = [], { content, openStart, openEnd } = slice;
      while (openStart > 1 && openEnd > 1 && content.childCount == 1 && content.firstChild.childCount == 1) {
          openStart--;
          openEnd--;
          let node = content.firstChild;
          context.push(node.type.name, node.attrs != node.type.defaultAttrs ? node.attrs : null);
          content = node.content;
      }
      let serializer = view.someProp("clipboardSerializer") || DOMSerializer.fromSchema(view.state.schema);
      let doc = detachedDoc(), wrap = doc.createElement("div");
      wrap.appendChild(serializer.serializeFragment(content, { document: doc }));
      let firstChild = wrap.firstChild, needsWrap, wrappers = 0;
      while (firstChild && firstChild.nodeType == 1 && (needsWrap = wrapMap[firstChild.nodeName.toLowerCase()])) {
          for (let i = needsWrap.length - 1; i >= 0; i--) {
              let wrapper = doc.createElement(needsWrap[i]);
              while (wrap.firstChild)
                  wrapper.appendChild(wrap.firstChild);
              wrap.appendChild(wrapper);
              wrappers++;
          }
          firstChild = wrap.firstChild;
      }
      if (firstChild && firstChild.nodeType == 1)
          firstChild.setAttribute("data-pm-slice", `${openStart} ${openEnd}${wrappers ? ` -${wrappers}` : ""} ${JSON.stringify(context)}`);
      let text = view.someProp("clipboardTextSerializer", f => f(slice, view)) ||
          slice.content.textBetween(0, slice.content.size, "\n\n");
      return { dom: wrap, text, slice };
  }
  // Read a slice of content from the clipboard (or drop data).
  function parseFromClipboard(view, text, html, plainText, $context) {
      let inCode = $context.parent.type.spec.code;
      let dom, slice;
      if (!html && !text)
          return null;
      let asText = text && (plainText || inCode || !html);
      if (asText) {
          view.someProp("transformPastedText", f => { text = f(text, inCode || plainText, view); });
          if (inCode)
              return text ? new Slice(Fragment.from(view.state.schema.text(text.replace(/\r\n?/g, "\n"))), 0, 0) : Slice.empty;
          let parsed = view.someProp("clipboardTextParser", f => f(text, $context, plainText, view));
          if (parsed) {
              slice = parsed;
          }
          else {
              let marks = $context.marks();
              let { schema } = view.state, serializer = DOMSerializer.fromSchema(schema);
              dom = document.createElement("div");
              text.split(/(?:\r\n?|\n)+/).forEach(block => {
                  let p = dom.appendChild(document.createElement("p"));
                  if (block)
                      p.appendChild(serializer.serializeNode(schema.text(block, marks)));
              });
          }
      }
      else {
          view.someProp("transformPastedHTML", f => { html = f(html, view); });
          dom = readHTML(html);
          if (webkit)
              restoreReplacedSpaces(dom);
      }
      let contextNode = dom && dom.querySelector("[data-pm-slice]");
      let sliceData = contextNode && /^(\d+) (\d+)(?: -(\d+))? (.*)/.exec(contextNode.getAttribute("data-pm-slice") || "");
      if (sliceData && sliceData[3])
          for (let i = +sliceData[3]; i > 0; i--) {
              let child = dom.firstChild;
              while (child && child.nodeType != 1)
                  child = child.nextSibling;
              if (!child)
                  break;
              dom = child;
          }
      if (!slice) {
          let parser = view.someProp("clipboardParser") || view.someProp("domParser") || DOMParser$1.fromSchema(view.state.schema);
          slice = parser.parseSlice(dom, {
              preserveWhitespace: !!(asText || sliceData),
              context: $context,
              ruleFromNode(dom) {
                  if (dom.nodeName == "BR" && !dom.nextSibling &&
                      dom.parentNode && !inlineParents.test(dom.parentNode.nodeName))
                      return { ignore: true };
                  return null;
              }
          });
      }
      if (sliceData) {
          slice = addContext(closeSlice(slice, +sliceData[1], +sliceData[2]), sliceData[4]);
      }
      else { // HTML wasn't created by ProseMirror. Make sure top-level siblings are coherent
          slice = Slice.maxOpen(normalizeSiblings(slice.content, $context), true);
          if (slice.openStart || slice.openEnd) {
              let openStart = 0, openEnd = 0;
              for (let node = slice.content.firstChild; openStart < slice.openStart && !node.type.spec.isolating; openStart++, node = node.firstChild) { }
              for (let node = slice.content.lastChild; openEnd < slice.openEnd && !node.type.spec.isolating; openEnd++, node = node.lastChild) { }
              slice = closeSlice(slice, openStart, openEnd);
          }
      }
      view.someProp("transformPasted", f => { slice = f(slice, view); });
      return slice;
  }
  const inlineParents = /^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var)$/i;
  // Takes a slice parsed with parseSlice, which means there hasn't been
  // any content-expression checking done on the top nodes, tries to
  // find a parent node in the current context that might fit the nodes,
  // and if successful, rebuilds the slice so that it fits into that parent.
  //
  // This addresses the problem that Transform.replace expects a
  // coherent slice, and will fail to place a set of siblings that don't
  // fit anywhere in the schema.
  function normalizeSiblings(fragment, $context) {
      if (fragment.childCount < 2)
          return fragment;
      for (let d = $context.depth; d >= 0; d--) {
          let parent = $context.node(d);
          let match = parent.contentMatchAt($context.index(d));
          let lastWrap, result = [];
          fragment.forEach(node => {
              if (!result)
                  return;
              let wrap = match.findWrapping(node.type), inLast;
              if (!wrap)
                  return result = null;
              if (inLast = result.length && lastWrap.length && addToSibling(wrap, lastWrap, node, result[result.length - 1], 0)) {
                  result[result.length - 1] = inLast;
              }
              else {
                  if (result.length)
                      result[result.length - 1] = closeRight(result[result.length - 1], lastWrap.length);
                  let wrapped = withWrappers(node, wrap);
                  result.push(wrapped);
                  match = match.matchType(wrapped.type);
                  lastWrap = wrap;
              }
          });
          if (result)
              return Fragment.from(result);
      }
      return fragment;
  }
  function withWrappers(node, wrap, from = 0) {
      for (let i = wrap.length - 1; i >= from; i--)
          node = wrap[i].create(null, Fragment.from(node));
      return node;
  }
  // Used to group adjacent nodes wrapped in similar parents by
  // normalizeSiblings into the same parent node
  function addToSibling(wrap, lastWrap, node, sibling, depth) {
      if (depth < wrap.length && depth < lastWrap.length && wrap[depth] == lastWrap[depth]) {
          let inner = addToSibling(wrap, lastWrap, node, sibling.lastChild, depth + 1);
          if (inner)
              return sibling.copy(sibling.content.replaceChild(sibling.childCount - 1, inner));
          let match = sibling.contentMatchAt(sibling.childCount);
          if (match.matchType(depth == wrap.length - 1 ? node.type : wrap[depth + 1]))
              return sibling.copy(sibling.content.append(Fragment.from(withWrappers(node, wrap, depth + 1))));
      }
  }
  function closeRight(node, depth) {
      if (depth == 0)
          return node;
      let fragment = node.content.replaceChild(node.childCount - 1, closeRight(node.lastChild, depth - 1));
      let fill = node.contentMatchAt(node.childCount).fillBefore(Fragment.empty, true);
      return node.copy(fragment.append(fill));
  }
  function closeRange(fragment, side, from, to, depth, openEnd) {
      let node = side < 0 ? fragment.firstChild : fragment.lastChild, inner = node.content;
      if (fragment.childCount > 1)
          openEnd = 0;
      if (depth < to - 1)
          inner = closeRange(inner, side, from, to, depth + 1, openEnd);
      if (depth >= from)
          inner = side < 0 ? node.contentMatchAt(0).fillBefore(inner, openEnd <= depth).append(inner)
              : inner.append(node.contentMatchAt(node.childCount).fillBefore(Fragment.empty, true));
      return fragment.replaceChild(side < 0 ? 0 : fragment.childCount - 1, node.copy(inner));
  }
  function closeSlice(slice, openStart, openEnd) {
      if (openStart < slice.openStart)
          slice = new Slice(closeRange(slice.content, -1, openStart, slice.openStart, 0, slice.openEnd), openStart, slice.openEnd);
      if (openEnd < slice.openEnd)
          slice = new Slice(closeRange(slice.content, 1, openEnd, slice.openEnd, 0, 0), slice.openStart, openEnd);
      return slice;
  }
  // Trick from jQuery -- some elements must be wrapped in other
  // elements for innerHTML to work. I.e. if you do `div.innerHTML =
  // "<td>..</td>"` the table cells are ignored.
  const wrapMap = {
      thead: ["table"],
      tbody: ["table"],
      tfoot: ["table"],
      caption: ["table"],
      colgroup: ["table"],
      col: ["table", "colgroup"],
      tr: ["table", "tbody"],
      td: ["table", "tbody", "tr"],
      th: ["table", "tbody", "tr"]
  };
  let _detachedDoc = null;
  function detachedDoc() {
      return _detachedDoc || (_detachedDoc = document.implementation.createHTMLDocument("title"));
  }
  function readHTML(html) {
      let metas = /^(\s*<meta [^>]*>)*/.exec(html);
      if (metas)
          html = html.slice(metas[0].length);
      let elt = detachedDoc().createElement("div");
      let firstTag = /<([a-z][^>\s]+)/i.exec(html), wrap;
      if (wrap = firstTag && wrapMap[firstTag[1].toLowerCase()])
          html = wrap.map(n => "<" + n + ">").join("") + html + wrap.map(n => "</" + n + ">").reverse().join("");
      elt.innerHTML = html;
      if (wrap)
          for (let i = 0; i < wrap.length; i++)
              elt = elt.querySelector(wrap[i]) || elt;
      return elt;
  }
  // Webkit browsers do some hard-to-predict replacement of regular
  // spaces with non-breaking spaces when putting content on the
  // clipboard. This tries to convert such non-breaking spaces (which
  // will be wrapped in a plain span on Chrome, a span with class
  // Apple-converted-space on Safari) back to regular spaces.
  function restoreReplacedSpaces(dom) {
      let nodes = dom.querySelectorAll(chrome ? "span:not([class]):not([style])" : "span.Apple-converted-space");
      for (let i = 0; i < nodes.length; i++) {
          let node = nodes[i];
          if (node.childNodes.length == 1 && node.textContent == "\u00a0" && node.parentNode)
              node.parentNode.replaceChild(dom.ownerDocument.createTextNode(" "), node);
      }
  }
  function addContext(slice, context) {
      if (!slice.size)
          return slice;
      let schema = slice.content.firstChild.type.schema, array;
      try {
          array = JSON.parse(context);
      }
      catch (e) {
          return slice;
      }
      let { content, openStart, openEnd } = slice;
      for (let i = array.length - 2; i >= 0; i -= 2) {
          let type = schema.nodes[array[i]];
          if (!type || type.hasRequiredAttrs())
              break;
          content = Fragment.from(type.create(array[i + 1], content));
          openStart++;
          openEnd++;
      }
      return new Slice(content, openStart, openEnd);
  }

  // A collection of DOM events that occur within the editor, and callback functions
  // to invoke when the event fires.
  const handlers = {};
  const editHandlers = {};
  const passiveHandlers = { touchstart: true, touchmove: true };
  class InputState {
      constructor() {
          this.shiftKey = false;
          this.mouseDown = null;
          this.lastKeyCode = null;
          this.lastKeyCodeTime = 0;
          this.lastClick = { time: 0, x: 0, y: 0, type: "" };
          this.lastSelectionOrigin = null;
          this.lastSelectionTime = 0;
          this.lastIOSEnter = 0;
          this.lastIOSEnterFallbackTimeout = -1;
          this.lastFocus = 0;
          this.lastTouch = 0;
          this.lastAndroidDelete = 0;
          this.composing = false;
          this.compositionNode = null;
          this.composingTimeout = -1;
          this.compositionNodes = [];
          this.compositionEndedAt = -2e8;
          this.compositionID = 1;
          // Set to a composition ID when there are pending changes at compositionend
          this.compositionPendingChanges = 0;
          this.domChangeCount = 0;
          this.eventHandlers = Object.create(null);
          this.hideSelectionGuard = null;
      }
  }
  function initInput(view) {
      for (let event in handlers) {
          let handler = handlers[event];
          view.dom.addEventListener(event, view.input.eventHandlers[event] = (event) => {
              if (eventBelongsToView(view, event) && !runCustomHandler(view, event) &&
                  (view.editable || !(event.type in editHandlers)))
                  handler(view, event);
          }, passiveHandlers[event] ? { passive: true } : undefined);
      }
      // On Safari, for reasons beyond my understanding, adding an input
      // event handler makes an issue where the composition vanishes when
      // you press enter go away.
      if (safari)
          view.dom.addEventListener("input", () => null);
      ensureListeners(view);
  }
  function setSelectionOrigin(view, origin) {
      view.input.lastSelectionOrigin = origin;
      view.input.lastSelectionTime = Date.now();
  }
  function destroyInput(view) {
      view.domObserver.stop();
      for (let type in view.input.eventHandlers)
          view.dom.removeEventListener(type, view.input.eventHandlers[type]);
      clearTimeout(view.input.composingTimeout);
      clearTimeout(view.input.lastIOSEnterFallbackTimeout);
  }
  function ensureListeners(view) {
      view.someProp("handleDOMEvents", currentHandlers => {
          for (let type in currentHandlers)
              if (!view.input.eventHandlers[type])
                  view.dom.addEventListener(type, view.input.eventHandlers[type] = event => runCustomHandler(view, event));
      });
  }
  function runCustomHandler(view, event) {
      return view.someProp("handleDOMEvents", handlers => {
          let handler = handlers[event.type];
          return handler ? handler(view, event) || event.defaultPrevented : false;
      });
  }
  function eventBelongsToView(view, event) {
      if (!event.bubbles)
          return true;
      if (event.defaultPrevented)
          return false;
      for (let node = event.target; node != view.dom; node = node.parentNode)
          if (!node || node.nodeType == 11 ||
              (node.pmViewDesc && node.pmViewDesc.stopEvent(event)))
              return false;
      return true;
  }
  function dispatchEvent(view, event) {
      if (!runCustomHandler(view, event) && handlers[event.type] &&
          (view.editable || !(event.type in editHandlers)))
          handlers[event.type](view, event);
  }
  editHandlers.keydown = (view, _event) => {
      let event = _event;
      view.input.shiftKey = event.keyCode == 16 || event.shiftKey;
      if (inOrNearComposition(view, event))
          return;
      view.input.lastKeyCode = event.keyCode;
      view.input.lastKeyCodeTime = Date.now();
      // Suppress enter key events on Chrome Android, because those tend
      // to be part of a confused sequence of composition events fired,
      // and handling them eagerly tends to corrupt the input.
      if (android && chrome && event.keyCode == 13)
          return;
      if (event.keyCode != 229)
          view.domObserver.forceFlush();
      // On iOS, if we preventDefault enter key presses, the virtual
      // keyboard gets confused. So the hack here is to set a flag that
      // makes the DOM change code recognize that what just happens should
      // be replaced by whatever the Enter key handlers do.
      if (ios && event.keyCode == 13 && !event.ctrlKey && !event.altKey && !event.metaKey) {
          let now = Date.now();
          view.input.lastIOSEnter = now;
          view.input.lastIOSEnterFallbackTimeout = setTimeout(() => {
              if (view.input.lastIOSEnter == now) {
                  view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")));
                  view.input.lastIOSEnter = 0;
              }
          }, 200);
      }
      else if (view.someProp("handleKeyDown", f => f(view, event)) || captureKeyDown(view, event)) {
          event.preventDefault();
      }
      else {
          setSelectionOrigin(view, "key");
      }
  };
  editHandlers.keyup = (view, event) => {
      if (event.keyCode == 16)
          view.input.shiftKey = false;
  };
  editHandlers.keypress = (view, _event) => {
      let event = _event;
      if (inOrNearComposition(view, event) || !event.charCode ||
          event.ctrlKey && !event.altKey || mac$3 && event.metaKey)
          return;
      if (view.someProp("handleKeyPress", f => f(view, event))) {
          event.preventDefault();
          return;
      }
      let sel = view.state.selection;
      if (!(sel instanceof TextSelection) || !sel.$from.sameParent(sel.$to)) {
          let text = String.fromCharCode(event.charCode);
          if (!/[\r\n]/.test(text) && !view.someProp("handleTextInput", f => f(view, sel.$from.pos, sel.$to.pos, text)))
              view.dispatch(view.state.tr.insertText(text).scrollIntoView());
          event.preventDefault();
      }
  };
  function eventCoords(event) { return { left: event.clientX, top: event.clientY }; }
  function isNear(event, click) {
      let dx = click.x - event.clientX, dy = click.y - event.clientY;
      return dx * dx + dy * dy < 100;
  }
  function runHandlerOnContext(view, propName, pos, inside, event) {
      if (inside == -1)
          return false;
      let $pos = view.state.doc.resolve(inside);
      for (let i = $pos.depth + 1; i > 0; i--) {
          if (view.someProp(propName, f => i > $pos.depth ? f(view, pos, $pos.nodeAfter, $pos.before(i), event, true)
              : f(view, pos, $pos.node(i), $pos.before(i), event, false)))
              return true;
      }
      return false;
  }
  function updateSelection(view, selection, origin) {
      if (!view.focused)
          view.focus();
      let tr = view.state.tr.setSelection(selection);
      tr.setMeta("pointer", true);
      view.dispatch(tr);
  }
  function selectClickedLeaf(view, inside) {
      if (inside == -1)
          return false;
      let $pos = view.state.doc.resolve(inside), node = $pos.nodeAfter;
      if (node && node.isAtom && NodeSelection.isSelectable(node)) {
          updateSelection(view, new NodeSelection($pos));
          return true;
      }
      return false;
  }
  function selectClickedNode(view, inside) {
      if (inside == -1)
          return false;
      let sel = view.state.selection, selectedNode, selectAt;
      if (sel instanceof NodeSelection)
          selectedNode = sel.node;
      let $pos = view.state.doc.resolve(inside);
      for (let i = $pos.depth + 1; i > 0; i--) {
          let node = i > $pos.depth ? $pos.nodeAfter : $pos.node(i);
          if (NodeSelection.isSelectable(node)) {
              if (selectedNode && sel.$from.depth > 0 &&
                  i >= sel.$from.depth && $pos.before(sel.$from.depth + 1) == sel.$from.pos)
                  selectAt = $pos.before(sel.$from.depth);
              else
                  selectAt = $pos.before(i);
              break;
          }
      }
      if (selectAt != null) {
          updateSelection(view, NodeSelection.create(view.state.doc, selectAt));
          return true;
      }
      else {
          return false;
      }
  }
  function handleSingleClick(view, pos, inside, event, selectNode) {
      return runHandlerOnContext(view, "handleClickOn", pos, inside, event) ||
          view.someProp("handleClick", f => f(view, pos, event)) ||
          (selectNode ? selectClickedNode(view, inside) : selectClickedLeaf(view, inside));
  }
  function handleDoubleClick(view, pos, inside, event) {
      return runHandlerOnContext(view, "handleDoubleClickOn", pos, inside, event) ||
          view.someProp("handleDoubleClick", f => f(view, pos, event));
  }
  function handleTripleClick$1(view, pos, inside, event) {
      return runHandlerOnContext(view, "handleTripleClickOn", pos, inside, event) ||
          view.someProp("handleTripleClick", f => f(view, pos, event)) ||
          defaultTripleClick(view, inside, event);
  }
  function defaultTripleClick(view, inside, event) {
      if (event.button != 0)
          return false;
      let doc = view.state.doc;
      if (inside == -1) {
          if (doc.inlineContent) {
              updateSelection(view, TextSelection.create(doc, 0, doc.content.size));
              return true;
          }
          return false;
      }
      let $pos = doc.resolve(inside);
      for (let i = $pos.depth + 1; i > 0; i--) {
          let node = i > $pos.depth ? $pos.nodeAfter : $pos.node(i);
          let nodePos = $pos.before(i);
          if (node.inlineContent)
              updateSelection(view, TextSelection.create(doc, nodePos + 1, nodePos + 1 + node.content.size));
          else if (NodeSelection.isSelectable(node))
              updateSelection(view, NodeSelection.create(doc, nodePos));
          else
              continue;
          return true;
      }
  }
  function forceDOMFlush(view) {
      return endComposition(view);
  }
  const selectNodeModifier = mac$3 ? "metaKey" : "ctrlKey";
  handlers.mousedown = (view, _event) => {
      let event = _event;
      view.input.shiftKey = event.shiftKey;
      let flushed = forceDOMFlush(view);
      let now = Date.now(), type = "singleClick";
      if (now - view.input.lastClick.time < 500 && isNear(event, view.input.lastClick) && !event[selectNodeModifier]) {
          if (view.input.lastClick.type == "singleClick")
              type = "doubleClick";
          else if (view.input.lastClick.type == "doubleClick")
              type = "tripleClick";
      }
      view.input.lastClick = { time: now, x: event.clientX, y: event.clientY, type };
      let pos = view.posAtCoords(eventCoords(event));
      if (!pos)
          return;
      if (type == "singleClick") {
          if (view.input.mouseDown)
              view.input.mouseDown.done();
          view.input.mouseDown = new MouseDown(view, pos, event, !!flushed);
      }
      else if ((type == "doubleClick" ? handleDoubleClick : handleTripleClick$1)(view, pos.pos, pos.inside, event)) {
          event.preventDefault();
      }
      else {
          setSelectionOrigin(view, "pointer");
      }
  };
  class MouseDown {
      constructor(view, pos, event, flushed) {
          this.view = view;
          this.pos = pos;
          this.event = event;
          this.flushed = flushed;
          this.delayedSelectionSync = false;
          this.mightDrag = null;
          this.startDoc = view.state.doc;
          this.selectNode = !!event[selectNodeModifier];
          this.allowDefault = event.shiftKey;
          let targetNode, targetPos;
          if (pos.inside > -1) {
              targetNode = view.state.doc.nodeAt(pos.inside);
              targetPos = pos.inside;
          }
          else {
              let $pos = view.state.doc.resolve(pos.pos);
              targetNode = $pos.parent;
              targetPos = $pos.depth ? $pos.before() : 0;
          }
          const target = flushed ? null : event.target;
          const targetDesc = target ? view.docView.nearestDesc(target, true) : null;
          this.target = targetDesc ? targetDesc.dom : null;
          let { selection } = view.state;
          if (event.button == 0 &&
              targetNode.type.spec.draggable && targetNode.type.spec.selectable !== false ||
              selection instanceof NodeSelection && selection.from <= targetPos && selection.to > targetPos)
              this.mightDrag = {
                  node: targetNode,
                  pos: targetPos,
                  addAttr: !!(this.target && !this.target.draggable),
                  setUneditable: !!(this.target && gecko && !this.target.hasAttribute("contentEditable"))
              };
          if (this.target && this.mightDrag && (this.mightDrag.addAttr || this.mightDrag.setUneditable)) {
              this.view.domObserver.stop();
              if (this.mightDrag.addAttr)
                  this.target.draggable = true;
              if (this.mightDrag.setUneditable)
                  setTimeout(() => {
                      if (this.view.input.mouseDown == this)
                          this.target.setAttribute("contentEditable", "false");
                  }, 20);
              this.view.domObserver.start();
          }
          view.root.addEventListener("mouseup", this.up = this.up.bind(this));
          view.root.addEventListener("mousemove", this.move = this.move.bind(this));
          setSelectionOrigin(view, "pointer");
      }
      done() {
          this.view.root.removeEventListener("mouseup", this.up);
          this.view.root.removeEventListener("mousemove", this.move);
          if (this.mightDrag && this.target) {
              this.view.domObserver.stop();
              if (this.mightDrag.addAttr)
                  this.target.removeAttribute("draggable");
              if (this.mightDrag.setUneditable)
                  this.target.removeAttribute("contentEditable");
              this.view.domObserver.start();
          }
          if (this.delayedSelectionSync)
              setTimeout(() => selectionToDOM(this.view));
          this.view.input.mouseDown = null;
      }
      up(event) {
          this.done();
          if (!this.view.dom.contains(event.target))
              return;
          let pos = this.pos;
          if (this.view.state.doc != this.startDoc)
              pos = this.view.posAtCoords(eventCoords(event));
          this.updateAllowDefault(event);
          if (this.allowDefault || !pos) {
              setSelectionOrigin(this.view, "pointer");
          }
          else if (handleSingleClick(this.view, pos.pos, pos.inside, event, this.selectNode)) {
              event.preventDefault();
          }
          else if (event.button == 0 &&
              (this.flushed ||
                  // Safari ignores clicks on draggable elements
                  (safari && this.mightDrag && !this.mightDrag.node.isAtom) ||
                  // Chrome will sometimes treat a node selection as a
                  // cursor, but still report that the node is selected
                  // when asked through getSelection. You'll then get a
                  // situation where clicking at the point where that
                  // (hidden) cursor is doesn't change the selection, and
                  // thus doesn't get a reaction from ProseMirror. This
                  // works around that.
                  (chrome && !this.view.state.selection.visible &&
                      Math.min(Math.abs(pos.pos - this.view.state.selection.from), Math.abs(pos.pos - this.view.state.selection.to)) <= 2))) {
              updateSelection(this.view, Selection.near(this.view.state.doc.resolve(pos.pos)));
              event.preventDefault();
          }
          else {
              setSelectionOrigin(this.view, "pointer");
          }
      }
      move(event) {
          this.updateAllowDefault(event);
          setSelectionOrigin(this.view, "pointer");
          if (event.buttons == 0)
              this.done();
      }
      updateAllowDefault(event) {
          if (!this.allowDefault && (Math.abs(this.event.x - event.clientX) > 4 ||
              Math.abs(this.event.y - event.clientY) > 4))
              this.allowDefault = true;
      }
  }
  handlers.touchstart = view => {
      view.input.lastTouch = Date.now();
      forceDOMFlush(view);
      setSelectionOrigin(view, "pointer");
  };
  handlers.touchmove = view => {
      view.input.lastTouch = Date.now();
      setSelectionOrigin(view, "pointer");
  };
  handlers.contextmenu = view => forceDOMFlush(view);
  function inOrNearComposition(view, event) {
      if (view.composing)
          return true;
      // See https://www.stum.de/2016/06/24/handling-ime-events-in-javascript/.
      // On Japanese input method editors (IMEs), the Enter key is used to confirm character
      // selection. On Safari, when Enter is pressed, compositionend and keydown events are
      // emitted. The keydown event triggers newline insertion, which we don't want.
      // This method returns true if the keydown event should be ignored.
      // We only ignore it once, as pressing Enter a second time *should* insert a newline.
      // Furthermore, the keydown event timestamp must be close to the compositionEndedAt timestamp.
      // This guards against the case where compositionend is triggered without the keyboard
      // (e.g. character confirmation may be done with the mouse), and keydown is triggered
      // afterwards- we wouldn't want to ignore the keydown event in this case.
      if (safari && Math.abs(event.timeStamp - view.input.compositionEndedAt) < 500) {
          view.input.compositionEndedAt = -2e8;
          return true;
      }
      return false;
  }
  // Drop active composition after 5 seconds of inactivity on Android
  const timeoutComposition = android ? 5000 : -1;
  editHandlers.compositionstart = editHandlers.compositionupdate = view => {
      if (!view.composing) {
          view.domObserver.flush();
          let { state } = view, $pos = state.selection.$from;
          if (state.selection.empty &&
              (state.storedMarks ||
                  (!$pos.textOffset && $pos.parentOffset && $pos.nodeBefore.marks.some(m => m.type.spec.inclusive === false)))) {
              // Need to wrap the cursor in mark nodes different from the ones in the DOM context
              view.markCursor = view.state.storedMarks || $pos.marks();
              endComposition(view, true);
              view.markCursor = null;
          }
          else {
              endComposition(view);
              // In firefox, if the cursor is after but outside a marked node,
              // the inserted text won't inherit the marks. So this moves it
              // inside if necessary.
              if (gecko && state.selection.empty && $pos.parentOffset && !$pos.textOffset && $pos.nodeBefore.marks.length) {
                  let sel = view.domSelectionRange();
                  for (let node = sel.focusNode, offset = sel.focusOffset; node && node.nodeType == 1 && offset != 0;) {
                      let before = offset < 0 ? node.lastChild : node.childNodes[offset - 1];
                      if (!before)
                          break;
                      if (before.nodeType == 3) {
                          view.domSelection().collapse(before, before.nodeValue.length);
                          break;
                      }
                      else {
                          node = before;
                          offset = -1;
                      }
                  }
              }
          }
          view.input.composing = true;
      }
      scheduleComposeEnd(view, timeoutComposition);
  };
  editHandlers.compositionend = (view, event) => {
      if (view.composing) {
          view.input.composing = false;
          view.input.compositionEndedAt = event.timeStamp;
          view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0;
          view.input.compositionNode = null;
          if (view.input.compositionPendingChanges)
              Promise.resolve().then(() => view.domObserver.flush());
          view.input.compositionID++;
          scheduleComposeEnd(view, 20);
      }
  };
  function scheduleComposeEnd(view, delay) {
      clearTimeout(view.input.composingTimeout);
      if (delay > -1)
          view.input.composingTimeout = setTimeout(() => endComposition(view), delay);
  }
  function clearComposition(view) {
      if (view.composing) {
          view.input.composing = false;
          view.input.compositionEndedAt = timestampFromCustomEvent();
      }
      while (view.input.compositionNodes.length > 0)
          view.input.compositionNodes.pop().markParentsDirty();
  }
  function findCompositionNode(view) {
      let sel = view.domSelectionRange();
      if (!sel.focusNode)
          return null;
      let textBefore = textNodeBefore$1(sel.focusNode, sel.focusOffset);
      let textAfter = textNodeAfter$1(sel.focusNode, sel.focusOffset);
      if (textBefore && textAfter && textBefore != textAfter) {
          let descAfter = textAfter.pmViewDesc;
          if (!descAfter || !descAfter.isText(textAfter.nodeValue)) {
              return textAfter;
          }
          else if (view.input.compositionNode == textAfter) {
              let descBefore = textBefore.pmViewDesc;
              if (!(!descBefore || !descBefore.isText(textBefore.nodeValue)))
                  return textAfter;
          }
      }
      return textBefore || textAfter;
  }
  function timestampFromCustomEvent() {
      let event = document.createEvent("Event");
      event.initEvent("event", true, true);
      return event.timeStamp;
  }
  /**
  @internal
  */
  function endComposition(view, forceUpdate = false) {
      if (android && view.domObserver.flushingSoon >= 0)
          return;
      view.domObserver.forceFlush();
      clearComposition(view);
      if (forceUpdate || view.docView && view.docView.dirty) {
          let sel = selectionFromDOM(view);
          if (sel && !sel.eq(view.state.selection))
              view.dispatch(view.state.tr.setSelection(sel));
          else
              view.updateState(view.state);
          return true;
      }
      return false;
  }
  function captureCopy(view, dom) {
      // The extra wrapper is somehow necessary on IE/Edge to prevent the
      // content from being mangled when it is put onto the clipboard
      if (!view.dom.parentNode)
          return;
      let wrap = view.dom.parentNode.appendChild(document.createElement("div"));
      wrap.appendChild(dom);
      wrap.style.cssText = "position: fixed; left: -10000px; top: 10px";
      let sel = getSelection(), range = document.createRange();
      range.selectNodeContents(dom);
      // Done because IE will fire a selectionchange moving the selection
      // to its start when removeAllRanges is called and the editor still
      // has focus (which will mess up the editor's selection state).
      view.dom.blur();
      sel.removeAllRanges();
      sel.addRange(range);
      setTimeout(() => {
          if (wrap.parentNode)
              wrap.parentNode.removeChild(wrap);
          view.focus();
      }, 50);
  }
  // This is very crude, but unfortunately both these browsers _pretend_
  // that they have a clipboard API—all the objects and methods are
  // there, they just don't work, and they are hard to test.
  const brokenClipboardAPI = (ie$1 && ie_version < 15) ||
      (ios && webkit_version < 604);
  handlers.copy = editHandlers.cut = (view, _event) => {
      let event = _event;
      let sel = view.state.selection, cut = event.type == "cut";
      if (sel.empty)
          return;
      // IE and Edge's clipboard interface is completely broken
      let data = brokenClipboardAPI ? null : event.clipboardData;
      let slice = sel.content(), { dom, text } = serializeForClipboard(view, slice);
      if (data) {
          event.preventDefault();
          data.clearData();
          data.setData("text/html", dom.innerHTML);
          data.setData("text/plain", text);
      }
      else {
          captureCopy(view, dom);
      }
      if (cut)
          view.dispatch(view.state.tr.deleteSelection().scrollIntoView().setMeta("uiEvent", "cut"));
  };
  function sliceSingleNode(slice) {
      return slice.openStart == 0 && slice.openEnd == 0 && slice.content.childCount == 1 ? slice.content.firstChild : null;
  }
  function capturePaste(view, event) {
      if (!view.dom.parentNode)
          return;
      let plainText = view.input.shiftKey || view.state.selection.$from.parent.type.spec.code;
      let target = view.dom.parentNode.appendChild(document.createElement(plainText ? "textarea" : "div"));
      if (!plainText)
          target.contentEditable = "true";
      target.style.cssText = "position: fixed; left: -10000px; top: 10px";
      target.focus();
      let plain = view.input.shiftKey && view.input.lastKeyCode != 45;
      setTimeout(() => {
          view.focus();
          if (target.parentNode)
              target.parentNode.removeChild(target);
          if (plainText)
              doPaste(view, target.value, null, plain, event);
          else
              doPaste(view, target.textContent, target.innerHTML, plain, event);
      }, 50);
  }
  function doPaste(view, text, html, preferPlain, event) {
      let slice = parseFromClipboard(view, text, html, preferPlain, view.state.selection.$from);
      if (view.someProp("handlePaste", f => f(view, event, slice || Slice.empty)))
          return true;
      if (!slice)
          return false;
      let singleNode = sliceSingleNode(slice);
      let tr = singleNode
          ? view.state.tr.replaceSelectionWith(singleNode, preferPlain)
          : view.state.tr.replaceSelection(slice);
      view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"));
      return true;
  }
  function getText(clipboardData) {
      let text = clipboardData.getData("text/plain") || clipboardData.getData("Text");
      if (text)
          return text;
      let uris = clipboardData.getData("text/uri-list");
      return uris ? uris.replace(/\r?\n/g, " ") : "";
  }
  editHandlers.paste = (view, _event) => {
      let event = _event;
      // Handling paste from JavaScript during composition is very poorly
      // handled by browsers, so as a dodgy but preferable kludge, we just
      // let the browser do its native thing there, except on Android,
      // where the editor is almost always composing.
      if (view.composing && !android)
          return;
      let data = brokenClipboardAPI ? null : event.clipboardData;
      let plain = view.input.shiftKey && view.input.lastKeyCode != 45;
      if (data && doPaste(view, getText(data), data.getData("text/html"), plain, event))
          event.preventDefault();
      else
          capturePaste(view, event);
  };
  class Dragging {
      constructor(slice, move, node) {
          this.slice = slice;
          this.move = move;
          this.node = node;
      }
  }
  const dragCopyModifier = mac$3 ? "altKey" : "ctrlKey";
  handlers.dragstart = (view, _event) => {
      let event = _event;
      let mouseDown = view.input.mouseDown;
      if (mouseDown)
          mouseDown.done();
      if (!event.dataTransfer)
          return;
      let sel = view.state.selection;
      let pos = sel.empty ? null : view.posAtCoords(eventCoords(event));
      let node;
      if (pos && pos.pos >= sel.from && pos.pos <= (sel instanceof NodeSelection ? sel.to - 1 : sel.to)) ;
      else if (mouseDown && mouseDown.mightDrag) {
          node = NodeSelection.create(view.state.doc, mouseDown.mightDrag.pos);
      }
      else if (event.target && event.target.nodeType == 1) {
          let desc = view.docView.nearestDesc(event.target, true);
          if (desc && desc.node.type.spec.draggable && desc != view.docView)
              node = NodeSelection.create(view.state.doc, desc.posBefore);
      }
      let draggedSlice = (node || view.state.selection).content();
      let { dom, text, slice } = serializeForClipboard(view, draggedSlice);
      event.dataTransfer.clearData();
      event.dataTransfer.setData(brokenClipboardAPI ? "Text" : "text/html", dom.innerHTML);
      // See https://github.com/ProseMirror/prosemirror/issues/1156
      event.dataTransfer.effectAllowed = "copyMove";
      if (!brokenClipboardAPI)
          event.dataTransfer.setData("text/plain", text);
      view.dragging = new Dragging(slice, !event[dragCopyModifier], node);
  };
  handlers.dragend = view => {
      let dragging = view.dragging;
      window.setTimeout(() => {
          if (view.dragging == dragging)
              view.dragging = null;
      }, 50);
  };
  editHandlers.dragover = editHandlers.dragenter = (_, e) => e.preventDefault();
  editHandlers.drop = (view, _event) => {
      let event = _event;
      let dragging = view.dragging;
      view.dragging = null;
      if (!event.dataTransfer)
          return;
      let eventPos = view.posAtCoords(eventCoords(event));
      if (!eventPos)
          return;
      let $mouse = view.state.doc.resolve(eventPos.pos);
      let slice = dragging && dragging.slice;
      if (slice) {
          view.someProp("transformPasted", f => { slice = f(slice, view); });
      }
      else {
          slice = parseFromClipboard(view, getText(event.dataTransfer), brokenClipboardAPI ? null : event.dataTransfer.getData("text/html"), false, $mouse);
      }
      let move = !!(dragging && !event[dragCopyModifier]);
      if (view.someProp("handleDrop", f => f(view, event, slice || Slice.empty, move))) {
          event.preventDefault();
          return;
      }
      if (!slice)
          return;
      event.preventDefault();
      let insertPos = slice ? dropPoint(view.state.doc, $mouse.pos, slice) : $mouse.pos;
      if (insertPos == null)
          insertPos = $mouse.pos;
      let tr = view.state.tr;
      if (move) {
          let { node } = dragging;
          if (node)
              node.replace(tr);
          else
              tr.deleteSelection();
      }
      let pos = tr.mapping.map(insertPos);
      let isNode = slice.openStart == 0 && slice.openEnd == 0 && slice.content.childCount == 1;
      let beforeInsert = tr.doc;
      if (isNode)
          tr.replaceRangeWith(pos, pos, slice.content.firstChild);
      else
          tr.replaceRange(pos, pos, slice);
      if (tr.doc.eq(beforeInsert))
          return;
      let $pos = tr.doc.resolve(pos);
      if (isNode && NodeSelection.isSelectable(slice.content.firstChild) &&
          $pos.nodeAfter && $pos.nodeAfter.sameMarkup(slice.content.firstChild)) {
          tr.setSelection(new NodeSelection($pos));
      }
      else {
          let end = tr.mapping.map(insertPos);
          tr.mapping.maps[tr.mapping.maps.length - 1].forEach((_from, _to, _newFrom, newTo) => end = newTo);
          tr.setSelection(selectionBetween(view, $pos, tr.doc.resolve(end)));
      }
      view.focus();
      view.dispatch(tr.setMeta("uiEvent", "drop"));
  };
  handlers.focus = view => {
      view.input.lastFocus = Date.now();
      if (!view.focused) {
          view.domObserver.stop();
          view.dom.classList.add("ProseMirror-focused");
          view.domObserver.start();
          view.focused = true;
          setTimeout(() => {
              if (view.docView && view.hasFocus() && !view.domObserver.currentSelection.eq(view.domSelectionRange()))
                  selectionToDOM(view);
          }, 20);
      }
  };
  handlers.blur = (view, _event) => {
      let event = _event;
      if (view.focused) {
          view.domObserver.stop();
          view.dom.classList.remove("ProseMirror-focused");
          view.domObserver.start();
          if (event.relatedTarget && view.dom.contains(event.relatedTarget))
              view.domObserver.currentSelection.clear();
          view.focused = false;
      }
  };
  handlers.beforeinput = (view, _event) => {
      let event = _event;
      // We should probably do more with beforeinput events, but support
      // is so spotty that I'm still waiting to see where they are going.
      // Very specific hack to deal with backspace sometimes failing on
      // Chrome Android when after an uneditable node.
      if (chrome && android && event.inputType == "deleteContentBackward") {
          view.domObserver.flushSoon();
          let { domChangeCount } = view.input;
          setTimeout(() => {
              if (view.input.domChangeCount != domChangeCount)
                  return; // Event already had some effect
              // This bug tends to close the virtual keyboard, so we refocus
              view.dom.blur();
              view.focus();
              if (view.someProp("handleKeyDown", f => f(view, keyEvent(8, "Backspace"))))
                  return;
              let { $cursor } = view.state.selection;
              // Crude approximation of backspace behavior when no command handled it
              if ($cursor && $cursor.pos > 0)
                  view.dispatch(view.state.tr.delete($cursor.pos - 1, $cursor.pos).scrollIntoView());
          }, 50);
      }
  };
  // Make sure all handlers get registered
  for (let prop in editHandlers)
      handlers[prop] = editHandlers[prop];

  function compareObjs(a, b) {
      if (a == b)
          return true;
      for (let p in a)
          if (a[p] !== b[p])
              return false;
      for (let p in b)
          if (!(p in a))
              return false;
      return true;
  }
  class WidgetType {
      constructor(toDOM, spec) {
          this.toDOM = toDOM;
          this.spec = spec || noSpec;
          this.side = this.spec.side || 0;
      }
      map(mapping, span, offset, oldOffset) {
          let { pos, deleted } = mapping.mapResult(span.from + oldOffset, this.side < 0 ? -1 : 1);
          return deleted ? null : new Decoration(pos - offset, pos - offset, this);
      }
      valid() { return true; }
      eq(other) {
          return this == other ||
              (other instanceof WidgetType &&
                  (this.spec.key && this.spec.key == other.spec.key ||
                      this.toDOM == other.toDOM && compareObjs(this.spec, other.spec)));
      }
      destroy(node) {
          if (this.spec.destroy)
              this.spec.destroy(node);
      }
  }
  class InlineType {
      constructor(attrs, spec) {
          this.attrs = attrs;
          this.spec = spec || noSpec;
      }
      map(mapping, span, offset, oldOffset) {
          let from = mapping.map(span.from + oldOffset, this.spec.inclusiveStart ? -1 : 1) - offset;
          let to = mapping.map(span.to + oldOffset, this.spec.inclusiveEnd ? 1 : -1) - offset;
          return from >= to ? null : new Decoration(from, to, this);
      }
      valid(_, span) { return span.from < span.to; }
      eq(other) {
          return this == other ||
              (other instanceof InlineType && compareObjs(this.attrs, other.attrs) &&
                  compareObjs(this.spec, other.spec));
      }
      static is(span) { return span.type instanceof InlineType; }
      destroy() { }
  }
  class NodeType {
      constructor(attrs, spec) {
          this.attrs = attrs;
          this.spec = spec || noSpec;
      }
      map(mapping, span, offset, oldOffset) {
          let from = mapping.mapResult(span.from + oldOffset, 1);
          if (from.deleted)
              return null;
          let to = mapping.mapResult(span.to + oldOffset, -1);
          if (to.deleted || to.pos <= from.pos)
              return null;
          return new Decoration(from.pos - offset, to.pos - offset, this);
      }
      valid(node, span) {
          let { index, offset } = node.content.findIndex(span.from), child;
          return offset == span.from && !(child = node.child(index)).isText && offset + child.nodeSize == span.to;
      }
      eq(other) {
          return this == other ||
              (other instanceof NodeType && compareObjs(this.attrs, other.attrs) &&
                  compareObjs(this.spec, other.spec));
      }
      destroy() { }
  }
  /**
  Decoration objects can be provided to the view through the
  [`decorations` prop](https://prosemirror.net/docs/ref/#view.EditorProps.decorations). They come in
  several variants—see the static members of this class for details.
  */
  class Decoration {
      /**
      @internal
      */
      constructor(
      /**
      The start position of the decoration.
      */
      from, 
      /**
      The end position. Will be the same as `from` for [widget
      decorations](https://prosemirror.net/docs/ref/#view.Decoration^widget).
      */
      to, 
      /**
      @internal
      */
      type) {
          this.from = from;
          this.to = to;
          this.type = type;
      }
      /**
      @internal
      */
      copy(from, to) {
          return new Decoration(from, to, this.type);
      }
      /**
      @internal
      */
      eq(other, offset = 0) {
          return this.type.eq(other.type) && this.from + offset == other.from && this.to + offset == other.to;
      }
      /**
      @internal
      */
      map(mapping, offset, oldOffset) {
          return this.type.map(mapping, this, offset, oldOffset);
      }
      /**
      Creates a widget decoration, which is a DOM node that's shown in
      the document at the given position. It is recommended that you
      delay rendering the widget by passing a function that will be
      called when the widget is actually drawn in a view, but you can
      also directly pass a DOM node. `getPos` can be used to find the
      widget's current document position.
      */
      static widget(pos, toDOM, spec) {
          return new Decoration(pos, pos, new WidgetType(toDOM, spec));
      }
      /**
      Creates an inline decoration, which adds the given attributes to
      each inline node between `from` and `to`.
      */
      static inline(from, to, attrs, spec) {
          return new Decoration(from, to, new InlineType(attrs, spec));
      }
      /**
      Creates a node decoration. `from` and `to` should point precisely
      before and after a node in the document. That node, and only that
      node, will receive the given attributes.
      */
      static node(from, to, attrs, spec) {
          return new Decoration(from, to, new NodeType(attrs, spec));
      }
      /**
      The spec provided when creating this decoration. Can be useful
      if you've stored extra information in that object.
      */
      get spec() { return this.type.spec; }
      /**
      @internal
      */
      get inline() { return this.type instanceof InlineType; }
      /**
      @internal
      */
      get widget() { return this.type instanceof WidgetType; }
  }
  const none = [], noSpec = {};
  /**
  A collection of [decorations](https://prosemirror.net/docs/ref/#view.Decoration), organized in such
  a way that the drawing algorithm can efficiently use and compare
  them. This is a persistent data structure—it is not modified,
  updates create a new value.
  */
  class DecorationSet {
      /**
      @internal
      */
      constructor(local, children) {
          this.local = local.length ? local : none;
          this.children = children.length ? children : none;
      }
      /**
      Create a set of decorations, using the structure of the given
      document. This will consume (modify) the `decorations` array, so
      you must make a copy if you want need to preserve that.
      */
      static create(doc, decorations) {
          return decorations.length ? buildTree(decorations, doc, 0, noSpec) : empty;
      }
      /**
      Find all decorations in this set which touch the given range
      (including decorations that start or end directly at the
      boundaries) and match the given predicate on their spec. When
      `start` and `end` are omitted, all decorations in the set are
      considered. When `predicate` isn't given, all decorations are
      assumed to match.
      */
      find(start, end, predicate) {
          let result = [];
          this.findInner(start == null ? 0 : start, end == null ? 1e9 : end, result, 0, predicate);
          return result;
      }
      findInner(start, end, result, offset, predicate) {
          for (let i = 0; i < this.local.length; i++) {
              let span = this.local[i];
              if (span.from <= end && span.to >= start && (!predicate || predicate(span.spec)))
                  result.push(span.copy(span.from + offset, span.to + offset));
          }
          for (let i = 0; i < this.children.length; i += 3) {
              if (this.children[i] < end && this.children[i + 1] > start) {
                  let childOff = this.children[i] + 1;
                  this.children[i + 2].findInner(start - childOff, end - childOff, result, offset + childOff, predicate);
              }
          }
      }
      /**
      Map the set of decorations in response to a change in the
      document.
      */
      map(mapping, doc, options) {
          if (this == empty || mapping.maps.length == 0)
              return this;
          return this.mapInner(mapping, doc, 0, 0, options || noSpec);
      }
      /**
      @internal
      */
      mapInner(mapping, node, offset, oldOffset, options) {
          let newLocal;
          for (let i = 0; i < this.local.length; i++) {
              let mapped = this.local[i].map(mapping, offset, oldOffset);
              if (mapped && mapped.type.valid(node, mapped))
                  (newLocal || (newLocal = [])).push(mapped);
              else if (options.onRemove)
                  options.onRemove(this.local[i].spec);
          }
          if (this.children.length)
              return mapChildren(this.children, newLocal || [], mapping, node, offset, oldOffset, options);
          else
              return newLocal ? new DecorationSet(newLocal.sort(byPos), none) : empty;
      }
      /**
      Add the given array of decorations to the ones in the set,
      producing a new set. Consumes the `decorations` array. Needs
      access to the current document to create the appropriate tree
      structure.
      */
      add(doc, decorations) {
          if (!decorations.length)
              return this;
          if (this == empty)
              return DecorationSet.create(doc, decorations);
          return this.addInner(doc, decorations, 0);
      }
      addInner(doc, decorations, offset) {
          let children, childIndex = 0;
          doc.forEach((childNode, childOffset) => {
              let baseOffset = childOffset + offset, found;
              if (!(found = takeSpansForNode(decorations, childNode, baseOffset)))
                  return;
              if (!children)
                  children = this.children.slice();
              while (childIndex < children.length && children[childIndex] < childOffset)
                  childIndex += 3;
              if (children[childIndex] == childOffset)
                  children[childIndex + 2] = children[childIndex + 2].addInner(childNode, found, baseOffset + 1);
              else
                  children.splice(childIndex, 0, childOffset, childOffset + childNode.nodeSize, buildTree(found, childNode, baseOffset + 1, noSpec));
              childIndex += 3;
          });
          let local = moveSpans(childIndex ? withoutNulls(decorations) : decorations, -offset);
          for (let i = 0; i < local.length; i++)
              if (!local[i].type.valid(doc, local[i]))
                  local.splice(i--, 1);
          return new DecorationSet(local.length ? this.local.concat(local).sort(byPos) : this.local, children || this.children);
      }
      /**
      Create a new set that contains the decorations in this set, minus
      the ones in the given array.
      */
      remove(decorations) {
          if (decorations.length == 0 || this == empty)
              return this;
          return this.removeInner(decorations, 0);
      }
      removeInner(decorations, offset) {
          let children = this.children, local = this.local;
          for (let i = 0; i < children.length; i += 3) {
              let found;
              let from = children[i] + offset, to = children[i + 1] + offset;
              for (let j = 0, span; j < decorations.length; j++)
                  if (span = decorations[j]) {
                      if (span.from > from && span.to < to) {
                          decorations[j] = null;
                          (found || (found = [])).push(span);
                      }
                  }
              if (!found)
                  continue;
              if (children == this.children)
                  children = this.children.slice();
              let removed = children[i + 2].removeInner(found, from + 1);
              if (removed != empty) {
                  children[i + 2] = removed;
              }
              else {
                  children.splice(i, 3);
                  i -= 3;
              }
          }
          if (local.length)
              for (let i = 0, span; i < decorations.length; i++)
                  if (span = decorations[i]) {
                      for (let j = 0; j < local.length; j++)
                          if (local[j].eq(span, offset)) {
                              if (local == this.local)
                                  local = this.local.slice();
                              local.splice(j--, 1);
                          }
                  }
          if (children == this.children && local == this.local)
              return this;
          return local.length || children.length ? new DecorationSet(local, children) : empty;
      }
      forChild(offset, node) {
          if (this == empty)
              return this;
          if (node.isLeaf)
              return DecorationSet.empty;
          let child, local;
          for (let i = 0; i < this.children.length; i += 3)
              if (this.children[i] >= offset) {
                  if (this.children[i] == offset)
                      child = this.children[i + 2];
                  break;
              }
          let start = offset + 1, end = start + node.content.size;
          for (let i = 0; i < this.local.length; i++) {
              let dec = this.local[i];
              if (dec.from < end && dec.to > start && (dec.type instanceof InlineType)) {
                  let from = Math.max(start, dec.from) - start, to = Math.min(end, dec.to) - start;
                  if (from < to)
                      (local || (local = [])).push(dec.copy(from, to));
              }
          }
          if (local) {
              let localSet = new DecorationSet(local.sort(byPos), none);
              return child ? new DecorationGroup([localSet, child]) : localSet;
          }
          return child || empty;
      }
      /**
      @internal
      */
      eq(other) {
          if (this == other)
              return true;
          if (!(other instanceof DecorationSet) ||
              this.local.length != other.local.length ||
              this.children.length != other.children.length)
              return false;
          for (let i = 0; i < this.local.length; i++)
              if (!this.local[i].eq(other.local[i]))
                  return false;
          for (let i = 0; i < this.children.length; i += 3)
              if (this.children[i] != other.children[i] ||
                  this.children[i + 1] != other.children[i + 1] ||
                  !this.children[i + 2].eq(other.children[i + 2]))
                  return false;
          return true;
      }
      /**
      @internal
      */
      locals(node) {
          return removeOverlap(this.localsInner(node));
      }
      /**
      @internal
      */
      localsInner(node) {
          if (this == empty)
              return none;
          if (node.inlineContent || !this.local.some(InlineType.is))
              return this.local;
          let result = [];
          for (let i = 0; i < this.local.length; i++) {
              if (!(this.local[i].type instanceof InlineType))
                  result.push(this.local[i]);
          }
          return result;
      }
  }
  /**
  The empty set of decorations.
  */
  DecorationSet.empty = new DecorationSet([], []);
  /**
  @internal
  */
  DecorationSet.removeOverlap = removeOverlap;
  const empty = DecorationSet.empty;
  // An abstraction that allows the code dealing with decorations to
  // treat multiple DecorationSet objects as if it were a single object
  // with (a subset of) the same interface.
  class DecorationGroup {
      constructor(members) {
          this.members = members;
      }
      map(mapping, doc) {
          const mappedDecos = this.members.map(member => member.map(mapping, doc, noSpec));
          return DecorationGroup.from(mappedDecos);
      }
      forChild(offset, child) {
          if (child.isLeaf)
              return DecorationSet.empty;
          let found = [];
          for (let i = 0; i < this.members.length; i++) {
              let result = this.members[i].forChild(offset, child);
              if (result == empty)
                  continue;
              if (result instanceof DecorationGroup)
                  found = found.concat(result.members);
              else
                  found.push(result);
          }
          return DecorationGroup.from(found);
      }
      eq(other) {
          if (!(other instanceof DecorationGroup) ||
              other.members.length != this.members.length)
              return false;
          for (let i = 0; i < this.members.length; i++)
              if (!this.members[i].eq(other.members[i]))
                  return false;
          return true;
      }
      locals(node) {
          let result, sorted = true;
          for (let i = 0; i < this.members.length; i++) {
              let locals = this.members[i].localsInner(node);
              if (!locals.length)
                  continue;
              if (!result) {
                  result = locals;
              }
              else {
                  if (sorted) {
                      result = result.slice();
                      sorted = false;
                  }
                  for (let j = 0; j < locals.length; j++)
                      result.push(locals[j]);
              }
          }
          return result ? removeOverlap(sorted ? result : result.sort(byPos)) : none;
      }
      // Create a group for the given array of decoration sets, or return
      // a single set when possible.
      static from(members) {
          switch (members.length) {
              case 0: return empty;
              case 1: return members[0];
              default: return new DecorationGroup(members.every(m => m instanceof DecorationSet) ? members :
                  members.reduce((r, m) => r.concat(m instanceof DecorationSet ? m : m.members), []));
          }
      }
  }
  function mapChildren(oldChildren, newLocal, mapping, node, offset, oldOffset, options) {
      let children = oldChildren.slice();
      // Mark the children that are directly touched by changes, and
      // move those that are after the changes.
      for (let i = 0, baseOffset = oldOffset; i < mapping.maps.length; i++) {
          let moved = 0;
          mapping.maps[i].forEach((oldStart, oldEnd, newStart, newEnd) => {
              let dSize = (newEnd - newStart) - (oldEnd - oldStart);
              for (let i = 0; i < children.length; i += 3) {
                  let end = children[i + 1];
                  if (end < 0 || oldStart > end + baseOffset - moved)
                      continue;
                  let start = children[i] + baseOffset - moved;
                  if (oldEnd >= start) {
                      children[i + 1] = oldStart <= start ? -2 : -1;
                  }
                  else if (oldStart >= baseOffset && dSize) {
                      children[i] += dSize;
                      children[i + 1] += dSize;
                  }
              }
              moved += dSize;
          });
          baseOffset = mapping.maps[i].map(baseOffset, -1);
      }
      // Find the child nodes that still correspond to a single node,
      // recursively call mapInner on them and update their positions.
      let mustRebuild = false;
      for (let i = 0; i < children.length; i += 3)
          if (children[i + 1] < 0) { // Touched nodes
              if (children[i + 1] == -2) {
                  mustRebuild = true;
                  children[i + 1] = -1;
                  continue;
              }
              let from = mapping.map(oldChildren[i] + oldOffset), fromLocal = from - offset;
              if (fromLocal < 0 || fromLocal >= node.content.size) {
                  mustRebuild = true;
                  continue;
              }
              // Must read oldChildren because children was tagged with -1
              let to = mapping.map(oldChildren[i + 1] + oldOffset, -1), toLocal = to - offset;
              let { index, offset: childOffset } = node.content.findIndex(fromLocal);
              let childNode = node.maybeChild(index);
              if (childNode && childOffset == fromLocal && childOffset + childNode.nodeSize == toLocal) {
                  let mapped = children[i + 2]
                      .mapInner(mapping, childNode, from + 1, oldChildren[i] + oldOffset + 1, options);
                  if (mapped != empty) {
                      children[i] = fromLocal;
                      children[i + 1] = toLocal;
                      children[i + 2] = mapped;
                  }
                  else {
                      children[i + 1] = -2;
                      mustRebuild = true;
                  }
              }
              else {
                  mustRebuild = true;
              }
          }
      // Remaining children must be collected and rebuilt into the appropriate structure
      if (mustRebuild) {
          let decorations = mapAndGatherRemainingDecorations(children, oldChildren, newLocal, mapping, offset, oldOffset, options);
          let built = buildTree(decorations, node, 0, options);
          newLocal = built.local;
          for (let i = 0; i < children.length; i += 3)
              if (children[i + 1] < 0) {
                  children.splice(i, 3);
                  i -= 3;
              }
          for (let i = 0, j = 0; i < built.children.length; i += 3) {
              let from = built.children[i];
              while (j < children.length && children[j] < from)
                  j += 3;
              children.splice(j, 0, built.children[i], built.children[i + 1], built.children[i + 2]);
          }
      }
      return new DecorationSet(newLocal.sort(byPos), children);
  }
  function moveSpans(spans, offset) {
      if (!offset || !spans.length)
          return spans;
      let result = [];
      for (let i = 0; i < spans.length; i++) {
          let span = spans[i];
          result.push(new Decoration(span.from + offset, span.to + offset, span.type));
      }
      return result;
  }
  function mapAndGatherRemainingDecorations(children, oldChildren, decorations, mapping, offset, oldOffset, options) {
      // Gather all decorations from the remaining marked children
      function gather(set, oldOffset) {
          for (let i = 0; i < set.local.length; i++) {
              let mapped = set.local[i].map(mapping, offset, oldOffset);
              if (mapped)
                  decorations.push(mapped);
              else if (options.onRemove)
                  options.onRemove(set.local[i].spec);
          }
          for (let i = 0; i < set.children.length; i += 3)
              gather(set.children[i + 2], set.children[i] + oldOffset + 1);
      }
      for (let i = 0; i < children.length; i += 3)
          if (children[i + 1] == -1)
              gather(children[i + 2], oldChildren[i] + oldOffset + 1);
      return decorations;
  }
  function takeSpansForNode(spans, node, offset) {
      if (node.isLeaf)
          return null;
      let end = offset + node.nodeSize, found = null;
      for (let i = 0, span; i < spans.length; i++) {
          if ((span = spans[i]) && span.from > offset && span.to < end) {
              (found || (found = [])).push(span);
              spans[i] = null;
          }
      }
      return found;
  }
  function withoutNulls(array) {
      let result = [];
      for (let i = 0; i < array.length; i++)
          if (array[i] != null)
              result.push(array[i]);
      return result;
  }
  // Build up a tree that corresponds to a set of decorations. `offset`
  // is a base offset that should be subtracted from the `from` and `to`
  // positions in the spans (so that we don't have to allocate new spans
  // for recursive calls).
  function buildTree(spans, node, offset, options) {
      let children = [], hasNulls = false;
      node.forEach((childNode, localStart) => {
          let found = takeSpansForNode(spans, childNode, localStart + offset);
          if (found) {
              hasNulls = true;
              let subtree = buildTree(found, childNode, offset + localStart + 1, options);
              if (subtree != empty)
                  children.push(localStart, localStart + childNode.nodeSize, subtree);
          }
      });
      let locals = moveSpans(hasNulls ? withoutNulls(spans) : spans, -offset).sort(byPos);
      for (let i = 0; i < locals.length; i++)
          if (!locals[i].type.valid(node, locals[i])) {
              if (options.onRemove)
                  options.onRemove(locals[i].spec);
              locals.splice(i--, 1);
          }
      return locals.length || children.length ? new DecorationSet(locals, children) : empty;
  }
  // Used to sort decorations so that ones with a low start position
  // come first, and within a set with the same start position, those
  // with an smaller end position come first.
  function byPos(a, b) {
      return a.from - b.from || a.to - b.to;
  }
  // Scan a sorted array of decorations for partially overlapping spans,
  // and split those so that only fully overlapping spans are left (to
  // make subsequent rendering easier). Will return the input array if
  // no partially overlapping spans are found (the common case).
  function removeOverlap(spans) {
      let working = spans;
      for (let i = 0; i < working.length - 1; i++) {
          let span = working[i];
          if (span.from != span.to)
              for (let j = i + 1; j < working.length; j++) {
                  let next = working[j];
                  if (next.from == span.from) {
                      if (next.to != span.to) {
                          if (working == spans)
                              working = spans.slice();
                          // Followed by a partially overlapping larger span. Split that
                          // span.
                          working[j] = next.copy(next.from, span.to);
                          insertAhead(working, j + 1, next.copy(span.to, next.to));
                      }
                      continue;
                  }
                  else {
                      if (next.from < span.to) {
                          if (working == spans)
                              working = spans.slice();
                          // The end of this one overlaps with a subsequent span. Split
                          // this one.
                          working[i] = span.copy(span.from, next.from);
                          insertAhead(working, j, span.copy(next.from, span.to));
                      }
                      break;
                  }
              }
      }
      return working;
  }
  function insertAhead(array, i, deco) {
      while (i < array.length && byPos(deco, array[i]) > 0)
          i++;
      array.splice(i, 0, deco);
  }
  // Get the decorations associated with the current props of a view.
  function viewDecorations(view) {
      let found = [];
      view.someProp("decorations", f => {
          let result = f(view.state);
          if (result && result != empty)
              found.push(result);
      });
      if (view.cursorWrapper)
          found.push(DecorationSet.create(view.state.doc, [view.cursorWrapper.deco]));
      return DecorationGroup.from(found);
  }

  const observeOptions = {
      childList: true,
      characterData: true,
      characterDataOldValue: true,
      attributes: true,
      attributeOldValue: true,
      subtree: true
  };
  // IE11 has very broken mutation observers, so we also listen to DOMCharacterDataModified
  const useCharData = ie$1 && ie_version <= 11;
  class SelectionState {
      constructor() {
          this.anchorNode = null;
          this.anchorOffset = 0;
          this.focusNode = null;
          this.focusOffset = 0;
      }
      set(sel) {
          this.anchorNode = sel.anchorNode;
          this.anchorOffset = sel.anchorOffset;
          this.focusNode = sel.focusNode;
          this.focusOffset = sel.focusOffset;
      }
      clear() {
          this.anchorNode = this.focusNode = null;
      }
      eq(sel) {
          return sel.anchorNode == this.anchorNode && sel.anchorOffset == this.anchorOffset &&
              sel.focusNode == this.focusNode && sel.focusOffset == this.focusOffset;
      }
  }
  class DOMObserver {
      constructor(view, handleDOMChange) {
          this.view = view;
          this.handleDOMChange = handleDOMChange;
          this.queue = [];
          this.flushingSoon = -1;
          this.observer = null;
          this.currentSelection = new SelectionState;
          this.onCharData = null;
          this.suppressingSelectionUpdates = false;
          this.observer = window.MutationObserver &&
              new window.MutationObserver(mutations => {
                  for (let i = 0; i < mutations.length; i++)
                      this.queue.push(mutations[i]);
                  // IE11 will sometimes (on backspacing out a single character
                  // text node after a BR node) call the observer callback
                  // before actually updating the DOM, which will cause
                  // ProseMirror to miss the change (see #930)
                  if (ie$1 && ie_version <= 11 && mutations.some(m => m.type == "childList" && m.removedNodes.length ||
                      m.type == "characterData" && m.oldValue.length > m.target.nodeValue.length))
                      this.flushSoon();
                  else
                      this.flush();
              });
          if (useCharData) {
              this.onCharData = e => {
                  this.queue.push({ target: e.target, type: "characterData", oldValue: e.prevValue });
                  this.flushSoon();
              };
          }
          this.onSelectionChange = this.onSelectionChange.bind(this);
      }
      flushSoon() {
          if (this.flushingSoon < 0)
              this.flushingSoon = window.setTimeout(() => { this.flushingSoon = -1; this.flush(); }, 20);
      }
      forceFlush() {
          if (this.flushingSoon > -1) {
              window.clearTimeout(this.flushingSoon);
              this.flushingSoon = -1;
              this.flush();
          }
      }
      start() {
          if (this.observer) {
              this.observer.takeRecords();
              this.observer.observe(this.view.dom, observeOptions);
          }
          if (this.onCharData)
              this.view.dom.addEventListener("DOMCharacterDataModified", this.onCharData);
          this.connectSelection();
      }
      stop() {
          if (this.observer) {
              let take = this.observer.takeRecords();
              if (take.length) {
                  for (let i = 0; i < take.length; i++)
                      this.queue.push(take[i]);
                  window.setTimeout(() => this.flush(), 20);
              }
              this.observer.disconnect();
          }
          if (this.onCharData)
              this.view.dom.removeEventListener("DOMCharacterDataModified", this.onCharData);
          this.disconnectSelection();
      }
      connectSelection() {
          this.view.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange);
      }
      disconnectSelection() {
          this.view.dom.ownerDocument.removeEventListener("selectionchange", this.onSelectionChange);
      }
      suppressSelectionUpdates() {
          this.suppressingSelectionUpdates = true;
          setTimeout(() => this.suppressingSelectionUpdates = false, 50);
      }
      onSelectionChange() {
          if (!hasFocusAndSelection(this.view))
              return;
          if (this.suppressingSelectionUpdates)
              return selectionToDOM(this.view);
          // Deletions on IE11 fire their events in the wrong order, giving
          // us a selection change event before the DOM changes are
          // reported.
          if (ie$1 && ie_version <= 11 && !this.view.state.selection.empty) {
              let sel = this.view.domSelectionRange();
              // Selection.isCollapsed isn't reliable on IE
              if (sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset))
                  return this.flushSoon();
          }
          this.flush();
      }
      setCurSelection() {
          this.currentSelection.set(this.view.domSelectionRange());
      }
      ignoreSelectionChange(sel) {
          if (!sel.focusNode)
              return true;
          let ancestors = new Set, container;
          for (let scan = sel.focusNode; scan; scan = parentNode(scan))
              ancestors.add(scan);
          for (let scan = sel.anchorNode; scan; scan = parentNode(scan))
              if (ancestors.has(scan)) {
                  container = scan;
                  break;
              }
          let desc = container && this.view.docView.nearestDesc(container);
          if (desc && desc.ignoreMutation({
              type: "selection",
              target: container.nodeType == 3 ? container.parentNode : container
          })) {
              this.setCurSelection();
              return true;
          }
      }
      pendingRecords() {
          if (this.observer)
              for (let mut of this.observer.takeRecords())
                  this.queue.push(mut);
          return this.queue;
      }
      flush() {
          let { view } = this;
          if (!view.docView || this.flushingSoon > -1)
              return;
          let mutations = this.pendingRecords();
          if (mutations.length)
              this.queue = [];
          let sel = view.domSelectionRange();
          let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel);
          let from = -1, to = -1, typeOver = false, added = [];
          if (view.editable) {
              for (let i = 0; i < mutations.length; i++) {
                  let result = this.registerMutation(mutations[i], added);
                  if (result) {
                      from = from < 0 ? result.from : Math.min(result.from, from);
                      to = to < 0 ? result.to : Math.max(result.to, to);
                      if (result.typeOver)
                          typeOver = true;
                  }
              }
          }
          if (gecko && added.length > 1) {
              let brs = added.filter(n => n.nodeName == "BR");
              if (brs.length == 2) {
                  let a = brs[0], b = brs[1];
                  if (a.parentNode && a.parentNode.parentNode == b.parentNode)
                      b.remove();
                  else
                      a.remove();
              }
          }
          let readSel = null;
          // If it looks like the browser has reset the selection to the
          // start of the document after focus, restore the selection from
          // the state
          if (from < 0 && newSel && view.input.lastFocus > Date.now() - 200 &&
              Math.max(view.input.lastTouch, view.input.lastClick.time) < Date.now() - 300 &&
              selectionCollapsed(sel) && (readSel = selectionFromDOM(view)) &&
              readSel.eq(Selection.near(view.state.doc.resolve(0), 1))) {
              view.input.lastFocus = 0;
              selectionToDOM(view);
              this.currentSelection.set(sel);
              view.scrollToSelection();
          }
          else if (from > -1 || newSel) {
              if (from > -1) {
                  view.docView.markDirty(from, to);
                  checkCSS(view);
              }
              this.handleDOMChange(from, to, typeOver, added);
              if (view.docView && view.docView.dirty)
                  view.updateState(view.state);
              else if (!this.currentSelection.eq(sel))
                  selectionToDOM(view);
              this.currentSelection.set(sel);
          }
      }
      registerMutation(mut, added) {
          // Ignore mutations inside nodes that were already noted as inserted
          if (added.indexOf(mut.target) > -1)
              return null;
          let desc = this.view.docView.nearestDesc(mut.target);
          if (mut.type == "attributes" &&
              (desc == this.view.docView || mut.attributeName == "contenteditable" ||
                  // Firefox sometimes fires spurious events for null/empty styles
                  (mut.attributeName == "style" && !mut.oldValue && !mut.target.getAttribute("style"))))
              return null;
          if (!desc || desc.ignoreMutation(mut))
              return null;
          if (mut.type == "childList") {
              for (let i = 0; i < mut.addedNodes.length; i++)
                  added.push(mut.addedNodes[i]);
              if (desc.contentDOM && desc.contentDOM != desc.dom && !desc.contentDOM.contains(mut.target))
                  return { from: desc.posBefore, to: desc.posAfter };
              let prev = mut.previousSibling, next = mut.nextSibling;
              if (ie$1 && ie_version <= 11 && mut.addedNodes.length) {
                  // IE11 gives us incorrect next/prev siblings for some
                  // insertions, so if there are added nodes, recompute those
                  for (let i = 0; i < mut.addedNodes.length; i++) {
                      let { previousSibling, nextSibling } = mut.addedNodes[i];
                      if (!previousSibling || Array.prototype.indexOf.call(mut.addedNodes, previousSibling) < 0)
                          prev = previousSibling;
                      if (!nextSibling || Array.prototype.indexOf.call(mut.addedNodes, nextSibling) < 0)
                          next = nextSibling;
                  }
              }
              let fromOffset = prev && prev.parentNode == mut.target
                  ? domIndex(prev) + 1 : 0;
              let from = desc.localPosFromDOM(mut.target, fromOffset, -1);
              let toOffset = next && next.parentNode == mut.target
                  ? domIndex(next) : mut.target.childNodes.length;
              let to = desc.localPosFromDOM(mut.target, toOffset, 1);
              return { from, to };
          }
          else if (mut.type == "attributes") {
              return { from: desc.posAtStart - desc.border, to: desc.posAtEnd + desc.border };
          }
          else { // "characterData"
              return {
                  from: desc.posAtStart,
                  to: desc.posAtEnd,
                  // An event was generated for a text change that didn't change
                  // any text. Mark the dom change to fall back to assuming the
                  // selection was typed over with an identical value if it can't
                  // find another change.
                  typeOver: mut.target.nodeValue == mut.oldValue
              };
          }
      }
  }
  let cssChecked = new WeakMap();
  let cssCheckWarned = false;
  function checkCSS(view) {
      if (cssChecked.has(view))
          return;
      cssChecked.set(view, null);
      if (['normal', 'nowrap', 'pre-line'].indexOf(getComputedStyle(view.dom).whiteSpace) !== -1) {
          view.requiresGeckoHackNode = gecko;
          if (cssCheckWarned)
              return;
          console["warn"]("ProseMirror expects the CSS white-space property to be set, preferably to 'pre-wrap'. It is recommended to load style/prosemirror.css from the prosemirror-view package.");
          cssCheckWarned = true;
      }
  }
  function rangeToSelectionRange(view, range) {
      let anchorNode = range.startContainer, anchorOffset = range.startOffset;
      let focusNode = range.endContainer, focusOffset = range.endOffset;
      let currentAnchor = view.domAtPos(view.state.selection.anchor);
      // Since such a range doesn't distinguish between anchor and head,
      // use a heuristic that flips it around if its end matches the
      // current anchor.
      if (isEquivalentPosition(currentAnchor.node, currentAnchor.offset, focusNode, focusOffset))
          [anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset];
      return { anchorNode, anchorOffset, focusNode, focusOffset };
  }
  // Used to work around a Safari Selection/shadow DOM bug
  // Based on https://github.com/codemirror/dev/issues/414 fix
  function safariShadowSelectionRange(view, selection) {
      if (selection.getComposedRanges) {
          let range = selection.getComposedRanges(view.root)[0];
          if (range)
              return rangeToSelectionRange(view, range);
      }
      let found;
      function read(event) {
          event.preventDefault();
          event.stopImmediatePropagation();
          found = event.getTargetRanges()[0];
      }
      // Because Safari (at least in 2018-2022) doesn't provide regular
      // access to the selection inside a shadowRoot, we have to perform a
      // ridiculous hack to get at it—using `execCommand` to trigger a
      // `beforeInput` event so that we can read the target range from the
      // event.
      view.dom.addEventListener("beforeinput", read, true);
      document.execCommand("indent");
      view.dom.removeEventListener("beforeinput", read, true);
      return found ? rangeToSelectionRange(view, found) : null;
  }

  // Note that all referencing and parsing is done with the
  // start-of-operation selection and document, since that's the one
  // that the DOM represents. If any changes came in in the meantime,
  // the modification is mapped over those before it is applied, in
  // readDOMChange.
  function parseBetween(view, from_, to_) {
      let { node: parent, fromOffset, toOffset, from, to } = view.docView.parseRange(from_, to_);
      let domSel = view.domSelectionRange();
      let find;
      let anchor = domSel.anchorNode;
      if (anchor && view.dom.contains(anchor.nodeType == 1 ? anchor : anchor.parentNode)) {
          find = [{ node: anchor, offset: domSel.anchorOffset }];
          if (!selectionCollapsed(domSel))
              find.push({ node: domSel.focusNode, offset: domSel.focusOffset });
      }
      // Work around issue in Chrome where backspacing sometimes replaces
      // the deleted content with a random BR node (issues #799, #831)
      if (chrome && view.input.lastKeyCode === 8) {
          for (let off = toOffset; off > fromOffset; off--) {
              let node = parent.childNodes[off - 1], desc = node.pmViewDesc;
              if (node.nodeName == "BR" && !desc) {
                  toOffset = off;
                  break;
              }
              if (!desc || desc.size)
                  break;
          }
      }
      let startDoc = view.state.doc;
      let parser = view.someProp("domParser") || DOMParser$1.fromSchema(view.state.schema);
      let $from = startDoc.resolve(from);
      let sel = null, doc = parser.parse(parent, {
          topNode: $from.parent,
          topMatch: $from.parent.contentMatchAt($from.index()),
          topOpen: true,
          from: fromOffset,
          to: toOffset,
          preserveWhitespace: $from.parent.type.whitespace == "pre" ? "full" : true,
          findPositions: find,
          ruleFromNode,
          context: $from
      });
      if (find && find[0].pos != null) {
          let anchor = find[0].pos, head = find[1] && find[1].pos;
          if (head == null)
              head = anchor;
          sel = { anchor: anchor + from, head: head + from };
      }
      return { doc, sel, from, to };
  }
  function ruleFromNode(dom) {
      let desc = dom.pmViewDesc;
      if (desc) {
          return desc.parseRule();
      }
      else if (dom.nodeName == "BR" && dom.parentNode) {
          // Safari replaces the list item or table cell with a BR
          // directly in the list node (?!) if you delete the last
          // character in a list item or table cell (#708, #862)
          if (safari && /^(ul|ol)$/i.test(dom.parentNode.nodeName)) {
              let skip = document.createElement("div");
              skip.appendChild(document.createElement("li"));
              return { skip };
          }
          else if (dom.parentNode.lastChild == dom || safari && /^(tr|table)$/i.test(dom.parentNode.nodeName)) {
              return { ignore: true };
          }
      }
      else if (dom.nodeName == "IMG" && dom.getAttribute("mark-placeholder")) {
          return { ignore: true };
      }
      return null;
  }
  const isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i;
  function readDOMChange(view, from, to, typeOver, addedNodes) {
      let compositionID = view.input.compositionPendingChanges || (view.composing ? view.input.compositionID : 0);
      view.input.compositionPendingChanges = 0;
      if (from < 0) {
          let origin = view.input.lastSelectionTime > Date.now() - 50 ? view.input.lastSelectionOrigin : null;
          let newSel = selectionFromDOM(view, origin);
          if (newSel && !view.state.selection.eq(newSel)) {
              if (chrome && android &&
                  view.input.lastKeyCode === 13 && Date.now() - 100 < view.input.lastKeyCodeTime &&
                  view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter"))))
                  return;
              let tr = view.state.tr.setSelection(newSel);
              if (origin == "pointer")
                  tr.setMeta("pointer", true);
              else if (origin == "key")
                  tr.scrollIntoView();
              if (compositionID)
                  tr.setMeta("composition", compositionID);
              view.dispatch(tr);
          }
          return;
      }
      let $before = view.state.doc.resolve(from);
      let shared = $before.sharedDepth(to);
      from = $before.before(shared + 1);
      to = view.state.doc.resolve(to).after(shared + 1);
      let sel = view.state.selection;
      let parse = parseBetween(view, from, to);
      let doc = view.state.doc, compare = doc.slice(parse.from, parse.to);
      let preferredPos, preferredSide;
      // Prefer anchoring to end when Backspace is pressed
      if (view.input.lastKeyCode === 8 && Date.now() - 100 < view.input.lastKeyCodeTime) {
          preferredPos = view.state.selection.to;
          preferredSide = "end";
      }
      else {
          preferredPos = view.state.selection.from;
          preferredSide = "start";
      }
      view.input.lastKeyCode = null;
      let change = findDiff(compare.content, parse.doc.content, parse.from, preferredPos, preferredSide);
      if ((ios && view.input.lastIOSEnter > Date.now() - 225 || android) &&
          addedNodes.some(n => n.nodeType == 1 && !isInline.test(n.nodeName)) &&
          (!change || change.endA >= change.endB) &&
          view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) {
          view.input.lastIOSEnter = 0;
          return;
      }
      if (!change) {
          if (typeOver && sel instanceof TextSelection && !sel.empty && sel.$head.sameParent(sel.$anchor) &&
              !view.composing && !(parse.sel && parse.sel.anchor != parse.sel.head)) {
              change = { start: sel.from, endA: sel.to, endB: sel.to };
          }
          else {
              if (parse.sel) {
                  let sel = resolveSelection(view, view.state.doc, parse.sel);
                  if (sel && !sel.eq(view.state.selection)) {
                      let tr = view.state.tr.setSelection(sel);
                      if (compositionID)
                          tr.setMeta("composition", compositionID);
                      view.dispatch(tr);
                  }
              }
              return;
          }
      }
      view.input.domChangeCount++;
      // Handle the case where overwriting a selection by typing matches
      // the start or end of the selected content, creating a change
      // that's smaller than what was actually overwritten.
      if (view.state.selection.from < view.state.selection.to &&
          change.start == change.endB &&
          view.state.selection instanceof TextSelection) {
          if (change.start > view.state.selection.from && change.start <= view.state.selection.from + 2 &&
              view.state.selection.from >= parse.from) {
              change.start = view.state.selection.from;
          }
          else if (change.endA < view.state.selection.to && change.endA >= view.state.selection.to - 2 &&
              view.state.selection.to <= parse.to) {
              change.endB += (view.state.selection.to - change.endA);
              change.endA = view.state.selection.to;
          }
      }
      // IE11 will insert a non-breaking space _ahead_ of the space after
      // the cursor space when adding a space before another space. When
      // that happened, adjust the change to cover the space instead.
      if (ie$1 && ie_version <= 11 && change.endB == change.start + 1 &&
          change.endA == change.start && change.start > parse.from &&
          parse.doc.textBetween(change.start - parse.from - 1, change.start - parse.from + 1) == " \u00a0") {
          change.start--;
          change.endA--;
          change.endB--;
      }
      let $from = parse.doc.resolveNoCache(change.start - parse.from);
      let $to = parse.doc.resolveNoCache(change.endB - parse.from);
      let $fromA = doc.resolve(change.start);
      let inlineChange = $from.sameParent($to) && $from.parent.inlineContent && $fromA.end() >= change.endA;
      let nextSel;
      // If this looks like the effect of pressing Enter (or was recorded
      // as being an iOS enter press), just dispatch an Enter key instead.
      if (((ios && view.input.lastIOSEnter > Date.now() - 225 &&
          (!inlineChange || addedNodes.some(n => n.nodeName == "DIV" || n.nodeName == "P"))) ||
          (!inlineChange && $from.pos < parse.doc.content.size && !$from.sameParent($to) &&
              (nextSel = Selection.findFrom(parse.doc.resolve($from.pos + 1), 1, true)) &&
              nextSel.head == $to.pos)) &&
          view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) {
          view.input.lastIOSEnter = 0;
          return;
      }
      // Same for backspace
      if (view.state.selection.anchor > change.start &&
          looksLikeBackspace(doc, change.start, change.endA, $from, $to) &&
          view.someProp("handleKeyDown", f => f(view, keyEvent(8, "Backspace")))) {
          if (android && chrome)
              view.domObserver.suppressSelectionUpdates(); // #820
          return;
      }
      // Chrome Android will occasionally, during composition, delete the
      // entire composition and then immediately insert it again. This is
      // used to detect that situation.
      if (chrome && android && change.endB == change.start)
          view.input.lastAndroidDelete = Date.now();
      // This tries to detect Android virtual keyboard
      // enter-and-pick-suggestion action. That sometimes (see issue
      // #1059) first fires a DOM mutation, before moving the selection to
      // the newly created block. And then, because ProseMirror cleans up
      // the DOM selection, it gives up moving the selection entirely,
      // leaving the cursor in the wrong place. When that happens, we drop
      // the new paragraph from the initial change, and fire a simulated
      // enter key afterwards.
      if (android && !inlineChange && $from.start() != $to.start() && $to.parentOffset == 0 && $from.depth == $to.depth &&
          parse.sel && parse.sel.anchor == parse.sel.head && parse.sel.head == change.endA) {
          change.endB -= 2;
          $to = parse.doc.resolveNoCache(change.endB - parse.from);
          setTimeout(() => {
              view.someProp("handleKeyDown", function (f) { return f(view, keyEvent(13, "Enter")); });
          }, 20);
      }
      let chFrom = change.start, chTo = change.endA;
      let tr, storedMarks, markChange;
      if (inlineChange) {
          if ($from.pos == $to.pos) { // Deletion
              // IE11 sometimes weirdly moves the DOM selection around after
              // backspacing out the first element in a textblock
              if (ie$1 && ie_version <= 11 && $from.parentOffset == 0) {
                  view.domObserver.suppressSelectionUpdates();
                  setTimeout(() => selectionToDOM(view), 20);
              }
              tr = view.state.tr.delete(chFrom, chTo);
              storedMarks = doc.resolve(change.start).marksAcross(doc.resolve(change.endA));
          }
          else if ( // Adding or removing a mark
          change.endA == change.endB &&
              (markChange = isMarkChange($from.parent.content.cut($from.parentOffset, $to.parentOffset), $fromA.parent.content.cut($fromA.parentOffset, change.endA - $fromA.start())))) {
              tr = view.state.tr;
              if (markChange.type == "add")
                  tr.addMark(chFrom, chTo, markChange.mark);
              else
                  tr.removeMark(chFrom, chTo, markChange.mark);
          }
          else if ($from.parent.child($from.index()).isText && $from.index() == $to.index() - ($to.textOffset ? 0 : 1)) {
              // Both positions in the same text node -- simply insert text
              let text = $from.parent.textBetween($from.parentOffset, $to.parentOffset);
              if (view.someProp("handleTextInput", f => f(view, chFrom, chTo, text)))
                  return;
              tr = view.state.tr.insertText(text, chFrom, chTo);
          }
      }
      if (!tr)
          tr = view.state.tr.replace(chFrom, chTo, parse.doc.slice(change.start - parse.from, change.endB - parse.from));
      if (parse.sel) {
          let sel = resolveSelection(view, tr.doc, parse.sel);
          // Chrome Android will sometimes, during composition, report the
          // selection in the wrong place. If it looks like that is
          // happening, don't update the selection.
          // Edge just doesn't move the cursor forward when you start typing
          // in an empty block or between br nodes.
          if (sel && !(chrome && android && view.composing && sel.empty &&
              (change.start != change.endB || view.input.lastAndroidDelete < Date.now() - 100) &&
              (sel.head == chFrom || sel.head == tr.mapping.map(chTo) - 1) ||
              ie$1 && sel.empty && sel.head == chFrom))
              tr.setSelection(sel);
      }
      if (storedMarks)
          tr.ensureMarks(storedMarks);
      if (compositionID)
          tr.setMeta("composition", compositionID);
      view.dispatch(tr.scrollIntoView());
  }
  function resolveSelection(view, doc, parsedSel) {
      if (Math.max(parsedSel.anchor, parsedSel.head) > doc.content.size)
          return null;
      return selectionBetween(view, doc.resolve(parsedSel.anchor), doc.resolve(parsedSel.head));
  }
  // Given two same-length, non-empty fragments of inline content,
  // determine whether the first could be created from the second by
  // removing or adding a single mark type.
  function isMarkChange(cur, prev) {
      let curMarks = cur.firstChild.marks, prevMarks = prev.firstChild.marks;
      let added = curMarks, removed = prevMarks, type, mark, update;
      for (let i = 0; i < prevMarks.length; i++)
          added = prevMarks[i].removeFromSet(added);
      for (let i = 0; i < curMarks.length; i++)
          removed = curMarks[i].removeFromSet(removed);
      if (added.length == 1 && removed.length == 0) {
          mark = added[0];
          type = "add";
          update = (node) => node.mark(mark.addToSet(node.marks));
      }
      else if (added.length == 0 && removed.length == 1) {
          mark = removed[0];
          type = "remove";
          update = (node) => node.mark(mark.removeFromSet(node.marks));
      }
      else {
          return null;
      }
      let updated = [];
      for (let i = 0; i < prev.childCount; i++)
          updated.push(update(prev.child(i)));
      if (Fragment.from(updated).eq(cur))
          return { mark, type };
  }
  function looksLikeBackspace(old, start, end, $newStart, $newEnd) {
      if ( // The content must have shrunk
      end - start <= $newEnd.pos - $newStart.pos ||
          // newEnd must point directly at or after the end of the block that newStart points into
          skipClosingAndOpening($newStart, true, false) < $newEnd.pos)
          return false;
      let $start = old.resolve(start);
      // Handle the case where, rather than joining blocks, the change just removed an entire block
      if (!$newStart.parent.isTextblock) {
          let after = $start.nodeAfter;
          return after != null && end == start + after.nodeSize;
      }
      // Start must be at the end of a block
      if ($start.parentOffset < $start.parent.content.size || !$start.parent.isTextblock)
          return false;
      let $next = old.resolve(skipClosingAndOpening($start, true, true));
      // The next textblock must start before end and end near it
      if (!$next.parent.isTextblock || $next.pos > end ||
          skipClosingAndOpening($next, true, false) < end)
          return false;
      // The fragments after the join point must match
      return $newStart.parent.content.cut($newStart.parentOffset).eq($next.parent.content);
  }
  function skipClosingAndOpening($pos, fromEnd, mayOpen) {
      let depth = $pos.depth, end = fromEnd ? $pos.end() : $pos.pos;
      while (depth > 0 && (fromEnd || $pos.indexAfter(depth) == $pos.node(depth).childCount)) {
          depth--;
          end++;
          fromEnd = false;
      }
      if (mayOpen) {
          let next = $pos.node(depth).maybeChild($pos.indexAfter(depth));
          while (next && !next.isLeaf) {
              next = next.firstChild;
              end++;
          }
      }
      return end;
  }
  function findDiff(a, b, pos, preferredPos, preferredSide) {
      let start = a.findDiffStart(b, pos);
      if (start == null)
          return null;
      let { a: endA, b: endB } = a.findDiffEnd(b, pos + a.size, pos + b.size);
      if (preferredSide == "end") {
          let adjust = Math.max(0, start - Math.min(endA, endB));
          preferredPos -= endA + adjust - start;
      }
      if (endA < start && a.size < b.size) {
          let move = preferredPos <= start && preferredPos >= endA ? start - preferredPos : 0;
          start -= move;
          if (start && start < b.size && isSurrogatePair(b.textBetween(start - 1, start + 1)))
              start += move ? 1 : -1;
          endB = start + (endB - endA);
          endA = start;
      }
      else if (endB < start) {
          let move = preferredPos <= start && preferredPos >= endB ? start - preferredPos : 0;
          start -= move;
          if (start && start < a.size && isSurrogatePair(a.textBetween(start - 1, start + 1)))
              start += move ? 1 : -1;
          endA = start + (endA - endB);
          endB = start;
      }
      return { start, endA, endB };
  }
  function isSurrogatePair(str) {
      if (str.length != 2)
          return false;
      let a = str.charCodeAt(0), b = str.charCodeAt(1);
      return a >= 0xDC00 && a <= 0xDFFF && b >= 0xD800 && b <= 0xDBFF;
  }
  /**
  An editor view manages the DOM structure that represents an
  editable document. Its state and behavior are determined by its
  [props](https://prosemirror.net/docs/ref/#view.DirectEditorProps).
  */
  class EditorView {
      /**
      Create a view. `place` may be a DOM node that the editor should
      be appended to, a function that will place it into the document,
      or an object whose `mount` property holds the node to use as the
      document container. If it is `null`, the editor will not be
      added to the document.
      */
      constructor(place, props) {
          this._root = null;
          /**
          @internal
          */
          this.focused = false;
          /**
          Kludge used to work around a Chrome bug @internal
          */
          this.trackWrites = null;
          this.mounted = false;
          /**
          @internal
          */
          this.markCursor = null;
          /**
          @internal
          */
          this.cursorWrapper = null;
          /**
          @internal
          */
          this.lastSelectedViewDesc = undefined;
          /**
          @internal
          */
          this.input = new InputState;
          this.prevDirectPlugins = [];
          this.pluginViews = [];
          /**
          Holds `true` when a hack node is needed in Firefox to prevent the
          [space is eaten issue](https://github.com/ProseMirror/prosemirror/issues/651)
          @internal
          */
          this.requiresGeckoHackNode = false;
          /**
          When editor content is being dragged, this object contains
          information about the dragged slice and whether it is being
          copied or moved. At any other time, it is null.
          */
          this.dragging = null;
          this._props = props;
          this.state = props.state;
          this.directPlugins = props.plugins || [];
          this.directPlugins.forEach(checkStateComponent);
          this.dispatch = this.dispatch.bind(this);
          this.dom = (place && place.mount) || document.createElement("div");
          if (place) {
              if (place.appendChild)
                  place.appendChild(this.dom);
              else if (typeof place == "function")
                  place(this.dom);
              else if (place.mount)
                  this.mounted = true;
          }
          this.editable = getEditable(this);
          updateCursorWrapper(this);
          this.nodeViews = buildNodeViews(this);
          this.docView = docViewDesc(this.state.doc, computeDocDeco(this), viewDecorations(this), this.dom, this);
          this.domObserver = new DOMObserver(this, (from, to, typeOver, added) => readDOMChange(this, from, to, typeOver, added));
          this.domObserver.start();
          initInput(this);
          this.updatePluginViews();
      }
      /**
      Holds `true` when a
      [composition](https://w3c.github.io/uievents/#events-compositionevents)
      is active.
      */
      get composing() { return this.input.composing; }
      /**
      The view's current [props](https://prosemirror.net/docs/ref/#view.EditorProps).
      */
      get props() {
          if (this._props.state != this.state) {
              let prev = this._props;
              this._props = {};
              for (let name in prev)
                  this._props[name] = prev[name];
              this._props.state = this.state;
          }
          return this._props;
      }
      /**
      Update the view's props. Will immediately cause an update to
      the DOM.
      */
      update(props) {
          if (props.handleDOMEvents != this._props.handleDOMEvents)
              ensureListeners(this);
          let prevProps = this._props;
          this._props = props;
          if (props.plugins) {
              props.plugins.forEach(checkStateComponent);
              this.directPlugins = props.plugins;
          }
          this.updateStateInner(props.state, prevProps);
      }
      /**
      Update the view by updating existing props object with the object
      given as argument. Equivalent to `view.update(Object.assign({},
      view.props, props))`.
      */
      setProps(props) {
          let updated = {};
          for (let name in this._props)
              updated[name] = this._props[name];
          updated.state = this.state;
          for (let name in props)
              updated[name] = props[name];
          this.update(updated);
      }
      /**
      Update the editor's `state` prop, without touching any of the
      other props.
      */
      updateState(state) {
          this.updateStateInner(state, this._props);
      }
      updateStateInner(state, prevProps) {
          var _a;
          let prev = this.state, redraw = false, updateSel = false;
          // When stored marks are added, stop composition, so that they can
          // be displayed.
          if (state.storedMarks && this.composing) {
              clearComposition(this);
              updateSel = true;
          }
          this.state = state;
          let pluginsChanged = prev.plugins != state.plugins || this._props.plugins != prevProps.plugins;
          if (pluginsChanged || this._props.plugins != prevProps.plugins || this._props.nodeViews != prevProps.nodeViews) {
              let nodeViews = buildNodeViews(this);
              if (changedNodeViews(nodeViews, this.nodeViews)) {
                  this.nodeViews = nodeViews;
                  redraw = true;
              }
          }
          if (pluginsChanged || prevProps.handleDOMEvents != this._props.handleDOMEvents) {
              ensureListeners(this);
          }
          this.editable = getEditable(this);
          updateCursorWrapper(this);
          let innerDeco = viewDecorations(this), outerDeco = computeDocDeco(this);
          let scroll = prev.plugins != state.plugins && !prev.doc.eq(state.doc) ? "reset"
              : state.scrollToSelection > prev.scrollToSelection ? "to selection" : "preserve";
          let updateDoc = redraw || !this.docView.matchesNode(state.doc, outerDeco, innerDeco);
          if (updateDoc || !state.selection.eq(prev.selection))
              updateSel = true;
          let oldScrollPos = scroll == "preserve" && updateSel && this.dom.style.overflowAnchor == null && storeScrollPos(this);
          if (updateSel) {
              this.domObserver.stop();
              // Work around an issue in Chrome, IE, and Edge where changing
              // the DOM around an active selection puts it into a broken
              // state where the thing the user sees differs from the
              // selection reported by the Selection object (#710, #973,
              // #1011, #1013, #1035).
              let forceSelUpdate = updateDoc && (ie$1 || chrome) && !this.composing &&
                  !prev.selection.empty && !state.selection.empty && selectionContextChanged(prev.selection, state.selection);
              if (updateDoc) {
                  // If the node that the selection points into is written to,
                  // Chrome sometimes starts misreporting the selection, so this
                  // tracks that and forces a selection reset when our update
                  // did write to the node.
                  let chromeKludge = chrome ? (this.trackWrites = this.domSelectionRange().focusNode) : null;
                  if (this.composing)
                      this.input.compositionNode = findCompositionNode(this);
                  if (redraw || !this.docView.update(state.doc, outerDeco, innerDeco, this)) {
                      this.docView.updateOuterDeco(outerDeco);
                      this.docView.destroy();
                      this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this);
                  }
                  if (chromeKludge && !this.trackWrites)
                      forceSelUpdate = true;
              }
              // Work around for an issue where an update arriving right between
              // a DOM selection change and the "selectionchange" event for it
              // can cause a spurious DOM selection update, disrupting mouse
              // drag selection.
              if (forceSelUpdate ||
                  !(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) &&
                      anchorInRightPlace(this))) {
                  selectionToDOM(this, forceSelUpdate);
              }
              else {
                  syncNodeSelection(this, state.selection);
                  this.domObserver.setCurSelection();
              }
              this.domObserver.start();
          }
          this.updatePluginViews(prev);
          if (((_a = this.dragging) === null || _a === void 0 ? void 0 : _a.node) && !prev.doc.eq(state.doc))
              this.updateDraggedNode(this.dragging, prev);
          if (scroll == "reset") {
              this.dom.scrollTop = 0;
          }
          else if (scroll == "to selection") {
              this.scrollToSelection();
          }
          else if (oldScrollPos) {
              resetScrollPos(oldScrollPos);
          }
      }
      /**
      @internal
      */
      scrollToSelection() {
          let startDOM = this.domSelectionRange().focusNode;
          if (this.someProp("handleScrollToSelection", f => f(this))) ;
          else if (this.state.selection instanceof NodeSelection) {
              let target = this.docView.domAfterPos(this.state.selection.from);
              if (target.nodeType == 1)
                  scrollRectIntoView(this, target.getBoundingClientRect(), startDOM);
          }
          else {
              scrollRectIntoView(this, this.coordsAtPos(this.state.selection.head, 1), startDOM);
          }
      }
      destroyPluginViews() {
          let view;
          while (view = this.pluginViews.pop())
              if (view.destroy)
                  view.destroy();
      }
      updatePluginViews(prevState) {
          if (!prevState || prevState.plugins != this.state.plugins || this.directPlugins != this.prevDirectPlugins) {
              this.prevDirectPlugins = this.directPlugins;
              this.destroyPluginViews();
              for (let i = 0; i < this.directPlugins.length; i++) {
                  let plugin = this.directPlugins[i];
                  if (plugin.spec.view)
                      this.pluginViews.push(plugin.spec.view(this));
              }
              for (let i = 0; i < this.state.plugins.length; i++) {
                  let plugin = this.state.plugins[i];
                  if (plugin.spec.view)
                      this.pluginViews.push(plugin.spec.view(this));
              }
          }
          else {
              for (let i = 0; i < this.pluginViews.length; i++) {
                  let pluginView = this.pluginViews[i];
                  if (pluginView.update)
                      pluginView.update(this, prevState);
              }
          }
      }
      updateDraggedNode(dragging, prev) {
          let sel = dragging.node, found = -1;
          if (this.state.doc.nodeAt(sel.from) == sel.node) {
              found = sel.from;
          }
          else {
              let movedPos = sel.from + (this.state.doc.content.size - prev.doc.content.size);
              let moved = movedPos > 0 && this.state.doc.nodeAt(movedPos);
              if (moved == sel.node)
                  found = movedPos;
          }
          this.dragging = new Dragging(dragging.slice, dragging.move, found < 0 ? undefined : NodeSelection.create(this.state.doc, found));
      }
      someProp(propName, f) {
          let prop = this._props && this._props[propName], value;
          if (prop != null && (value = f ? f(prop) : prop))
              return value;
          for (let i = 0; i < this.directPlugins.length; i++) {
              let prop = this.directPlugins[i].props[propName];
              if (prop != null && (value = f ? f(prop) : prop))
                  return value;
          }
          let plugins = this.state.plugins;
          if (plugins)
              for (let i = 0; i < plugins.length; i++) {
                  let prop = plugins[i].props[propName];
                  if (prop != null && (value = f ? f(prop) : prop))
                      return value;
              }
      }
      /**
      Query whether the view has focus.
      */
      hasFocus() {
          // Work around IE not handling focus correctly if resize handles are shown.
          // If the cursor is inside an element with resize handles, activeElement
          // will be that element instead of this.dom.
          if (ie$1) {
              // If activeElement is within this.dom, and there are no other elements
              // setting `contenteditable` to false in between, treat it as focused.
              let node = this.root.activeElement;
              if (node == this.dom)
                  return true;
              if (!node || !this.dom.contains(node))
                  return false;
              while (node && this.dom != node && this.dom.contains(node)) {
                  if (node.contentEditable == 'false')
                      return false;
                  node = node.parentElement;
              }
              return true;
          }
          return this.root.activeElement == this.dom;
      }
      /**
      Focus the editor.
      */
      focus() {
          this.domObserver.stop();
          if (this.editable)
              focusPreventScroll(this.dom);
          selectionToDOM(this);
          this.domObserver.start();
      }
      /**
      Get the document root in which the editor exists. This will
      usually be the top-level `document`, but might be a [shadow
      DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM)
      root if the editor is inside one.
      */
      get root() {
          let cached = this._root;
          if (cached == null)
              for (let search = this.dom.parentNode; search; search = search.parentNode) {
                  if (search.nodeType == 9 || (search.nodeType == 11 && search.host)) {
                      if (!search.getSelection)
                          Object.getPrototypeOf(search).getSelection = () => search.ownerDocument.getSelection();
                      return this._root = search;
                  }
              }
          return cached || document;
      }
      /**
      When an existing editor view is moved to a new document or
      shadow tree, call this to make it recompute its root.
      */
      updateRoot() {
          this._root = null;
      }
      /**
      Given a pair of viewport coordinates, return the document
      position that corresponds to them. May return null if the given
      coordinates aren't inside of the editor. When an object is
      returned, its `pos` property is the position nearest to the
      coordinates, and its `inside` property holds the position of the
      inner node that the position falls inside of, or -1 if it is at
      the top level, not in any node.
      */
      posAtCoords(coords) {
          return posAtCoords(this, coords);
      }
      /**
      Returns the viewport rectangle at a given document position.
      `left` and `right` will be the same number, as this returns a
      flat cursor-ish rectangle. If the position is between two things
      that aren't directly adjacent, `side` determines which element
      is used. When < 0, the element before the position is used,
      otherwise the element after.
      */
      coordsAtPos(pos, side = 1) {
          return coordsAtPos(this, pos, side);
      }
      /**
      Find the DOM position that corresponds to the given document
      position. When `side` is negative, find the position as close as
      possible to the content before the position. When positive,
      prefer positions close to the content after the position. When
      zero, prefer as shallow a position as possible.
      
      Note that you should **not** mutate the editor's internal DOM,
      only inspect it (and even that is usually not necessary).
      */
      domAtPos(pos, side = 0) {
          return this.docView.domFromPos(pos, side);
      }
      /**
      Find the DOM node that represents the document node after the
      given position. May return `null` when the position doesn't point
      in front of a node or if the node is inside an opaque node view.
      
      This is intended to be able to call things like
      `getBoundingClientRect` on that DOM node. Do **not** mutate the
      editor DOM directly, or add styling this way, since that will be
      immediately overriden by the editor as it redraws the node.
      */
      nodeDOM(pos) {
          let desc = this.docView.descAt(pos);
          return desc ? desc.nodeDOM : null;
      }
      /**
      Find the document position that corresponds to a given DOM
      position. (Whenever possible, it is preferable to inspect the
      document structure directly, rather than poking around in the
      DOM, but sometimes—for example when interpreting an event
      target—you don't have a choice.)
      
      The `bias` parameter can be used to influence which side of a DOM
      node to use when the position is inside a leaf node.
      */
      posAtDOM(node, offset, bias = -1) {
          let pos = this.docView.posFromDOM(node, offset, bias);
          if (pos == null)
              throw new RangeError("DOM position not inside the editor");
          return pos;
      }
      /**
      Find out whether the selection is at the end of a textblock when
      moving in a given direction. When, for example, given `"left"`,
      it will return true if moving left from the current cursor
      position would leave that position's parent textblock. Will apply
      to the view's current state by default, but it is possible to
      pass a different state.
      */
      endOfTextblock(dir, state) {
          return endOfTextblock(this, state || this.state, dir);
      }
      /**
      Run the editor's paste logic with the given HTML string. The
      `event`, if given, will be passed to the
      [`handlePaste`](https://prosemirror.net/docs/ref/#view.EditorProps.handlePaste) hook.
      */
      pasteHTML(html, event) {
          return doPaste(this, "", html, false, event || new ClipboardEvent("paste"));
      }
      /**
      Run the editor's paste logic with the given plain-text input.
      */
      pasteText(text, event) {
          return doPaste(this, text, null, true, event || new ClipboardEvent("paste"));
      }
      /**
      Removes the editor from the DOM and destroys all [node
      views](https://prosemirror.net/docs/ref/#view.NodeView).
      */
      destroy() {
          if (!this.docView)
              return;
          destroyInput(this);
          this.destroyPluginViews();
          if (this.mounted) {
              this.docView.update(this.state.doc, [], viewDecorations(this), this);
              this.dom.textContent = "";
          }
          else if (this.dom.parentNode) {
              this.dom.parentNode.removeChild(this.dom);
          }
          this.docView.destroy();
          this.docView = null;
          clearReusedRange();
      }
      /**
      This is true when the view has been
      [destroyed](https://prosemirror.net/docs/ref/#view.EditorView.destroy) (and thus should not be
      used anymore).
      */
      get isDestroyed() {
          return this.docView == null;
      }
      /**
      Used for testing.
      */
      dispatchEvent(event) {
          return dispatchEvent(this, event);
      }
      /**
      Dispatch a transaction. Will call
      [`dispatchTransaction`](https://prosemirror.net/docs/ref/#view.DirectEditorProps.dispatchTransaction)
      when given, and otherwise defaults to applying the transaction to
      the current state and calling
      [`updateState`](https://prosemirror.net/docs/ref/#view.EditorView.updateState) with the result.
      This method is bound to the view instance, so that it can be
      easily passed around.
      */
      dispatch(tr) {
          let dispatchTransaction = this._props.dispatchTransaction;
          if (dispatchTransaction)
              dispatchTransaction.call(this, tr);
          else
              this.updateState(this.state.apply(tr));
      }
      /**
      @internal
      */
      domSelectionRange() {
          let sel = this.domSelection();
          return safari && this.root.nodeType === 11 &&
              deepActiveElement(this.dom.ownerDocument) == this.dom && safariShadowSelectionRange(this, sel) || sel;
      }
      /**
      @internal
      */
      domSelection() {
          return this.root.getSelection();
      }
  }
  function computeDocDeco(view) {
      let attrs = Object.create(null);
      attrs.class = "ProseMirror";
      attrs.contenteditable = String(view.editable);
      view.someProp("attributes", value => {
          if (typeof value == "function")
              value = value(view.state);
          if (value)
              for (let attr in value) {
                  if (attr == "class")
                      attrs.class += " " + value[attr];
                  else if (attr == "style")
                      attrs.style = (attrs.style ? attrs.style + ";" : "") + value[attr];
                  else if (!attrs[attr] && attr != "contenteditable" && attr != "nodeName")
                      attrs[attr] = String(value[attr]);
              }
      });
      if (!attrs.translate)
          attrs.translate = "no";
      return [Decoration.node(0, view.state.doc.content.size, attrs)];
  }
  function updateCursorWrapper(view) {
      if (view.markCursor) {
          let dom = document.createElement("img");
          dom.className = "ProseMirror-separator";
          dom.setAttribute("mark-placeholder", "true");
          dom.setAttribute("alt", "");
          view.cursorWrapper = { dom, deco: Decoration.widget(view.state.selection.head, dom, { raw: true, marks: view.markCursor }) };
      }
      else {
          view.cursorWrapper = null;
      }
  }
  function getEditable(view) {
      return !view.someProp("editable", value => value(view.state) === false);
  }
  function selectionContextChanged(sel1, sel2) {
      let depth = Math.min(sel1.$anchor.sharedDepth(sel1.head), sel2.$anchor.sharedDepth(sel2.head));
      return sel1.$anchor.start(depth) != sel2.$anchor.start(depth);
  }
  function buildNodeViews(view) {
      let result = Object.create(null);
      function add(obj) {
          for (let prop in obj)
              if (!Object.prototype.hasOwnProperty.call(result, prop))
                  result[prop] = obj[prop];
      }
      view.someProp("nodeViews", add);
      view.someProp("markViews", add);
      return result;
  }
  function changedNodeViews(a, b) {
      let nA = 0, nB = 0;
      for (let prop in a) {
          if (a[prop] != b[prop])
              return true;
          nA++;
      }
      for (let _ in b)
          nB++;
      return nA != nB;
  }
  function checkStateComponent(plugin) {
      if (plugin.spec.state || plugin.spec.filterTransaction || plugin.spec.appendTransaction)
          throw new RangeError("Plugins passed directly to the view must not have a state component");
  }

  /**
  Input rules are regular expressions describing a piece of text
  that, when typed, causes something to happen. This might be
  changing two dashes into an emdash, wrapping a paragraph starting
  with `"> "` into a blockquote, or something entirely different.
  */
  class InputRule {
      // :: (RegExp, union<string, (state: EditorState, match: [string], start: number, end: number) → ?Transaction>)
      /**
      Create an input rule. The rule applies when the user typed
      something and the text directly in front of the cursor matches
      `match`, which should end with `$`.
      
      The `handler` can be a string, in which case the matched text, or
      the first matched group in the regexp, is replaced by that
      string.
      
      Or a it can be a function, which will be called with the match
      array produced by
      [`RegExp.exec`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec),
      as well as the start and end of the matched range, and which can
      return a [transaction](https://prosemirror.net/docs/ref/#state.Transaction) that describes the
      rule's effect, or null to indicate the input was not handled.
      */
      constructor(
      /**
      @internal
      */
      match, handler, options = {}) {
          this.match = match;
          this.match = match;
          this.handler = typeof handler == "string" ? stringHandler(handler) : handler;
          this.undoable = options.undoable !== false;
          this.inCode = options.inCode || false;
      }
  }
  function stringHandler(string) {
      return function (state, match, start, end) {
          let insert = string;
          if (match[1]) {
              let offset = match[0].lastIndexOf(match[1]);
              insert += match[0].slice(offset + match[1].length);
              start += offset;
              let cutOff = start - end;
              if (cutOff > 0) {
                  insert = match[0].slice(offset - cutOff, offset) + insert;
                  start = end;
              }
          }
          return state.tr.insertText(insert, start, end);
      };
  }
  const MAX_MATCH = 500;
  /**
  Create an input rules plugin. When enabled, it will cause text
  input that matches any of the given rules to trigger the rule's
  action.
  */
  function inputRules({ rules }) {
      let plugin = new Plugin({
          state: {
              init() { return null; },
              apply(tr, prev) {
                  let stored = tr.getMeta(this);
                  if (stored)
                      return stored;
                  return tr.selectionSet || tr.docChanged ? null : prev;
              }
          },
          props: {
              handleTextInput(view, from, to, text) {
                  return run(view, from, to, text, rules, plugin);
              },
              handleDOMEvents: {
                  compositionend: (view) => {
                      setTimeout(() => {
                          let { $cursor } = view.state.selection;
                          if ($cursor)
                              run(view, $cursor.pos, $cursor.pos, "", rules, plugin);
                      });
                  }
              }
          },
          isInputRules: true
      });
      return plugin;
  }
  function run(view, from, to, text, rules, plugin) {
      if (view.composing)
          return false;
      let state = view.state, $from = state.doc.resolve(from);
      let textBefore = $from.parent.textBetween(Math.max(0, $from.parentOffset - MAX_MATCH), $from.parentOffset, null, "\ufffc") + text;
      for (let i = 0; i < rules.length; i++) {
          let rule = rules[i];
          if ($from.parent.type.spec.code) {
              if (!rule.inCode)
                  continue;
          }
          else if (rule.inCode === "only") {
              continue;
          }
          let match = rule.match.exec(textBefore);
          let tr = match && rule.handler(state, match, from - (match[0].length - text.length), to);
          if (!tr)
              continue;
          if (rule.undoable)
              tr.setMeta(plugin, { transform: tr, from, to, text });
          view.dispatch(tr);
          return true;
      }
      return false;
  }
  /**
  This is a command that will undo an input rule, if applying such a
  rule was the last thing that the user did.
  */
  const undoInputRule = (state, dispatch) => {
      let plugins = state.plugins;
      for (let i = 0; i < plugins.length; i++) {
          let plugin = plugins[i], undoable;
          if (plugin.spec.isInputRules && (undoable = plugin.getState(state))) {
              if (dispatch) {
                  let tr = state.tr, toUndo = undoable.transform;
                  for (let j = toUndo.steps.length - 1; j >= 0; j--)
                      tr.step(toUndo.steps[j].invert(toUndo.docs[j]));
                  if (undoable.text) {
                      let marks = tr.doc.resolve(undoable.from).marks();
                      tr.replaceWith(undoable.from, undoable.to, state.schema.text(undoable.text, marks));
                  }
                  else {
                      tr.delete(undoable.from, undoable.to);
                  }
                  dispatch(tr);
              }
              return true;
          }
      }
      return false;
  };

  /**
  Converts double dashes to an emdash.
  */
  const emDash = new InputRule(/--$/, "—");
  /**
  Converts three dots to an ellipsis character.
  */
  const ellipsis = new InputRule(/\.\.\.$/, "…");
  /**
  “Smart” opening double quotes.
  */
  const openDoubleQuote = new InputRule(/(?:^|[\s\{\[\(\<'"\u2018\u201C])(")$/, "“");
  /**
  “Smart” closing double quotes.
  */
  const closeDoubleQuote = new InputRule(/"$/, "”");
  /**
  “Smart” opening single quotes.
  */
  const openSingleQuote = new InputRule(/(?:^|[\s\{\[\(\<'"\u2018\u201C])(')$/, "‘");
  /**
  “Smart” closing single quotes.
  */
  const closeSingleQuote = new InputRule(/'$/, "’");
  /**
  Smart-quote related input rules.
  */
  const smartQuotes = [openDoubleQuote, closeDoubleQuote, openSingleQuote, closeSingleQuote];

  /**
  Build an input rule for automatically wrapping a textblock when a
  given string is typed. The `regexp` argument is
  directly passed through to the `InputRule` constructor. You'll
  probably want the regexp to start with `^`, so that the pattern can
  only occur at the start of a textblock.

  `nodeType` is the type of node to wrap in. If it needs attributes,
  you can either pass them directly, or pass a function that will
  compute them from the regular expression match.

  By default, if there's a node with the same type above the newly
  wrapped node, the rule will try to [join](https://prosemirror.net/docs/ref/#transform.Transform.join) those
  two nodes. You can pass a join predicate, which takes a regular
  expression match and the node before the wrapped node, and can
  return a boolean to indicate whether a join should happen.
  */
  function wrappingInputRule(regexp, nodeType, getAttrs = null, joinPredicate) {
      return new InputRule(regexp, (state, match, start, end) => {
          let attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
          let tr = state.tr.delete(start, end);
          let $start = tr.doc.resolve(start), range = $start.blockRange(), wrapping = range && findWrapping(range, nodeType, attrs);
          if (!wrapping)
              return null;
          tr.wrap(range, wrapping);
          let before = tr.doc.resolve(start - 1).nodeBefore;
          if (before && before.type == nodeType && canJoin(tr.doc, start - 1) &&
              (!joinPredicate || joinPredicate(match, before)))
              tr.join(start - 1);
          return tr;
      });
  }
  /**
  Build an input rule that changes the type of a textblock when the
  matched text is typed into it. You'll usually want to start your
  regexp with `^` to that it is only matched at the start of a
  textblock. The optional `getAttrs` parameter can be used to compute
  the new node's attributes, and works the same as in the
  `wrappingInputRule` function.
  */
  function textblockTypeInputRule(regexp, nodeType, getAttrs = null) {
      return new InputRule(regexp, (state, match, start, end) => {
          let $start = state.doc.resolve(start);
          let attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
          if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), nodeType))
              return null;
          return state.tr
              .delete(start, end)
              .setBlockType(start, start, nodeType, attrs);
      });
  }

  var index$4 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    InputRule: InputRule,
    closeDoubleQuote: closeDoubleQuote,
    closeSingleQuote: closeSingleQuote,
    ellipsis: ellipsis,
    emDash: emDash,
    inputRules: inputRules,
    openDoubleQuote: openDoubleQuote,
    openSingleQuote: openSingleQuote,
    smartQuotes: smartQuotes,
    textblockTypeInputRule: textblockTypeInputRule,
    undoInputRule: undoInputRule,
    wrappingInputRule: wrappingInputRule
  });

  /**
   * @abstract
   */
  class ProseMirrorPlugin {
    /**
     * An abstract class for building a ProseMirror Plugin.
     * @see {Plugin}
     * @param {Schema} schema  The schema to build the plugin against.
     */
    constructor(schema) {
      /**
       * The ProseMirror schema to build the plugin against.
       * @type {Schema}
       */
      Object.defineProperty(this, "schema", {value: schema});
    }

    /* -------------------------------------------- */

    /**
     * Build the plugin.
     * @param {Schema} schema     The ProseMirror schema to build the plugin against.
     * @param {object} [options]  Additional options to pass to the plugin.
     * @returns {Plugin}
     * @abstract
     */
    static build(schema, options={}) {
      throw new Error("Subclasses of ProseMirrorPlugin must implement a static build method.");
    }
  }

  /**
   * A class responsible for building the input rules for the ProseMirror editor.
   * @extends {ProseMirrorPlugin}
   */
  class ProseMirrorInputRules extends ProseMirrorPlugin {
    /**
     * Build the plugin.
     * @param {Schema} schema     The ProseMirror schema to build the plugin against.
     * @param {object} [options]  Additional options to pass to the plugin.
     * @param {number} [options.minHeadingLevel=0]  The minimum heading level to start from when generating heading input
     *                                              rules. The resulting heading level for a heading rule is equal to the
     *                                              number of leading hashes minus this number.
     * */
    static build(schema, {minHeadingLevel=0}={}) {
      const rules = new this(schema, {minHeadingLevel});
      return inputRules({rules: rules.buildRules()});
    }

    /* -------------------------------------------- */

    /**
     * Build input rules for node types present in the schema.
     * @returns {InputRule[]}
     */
    buildRules() {
      const rules = [ellipsis, ProseMirrorInputRules.#emDashRule()];
      if ( "blockquote" in this.schema.nodes ) rules.push(this.#blockQuoteRule());
      if ( "ordered_list" in this.schema.nodes ) rules.push(this.#orderedListRule());
      if ( "bullet_list" in this.schema.nodes ) rules.push(this.#bulletListRule());
      if ( "code_block" in this.schema.nodes ) rules.push(this.#codeBlockRule());
      if ( "heading" in this.schema.nodes ) rules.push(this.#headingRule(1, 6));
      if ( "horizontal_rule" in this.schema.nodes ) rules.push(this.#hrRule());
      return rules;
    }

    /* -------------------------------------------- */

    /**
     * Turn a "&gt;" at the start of a textblock into a blockquote.
     * @returns {InputRule}
     * @private
     */
    #blockQuoteRule() {
      return wrappingInputRule(/^\s*>\s$/, this.schema.nodes.blockquote);
    }

    /* -------------------------------------------- */

    /**
     * Turn a number followed by a dot at the start of a textblock into an ordered list.
     * @returns {InputRule}
     * @private
     */
    #orderedListRule() {
      return wrappingInputRule(
        /^(\d+)\.\s$/, this.schema.nodes.ordered_list,
        match => ({order: Number(match[1])}),
        (match, node) => (node.childCount + node.attrs.order) === Number(match[1])
      );
    }

    /* -------------------------------------------- */

    /**
     * Turn a -, +, or * at the start of a textblock into a bulleted list.
     * @returns {InputRule}
     * @private
     */
    #bulletListRule() {
      return wrappingInputRule(/^\s*[-+*]\s$/, this.schema.nodes.bullet_list);
    }

    /* -------------------------------------------- */

    /**
     * Turn three backticks at the start of a textblock into a code block.
     * @returns {InputRule}
     * @private
     */
    #codeBlockRule() {
      return textblockTypeInputRule(/^```$/, this.schema.nodes.code_block);
    }

    /* -------------------------------------------- */

    /**
     * Turns a double dash anywhere into an em-dash. Does not match at the start of the line to avoid conflict with the
     * HR rule.
     * @returns {InputRule}
     * @private
     */
    static #emDashRule() {
      return new InputRule(/[^-]+(--)/, "—");
    }

    /* -------------------------------------------- */

    /**
     * Turns a number of # characters followed by a space at the start of a textblock into a heading up to a maximum
     * level.
     * @param {number} minLevel  The minimum heading level to start generating input rules for.
     * @param {number} maxLevel  The maximum number of heading levels.
     * @returns {InputRule}
     * @private
     */
    #headingRule(minLevel, maxLevel) {
      const range = maxLevel - minLevel + 1;
      return textblockTypeInputRule(
        new RegExp(`^(#{1,${range}})\\s$`), this.schema.nodes.heading,
        match => {
          const level = match[1].length;
          return {level: level + minLevel - 1};
        }
      );
    }

    /* -------------------------------------------- */

    /**
     * Turns three hyphens at the start of a line into a horizontal rule.
     * @returns {InputRule}
     * @private
     */
    #hrRule() {
      const hr = this.schema.nodes.horizontal_rule;
      return new InputRule(/^---$/, (state, match, start, end) => {
        return state.tr.replaceRangeWith(start, end, hr.create()).scrollIntoView();
      });
    }
  }

  var base = {
    8: "Backspace",
    9: "Tab",
    10: "Enter",
    12: "NumLock",
    13: "Enter",
    16: "Shift",
    17: "Control",
    18: "Alt",
    20: "CapsLock",
    27: "Escape",
    32: " ",
    33: "PageUp",
    34: "PageDown",
    35: "End",
    36: "Home",
    37: "ArrowLeft",
    38: "ArrowUp",
    39: "ArrowRight",
    40: "ArrowDown",
    44: "PrintScreen",
    45: "Insert",
    46: "Delete",
    59: ";",
    61: "=",
    91: "Meta",
    92: "Meta",
    106: "*",
    107: "+",
    108: ",",
    109: "-",
    110: ".",
    111: "/",
    144: "NumLock",
    145: "ScrollLock",
    160: "Shift",
    161: "Shift",
    162: "Control",
    163: "Control",
    164: "Alt",
    165: "Alt",
    173: "-",
    186: ";",
    187: "=",
    188: ",",
    189: "-",
    190: ".",
    191: "/",
    192: "`",
    219: "[",
    220: "\\",
    221: "]",
    222: "'"
  };

  var shift = {
    48: ")",
    49: "!",
    50: "@",
    51: "#",
    52: "$",
    53: "%",
    54: "^",
    55: "&",
    56: "*",
    57: "(",
    59: ":",
    61: "+",
    173: "_",
    186: ":",
    187: "+",
    188: "<",
    189: "_",
    190: ">",
    191: "?",
    192: "~",
    219: "{",
    220: "|",
    221: "}",
    222: "\""
  };

  var mac$2 = typeof navigator != "undefined" && /Mac/.test(navigator.platform);
  var ie = typeof navigator != "undefined" && /MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent);

  // Fill in the digit keys
  for (var i = 0; i < 10; i++) base[48 + i] = base[96 + i] = String(i);

  // The function keys
  for (var i = 1; i <= 24; i++) base[i + 111] = "F" + i;

  // And the alphabetic keys
  for (var i = 65; i <= 90; i++) {
    base[i] = String.fromCharCode(i + 32);
    shift[i] = String.fromCharCode(i);
  }

  // For each code that doesn't have a shift-equivalent, copy the base name
  for (var code$1 in base) if (!shift.hasOwnProperty(code$1)) shift[code$1] = base[code$1];

  function keyName(event) {
    // On macOS, keys held with Shift and Cmd don't reflect the effect of Shift in `.key`.
    // On IE, shift effect is never included in `.key`.
    var ignoreKey = mac$2 && event.metaKey && event.shiftKey && !event.ctrlKey && !event.altKey ||
        ie && event.shiftKey && event.key && event.key.length == 1 ||
        event.key == "Unidentified";
    var name = (!ignoreKey && event.key) ||
      (event.shiftKey ? shift : base)[event.keyCode] ||
      event.key || "Unidentified";
    // Edge sometimes produces wrong names (Issue #3)
    if (name == "Esc") name = "Escape";
    if (name == "Del") name = "Delete";
    // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8860571/
    if (name == "Left") name = "ArrowLeft";
    if (name == "Up") name = "ArrowUp";
    if (name == "Right") name = "ArrowRight";
    if (name == "Down") name = "ArrowDown";
    return name
  }

  const mac$1 = typeof navigator != "undefined" ? /Mac|iP(hone|[oa]d)/.test(navigator.platform) : false;
  function normalizeKeyName(name) {
      let parts = name.split(/-(?!$)/), result = parts[parts.length - 1];
      if (result == "Space")
          result = " ";
      let alt, ctrl, shift, meta;
      for (let i = 0; i < parts.length - 1; i++) {
          let mod = parts[i];
          if (/^(cmd|meta|m)$/i.test(mod))
              meta = true;
          else if (/^a(lt)?$/i.test(mod))
              alt = true;
          else if (/^(c|ctrl|control)$/i.test(mod))
              ctrl = true;
          else if (/^s(hift)?$/i.test(mod))
              shift = true;
          else if (/^mod$/i.test(mod)) {
              if (mac$1)
                  meta = true;
              else
                  ctrl = true;
          }
          else
              throw new Error("Unrecognized modifier name: " + mod);
      }
      if (alt)
          result = "Alt-" + result;
      if (ctrl)
          result = "Ctrl-" + result;
      if (meta)
          result = "Meta-" + result;
      if (shift)
          result = "Shift-" + result;
      return result;
  }
  function normalize(map) {
      let copy = Object.create(null);
      for (let prop in map)
          copy[normalizeKeyName(prop)] = map[prop];
      return copy;
  }
  function modifiers(name, event, shift = true) {
      if (event.altKey)
          name = "Alt-" + name;
      if (event.ctrlKey)
          name = "Ctrl-" + name;
      if (event.metaKey)
          name = "Meta-" + name;
      if (shift && event.shiftKey)
          name = "Shift-" + name;
      return name;
  }
  /**
  Create a keymap plugin for the given set of bindings.

  Bindings should map key names to [command](https://prosemirror.net/docs/ref/#commands)-style
  functions, which will be called with `(EditorState, dispatch,
  EditorView)` arguments, and should return true when they've handled
  the key. Note that the view argument isn't part of the command
  protocol, but can be used as an escape hatch if a binding needs to
  directly interact with the UI.

  Key names may be strings like `"Shift-Ctrl-Enter"`—a key
  identifier prefixed with zero or more modifiers. Key identifiers
  are based on the strings that can appear in
  [`KeyEvent.key`](https:developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key).
  Use lowercase letters to refer to letter keys (or uppercase letters
  if you want shift to be held). You may use `"Space"` as an alias
  for the `" "` name.

  Modifiers can be given in any order. `Shift-` (or `s-`), `Alt-` (or
  `a-`), `Ctrl-` (or `c-` or `Control-`) and `Cmd-` (or `m-` or
  `Meta-`) are recognized. For characters that are created by holding
  shift, the `Shift-` prefix is implied, and should not be added
  explicitly.

  You can use `Mod-` as a shorthand for `Cmd-` on Mac and `Ctrl-` on
  other platforms.

  You can add multiple keymap plugins to an editor. The order in
  which they appear determines their precedence (the ones early in
  the array get to dispatch first).
  */
  function keymap(bindings) {
      return new Plugin({ props: { handleKeyDown: keydownHandler(bindings) } });
  }
  /**
  Given a set of bindings (using the same format as
  [`keymap`](https://prosemirror.net/docs/ref/#keymap.keymap)), return a [keydown
  handler](https://prosemirror.net/docs/ref/#view.EditorProps.handleKeyDown) that handles them.
  */
  function keydownHandler(bindings) {
      let map = normalize(bindings);
      return function (view, event) {
          let name = keyName(event), baseName, direct = map[modifiers(name, event)];
          if (direct && direct(view.state, view.dispatch, view))
              return true;
          // A character key
          if (name.length == 1 && name != " ") {
              if (event.shiftKey) {
                  // In case the name was already modified by shift, try looking
                  // it up without its shift modifier
                  let noShift = map[modifiers(name, event, false)];
                  if (noShift && noShift(view.state, view.dispatch, view))
                      return true;
              }
              if ((event.shiftKey || event.altKey || event.metaKey || name.charCodeAt(0) > 127) &&
                  (baseName = base[event.keyCode]) && baseName != name) {
                  // Try falling back to the keyCode when there's a modifier
                  // active or the character produced isn't ASCII, and our table
                  // produces a different name from the the keyCode. See #668,
                  // #1060
                  let fromCode = map[modifiers(baseName, event)];
                  if (fromCode && fromCode(view.state, view.dispatch, view))
                      return true;
              }
          }
          return false;
      };
  }

  /**
  Delete the selection, if there is one.
  */
  const deleteSelection = (state, dispatch) => {
      if (state.selection.empty)
          return false;
      if (dispatch)
          dispatch(state.tr.deleteSelection().scrollIntoView());
      return true;
  };
  function atBlockStart(state, view) {
      let { $cursor } = state.selection;
      if (!$cursor || (view ? !view.endOfTextblock("backward", state)
          : $cursor.parentOffset > 0))
          return null;
      return $cursor;
  }
  /**
  If the selection is empty and at the start of a textblock, try to
  reduce the distance between that block and the one before it—if
  there's a block directly before it that can be joined, join them.
  If not, try to move the selected block closer to the next one in
  the document structure by lifting it out of its parent or moving it
  into a parent of the previous block. Will use the view for accurate
  (bidi-aware) start-of-textblock detection if given.
  */
  const joinBackward = (state, dispatch, view) => {
      let $cursor = atBlockStart(state, view);
      if (!$cursor)
          return false;
      let $cut = findCutBefore($cursor);
      // If there is no node before this, try to lift
      if (!$cut) {
          let range = $cursor.blockRange(), target = range && liftTarget(range);
          if (target == null)
              return false;
          if (dispatch)
              dispatch(state.tr.lift(range, target).scrollIntoView());
          return true;
      }
      let before = $cut.nodeBefore;
      // Apply the joining algorithm
      if (!before.type.spec.isolating && deleteBarrier(state, $cut, dispatch))
          return true;
      // If the node below has no content and the node above is
      // selectable, delete the node below and select the one above.
      if ($cursor.parent.content.size == 0 &&
          (textblockAt(before, "end") || NodeSelection.isSelectable(before))) {
          let delStep = replaceStep(state.doc, $cursor.before(), $cursor.after(), Slice.empty);
          if (delStep && delStep.slice.size < delStep.to - delStep.from) {
              if (dispatch) {
                  let tr = state.tr.step(delStep);
                  tr.setSelection(textblockAt(before, "end") ? Selection.findFrom(tr.doc.resolve(tr.mapping.map($cut.pos, -1)), -1)
                      : NodeSelection.create(tr.doc, $cut.pos - before.nodeSize));
                  dispatch(tr.scrollIntoView());
              }
              return true;
          }
      }
      // If the node before is an atom, delete it
      if (before.isAtom && $cut.depth == $cursor.depth - 1) {
          if (dispatch)
              dispatch(state.tr.delete($cut.pos - before.nodeSize, $cut.pos).scrollIntoView());
          return true;
      }
      return false;
  };
  /**
  A more limited form of [`joinBackward`]($commands.joinBackward)
  that only tries to join the current textblock to the one before
  it, if the cursor is at the start of a textblock.
  */
  const joinTextblockBackward = (state, dispatch, view) => {
      let $cursor = atBlockStart(state, view);
      if (!$cursor)
          return false;
      let $cut = findCutBefore($cursor);
      return $cut ? joinTextblocksAround(state, $cut, dispatch) : false;
  };
  /**
  A more limited form of [`joinForward`]($commands.joinForward)
  that only tries to join the current textblock to the one after
  it, if the cursor is at the end of a textblock.
  */
  const joinTextblockForward = (state, dispatch, view) => {
      let $cursor = atBlockEnd(state, view);
      if (!$cursor)
          return false;
      let $cut = findCutAfter($cursor);
      return $cut ? joinTextblocksAround(state, $cut, dispatch) : false;
  };
  function joinTextblocksAround(state, $cut, dispatch) {
      let before = $cut.nodeBefore, beforeText = before, beforePos = $cut.pos - 1;
      for (; !beforeText.isTextblock; beforePos--) {
          if (beforeText.type.spec.isolating)
              return false;
          let child = beforeText.lastChild;
          if (!child)
              return false;
          beforeText = child;
      }
      let after = $cut.nodeAfter, afterText = after, afterPos = $cut.pos + 1;
      for (; !afterText.isTextblock; afterPos++) {
          if (afterText.type.spec.isolating)
              return false;
          let child = afterText.firstChild;
          if (!child)
              return false;
          afterText = child;
      }
      let step = replaceStep(state.doc, beforePos, afterPos, Slice.empty);
      if (!step || step.from != beforePos ||
          step instanceof ReplaceStep && step.slice.size >= afterPos - beforePos)
          return false;
      if (dispatch) {
          let tr = state.tr.step(step);
          tr.setSelection(TextSelection.create(tr.doc, beforePos));
          dispatch(tr.scrollIntoView());
      }
      return true;
  }
  function textblockAt(node, side, only = false) {
      for (let scan = node; scan; scan = (side == "start" ? scan.firstChild : scan.lastChild)) {
          if (scan.isTextblock)
              return true;
          if (only && scan.childCount != 1)
              return false;
      }
      return false;
  }
  /**
  When the selection is empty and at the start of a textblock, select
  the node before that textblock, if possible. This is intended to be
  bound to keys like backspace, after
  [`joinBackward`](https://prosemirror.net/docs/ref/#commands.joinBackward) or other deleting
  commands, as a fall-back behavior when the schema doesn't allow
  deletion at the selected point.
  */
  const selectNodeBackward = (state, dispatch, view) => {
      let { $head, empty } = state.selection, $cut = $head;
      if (!empty)
          return false;
      if ($head.parent.isTextblock) {
          if (view ? !view.endOfTextblock("backward", state) : $head.parentOffset > 0)
              return false;
          $cut = findCutBefore($head);
      }
      let node = $cut && $cut.nodeBefore;
      if (!node || !NodeSelection.isSelectable(node))
          return false;
      if (dispatch)
          dispatch(state.tr.setSelection(NodeSelection.create(state.doc, $cut.pos - node.nodeSize)).scrollIntoView());
      return true;
  };
  function findCutBefore($pos) {
      if (!$pos.parent.type.spec.isolating)
          for (let i = $pos.depth - 1; i >= 0; i--) {
              if ($pos.index(i) > 0)
                  return $pos.doc.resolve($pos.before(i + 1));
              if ($pos.node(i).type.spec.isolating)
                  break;
          }
      return null;
  }
  function atBlockEnd(state, view) {
      let { $cursor } = state.selection;
      if (!$cursor || (view ? !view.endOfTextblock("forward", state)
          : $cursor.parentOffset < $cursor.parent.content.size))
          return null;
      return $cursor;
  }
  /**
  If the selection is empty and the cursor is at the end of a
  textblock, try to reduce or remove the boundary between that block
  and the one after it, either by joining them or by moving the other
  block closer to this one in the tree structure. Will use the view
  for accurate start-of-textblock detection if given.
  */
  const joinForward = (state, dispatch, view) => {
      let $cursor = atBlockEnd(state, view);
      if (!$cursor)
          return false;
      let $cut = findCutAfter($cursor);
      // If there is no node after this, there's nothing to do
      if (!$cut)
          return false;
      let after = $cut.nodeAfter;
      // Try the joining algorithm
      if (deleteBarrier(state, $cut, dispatch))
          return true;
      // If the node above has no content and the node below is
      // selectable, delete the node above and select the one below.
      if ($cursor.parent.content.size == 0 &&
          (textblockAt(after, "start") || NodeSelection.isSelectable(after))) {
          let delStep = replaceStep(state.doc, $cursor.before(), $cursor.after(), Slice.empty);
          if (delStep && delStep.slice.size < delStep.to - delStep.from) {
              if (dispatch) {
                  let tr = state.tr.step(delStep);
                  tr.setSelection(textblockAt(after, "start") ? Selection.findFrom(tr.doc.resolve(tr.mapping.map($cut.pos)), 1)
                      : NodeSelection.create(tr.doc, tr.mapping.map($cut.pos)));
                  dispatch(tr.scrollIntoView());
              }
              return true;
          }
      }
      // If the next node is an atom, delete it
      if (after.isAtom && $cut.depth == $cursor.depth - 1) {
          if (dispatch)
              dispatch(state.tr.delete($cut.pos, $cut.pos + after.nodeSize).scrollIntoView());
          return true;
      }
      return false;
  };
  /**
  When the selection is empty and at the end of a textblock, select
  the node coming after that textblock, if possible. This is intended
  to be bound to keys like delete, after
  [`joinForward`](https://prosemirror.net/docs/ref/#commands.joinForward) and similar deleting
  commands, to provide a fall-back behavior when the schema doesn't
  allow deletion at the selected point.
  */
  const selectNodeForward = (state, dispatch, view) => {
      let { $head, empty } = state.selection, $cut = $head;
      if (!empty)
          return false;
      if ($head.parent.isTextblock) {
          if (view ? !view.endOfTextblock("forward", state) : $head.parentOffset < $head.parent.content.size)
              return false;
          $cut = findCutAfter($head);
      }
      let node = $cut && $cut.nodeAfter;
      if (!node || !NodeSelection.isSelectable(node))
          return false;
      if (dispatch)
          dispatch(state.tr.setSelection(NodeSelection.create(state.doc, $cut.pos)).scrollIntoView());
      return true;
  };
  function findCutAfter($pos) {
      if (!$pos.parent.type.spec.isolating)
          for (let i = $pos.depth - 1; i >= 0; i--) {
              let parent = $pos.node(i);
              if ($pos.index(i) + 1 < parent.childCount)
                  return $pos.doc.resolve($pos.after(i + 1));
              if (parent.type.spec.isolating)
                  break;
          }
      return null;
  }
  /**
  Join the selected block or, if there is a text selection, the
  closest ancestor block of the selection that can be joined, with
  the sibling above it.
  */
  const joinUp = (state, dispatch) => {
      let sel = state.selection, nodeSel = sel instanceof NodeSelection, point;
      if (nodeSel) {
          if (sel.node.isTextblock || !canJoin(state.doc, sel.from))
              return false;
          point = sel.from;
      }
      else {
          point = joinPoint(state.doc, sel.from, -1);
          if (point == null)
              return false;
      }
      if (dispatch) {
          let tr = state.tr.join(point);
          if (nodeSel)
              tr.setSelection(NodeSelection.create(tr.doc, point - state.doc.resolve(point).nodeBefore.nodeSize));
          dispatch(tr.scrollIntoView());
      }
      return true;
  };
  /**
  Join the selected block, or the closest ancestor of the selection
  that can be joined, with the sibling after it.
  */
  const joinDown = (state, dispatch) => {
      let sel = state.selection, point;
      if (sel instanceof NodeSelection) {
          if (sel.node.isTextblock || !canJoin(state.doc, sel.to))
              return false;
          point = sel.to;
      }
      else {
          point = joinPoint(state.doc, sel.to, 1);
          if (point == null)
              return false;
      }
      if (dispatch)
          dispatch(state.tr.join(point).scrollIntoView());
      return true;
  };
  /**
  Lift the selected block, or the closest ancestor block of the
  selection that can be lifted, out of its parent node.
  */
  const lift = (state, dispatch) => {
      let { $from, $to } = state.selection;
      let range = $from.blockRange($to), target = range && liftTarget(range);
      if (target == null)
          return false;
      if (dispatch)
          dispatch(state.tr.lift(range, target).scrollIntoView());
      return true;
  };
  /**
  If the selection is in a node whose type has a truthy
  [`code`](https://prosemirror.net/docs/ref/#model.NodeSpec.code) property in its spec, replace the
  selection with a newline character.
  */
  const newlineInCode = (state, dispatch) => {
      let { $head, $anchor } = state.selection;
      if (!$head.parent.type.spec.code || !$head.sameParent($anchor))
          return false;
      if (dispatch)
          dispatch(state.tr.insertText("\n").scrollIntoView());
      return true;
  };
  function defaultBlockAt(match) {
      for (let i = 0; i < match.edgeCount; i++) {
          let { type } = match.edge(i);
          if (type.isTextblock && !type.hasRequiredAttrs())
              return type;
      }
      return null;
  }
  /**
  When the selection is in a node with a truthy
  [`code`](https://prosemirror.net/docs/ref/#model.NodeSpec.code) property in its spec, create a
  default block after the code block, and move the cursor there.
  */
  const exitCode = (state, dispatch) => {
      let { $head, $anchor } = state.selection;
      if (!$head.parent.type.spec.code || !$head.sameParent($anchor))
          return false;
      let above = $head.node(-1), after = $head.indexAfter(-1), type = defaultBlockAt(above.contentMatchAt(after));
      if (!type || !above.canReplaceWith(after, after, type))
          return false;
      if (dispatch) {
          let pos = $head.after(), tr = state.tr.replaceWith(pos, pos, type.createAndFill());
          tr.setSelection(Selection.near(tr.doc.resolve(pos), 1));
          dispatch(tr.scrollIntoView());
      }
      return true;
  };
  /**
  If a block node is selected, create an empty paragraph before (if
  it is its parent's first child) or after it.
  */
  const createParagraphNear = (state, dispatch) => {
      let sel = state.selection, { $from, $to } = sel;
      if (sel instanceof AllSelection || $from.parent.inlineContent || $to.parent.inlineContent)
          return false;
      let type = defaultBlockAt($to.parent.contentMatchAt($to.indexAfter()));
      if (!type || !type.isTextblock)
          return false;
      if (dispatch) {
          let side = (!$from.parentOffset && $to.index() < $to.parent.childCount ? $from : $to).pos;
          let tr = state.tr.insert(side, type.createAndFill());
          tr.setSelection(TextSelection.create(tr.doc, side + 1));
          dispatch(tr.scrollIntoView());
      }
      return true;
  };
  /**
  If the cursor is in an empty textblock that can be lifted, lift the
  block.
  */
  const liftEmptyBlock = (state, dispatch) => {
      let { $cursor } = state.selection;
      if (!$cursor || $cursor.parent.content.size)
          return false;
      if ($cursor.depth > 1 && $cursor.after() != $cursor.end(-1)) {
          let before = $cursor.before();
          if (canSplit(state.doc, before)) {
              if (dispatch)
                  dispatch(state.tr.split(before).scrollIntoView());
              return true;
          }
      }
      let range = $cursor.blockRange(), target = range && liftTarget(range);
      if (target == null)
          return false;
      if (dispatch)
          dispatch(state.tr.lift(range, target).scrollIntoView());
      return true;
  };
  /**
  Create a variant of [`splitBlock`](https://prosemirror.net/docs/ref/#commands.splitBlock) that uses
  a custom function to determine the type of the newly split off block.
  */
  function splitBlockAs(splitNode) {
      return (state, dispatch) => {
          let { $from, $to } = state.selection;
          if (state.selection instanceof NodeSelection && state.selection.node.isBlock) {
              if (!$from.parentOffset || !canSplit(state.doc, $from.pos))
                  return false;
              if (dispatch)
                  dispatch(state.tr.split($from.pos).scrollIntoView());
              return true;
          }
          if (!$from.parent.isBlock)
              return false;
          if (dispatch) {
              let atEnd = $to.parentOffset == $to.parent.content.size;
              let tr = state.tr;
              if (state.selection instanceof TextSelection || state.selection instanceof AllSelection)
                  tr.deleteSelection();
              let deflt = $from.depth == 0 ? null : defaultBlockAt($from.node(-1).contentMatchAt($from.indexAfter(-1)));
              let splitType = splitNode && splitNode($to.parent, atEnd);
              let types = splitType ? [splitType] : atEnd && deflt ? [{ type: deflt }] : undefined;
              let can = canSplit(tr.doc, tr.mapping.map($from.pos), 1, types);
              if (!types && !can && canSplit(tr.doc, tr.mapping.map($from.pos), 1, deflt ? [{ type: deflt }] : undefined)) {
                  if (deflt)
                      types = [{ type: deflt }];
                  can = true;
              }
              if (can) {
                  tr.split(tr.mapping.map($from.pos), 1, types);
                  if (!atEnd && !$from.parentOffset && $from.parent.type != deflt) {
                      let first = tr.mapping.map($from.before()), $first = tr.doc.resolve(first);
                      if (deflt && $from.node(-1).canReplaceWith($first.index(), $first.index() + 1, deflt))
                          tr.setNodeMarkup(tr.mapping.map($from.before()), deflt);
                  }
              }
              dispatch(tr.scrollIntoView());
          }
          return true;
      };
  }
  /**
  Split the parent block of the selection. If the selection is a text
  selection, also delete its content.
  */
  const splitBlock = splitBlockAs();
  /**
  Acts like [`splitBlock`](https://prosemirror.net/docs/ref/#commands.splitBlock), but without
  resetting the set of active marks at the cursor.
  */
  const splitBlockKeepMarks = (state, dispatch) => {
      return splitBlock(state, dispatch && (tr => {
          let marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
          if (marks)
              tr.ensureMarks(marks);
          dispatch(tr);
      }));
  };
  /**
  Move the selection to the node wrapping the current selection, if
  any. (Will not select the document node.)
  */
  const selectParentNode = (state, dispatch) => {
      let { $from, to } = state.selection, pos;
      let same = $from.sharedDepth(to);
      if (same == 0)
          return false;
      pos = $from.before(same);
      if (dispatch)
          dispatch(state.tr.setSelection(NodeSelection.create(state.doc, pos)));
      return true;
  };
  /**
  Select the whole document.
  */
  const selectAll = (state, dispatch) => {
      if (dispatch)
          dispatch(state.tr.setSelection(new AllSelection(state.doc)));
      return true;
  };
  function joinMaybeClear(state, $pos, dispatch) {
      let before = $pos.nodeBefore, after = $pos.nodeAfter, index = $pos.index();
      if (!before || !after || !before.type.compatibleContent(after.type))
          return false;
      if (!before.content.size && $pos.parent.canReplace(index - 1, index)) {
          if (dispatch)
              dispatch(state.tr.delete($pos.pos - before.nodeSize, $pos.pos).scrollIntoView());
          return true;
      }
      if (!$pos.parent.canReplace(index, index + 1) || !(after.isTextblock || canJoin(state.doc, $pos.pos)))
          return false;
      if (dispatch)
          dispatch(state.tr
              .clearIncompatible($pos.pos, before.type, before.contentMatchAt(before.childCount))
              .join($pos.pos)
              .scrollIntoView());
      return true;
  }
  function deleteBarrier(state, $cut, dispatch) {
      let before = $cut.nodeBefore, after = $cut.nodeAfter, conn, match;
      if (before.type.spec.isolating || after.type.spec.isolating)
          return false;
      if (joinMaybeClear(state, $cut, dispatch))
          return true;
      let canDelAfter = $cut.parent.canReplace($cut.index(), $cut.index() + 1);
      if (canDelAfter &&
          (conn = (match = before.contentMatchAt(before.childCount)).findWrapping(after.type)) &&
          match.matchType(conn[0] || after.type).validEnd) {
          if (dispatch) {
              let end = $cut.pos + after.nodeSize, wrap = Fragment.empty;
              for (let i = conn.length - 1; i >= 0; i--)
                  wrap = Fragment.from(conn[i].create(null, wrap));
              wrap = Fragment.from(before.copy(wrap));
              let tr = state.tr.step(new ReplaceAroundStep($cut.pos - 1, end, $cut.pos, end, new Slice(wrap, 1, 0), conn.length, true));
              let joinAt = end + 2 * conn.length;
              if (canJoin(tr.doc, joinAt))
                  tr.join(joinAt);
              dispatch(tr.scrollIntoView());
          }
          return true;
      }
      let selAfter = Selection.findFrom($cut, 1);
      let range = selAfter && selAfter.$from.blockRange(selAfter.$to), target = range && liftTarget(range);
      if (target != null && target >= $cut.depth) {
          if (dispatch)
              dispatch(state.tr.lift(range, target).scrollIntoView());
          return true;
      }
      if (canDelAfter && textblockAt(after, "start", true) && textblockAt(before, "end")) {
          let at = before, wrap = [];
          for (;;) {
              wrap.push(at);
              if (at.isTextblock)
                  break;
              at = at.lastChild;
          }
          let afterText = after, afterDepth = 1;
          for (; !afterText.isTextblock; afterText = afterText.firstChild)
              afterDepth++;
          if (at.canReplace(at.childCount, at.childCount, afterText.content)) {
              if (dispatch) {
                  let end = Fragment.empty;
                  for (let i = wrap.length - 1; i >= 0; i--)
                      end = Fragment.from(wrap[i].copy(end));
                  let tr = state.tr.step(new ReplaceAroundStep($cut.pos - wrap.length, $cut.pos + after.nodeSize, $cut.pos + afterDepth, $cut.pos + after.nodeSize - afterDepth, new Slice(end, wrap.length, 0), 0, true));
                  dispatch(tr.scrollIntoView());
              }
              return true;
          }
      }
      return false;
  }
  function selectTextblockSide(side) {
      return function (state, dispatch) {
          let sel = state.selection, $pos = side < 0 ? sel.$from : sel.$to;
          let depth = $pos.depth;
          while ($pos.node(depth).isInline) {
              if (!depth)
                  return false;
              depth--;
          }
          if (!$pos.node(depth).isTextblock)
              return false;
          if (dispatch)
              dispatch(state.tr.setSelection(TextSelection.create(state.doc, side < 0 ? $pos.start(depth) : $pos.end(depth))));
          return true;
      };
  }
  /**
  Moves the cursor to the start of current text block.
  */
  const selectTextblockStart = selectTextblockSide(-1);
  /**
  Moves the cursor to the end of current text block.
  */
  const selectTextblockEnd = selectTextblockSide(1);
  // Parameterized commands
  /**
  Wrap the selection in a node of the given type with the given
  attributes.
  */
  function wrapIn(nodeType, attrs = null) {
      return function (state, dispatch) {
          let { $from, $to } = state.selection;
          let range = $from.blockRange($to), wrapping = range && findWrapping(range, nodeType, attrs);
          if (!wrapping)
              return false;
          if (dispatch)
              dispatch(state.tr.wrap(range, wrapping).scrollIntoView());
          return true;
      };
  }
  /**
  Returns a command that tries to set the selected textblocks to the
  given node type with the given attributes.
  */
  function setBlockType(nodeType, attrs = null) {
      return function (state, dispatch) {
          let applicable = false;
          for (let i = 0; i < state.selection.ranges.length && !applicable; i++) {
              let { $from: { pos: from }, $to: { pos: to } } = state.selection.ranges[i];
              state.doc.nodesBetween(from, to, (node, pos) => {
                  if (applicable)
                      return false;
                  if (!node.isTextblock || node.hasMarkup(nodeType, attrs))
                      return;
                  if (node.type == nodeType) {
                      applicable = true;
                  }
                  else {
                      let $pos = state.doc.resolve(pos), index = $pos.index();
                      applicable = $pos.parent.canReplaceWith(index, index + 1, nodeType);
                  }
              });
          }
          if (!applicable)
              return false;
          if (dispatch) {
              let tr = state.tr;
              for (let i = 0; i < state.selection.ranges.length; i++) {
                  let { $from: { pos: from }, $to: { pos: to } } = state.selection.ranges[i];
                  tr.setBlockType(from, to, nodeType, attrs);
              }
              dispatch(tr.scrollIntoView());
          }
          return true;
      };
  }
  function markApplies(doc, ranges, type) {
      for (let i = 0; i < ranges.length; i++) {
          let { $from, $to } = ranges[i];
          let can = $from.depth == 0 ? doc.inlineContent && doc.type.allowsMarkType(type) : false;
          doc.nodesBetween($from.pos, $to.pos, node => {
              if (can)
                  return false;
              can = node.inlineContent && node.type.allowsMarkType(type);
          });
          if (can)
              return true;
      }
      return false;
  }
  /**
  Create a command function that toggles the given mark with the
  given attributes. Will return `false` when the current selection
  doesn't support that mark. This will remove the mark if any marks
  of that type exist in the selection, or add it otherwise. If the
  selection is empty, this applies to the [stored
  marks](https://prosemirror.net/docs/ref/#state.EditorState.storedMarks) instead of a range of the
  document.
  */
  function toggleMark(markType, attrs = null) {
      return function (state, dispatch) {
          let { empty, $cursor, ranges } = state.selection;
          if ((empty && !$cursor) || !markApplies(state.doc, ranges, markType))
              return false;
          if (dispatch) {
              if ($cursor) {
                  if (markType.isInSet(state.storedMarks || $cursor.marks()))
                      dispatch(state.tr.removeStoredMark(markType));
                  else
                      dispatch(state.tr.addStoredMark(markType.create(attrs)));
              }
              else {
                  let has = false, tr = state.tr;
                  for (let i = 0; !has && i < ranges.length; i++) {
                      let { $from, $to } = ranges[i];
                      has = state.doc.rangeHasMark($from.pos, $to.pos, markType);
                  }
                  for (let i = 0; i < ranges.length; i++) {
                      let { $from, $to } = ranges[i];
                      if (has) {
                          tr.removeMark($from.pos, $to.pos, markType);
                      }
                      else {
                          let from = $from.pos, to = $to.pos, start = $from.nodeAfter, end = $to.nodeBefore;
                          let spaceStart = start && start.isText ? /^\s*/.exec(start.text)[0].length : 0;
                          let spaceEnd = end && end.isText ? /\s*$/.exec(end.text)[0].length : 0;
                          if (from + spaceStart < to) {
                              from += spaceStart;
                              to -= spaceEnd;
                          }
                          tr.addMark(from, to, markType.create(attrs));
                      }
                  }
                  dispatch(tr.scrollIntoView());
              }
          }
          return true;
      };
  }
  function wrapDispatchForJoin(dispatch, isJoinable) {
      return (tr) => {
          if (!tr.isGeneric)
              return dispatch(tr);
          let ranges = [];
          for (let i = 0; i < tr.mapping.maps.length; i++) {
              let map = tr.mapping.maps[i];
              for (let j = 0; j < ranges.length; j++)
                  ranges[j] = map.map(ranges[j]);
              map.forEach((_s, _e, from, to) => ranges.push(from, to));
          }
          // Figure out which joinable points exist inside those ranges,
          // by checking all node boundaries in their parent nodes.
          let joinable = [];
          for (let i = 0; i < ranges.length; i += 2) {
              let from = ranges[i], to = ranges[i + 1];
              let $from = tr.doc.resolve(from), depth = $from.sharedDepth(to), parent = $from.node(depth);
              for (let index = $from.indexAfter(depth), pos = $from.after(depth + 1); pos <= to; ++index) {
                  let after = parent.maybeChild(index);
                  if (!after)
                      break;
                  if (index && joinable.indexOf(pos) == -1) {
                      let before = parent.child(index - 1);
                      if (before.type == after.type && isJoinable(before, after))
                          joinable.push(pos);
                  }
                  pos += after.nodeSize;
              }
          }
          // Join the joinable points
          joinable.sort((a, b) => a - b);
          for (let i = joinable.length - 1; i >= 0; i--) {
              if (canJoin(tr.doc, joinable[i]))
                  tr.join(joinable[i]);
          }
          dispatch(tr);
      };
  }
  /**
  Wrap a command so that, when it produces a transform that causes
  two joinable nodes to end up next to each other, those are joined.
  Nodes are considered joinable when they are of the same type and
  when the `isJoinable` predicate returns true for them or, if an
  array of strings was passed, if their node type name is in that
  array.
  */
  function autoJoin(command, isJoinable) {
      let canJoin = Array.isArray(isJoinable) ? (node) => isJoinable.indexOf(node.type.name) > -1
          : isJoinable;
      return (state, dispatch, view) => command(state, dispatch && wrapDispatchForJoin(dispatch, canJoin), view);
  }
  /**
  Combine a number of command functions into a single function (which
  calls them one by one until one returns true).
  */
  function chainCommands(...commands) {
      return function (state, dispatch, view) {
          for (let i = 0; i < commands.length; i++)
              if (commands[i](state, dispatch, view))
                  return true;
          return false;
      };
  }
  let backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward);
  let del = chainCommands(deleteSelection, joinForward, selectNodeForward);
  /**
  A basic keymap containing bindings not specific to any schema.
  Binds the following keys (when multiple commands are listed, they
  are chained with [`chainCommands`](https://prosemirror.net/docs/ref/#commands.chainCommands)):

  * **Enter** to `newlineInCode`, `createParagraphNear`, `liftEmptyBlock`, `splitBlock`
  * **Mod-Enter** to `exitCode`
  * **Backspace** and **Mod-Backspace** to `deleteSelection`, `joinBackward`, `selectNodeBackward`
  * **Delete** and **Mod-Delete** to `deleteSelection`, `joinForward`, `selectNodeForward`
  * **Mod-Delete** to `deleteSelection`, `joinForward`, `selectNodeForward`
  * **Mod-a** to `selectAll`
  */
  const pcBaseKeymap = {
      "Enter": chainCommands(newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock),
      "Mod-Enter": exitCode,
      "Backspace": backspace,
      "Mod-Backspace": backspace,
      "Shift-Backspace": backspace,
      "Delete": del,
      "Mod-Delete": del,
      "Mod-a": selectAll
  };
  /**
  A copy of `pcBaseKeymap` that also binds **Ctrl-h** like Backspace,
  **Ctrl-d** like Delete, **Alt-Backspace** like Ctrl-Backspace, and
  **Ctrl-Alt-Backspace**, **Alt-Delete**, and **Alt-d** like
  Ctrl-Delete.
  */
  const macBaseKeymap = {
      "Ctrl-h": pcBaseKeymap["Backspace"],
      "Alt-Backspace": pcBaseKeymap["Mod-Backspace"],
      "Ctrl-d": pcBaseKeymap["Delete"],
      "Ctrl-Alt-Backspace": pcBaseKeymap["Mod-Delete"],
      "Alt-Delete": pcBaseKeymap["Mod-Delete"],
      "Alt-d": pcBaseKeymap["Mod-Delete"],
      "Ctrl-a": selectTextblockStart,
      "Ctrl-e": selectTextblockEnd
  };
  for (let key in pcBaseKeymap)
      macBaseKeymap[key] = pcBaseKeymap[key];
  const mac = typeof navigator != "undefined" ? /Mac|iP(hone|[oa]d)/.test(navigator.platform)
      // @ts-ignore
      : typeof os != "undefined" && os.platform ? os.platform() == "darwin" : false;
  /**
  Depending on the detected platform, this will hold
  [`pcBasekeymap`](https://prosemirror.net/docs/ref/#commands.pcBaseKeymap) or
  [`macBaseKeymap`](https://prosemirror.net/docs/ref/#commands.macBaseKeymap).
  */
  const baseKeymap = mac ? macBaseKeymap : pcBaseKeymap;

  var index$3 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    autoJoin: autoJoin,
    baseKeymap: baseKeymap,
    chainCommands: chainCommands,
    createParagraphNear: createParagraphNear,
    deleteSelection: deleteSelection,
    exitCode: exitCode,
    joinBackward: joinBackward,
    joinDown: joinDown,
    joinForward: joinForward,
    joinTextblockBackward: joinTextblockBackward,
    joinTextblockForward: joinTextblockForward,
    joinUp: joinUp,
    lift: lift,
    liftEmptyBlock: liftEmptyBlock,
    macBaseKeymap: macBaseKeymap,
    newlineInCode: newlineInCode,
    pcBaseKeymap: pcBaseKeymap,
    selectAll: selectAll,
    selectNodeBackward: selectNodeBackward,
    selectNodeForward: selectNodeForward,
    selectParentNode: selectParentNode,
    selectTextblockEnd: selectTextblockEnd,
    selectTextblockStart: selectTextblockStart,
    setBlockType: setBlockType,
    splitBlock: splitBlock,
    splitBlockAs: splitBlockAs,
    splitBlockKeepMarks: splitBlockKeepMarks,
    toggleMark: toggleMark,
    wrapIn: wrapIn
  });

  /**
  Create a plugin that, when added to a ProseMirror instance,
  causes a decoration to show up at the drop position when something
  is dragged over the editor.

  Nodes may add a `disableDropCursor` property to their spec to
  control the showing of a drop cursor inside them. This may be a
  boolean or a function, which will be called with a view and a
  position, and should return a boolean.
  */
  function dropCursor(options = {}) {
      return new Plugin({
          view(editorView) { return new DropCursorView(editorView, options); }
      });
  }
  class DropCursorView {
      constructor(editorView, options) {
          var _a;
          this.editorView = editorView;
          this.cursorPos = null;
          this.element = null;
          this.timeout = -1;
          this.width = (_a = options.width) !== null && _a !== void 0 ? _a : 1;
          this.color = options.color === false ? undefined : (options.color || "black");
          this.class = options.class;
          this.handlers = ["dragover", "dragend", "drop", "dragleave"].map(name => {
              let handler = (e) => { this[name](e); };
              editorView.dom.addEventListener(name, handler);
              return { name, handler };
          });
      }
      destroy() {
          this.handlers.forEach(({ name, handler }) => this.editorView.dom.removeEventListener(name, handler));
      }
      update(editorView, prevState) {
          if (this.cursorPos != null && prevState.doc != editorView.state.doc) {
              if (this.cursorPos > editorView.state.doc.content.size)
                  this.setCursor(null);
              else
                  this.updateOverlay();
          }
      }
      setCursor(pos) {
          if (pos == this.cursorPos)
              return;
          this.cursorPos = pos;
          if (pos == null) {
              this.element.parentNode.removeChild(this.element);
              this.element = null;
          }
          else {
              this.updateOverlay();
          }
      }
      updateOverlay() {
          let $pos = this.editorView.state.doc.resolve(this.cursorPos);
          let isBlock = !$pos.parent.inlineContent, rect;
          if (isBlock) {
              let before = $pos.nodeBefore, after = $pos.nodeAfter;
              if (before || after) {
                  let node = this.editorView.nodeDOM(this.cursorPos - (before ? before.nodeSize : 0));
                  if (node) {
                      let nodeRect = node.getBoundingClientRect();
                      let top = before ? nodeRect.bottom : nodeRect.top;
                      if (before && after)
                          top = (top + this.editorView.nodeDOM(this.cursorPos).getBoundingClientRect().top) / 2;
                      rect = { left: nodeRect.left, right: nodeRect.right, top: top - this.width / 2, bottom: top + this.width / 2 };
                  }
              }
          }
          if (!rect) {
              let coords = this.editorView.coordsAtPos(this.cursorPos);
              rect = { left: coords.left - this.width / 2, right: coords.left + this.width / 2, top: coords.top, bottom: coords.bottom };
          }
          let parent = this.editorView.dom.offsetParent;
          if (!this.element) {
              this.element = parent.appendChild(document.createElement("div"));
              if (this.class)
                  this.element.className = this.class;
              this.element.style.cssText = "position: absolute; z-index: 50; pointer-events: none;";
              if (this.color) {
                  this.element.style.backgroundColor = this.color;
              }
          }
          this.element.classList.toggle("prosemirror-dropcursor-block", isBlock);
          this.element.classList.toggle("prosemirror-dropcursor-inline", !isBlock);
          let parentLeft, parentTop;
          if (!parent || parent == document.body && getComputedStyle(parent).position == "static") {
              parentLeft = -pageXOffset;
              parentTop = -pageYOffset;
          }
          else {
              let rect = parent.getBoundingClientRect();
              parentLeft = rect.left - parent.scrollLeft;
              parentTop = rect.top - parent.scrollTop;
          }
          this.element.style.left = (rect.left - parentLeft) + "px";
          this.element.style.top = (rect.top - parentTop) + "px";
          this.element.style.width = (rect.right - rect.left) + "px";
          this.element.style.height = (rect.bottom - rect.top) + "px";
      }
      scheduleRemoval(timeout) {
          clearTimeout(this.timeout);
          this.timeout = setTimeout(() => this.setCursor(null), timeout);
      }
      dragover(event) {
          if (!this.editorView.editable)
              return;
          let pos = this.editorView.posAtCoords({ left: event.clientX, top: event.clientY });
          let node = pos && pos.inside >= 0 && this.editorView.state.doc.nodeAt(pos.inside);
          let disableDropCursor = node && node.type.spec.disableDropCursor;
          let disabled = typeof disableDropCursor == "function" ? disableDropCursor(this.editorView, pos, event) : disableDropCursor;
          if (pos && !disabled) {
              let target = pos.pos;
              if (this.editorView.dragging && this.editorView.dragging.slice) {
                  let point = dropPoint(this.editorView.state.doc, target, this.editorView.dragging.slice);
                  if (point != null)
                      target = point;
              }
              this.setCursor(target);
              this.scheduleRemoval(5000);
          }
      }
      dragend() {
          this.scheduleRemoval(20);
      }
      drop() {
          this.scheduleRemoval(20);
      }
      dragleave(event) {
          if (event.target == this.editorView.dom || !this.editorView.dom.contains(event.relatedTarget))
              this.setCursor(null);
      }
  }

  /**
  Gap cursor selections are represented using this class. Its
  `$anchor` and `$head` properties both point at the cursor position.
  */
  class GapCursor extends Selection {
      /**
      Create a gap cursor.
      */
      constructor($pos) {
          super($pos, $pos);
      }
      map(doc, mapping) {
          let $pos = doc.resolve(mapping.map(this.head));
          return GapCursor.valid($pos) ? new GapCursor($pos) : Selection.near($pos);
      }
      content() { return Slice.empty; }
      eq(other) {
          return other instanceof GapCursor && other.head == this.head;
      }
      toJSON() {
          return { type: "gapcursor", pos: this.head };
      }
      /**
      @internal
      */
      static fromJSON(doc, json) {
          if (typeof json.pos != "number")
              throw new RangeError("Invalid input for GapCursor.fromJSON");
          return new GapCursor(doc.resolve(json.pos));
      }
      /**
      @internal
      */
      getBookmark() { return new GapBookmark(this.anchor); }
      /**
      @internal
      */
      static valid($pos) {
          let parent = $pos.parent;
          if (parent.isTextblock || !closedBefore($pos) || !closedAfter($pos))
              return false;
          let override = parent.type.spec.allowGapCursor;
          if (override != null)
              return override;
          let deflt = parent.contentMatchAt($pos.index()).defaultType;
          return deflt && deflt.isTextblock;
      }
      /**
      @internal
      */
      static findGapCursorFrom($pos, dir, mustMove = false) {
          search: for (;;) {
              if (!mustMove && GapCursor.valid($pos))
                  return $pos;
              let pos = $pos.pos, next = null;
              // Scan up from this position
              for (let d = $pos.depth;; d--) {
                  let parent = $pos.node(d);
                  if (dir > 0 ? $pos.indexAfter(d) < parent.childCount : $pos.index(d) > 0) {
                      next = parent.child(dir > 0 ? $pos.indexAfter(d) : $pos.index(d) - 1);
                      break;
                  }
                  else if (d == 0) {
                      return null;
                  }
                  pos += dir;
                  let $cur = $pos.doc.resolve(pos);
                  if (GapCursor.valid($cur))
                      return $cur;
              }
              // And then down into the next node
              for (;;) {
                  let inside = dir > 0 ? next.firstChild : next.lastChild;
                  if (!inside) {
                      if (next.isAtom && !next.isText && !NodeSelection.isSelectable(next)) {
                          $pos = $pos.doc.resolve(pos + next.nodeSize * dir);
                          mustMove = false;
                          continue search;
                      }
                      break;
                  }
                  next = inside;
                  pos += dir;
                  let $cur = $pos.doc.resolve(pos);
                  if (GapCursor.valid($cur))
                      return $cur;
              }
              return null;
          }
      }
  }
  GapCursor.prototype.visible = false;
  GapCursor.findFrom = GapCursor.findGapCursorFrom;
  Selection.jsonID("gapcursor", GapCursor);
  class GapBookmark {
      constructor(pos) {
          this.pos = pos;
      }
      map(mapping) {
          return new GapBookmark(mapping.map(this.pos));
      }
      resolve(doc) {
          let $pos = doc.resolve(this.pos);
          return GapCursor.valid($pos) ? new GapCursor($pos) : Selection.near($pos);
      }
  }
  function closedBefore($pos) {
      for (let d = $pos.depth; d >= 0; d--) {
          let index = $pos.index(d), parent = $pos.node(d);
          // At the start of this parent, look at next one
          if (index == 0) {
              if (parent.type.spec.isolating)
                  return true;
              continue;
          }
          // See if the node before (or its first ancestor) is closed
          for (let before = parent.child(index - 1);; before = before.lastChild) {
              if ((before.childCount == 0 && !before.inlineContent) || before.isAtom || before.type.spec.isolating)
                  return true;
              if (before.inlineContent)
                  return false;
          }
      }
      // Hit start of document
      return true;
  }
  function closedAfter($pos) {
      for (let d = $pos.depth; d >= 0; d--) {
          let index = $pos.indexAfter(d), parent = $pos.node(d);
          if (index == parent.childCount) {
              if (parent.type.spec.isolating)
                  return true;
              continue;
          }
          for (let after = parent.child(index);; after = after.firstChild) {
              if ((after.childCount == 0 && !after.inlineContent) || after.isAtom || after.type.spec.isolating)
                  return true;
              if (after.inlineContent)
                  return false;
          }
      }
      return true;
  }

  /**
  Create a gap cursor plugin. When enabled, this will capture clicks
  near and arrow-key-motion past places that don't have a normally
  selectable position nearby, and create a gap cursor selection for
  them. The cursor is drawn as an element with class
  `ProseMirror-gapcursor`. You can either include
  `style/gapcursor.css` from the package's directory or add your own
  styles to make it visible.
  */
  function gapCursor() {
      return new Plugin({
          props: {
              decorations: drawGapCursor,
              createSelectionBetween(_view, $anchor, $head) {
                  return $anchor.pos == $head.pos && GapCursor.valid($head) ? new GapCursor($head) : null;
              },
              handleClick,
              handleKeyDown: handleKeyDown$1,
              handleDOMEvents: { beforeinput: beforeinput }
          }
      });
  }
  const handleKeyDown$1 = keydownHandler({
      "ArrowLeft": arrow$1("horiz", -1),
      "ArrowRight": arrow$1("horiz", 1),
      "ArrowUp": arrow$1("vert", -1),
      "ArrowDown": arrow$1("vert", 1)
  });
  function arrow$1(axis, dir) {
      const dirStr = axis == "vert" ? (dir > 0 ? "down" : "up") : (dir > 0 ? "right" : "left");
      return function (state, dispatch, view) {
          let sel = state.selection;
          let $start = dir > 0 ? sel.$to : sel.$from, mustMove = sel.empty;
          if (sel instanceof TextSelection) {
              if (!view.endOfTextblock(dirStr) || $start.depth == 0)
                  return false;
              mustMove = false;
              $start = state.doc.resolve(dir > 0 ? $start.after() : $start.before());
          }
          let $found = GapCursor.findGapCursorFrom($start, dir, mustMove);
          if (!$found)
              return false;
          if (dispatch)
              dispatch(state.tr.setSelection(new GapCursor($found)));
          return true;
      };
  }
  function handleClick(view, pos, event) {
      if (!view || !view.editable)
          return false;
      let $pos = view.state.doc.resolve(pos);
      if (!GapCursor.valid($pos))
          return false;
      let clickPos = view.posAtCoords({ left: event.clientX, top: event.clientY });
      if (clickPos && clickPos.inside > -1 && NodeSelection.isSelectable(view.state.doc.nodeAt(clickPos.inside)))
          return false;
      view.dispatch(view.state.tr.setSelection(new GapCursor($pos)));
      return true;
  }
  // This is a hack that, when a composition starts while a gap cursor
  // is active, quickly creates an inline context for the composition to
  // happen in, to avoid it being aborted by the DOM selection being
  // moved into a valid position.
  function beforeinput(view, event) {
      if (event.inputType != "insertCompositionText" || !(view.state.selection instanceof GapCursor))
          return false;
      let { $from } = view.state.selection;
      let insert = $from.parent.contentMatchAt($from.index()).findWrapping(view.state.schema.nodes.text);
      if (!insert)
          return false;
      let frag = Fragment.empty;
      for (let i = insert.length - 1; i >= 0; i--)
          frag = Fragment.from(insert[i].createAndFill(null, frag));
      let tr = view.state.tr.replace($from.pos, $from.pos, new Slice(frag, 0, 0));
      tr.setSelection(TextSelection.near(tr.doc.resolve($from.pos + 1)));
      view.dispatch(tr);
      return false;
  }
  function drawGapCursor(state) {
      if (!(state.selection instanceof GapCursor))
          return null;
      let node = document.createElement("div");
      node.className = "ProseMirror-gapcursor";
      return DecorationSet.create(state.doc, [Decoration.widget(state.selection.head, node, { key: "gapcursor" })]);
  }

  var GOOD_LEAF_SIZE = 200;

  // :: class<T> A rope sequence is a persistent sequence data structure
  // that supports appending, prepending, and slicing without doing a
  // full copy. It is represented as a mostly-balanced tree.
  var RopeSequence = function RopeSequence () {};

  RopeSequence.prototype.append = function append (other) {
    if (!other.length) { return this }
    other = RopeSequence.from(other);

    return (!this.length && other) ||
      (other.length < GOOD_LEAF_SIZE && this.leafAppend(other)) ||
      (this.length < GOOD_LEAF_SIZE && other.leafPrepend(this)) ||
      this.appendInner(other)
  };

  // :: (union<[T], RopeSequence<T>>) → RopeSequence<T>
  // Prepend an array or other rope to this one, returning a new rope.
  RopeSequence.prototype.prepend = function prepend (other) {
    if (!other.length) { return this }
    return RopeSequence.from(other).append(this)
  };

  RopeSequence.prototype.appendInner = function appendInner (other) {
    return new Append(this, other)
  };

  // :: (?number, ?number) → RopeSequence<T>
  // Create a rope repesenting a sub-sequence of this rope.
  RopeSequence.prototype.slice = function slice (from, to) {
      if ( from === void 0 ) from = 0;
      if ( to === void 0 ) to = this.length;

    if (from >= to) { return RopeSequence.empty }
    return this.sliceInner(Math.max(0, from), Math.min(this.length, to))
  };

  // :: (number) → T
  // Retrieve the element at the given position from this rope.
  RopeSequence.prototype.get = function get (i) {
    if (i < 0 || i >= this.length) { return undefined }
    return this.getInner(i)
  };

  // :: ((element: T, index: number) → ?bool, ?number, ?number)
  // Call the given function for each element between the given
  // indices. This tends to be more efficient than looping over the
  // indices and calling `get`, because it doesn't have to descend the
  // tree for every element.
  RopeSequence.prototype.forEach = function forEach (f, from, to) {
      if ( from === void 0 ) from = 0;
      if ( to === void 0 ) to = this.length;

    if (from <= to)
      { this.forEachInner(f, from, to, 0); }
    else
      { this.forEachInvertedInner(f, from, to, 0); }
  };

  // :: ((element: T, index: number) → U, ?number, ?number) → [U]
  // Map the given functions over the elements of the rope, producing
  // a flat array.
  RopeSequence.prototype.map = function map (f, from, to) {
      if ( from === void 0 ) from = 0;
      if ( to === void 0 ) to = this.length;

    var result = [];
    this.forEach(function (elt, i) { return result.push(f(elt, i)); }, from, to);
    return result
  };

  // :: (?union<[T], RopeSequence<T>>) → RopeSequence<T>
  // Create a rope representing the given array, or return the rope
  // itself if a rope was given.
  RopeSequence.from = function from (values) {
    if (values instanceof RopeSequence) { return values }
    return values && values.length ? new Leaf(values) : RopeSequence.empty
  };

  var Leaf = /*@__PURE__*/(function (RopeSequence) {
    function Leaf(values) {
      RopeSequence.call(this);
      this.values = values;
    }

    if ( RopeSequence ) Leaf.__proto__ = RopeSequence;
    Leaf.prototype = Object.create( RopeSequence && RopeSequence.prototype );
    Leaf.prototype.constructor = Leaf;

    var prototypeAccessors = { length: { configurable: true },depth: { configurable: true } };

    Leaf.prototype.flatten = function flatten () {
      return this.values
    };

    Leaf.prototype.sliceInner = function sliceInner (from, to) {
      if (from == 0 && to == this.length) { return this }
      return new Leaf(this.values.slice(from, to))
    };

    Leaf.prototype.getInner = function getInner (i) {
      return this.values[i]
    };

    Leaf.prototype.forEachInner = function forEachInner (f, from, to, start) {
      for (var i = from; i < to; i++)
        { if (f(this.values[i], start + i) === false) { return false } }
    };

    Leaf.prototype.forEachInvertedInner = function forEachInvertedInner (f, from, to, start) {
      for (var i = from - 1; i >= to; i--)
        { if (f(this.values[i], start + i) === false) { return false } }
    };

    Leaf.prototype.leafAppend = function leafAppend (other) {
      if (this.length + other.length <= GOOD_LEAF_SIZE)
        { return new Leaf(this.values.concat(other.flatten())) }
    };

    Leaf.prototype.leafPrepend = function leafPrepend (other) {
      if (this.length + other.length <= GOOD_LEAF_SIZE)
        { return new Leaf(other.flatten().concat(this.values)) }
    };

    prototypeAccessors.length.get = function () { return this.values.length };

    prototypeAccessors.depth.get = function () { return 0 };

    Object.defineProperties( Leaf.prototype, prototypeAccessors );

    return Leaf;
  }(RopeSequence));

  // :: RopeSequence
  // The empty rope sequence.
  RopeSequence.empty = new Leaf([]);

  var Append = /*@__PURE__*/(function (RopeSequence) {
    function Append(left, right) {
      RopeSequence.call(this);
      this.left = left;
      this.right = right;
      this.length = left.length + right.length;
      this.depth = Math.max(left.depth, right.depth) + 1;
    }

    if ( RopeSequence ) Append.__proto__ = RopeSequence;
    Append.prototype = Object.create( RopeSequence && RopeSequence.prototype );
    Append.prototype.constructor = Append;

    Append.prototype.flatten = function flatten () {
      return this.left.flatten().concat(this.right.flatten())
    };

    Append.prototype.getInner = function getInner (i) {
      return i < this.left.length ? this.left.get(i) : this.right.get(i - this.left.length)
    };

    Append.prototype.forEachInner = function forEachInner (f, from, to, start) {
      var leftLen = this.left.length;
      if (from < leftLen &&
          this.left.forEachInner(f, from, Math.min(to, leftLen), start) === false)
        { return false }
      if (to > leftLen &&
          this.right.forEachInner(f, Math.max(from - leftLen, 0), Math.min(this.length, to) - leftLen, start + leftLen) === false)
        { return false }
    };

    Append.prototype.forEachInvertedInner = function forEachInvertedInner (f, from, to, start) {
      var leftLen = this.left.length;
      if (from > leftLen &&
          this.right.forEachInvertedInner(f, from - leftLen, Math.max(to, leftLen) - leftLen, start + leftLen) === false)
        { return false }
      if (to < leftLen &&
          this.left.forEachInvertedInner(f, Math.min(from, leftLen), to, start) === false)
        { return false }
    };

    Append.prototype.sliceInner = function sliceInner (from, to) {
      if (from == 0 && to == this.length) { return this }
      var leftLen = this.left.length;
      if (to <= leftLen) { return this.left.slice(from, to) }
      if (from >= leftLen) { return this.right.slice(from - leftLen, to - leftLen) }
      return this.left.slice(from, leftLen).append(this.right.slice(0, to - leftLen))
    };

    Append.prototype.leafAppend = function leafAppend (other) {
      var inner = this.right.leafAppend(other);
      if (inner) { return new Append(this.left, inner) }
    };

    Append.prototype.leafPrepend = function leafPrepend (other) {
      var inner = this.left.leafPrepend(other);
      if (inner) { return new Append(inner, this.right) }
    };

    Append.prototype.appendInner = function appendInner (other) {
      if (this.left.depth >= Math.max(this.right.depth, other.depth) + 1)
        { return new Append(this.left, new Append(this.right, other)) }
      return new Append(this, other)
    };

    return Append;
  }(RopeSequence));

  // ProseMirror's history isn't simply a way to roll back to a previous
  // state, because ProseMirror supports applying changes without adding
  // them to the history (for example during collaboration).
  //
  // To this end, each 'Branch' (one for the undo history and one for
  // the redo history) keeps an array of 'Items', which can optionally
  // hold a step (an actual undoable change), and always hold a position
  // map (which is needed to move changes below them to apply to the
  // current document).
  //
  // An item that has both a step and a selection bookmark is the start
  // of an 'event' — a group of changes that will be undone or redone at
  // once. (It stores only the bookmark, since that way we don't have to
  // provide a document until the selection is actually applied, which
  // is useful when compressing.)
  // Used to schedule history compression
  const max_empty_items = 500;
  class Branch {
      constructor(items, eventCount) {
          this.items = items;
          this.eventCount = eventCount;
      }
      // Pop the latest event off the branch's history and apply it
      // to a document transform.
      popEvent(state, preserveItems) {
          if (this.eventCount == 0)
              return null;
          let end = this.items.length;
          for (;; end--) {
              let next = this.items.get(end - 1);
              if (next.selection) {
                  --end;
                  break;
              }
          }
          let remap, mapFrom;
          if (preserveItems) {
              remap = this.remapping(end, this.items.length);
              mapFrom = remap.maps.length;
          }
          let transform = state.tr;
          let selection, remaining;
          let addAfter = [], addBefore = [];
          this.items.forEach((item, i) => {
              if (!item.step) {
                  if (!remap) {
                      remap = this.remapping(end, i + 1);
                      mapFrom = remap.maps.length;
                  }
                  mapFrom--;
                  addBefore.push(item);
                  return;
              }
              if (remap) {
                  addBefore.push(new Item(item.map));
                  let step = item.step.map(remap.slice(mapFrom)), map;
                  if (step && transform.maybeStep(step).doc) {
                      map = transform.mapping.maps[transform.mapping.maps.length - 1];
                      addAfter.push(new Item(map, undefined, undefined, addAfter.length + addBefore.length));
                  }
                  mapFrom--;
                  if (map)
                      remap.appendMap(map, mapFrom);
              }
              else {
                  transform.maybeStep(item.step);
              }
              if (item.selection) {
                  selection = remap ? item.selection.map(remap.slice(mapFrom)) : item.selection;
                  remaining = new Branch(this.items.slice(0, end).append(addBefore.reverse().concat(addAfter)), this.eventCount - 1);
                  return false;
              }
          }, this.items.length, 0);
          return { remaining: remaining, transform, selection: selection };
      }
      // Create a new branch with the given transform added.
      addTransform(transform, selection, histOptions, preserveItems) {
          let newItems = [], eventCount = this.eventCount;
          let oldItems = this.items, lastItem = !preserveItems && oldItems.length ? oldItems.get(oldItems.length - 1) : null;
          for (let i = 0; i < transform.steps.length; i++) {
              let step = transform.steps[i].invert(transform.docs[i]);
              let item = new Item(transform.mapping.maps[i], step, selection), merged;
              if (merged = lastItem && lastItem.merge(item)) {
                  item = merged;
                  if (i)
                      newItems.pop();
                  else
                      oldItems = oldItems.slice(0, oldItems.length - 1);
              }
              newItems.push(item);
              if (selection) {
                  eventCount++;
                  selection = undefined;
              }
              if (!preserveItems)
                  lastItem = item;
          }
          let overflow = eventCount - histOptions.depth;
          if (overflow > DEPTH_OVERFLOW) {
              oldItems = cutOffEvents(oldItems, overflow);
              eventCount -= overflow;
          }
          return new Branch(oldItems.append(newItems), eventCount);
      }
      remapping(from, to) {
          let maps = new Mapping;
          this.items.forEach((item, i) => {
              let mirrorPos = item.mirrorOffset != null && i - item.mirrorOffset >= from
                  ? maps.maps.length - item.mirrorOffset : undefined;
              maps.appendMap(item.map, mirrorPos);
          }, from, to);
          return maps;
      }
      addMaps(array) {
          if (this.eventCount == 0)
              return this;
          return new Branch(this.items.append(array.map(map => new Item(map))), this.eventCount);
      }
      // When the collab module receives remote changes, the history has
      // to know about those, so that it can adjust the steps that were
      // rebased on top of the remote changes, and include the position
      // maps for the remote changes in its array of items.
      rebased(rebasedTransform, rebasedCount) {
          if (!this.eventCount)
              return this;
          let rebasedItems = [], start = Math.max(0, this.items.length - rebasedCount);
          let mapping = rebasedTransform.mapping;
          let newUntil = rebasedTransform.steps.length;
          let eventCount = this.eventCount;
          this.items.forEach(item => { if (item.selection)
              eventCount--; }, start);
          let iRebased = rebasedCount;
          this.items.forEach(item => {
              let pos = mapping.getMirror(--iRebased);
              if (pos == null)
                  return;
              newUntil = Math.min(newUntil, pos);
              let map = mapping.maps[pos];
              if (item.step) {
                  let step = rebasedTransform.steps[pos].invert(rebasedTransform.docs[pos]);
                  let selection = item.selection && item.selection.map(mapping.slice(iRebased + 1, pos));
                  if (selection)
                      eventCount++;
                  rebasedItems.push(new Item(map, step, selection));
              }
              else {
                  rebasedItems.push(new Item(map));
              }
          }, start);
          let newMaps = [];
          for (let i = rebasedCount; i < newUntil; i++)
              newMaps.push(new Item(mapping.maps[i]));
          let items = this.items.slice(0, start).append(newMaps).append(rebasedItems);
          let branch = new Branch(items, eventCount);
          if (branch.emptyItemCount() > max_empty_items)
              branch = branch.compress(this.items.length - rebasedItems.length);
          return branch;
      }
      emptyItemCount() {
          let count = 0;
          this.items.forEach(item => { if (!item.step)
              count++; });
          return count;
      }
      // Compressing a branch means rewriting it to push the air (map-only
      // items) out. During collaboration, these naturally accumulate
      // because each remote change adds one. The `upto` argument is used
      // to ensure that only the items below a given level are compressed,
      // because `rebased` relies on a clean, untouched set of items in
      // order to associate old items with rebased steps.
      compress(upto = this.items.length) {
          let remap = this.remapping(0, upto), mapFrom = remap.maps.length;
          let items = [], events = 0;
          this.items.forEach((item, i) => {
              if (i >= upto) {
                  items.push(item);
                  if (item.selection)
                      events++;
              }
              else if (item.step) {
                  let step = item.step.map(remap.slice(mapFrom)), map = step && step.getMap();
                  mapFrom--;
                  if (map)
                      remap.appendMap(map, mapFrom);
                  if (step) {
                      let selection = item.selection && item.selection.map(remap.slice(mapFrom));
                      if (selection)
                          events++;
                      let newItem = new Item(map.invert(), step, selection), merged, last = items.length - 1;
                      if (merged = items.length && items[last].merge(newItem))
                          items[last] = merged;
                      else
                          items.push(newItem);
                  }
              }
              else if (item.map) {
                  mapFrom--;
              }
          }, this.items.length, 0);
          return new Branch(RopeSequence.from(items.reverse()), events);
      }
  }
  Branch.empty = new Branch(RopeSequence.empty, 0);
  function cutOffEvents(items, n) {
      let cutPoint;
      items.forEach((item, i) => {
          if (item.selection && (n-- == 0)) {
              cutPoint = i;
              return false;
          }
      });
      return items.slice(cutPoint);
  }
  class Item {
      constructor(
      // The (forward) step map for this item.
      map, 
      // The inverted step
      step, 
      // If this is non-null, this item is the start of a group, and
      // this selection is the starting selection for the group (the one
      // that was active before the first step was applied)
      selection, 
      // If this item is the inverse of a previous mapping on the stack,
      // this points at the inverse's offset
      mirrorOffset) {
          this.map = map;
          this.step = step;
          this.selection = selection;
          this.mirrorOffset = mirrorOffset;
      }
      merge(other) {
          if (this.step && other.step && !other.selection) {
              let step = other.step.merge(this.step);
              if (step)
                  return new Item(step.getMap().invert(), step, this.selection);
          }
      }
  }
  // The value of the state field that tracks undo/redo history for that
  // state. Will be stored in the plugin state when the history plugin
  // is active.
  class HistoryState {
      constructor(done, undone, prevRanges, prevTime, prevComposition) {
          this.done = done;
          this.undone = undone;
          this.prevRanges = prevRanges;
          this.prevTime = prevTime;
          this.prevComposition = prevComposition;
      }
  }
  const DEPTH_OVERFLOW = 20;
  // Record a transformation in undo history.
  function applyTransaction(history, state, tr, options) {
      let historyTr = tr.getMeta(historyKey), rebased;
      if (historyTr)
          return historyTr.historyState;
      if (tr.getMeta(closeHistoryKey))
          history = new HistoryState(history.done, history.undone, null, 0, -1);
      let appended = tr.getMeta("appendedTransaction");
      if (tr.steps.length == 0) {
          return history;
      }
      else if (appended && appended.getMeta(historyKey)) {
          if (appended.getMeta(historyKey).redo)
              return new HistoryState(history.done.addTransform(tr, undefined, options, mustPreserveItems(state)), history.undone, rangesFor(tr.mapping.maps[tr.steps.length - 1]), history.prevTime, history.prevComposition);
          else
              return new HistoryState(history.done, history.undone.addTransform(tr, undefined, options, mustPreserveItems(state)), null, history.prevTime, history.prevComposition);
      }
      else if (tr.getMeta("addToHistory") !== false && !(appended && appended.getMeta("addToHistory") === false)) {
          // Group transforms that occur in quick succession into one event.
          let composition = tr.getMeta("composition");
          let newGroup = history.prevTime == 0 ||
              (!appended && history.prevComposition != composition &&
                  (history.prevTime < (tr.time || 0) - options.newGroupDelay || !isAdjacentTo(tr, history.prevRanges)));
          let prevRanges = appended ? mapRanges(history.prevRanges, tr.mapping) : rangesFor(tr.mapping.maps[tr.steps.length - 1]);
          return new HistoryState(history.done.addTransform(tr, newGroup ? state.selection.getBookmark() : undefined, options, mustPreserveItems(state)), Branch.empty, prevRanges, tr.time, composition == null ? history.prevComposition : composition);
      }
      else if (rebased = tr.getMeta("rebased")) {
          // Used by the collab module to tell the history that some of its
          // content has been rebased.
          return new HistoryState(history.done.rebased(tr, rebased), history.undone.rebased(tr, rebased), mapRanges(history.prevRanges, tr.mapping), history.prevTime, history.prevComposition);
      }
      else {
          return new HistoryState(history.done.addMaps(tr.mapping.maps), history.undone.addMaps(tr.mapping.maps), mapRanges(history.prevRanges, tr.mapping), history.prevTime, history.prevComposition);
      }
  }
  function isAdjacentTo(transform, prevRanges) {
      if (!prevRanges)
          return false;
      if (!transform.docChanged)
          return true;
      let adjacent = false;
      transform.mapping.maps[0].forEach((start, end) => {
          for (let i = 0; i < prevRanges.length; i += 2)
              if (start <= prevRanges[i + 1] && end >= prevRanges[i])
                  adjacent = true;
      });
      return adjacent;
  }
  function rangesFor(map) {
      let result = [];
      map.forEach((_from, _to, from, to) => result.push(from, to));
      return result;
  }
  function mapRanges(ranges, mapping) {
      if (!ranges)
          return null;
      let result = [];
      for (let i = 0; i < ranges.length; i += 2) {
          let from = mapping.map(ranges[i], 1), to = mapping.map(ranges[i + 1], -1);
          if (from <= to)
              result.push(from, to);
      }
      return result;
  }
  // Apply the latest event from one branch to the document and shift the event
  // onto the other branch.
  function histTransaction(history, state, redo) {
      let preserveItems = mustPreserveItems(state);
      let histOptions = historyKey.get(state).spec.config;
      let pop = (redo ? history.undone : history.done).popEvent(state, preserveItems);
      if (!pop)
          return null;
      let selection = pop.selection.resolve(pop.transform.doc);
      let added = (redo ? history.done : history.undone).addTransform(pop.transform, state.selection.getBookmark(), histOptions, preserveItems);
      let newHist = new HistoryState(redo ? added : pop.remaining, redo ? pop.remaining : added, null, 0, -1);
      return pop.transform.setSelection(selection).setMeta(historyKey, { redo, historyState: newHist });
  }
  let cachedPreserveItems = false, cachedPreserveItemsPlugins = null;
  // Check whether any plugin in the given state has a
  // `historyPreserveItems` property in its spec, in which case we must
  // preserve steps exactly as they came in, so that they can be
  // rebased.
  function mustPreserveItems(state) {
      let plugins = state.plugins;
      if (cachedPreserveItemsPlugins != plugins) {
          cachedPreserveItems = false;
          cachedPreserveItemsPlugins = plugins;
          for (let i = 0; i < plugins.length; i++)
              if (plugins[i].spec.historyPreserveItems) {
                  cachedPreserveItems = true;
                  break;
              }
      }
      return cachedPreserveItems;
  }
  const historyKey = new PluginKey("history");
  const closeHistoryKey = new PluginKey("closeHistory");
  /**
  Returns a plugin that enables the undo history for an editor. The
  plugin will track undo and redo stacks, which can be used with the
  [`undo`](https://prosemirror.net/docs/ref/#history.undo) and [`redo`](https://prosemirror.net/docs/ref/#history.redo) commands.

  You can set an `"addToHistory"` [metadata
  property](https://prosemirror.net/docs/ref/#state.Transaction.setMeta) of `false` on a transaction
  to prevent it from being rolled back by undo.
  */
  function history(config = {}) {
      config = { depth: config.depth || 100,
          newGroupDelay: config.newGroupDelay || 500 };
      return new Plugin({
          key: historyKey,
          state: {
              init() {
                  return new HistoryState(Branch.empty, Branch.empty, null, 0, -1);
              },
              apply(tr, hist, state) {
                  return applyTransaction(hist, state, tr, config);
              }
          },
          config,
          props: {
              handleDOMEvents: {
                  beforeinput(view, e) {
                      let inputType = e.inputType;
                      let command = inputType == "historyUndo" ? undo : inputType == "historyRedo" ? redo : null;
                      if (!command)
                          return false;
                      e.preventDefault();
                      return command(view.state, view.dispatch);
                  }
              }
          }
      });
  }
  function buildCommand(redo, scroll) {
      return (state, dispatch) => {
          let hist = historyKey.getState(state);
          if (!hist || (redo ? hist.undone : hist.done).eventCount == 0)
              return false;
          if (dispatch) {
              let tr = histTransaction(hist, state, redo);
              if (tr)
                  dispatch(scroll ? tr.scrollIntoView() : tr);
          }
          return true;
      };
  }
  /**
  A command function that undoes the last change, if any.
  */
  const undo = buildCommand(false, true);
  /**
  A command function that redoes the last undone change, if any.
  */
  const redo = buildCommand(true, true);

  const olDOM = ["ol", 0], ulDOM = ["ul", 0], liDOM = ["li", 0];
  /**
  An ordered list [node spec](https://prosemirror.net/docs/ref/#model.NodeSpec). Has a single
  attribute, `order`, which determines the number at which the list
  starts counting, and defaults to 1. Represented as an `<ol>`
  element.
  */
  const orderedList = {
      attrs: { order: { default: 1 } },
      parseDOM: [{ tag: "ol", getAttrs(dom) {
                  return { order: dom.hasAttribute("start") ? +dom.getAttribute("start") : 1 };
              } }],
      toDOM(node) {
          return node.attrs.order == 1 ? olDOM : ["ol", { start: node.attrs.order }, 0];
      }
  };
  /**
  A bullet list node spec, represented in the DOM as `<ul>`.
  */
  const bulletList = {
      parseDOM: [{ tag: "ul" }],
      toDOM() { return ulDOM; }
  };
  /**
  A list item (`<li>`) spec.
  */
  const listItem = {
      parseDOM: [{ tag: "li" }],
      toDOM() { return liDOM; },
      defining: true
  };
  function add(obj, props) {
      let copy = {};
      for (let prop in obj)
          copy[prop] = obj[prop];
      for (let prop in props)
          copy[prop] = props[prop];
      return copy;
  }
  /**
  Convenience function for adding list-related node types to a map
  specifying the nodes for a schema. Adds
  [`orderedList`](https://prosemirror.net/docs/ref/#schema-list.orderedList) as `"ordered_list"`,
  [`bulletList`](https://prosemirror.net/docs/ref/#schema-list.bulletList) as `"bullet_list"`, and
  [`listItem`](https://prosemirror.net/docs/ref/#schema-list.listItem) as `"list_item"`.

  `itemContent` determines the content expression for the list items.
  If you want the commands defined in this module to apply to your
  list structure, it should have a shape like `"paragraph block*"` or
  `"paragraph (ordered_list | bullet_list)*"`. `listGroup` can be
  given to assign a group name to the list node types, for example
  `"block"`.
  */
  function addListNodes(nodes, itemContent, listGroup) {
      return nodes.append({
          ordered_list: add(orderedList, { content: "list_item+", group: listGroup }),
          bullet_list: add(bulletList, { content: "list_item+", group: listGroup }),
          list_item: add(listItem, { content: itemContent })
      });
  }
  /**
  Returns a command function that wraps the selection in a list with
  the given type an attributes. If `dispatch` is null, only return a
  value to indicate whether this is possible, but don't actually
  perform the change.
  */
  function wrapInList(listType, attrs = null) {
      return function (state, dispatch) {
          let { $from, $to } = state.selection;
          let range = $from.blockRange($to), doJoin = false, outerRange = range;
          if (!range)
              return false;
          // This is at the top of an existing list item
          if (range.depth >= 2 && $from.node(range.depth - 1).type.compatibleContent(listType) && range.startIndex == 0) {
              // Don't do anything if this is the top of the list
              if ($from.index(range.depth - 1) == 0)
                  return false;
              let $insert = state.doc.resolve(range.start - 2);
              outerRange = new NodeRange($insert, $insert, range.depth);
              if (range.endIndex < range.parent.childCount)
                  range = new NodeRange($from, state.doc.resolve($to.end(range.depth)), range.depth);
              doJoin = true;
          }
          let wrap = findWrapping(outerRange, listType, attrs, range);
          if (!wrap)
              return false;
          if (dispatch)
              dispatch(doWrapInList(state.tr, range, wrap, doJoin, listType).scrollIntoView());
          return true;
      };
  }
  function doWrapInList(tr, range, wrappers, joinBefore, listType) {
      let content = Fragment.empty;
      for (let i = wrappers.length - 1; i >= 0; i--)
          content = Fragment.from(wrappers[i].type.create(wrappers[i].attrs, content));
      tr.step(new ReplaceAroundStep(range.start - (joinBefore ? 2 : 0), range.end, range.start, range.end, new Slice(content, 0, 0), wrappers.length, true));
      let found = 0;
      for (let i = 0; i < wrappers.length; i++)
          if (wrappers[i].type == listType)
              found = i + 1;
      let splitDepth = wrappers.length - found;
      let splitPos = range.start + wrappers.length - (joinBefore ? 2 : 0), parent = range.parent;
      for (let i = range.startIndex, e = range.endIndex, first = true; i < e; i++, first = false) {
          if (!first && canSplit(tr.doc, splitPos, splitDepth)) {
              tr.split(splitPos, splitDepth);
              splitPos += 2 * splitDepth;
          }
          splitPos += parent.child(i).nodeSize;
      }
      return tr;
  }
  /**
  Build a command that splits a non-empty textblock at the top level
  of a list item by also splitting that list item.
  */
  function splitListItem(itemType, itemAttrs) {
      return function (state, dispatch) {
          let { $from, $to, node } = state.selection;
          if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to))
              return false;
          let grandParent = $from.node(-1);
          if (grandParent.type != itemType)
              return false;
          if ($from.parent.content.size == 0 && $from.node(-1).childCount == $from.indexAfter(-1)) {
              // In an empty block. If this is a nested list, the wrapping
              // list item should be split. Otherwise, bail out and let next
              // command handle lifting.
              if ($from.depth == 3 || $from.node(-3).type != itemType ||
                  $from.index(-2) != $from.node(-2).childCount - 1)
                  return false;
              if (dispatch) {
                  let wrap = Fragment.empty;
                  let depthBefore = $from.index(-1) ? 1 : $from.index(-2) ? 2 : 3;
                  // Build a fragment containing empty versions of the structure
                  // from the outer list item to the parent node of the cursor
                  for (let d = $from.depth - depthBefore; d >= $from.depth - 3; d--)
                      wrap = Fragment.from($from.node(d).copy(wrap));
                  let depthAfter = $from.indexAfter(-1) < $from.node(-2).childCount ? 1
                      : $from.indexAfter(-2) < $from.node(-3).childCount ? 2 : 3;
                  // Add a second list item with an empty default start node
                  wrap = wrap.append(Fragment.from(itemType.createAndFill()));
                  let start = $from.before($from.depth - (depthBefore - 1));
                  let tr = state.tr.replace(start, $from.after(-depthAfter), new Slice(wrap, 4 - depthBefore, 0));
                  let sel = -1;
                  tr.doc.nodesBetween(start, tr.doc.content.size, (node, pos) => {
                      if (sel > -1)
                          return false;
                      if (node.isTextblock && node.content.size == 0)
                          sel = pos + 1;
                  });
                  if (sel > -1)
                      tr.setSelection(Selection.near(tr.doc.resolve(sel)));
                  dispatch(tr.scrollIntoView());
              }
              return true;
          }
          let nextType = $to.pos == $from.end() ? grandParent.contentMatchAt(0).defaultType : null;
          let tr = state.tr.delete($from.pos, $to.pos);
          let types = nextType ? [itemAttrs ? { type: itemType, attrs: itemAttrs } : null, { type: nextType }] : undefined;
          if (!canSplit(tr.doc, $from.pos, 2, types))
              return false;
          if (dispatch)
              dispatch(tr.split($from.pos, 2, types).scrollIntoView());
          return true;
      };
  }
  /**
  Create a command to lift the list item around the selection up into
  a wrapping list.
  */
  function liftListItem(itemType) {
      return function (state, dispatch) {
          let { $from, $to } = state.selection;
          let range = $from.blockRange($to, node => node.childCount > 0 && node.firstChild.type == itemType);
          if (!range)
              return false;
          if (!dispatch)
              return true;
          if ($from.node(range.depth - 1).type == itemType) // Inside a parent list
              return liftToOuterList(state, dispatch, itemType, range);
          else // Outer list node
              return liftOutOfList(state, dispatch, range);
      };
  }
  function liftToOuterList(state, dispatch, itemType, range) {
      let tr = state.tr, end = range.end, endOfList = range.$to.end(range.depth);
      if (end < endOfList) {
          // There are siblings after the lifted items, which must become
          // children of the last item
          tr.step(new ReplaceAroundStep(end - 1, endOfList, end, endOfList, new Slice(Fragment.from(itemType.create(null, range.parent.copy())), 1, 0), 1, true));
          range = new NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth);
      }
      const target = liftTarget(range);
      if (target == null)
          return false;
      tr.lift(range, target);
      let after = tr.mapping.map(end, -1) - 1;
      if (canJoin(tr.doc, after))
          tr.join(after);
      dispatch(tr.scrollIntoView());
      return true;
  }
  function liftOutOfList(state, dispatch, range) {
      let tr = state.tr, list = range.parent;
      // Merge the list items into a single big item
      for (let pos = range.end, i = range.endIndex - 1, e = range.startIndex; i > e; i--) {
          pos -= list.child(i).nodeSize;
          tr.delete(pos - 1, pos + 1);
      }
      let $start = tr.doc.resolve(range.start), item = $start.nodeAfter;
      if (tr.mapping.map(range.end) != range.start + $start.nodeAfter.nodeSize)
          return false;
      let atStart = range.startIndex == 0, atEnd = range.endIndex == list.childCount;
      let parent = $start.node(-1), indexBefore = $start.index(-1);
      if (!parent.canReplace(indexBefore + (atStart ? 0 : 1), indexBefore + 1, item.content.append(atEnd ? Fragment.empty : Fragment.from(list))))
          return false;
      let start = $start.pos, end = start + item.nodeSize;
      // Strip off the surrounding list. At the sides where we're not at
      // the end of the list, the existing list is closed. At sides where
      // this is the end, it is overwritten to its end.
      tr.step(new ReplaceAroundStep(start - (atStart ? 1 : 0), end + (atEnd ? 1 : 0), start + 1, end - 1, new Slice((atStart ? Fragment.empty : Fragment.from(list.copy(Fragment.empty)))
          .append(atEnd ? Fragment.empty : Fragment.from(list.copy(Fragment.empty))), atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1));
      dispatch(tr.scrollIntoView());
      return true;
  }
  /**
  Create a command to sink the list item around the selection down
  into an inner list.
  */
  function sinkListItem(itemType) {
      return function (state, dispatch) {
          let { $from, $to } = state.selection;
          let range = $from.blockRange($to, node => node.childCount > 0 && node.firstChild.type == itemType);
          if (!range)
              return false;
          let startIndex = range.startIndex;
          if (startIndex == 0)
              return false;
          let parent = range.parent, nodeBefore = parent.child(startIndex - 1);
          if (nodeBefore.type != itemType)
              return false;
          if (dispatch) {
              let nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type == parent.type;
              let inner = Fragment.from(nestedBefore ? itemType.create() : null);
              let slice = new Slice(Fragment.from(itemType.create(null, Fragment.from(parent.type.create(null, inner)))), nestedBefore ? 3 : 1, 0);
              let before = range.start, after = range.end;
              dispatch(state.tr.step(new ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after, before, after, slice, 1, true))
                  .scrollIntoView());
          }
          return true;
      };
  }

  var index$2 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    addListNodes: addListNodes,
    bulletList: bulletList,
    liftListItem: liftListItem,
    listItem: listItem,
    orderedList: orderedList,
    sinkListItem: sinkListItem,
    splitListItem: splitListItem,
    wrapInList: wrapInList
  });

  /**
   * A class responsible for building the keyboard commands for the ProseMirror editor.
   * @extends {ProseMirrorPlugin}
   */
  class ProseMirrorKeyMaps extends ProseMirrorPlugin {
    /**
     * @param {Schema} schema              The ProseMirror schema to build keymaps for.
     * @param {object} [options]           Additional options to configure the plugin's behaviour.
     * @param {Function} [options.onSave]  A function to call when Ctrl+S is pressed.
     */
    constructor(schema, {onSave}={}) {
      super(schema);

      /**
       * A function to call when Ctrl+S is pressed.
       * @type {Function}
       */
      Object.defineProperty(this, "onSave", {value: onSave, writable: false});
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    static build(schema, options={}) {
      const keymaps = new this(schema, options);
      return keymap(keymaps.buildMapping());
    }

    /* -------------------------------------------- */

    /**
     * @callback ProseMirrorCommand
     * @param {EditorState} state               The current editor state.
     * @param {function(Transaction)} dispatch  A function to dispatch a transaction.
     * @param {EditorView} view                 Escape-hatch for when the command needs to interact directly with the UI.
     * @returns {boolean}                       Whether the command has performed any action and consumed the event.
     */

    /**
     * Build keyboard commands for nodes and marks present in the schema.
     * @returns {Record<string, ProseMirrorCommand>}  An object of keyboard shortcuts to editor functions.
     */
    buildMapping() {
      // TODO: Figure out how to integrate this with our keybindings system.
      const mapping = {};

      // Undo, Redo, Backspace.
      mapping["Mod-z"] = undo;
      mapping["Shift-Mod-z"] = redo;
      mapping["Backspace"] = undoInputRule;

      // ProseMirror-specific block operations.
      mapping["Alt-ArrowUp"] = joinUp;
      mapping["Alt-ArrowDown"] = joinDown;
      mapping["Mod-BracketLeft"] = lift;
      mapping["Escape"] = selectParentNode;

      // Bold.
      if ( "strong" in this.schema.marks ) {
        mapping["Mod-b"] = toggleMark(this.schema.marks.strong);
        mapping["Mod-B"] = toggleMark(this.schema.marks.strong);
      }

      // Italic.
      if ( "em" in this.schema.marks ) {
        mapping["Mod-i"] = toggleMark(this.schema.marks.em);
        mapping["Mod-I"] = toggleMark(this.schema.marks.em);
      }

      // Underline.
      if ( "underline" in this.schema.marks ) {
        mapping["Mod-u"] = toggleMark(this.schema.marks.underline);
        mapping["Mod-U"] = toggleMark(this.schema.marks.underline);
      }

      // Inline code.
      if ( "code" in this.schema.marks ) mapping["Mod-`"] = toggleMark(this.schema.marks.code);

      // Bulleted list.
      if ( "bullet_list" in this.schema.nodes ) mapping["Shift-Mod-8"] = wrapInList(this.schema.nodes.bullet_list);

      // Numbered list.
      if ( "ordered_list" in this.schema.nodes ) mapping["Shift-Mod-9"] = wrapInList(this.schema.nodes.ordered_list);

      // Blockquotes.
      if ( "blockquote" in this.schema.nodes ) mapping["Mod->"] = wrapInList(this.schema.nodes.blockquote);

      // Line breaks.
      if ( "hard_break" in this.schema.nodes ) this.#lineBreakMapping(mapping);

      // Block splitting.
      this.#newLineMapping(mapping);

      // List items.
      if ( "list_item" in this.schema.nodes ) {
        const li = this.schema.nodes.list_item;
        mapping["Shift-Tab"] = liftListItem(li);
        mapping["Tab"] = sinkListItem(li);
      }

      // Paragraphs.
      if ( "paragraph" in this.schema.nodes ) mapping["Shift-Mod-0"] = setBlockType(this.schema.nodes.paragraph);

      // Code blocks.
      if ( "code_block" in this.schema.nodes ) mapping["Shift-Mod-\\"] = setBlockType(this.schema.nodes.code_block);

      // Headings.
      if ( "heading" in this.schema.nodes ) this.#headingsMapping(mapping, 6);

      // Horizontal rules.
      if ( "horizontal_rule" in this.schema.nodes ) this.#horizontalRuleMapping(mapping);

      // Saving.
      if ( this.onSave ) this.#addSaveMapping(mapping);

      return mapping;
    }

    /* -------------------------------------------- */

    /**
     * Implement keyboard commands for heading levels.
     * @param {Record<string, ProseMirrorCommand>} mapping  The keyboard mapping.
     * @param {number} maxLevel                     The maximum level of headings.
     */
    #headingsMapping(mapping, maxLevel) {
      const h = this.schema.nodes.heading;
      Array.fromRange(maxLevel, 1).forEach(level => mapping[`Shift-Mod-${level}`] = setBlockType(h, {level}));
    }

    /* -------------------------------------------- */

    /**
     * Implement keyboard commands for horizontal rules.
     * @param {Record<string, ProseMirrorCommand>} mapping  The keyboard mapping.
     */
    #horizontalRuleMapping(mapping) {
      const hr = this.schema.nodes.horizontal_rule;
      mapping["Mod-_"] = (state, dispatch) => {
        dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
        return true;
      };
    }

    /* -------------------------------------------- */

    /**
     * Implement line-break keyboard commands.
     * @param {Record<string, ProseMirrorCommand>} mapping  The keyboard mapping.
     */
    #lineBreakMapping(mapping) {
      const br = this.schema.nodes.hard_break;

      // Exit a code block if we're in one, then create a line-break.
      const cmd = chainCommands(exitCode, (state, dispatch) => {
        dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView());
        return true;
      });

      mapping["Mod-Enter"] = cmd;
      mapping["Shift-Enter"] = cmd;
    }

    /* -------------------------------------------- */

    /**
     * Implement some custom logic for how to split special blocks.
     * @param {Record<string, ProseMirrorCommand>} mapping  The keyboard mapping.
     */
    #newLineMapping(mapping) {
      const cmds = Object.values(this.schema.nodes).reduce((arr, node) => {
        if ( node.split instanceof Function ) arr.push(node.split);
        return arr;
      }, []);
      if ( !cmds.length ) return;
      mapping["Enter"] = cmds.length < 2 ? cmds[0] : chainCommands(...cmds);
    }

    /* -------------------------------------------- */

    /**
     * Implement save shortcut.
     * @param {Record<string, ProseMirrorCommand>} mapping  The keyboard mapping.
     */
    #addSaveMapping(mapping) {
      mapping["Mod-s"] = () => {
        this.onSave();
        return true;
      };
    }
  }

  class ProseMirrorDropDown {
    /**
     * A class responsible for rendering a menu drop-down.
     * @param {string} title                             The default title.
     * @param {ProseMirrorDropDownEntry[]} items         The configured menu items.
     * @param {object} [options]
     * @param {string} [options.cssClass]                The menu CSS class name. Required if providing an action.
     * @param {string} [options.icon]                    Use an icon for the dropdown rather than a text label.
     * @param {function(MouseEvent)} [options.onAction]  A callback to fire when a menu item is clicked.
     */
    constructor(title, items, {cssClass, icon, onAction}={}) {
      /**
       * The default title for this drop-down.
       * @type {string}
       */
      Object.defineProperty(this, "title", {value: title, writable: false});

      /**
       * The items configured for this drop-down.
       * @type {ProseMirrorDropDownEntry[]}
       */
      Object.defineProperty(this, "items", {value: items, writable: false});
      this.#icon = icon;
      this.#cssClass = cssClass;
      this.#onAction = onAction;
    }

    /* -------------------------------------------- */

    /**
     * The menu CSS class name.
     * @type {string}
     */
    #cssClass;

    /* -------------------------------------------- */

    /**
     * The icon to use instead of a text label, if any.
     * @type {string}
     */
    #icon;

    /* -------------------------------------------- */

    /**
     * The callback to fire when a menu item is clicked.
     * @type {function(MouseEvent)}
     */
    #onAction;

    /* -------------------------------------------- */

    /**
     * Attach event listeners.
     * @param {HTMLMenuElement} html  The root menu element.
     */
    activateListeners(html) {
      if ( !this.#onAction ) return;
      html.querySelector(`.pm-dropdown.${this.#cssClass}`).onclick = event => this.#onActivate(event);
    }

    /* -------------------------------------------- */

    /**
     * Construct the drop-down menu's HTML.
     * @returns {string}  HTML contents as a string.
     */
    render() {

      // Record which dropdown options are currently active
      const activeItems = [];
      this.forEachItem(item => {
        if ( !item.active ) return;
        activeItems.push(item);
      });
      activeItems.sort((a, b) => a.priority - b.priority);
      const activeItem = activeItems.shift();

      // Render the dropdown
      const active = game.i18n.localize(activeItem ? activeItem.title : this.title);
      const items = this.constructor._renderMenu(this.items);
      return `
      <button type="button" class="pm-dropdown ${this.#icon ? "icon" : ""} ${this.#cssClass}">
        ${this.#icon ? this.#icon : `<span>${active}</span>`}
        <i class="fa-solid fa-chevron-down"></i>
        ${items}
      </button>
    `;
    }

    /* -------------------------------------------- */

    /**
     * Recurse through the menu structure and apply a function to each item in it.
     * @param {function(ProseMirrorDropDownEntry):boolean} fn  The function to call on each item. Return false to prevent
     *                                                         iterating over any further items.
     */
    forEachItem(fn) {
      const forEach = items => {
        for ( const item of items ) {
          const result = fn(item);
          if ( result === false ) break;
          if ( item.children?.length ) forEach(item.children);
        }
      };
      forEach(this.items);
    }

    /* -------------------------------------------- */

    /**
     * Handle spawning a drop-down menu.
     * @param {PointerEvent} event  The triggering event.
     * @protected
     */
    #onActivate(event) {
      document.getElementById("prosemirror-dropdown")?.remove();
      const menu = event.currentTarget.querySelector(":scope > ul");
      if ( !menu ) return;
      const { top, left, bottom } = event.currentTarget.getBoundingClientRect();
      const dropdown = document.createElement("div");
      dropdown.id = "prosemirror-dropdown";
      // Apply theme if App V2.
      if ( menu.closest(".application") ) {
        dropdown.classList.add(document.body.classList.contains("theme-dark") ? "theme-dark" : "theme-light");
      }
      dropdown.append(menu.cloneNode(true));
      Object.assign(dropdown.style, { left: `${left}px`, top: `${bottom}px` });
      document.body.append(dropdown);
      dropdown.querySelectorAll(`li`).forEach(item => {
        item.onclick = event => this.#onAction(event);
        item.onpointerover = event => this.#onHoverItem(event);
      });
      requestAnimationFrame(() => {
        const { width, height } = dropdown.querySelector(":scope > ul").getBoundingClientRect();
        const { clientWidth, clientHeight } = document.documentElement;
        if ( left + width > clientWidth ) dropdown.style.left = `${left - width}px`;
        if ( bottom + height > clientHeight ) dropdown.style.top = `${top - height}px`;
      });
    }

    /* -------------------------------------------- */

    /**
     * Adjust menu position when hovering over items.
     * @param {PointerEvent} event  The triggering event.
     */
    #onHoverItem(event) {
      const menu = event.currentTarget.querySelector(":scope > ul");
      if ( !menu ) return;
      const { clientWidth, clientHeight } = document.documentElement;
      const { top } = event.currentTarget.getBoundingClientRect();
      const { x, width, height } = menu.getBoundingClientRect();
      if ( top + height > clientHeight ) menu.style.top = `-${top + height - clientHeight}px`;
      if ( x + width > clientWidth ) menu.style.left = `-${width}px`;
    }

    /* -------------------------------------------- */

    /**
     * Render a list of drop-down menu items.
     * @param {ProseMirrorDropDownEntry[]} entries  The menu items.
     * @returns {string}  HTML contents as a string.
     * @protected
     */
    static _renderMenu(entries) {
      const groups = entries.reduce((arr, item) => {
        const group = item.group ?? 0;
        arr[group] ??= [];
        arr[group].push(this._renderMenuItem(item));
        return arr;
      }, []);
      const items = groups.reduce((arr, group) => {
        if ( group?.length ) arr.push(group.join(""));
        return arr;
      }, []);
      return `<ul>${items.join('<li class="divider"></li>')}</ul>`;
    }

    /* -------------------------------------------- */

    /**
     * Render an individual drop-down menu item.
     * @param {ProseMirrorDropDownEntry} item  The menu item.
     * @returns {string}  HTML contents as a string.
     * @protected
     */
    static _renderMenuItem(item) {
      const parts = [`<li data-action="${item.action}" class="${item.class ?? ""}">`];
      parts.push(`<span style="${item.style ?? ""}">${game.i18n.localize(item.title)}</span>`);
      if ( item.active && !item.children?.length ) parts.push('<i class="fa-solid fa-check"></i>');
      if ( item.children?.length ) {
        parts.push('<i class="fa-solid fa-chevron-right"></i>', this._renderMenu(item.children));
      }
      parts.push("</li>");
      return parts.join("");
    }
  }

  // src/index.ts

  // src/tablemap.ts
  var readFromCache;
  var addToCache;
  if (typeof WeakMap != "undefined") {
    let cache = /* @__PURE__ */ new WeakMap();
    readFromCache = (key) => cache.get(key);
    addToCache = (key, value) => {
      cache.set(key, value);
      return value;
    };
  } else {
    const cache = [];
    const cacheSize = 10;
    let cachePos = 0;
    readFromCache = (key) => {
      for (let i = 0; i < cache.length; i += 2)
        if (cache[i] == key)
          return cache[i + 1];
    };
    addToCache = (key, value) => {
      if (cachePos == cacheSize)
        cachePos = 0;
      cache[cachePos++] = key;
      return cache[cachePos++] = value;
    };
  }
  var TableMap = class {
    constructor(width, height, map, problems) {
      this.width = width;
      this.height = height;
      this.map = map;
      this.problems = problems;
    }
    // Find the dimensions of the cell at the given position.
    findCell(pos) {
      for (let i = 0; i < this.map.length; i++) {
        const curPos = this.map[i];
        if (curPos != pos)
          continue;
        const left = i % this.width;
        const top = i / this.width | 0;
        let right = left + 1;
        let bottom = top + 1;
        for (let j = 1; right < this.width && this.map[i + j] == curPos; j++) {
          right++;
        }
        for (let j = 1; bottom < this.height && this.map[i + this.width * j] == curPos; j++) {
          bottom++;
        }
        return { left, top, right, bottom };
      }
      throw new RangeError(`No cell with offset ${pos} found`);
    }
    // Find the left side of the cell at the given position.
    colCount(pos) {
      for (let i = 0; i < this.map.length; i++) {
        if (this.map[i] == pos) {
          return i % this.width;
        }
      }
      throw new RangeError(`No cell with offset ${pos} found`);
    }
    // Find the next cell in the given direction, starting from the cell
    // at `pos`, if any.
    nextCell(pos, axis, dir) {
      const { left, right, top, bottom } = this.findCell(pos);
      if (axis == "horiz") {
        if (dir < 0 ? left == 0 : right == this.width)
          return null;
        return this.map[top * this.width + (dir < 0 ? left - 1 : right)];
      } else {
        if (dir < 0 ? top == 0 : bottom == this.height)
          return null;
        return this.map[left + this.width * (dir < 0 ? top - 1 : bottom)];
      }
    }
    // Get the rectangle spanning the two given cells.
    rectBetween(a, b) {
      const {
        left: leftA,
        right: rightA,
        top: topA,
        bottom: bottomA
      } = this.findCell(a);
      const {
        left: leftB,
        right: rightB,
        top: topB,
        bottom: bottomB
      } = this.findCell(b);
      return {
        left: Math.min(leftA, leftB),
        top: Math.min(topA, topB),
        right: Math.max(rightA, rightB),
        bottom: Math.max(bottomA, bottomB)
      };
    }
    // Return the position of all cells that have the top left corner in
    // the given rectangle.
    cellsInRect(rect) {
      const result = [];
      const seen = {};
      for (let row = rect.top; row < rect.bottom; row++) {
        for (let col = rect.left; col < rect.right; col++) {
          const index = row * this.width + col;
          const pos = this.map[index];
          if (seen[pos])
            continue;
          seen[pos] = true;
          if (col == rect.left && col && this.map[index - 1] == pos || row == rect.top && row && this.map[index - this.width] == pos) {
            continue;
          }
          result.push(pos);
        }
      }
      return result;
    }
    // Return the position at which the cell at the given row and column
    // starts, or would start, if a cell started there.
    positionAt(row, col, table) {
      for (let i = 0, rowStart = 0; ; i++) {
        const rowEnd = rowStart + table.child(i).nodeSize;
        if (i == row) {
          let index = col + row * this.width;
          const rowEndIndex = (row + 1) * this.width;
          while (index < rowEndIndex && this.map[index] < rowStart)
            index++;
          return index == rowEndIndex ? rowEnd - 1 : this.map[index];
        }
        rowStart = rowEnd;
      }
    }
    // Find the table map for the given table node.
    static get(table) {
      return readFromCache(table) || addToCache(table, computeMap(table));
    }
  };
  function computeMap(table) {
    if (table.type.spec.tableRole != "table")
      throw new RangeError("Not a table node: " + table.type.name);
    const width = findWidth(table), height = table.childCount;
    const map = [];
    let mapPos = 0;
    let problems = null;
    const colWidths = [];
    for (let i = 0, e = width * height; i < e; i++)
      map[i] = 0;
    for (let row = 0, pos = 0; row < height; row++) {
      const rowNode = table.child(row);
      pos++;
      for (let i = 0; ; i++) {
        while (mapPos < map.length && map[mapPos] != 0)
          mapPos++;
        if (i == rowNode.childCount)
          break;
        const cellNode = rowNode.child(i);
        const { colspan, rowspan, colwidth } = cellNode.attrs;
        for (let h = 0; h < rowspan; h++) {
          if (h + row >= height) {
            (problems || (problems = [])).push({
              type: "overlong_rowspan",
              pos,
              n: rowspan - h
            });
            break;
          }
          const start = mapPos + h * width;
          for (let w = 0; w < colspan; w++) {
            if (map[start + w] == 0)
              map[start + w] = pos;
            else
              (problems || (problems = [])).push({
                type: "collision",
                row,
                pos,
                n: colspan - w
              });
            const colW = colwidth && colwidth[w];
            if (colW) {
              const widthIndex = (start + w) % width * 2, prev = colWidths[widthIndex];
              if (prev == null || prev != colW && colWidths[widthIndex + 1] == 1) {
                colWidths[widthIndex] = colW;
                colWidths[widthIndex + 1] = 1;
              } else if (prev == colW) {
                colWidths[widthIndex + 1]++;
              }
            }
          }
        }
        mapPos += colspan;
        pos += cellNode.nodeSize;
      }
      const expectedPos = (row + 1) * width;
      let missing = 0;
      while (mapPos < expectedPos)
        if (map[mapPos++] == 0)
          missing++;
      if (missing)
        (problems || (problems = [])).push({ type: "missing", row, n: missing });
      pos++;
    }
    const tableMap = new TableMap(width, height, map, problems);
    let badWidths = false;
    for (let i = 0; !badWidths && i < colWidths.length; i += 2)
      if (colWidths[i] != null && colWidths[i + 1] < height)
        badWidths = true;
    if (badWidths)
      findBadColWidths(tableMap, colWidths, table);
    return tableMap;
  }
  function findWidth(table) {
    let width = -1;
    let hasRowSpan = false;
    for (let row = 0; row < table.childCount; row++) {
      const rowNode = table.child(row);
      let rowWidth = 0;
      if (hasRowSpan)
        for (let j = 0; j < row; j++) {
          const prevRow = table.child(j);
          for (let i = 0; i < prevRow.childCount; i++) {
            const cell = prevRow.child(i);
            if (j + cell.attrs.rowspan > row)
              rowWidth += cell.attrs.colspan;
          }
        }
      for (let i = 0; i < rowNode.childCount; i++) {
        const cell = rowNode.child(i);
        rowWidth += cell.attrs.colspan;
        if (cell.attrs.rowspan > 1)
          hasRowSpan = true;
      }
      if (width == -1)
        width = rowWidth;
      else if (width != rowWidth)
        width = Math.max(width, rowWidth);
    }
    return width;
  }
  function findBadColWidths(map, colWidths, table) {
    if (!map.problems)
      map.problems = [];
    const seen = {};
    for (let i = 0; i < map.map.length; i++) {
      const pos = map.map[i];
      if (seen[pos])
        continue;
      seen[pos] = true;
      const node = table.nodeAt(pos);
      if (!node) {
        throw new RangeError(`No cell with offset ${pos} found`);
      }
      let updated = null;
      const attrs = node.attrs;
      for (let j = 0; j < attrs.colspan; j++) {
        const col = (i + j) % map.width;
        const colWidth = colWidths[col * 2];
        if (colWidth != null && (!attrs.colwidth || attrs.colwidth[j] != colWidth))
          (updated || (updated = freshColWidth(attrs)))[j] = colWidth;
      }
      if (updated)
        map.problems.unshift({
          type: "colwidth mismatch",
          pos,
          colwidth: updated
        });
    }
  }
  function freshColWidth(attrs) {
    if (attrs.colwidth)
      return attrs.colwidth.slice();
    const result = [];
    for (let i = 0; i < attrs.colspan; i++)
      result.push(0);
    return result;
  }

  // src/schema.ts
  function getCellAttrs(dom, extraAttrs) {
    if (typeof dom === "string") {
      return {};
    }
    const widthAttr = dom.getAttribute("data-colwidth");
    const widths = widthAttr && /^\d+(,\d+)*$/.test(widthAttr) ? widthAttr.split(",").map((s) => Number(s)) : null;
    const colspan = Number(dom.getAttribute("colspan") || 1);
    const result = {
      colspan,
      rowspan: Number(dom.getAttribute("rowspan") || 1),
      colwidth: widths && widths.length == colspan ? widths : null
    };
    for (const prop in extraAttrs) {
      const getter = extraAttrs[prop].getFromDOM;
      const value = getter && getter(dom);
      if (value != null) {
        result[prop] = value;
      }
    }
    return result;
  }
  function setCellAttrs(node, extraAttrs) {
    const attrs = {};
    if (node.attrs.colspan != 1)
      attrs.colspan = node.attrs.colspan;
    if (node.attrs.rowspan != 1)
      attrs.rowspan = node.attrs.rowspan;
    if (node.attrs.colwidth)
      attrs["data-colwidth"] = node.attrs.colwidth.join(",");
    for (const prop in extraAttrs) {
      const setter = extraAttrs[prop].setDOMAttr;
      if (setter)
        setter(node.attrs[prop], attrs);
    }
    return attrs;
  }
  function tableNodes(options) {
    const extraAttrs = options.cellAttributes || {};
    const cellAttrs = {
      colspan: { default: 1 },
      rowspan: { default: 1 },
      colwidth: { default: null }
    };
    for (const prop in extraAttrs)
      cellAttrs[prop] = { default: extraAttrs[prop].default };
    return {
      table: {
        content: "table_row+",
        tableRole: "table",
        isolating: true,
        group: options.tableGroup,
        parseDOM: [{ tag: "table" }],
        toDOM() {
          return ["table", ["tbody", 0]];
        }
      },
      table_row: {
        content: "(table_cell | table_header)*",
        tableRole: "row",
        parseDOM: [{ tag: "tr" }],
        toDOM() {
          return ["tr", 0];
        }
      },
      table_cell: {
        content: options.cellContent,
        attrs: cellAttrs,
        tableRole: "cell",
        isolating: true,
        parseDOM: [
          { tag: "td", getAttrs: (dom) => getCellAttrs(dom, extraAttrs) }
        ],
        toDOM(node) {
          return ["td", setCellAttrs(node, extraAttrs), 0];
        }
      },
      table_header: {
        content: options.cellContent,
        attrs: cellAttrs,
        tableRole: "header_cell",
        isolating: true,
        parseDOM: [
          { tag: "th", getAttrs: (dom) => getCellAttrs(dom, extraAttrs) }
        ],
        toDOM(node) {
          return ["th", setCellAttrs(node, extraAttrs), 0];
        }
      }
    };
  }
  function tableNodeTypes(schema) {
    let result = schema.cached.tableNodeTypes;
    if (!result) {
      result = schema.cached.tableNodeTypes = {};
      for (const name in schema.nodes) {
        const type = schema.nodes[name], role = type.spec.tableRole;
        if (role)
          result[role] = type;
      }
    }
    return result;
  }

  // src/util.ts
  var tableEditingKey = new PluginKey("selectingCells");
  function cellAround($pos) {
    for (let d = $pos.depth - 1; d > 0; d--)
      if ($pos.node(d).type.spec.tableRole == "row")
        return $pos.node(0).resolve($pos.before(d + 1));
    return null;
  }
  function cellWrapping($pos) {
    for (let d = $pos.depth; d > 0; d--) {
      const role = $pos.node(d).type.spec.tableRole;
      if (role === "cell" || role === "header_cell")
        return $pos.node(d);
    }
    return null;
  }
  function isInTable(state) {
    const $head = state.selection.$head;
    for (let d = $head.depth; d > 0; d--)
      if ($head.node(d).type.spec.tableRole == "row")
        return true;
    return false;
  }
  function selectionCell(state) {
    const sel = state.selection;
    if ("$anchorCell" in sel && sel.$anchorCell) {
      return sel.$anchorCell.pos > sel.$headCell.pos ? sel.$anchorCell : sel.$headCell;
    } else if ("node" in sel && sel.node && sel.node.type.spec.tableRole == "cell") {
      return sel.$anchor;
    }
    const $cell = cellAround(sel.$head) || cellNear(sel.$head);
    if ($cell) {
      return $cell;
    }
    throw new RangeError(`No cell found around position ${sel.head}`);
  }
  function cellNear($pos) {
    for (let after = $pos.nodeAfter, pos = $pos.pos; after; after = after.firstChild, pos++) {
      const role = after.type.spec.tableRole;
      if (role == "cell" || role == "header_cell")
        return $pos.doc.resolve(pos);
    }
    for (let before = $pos.nodeBefore, pos = $pos.pos; before; before = before.lastChild, pos--) {
      const role = before.type.spec.tableRole;
      if (role == "cell" || role == "header_cell")
        return $pos.doc.resolve(pos - before.nodeSize);
    }
  }
  function pointsAtCell($pos) {
    return $pos.parent.type.spec.tableRole == "row" && !!$pos.nodeAfter;
  }
  function moveCellForward($pos) {
    return $pos.node(0).resolve($pos.pos + $pos.nodeAfter.nodeSize);
  }
  function inSameTable($cellA, $cellB) {
    return $cellA.depth == $cellB.depth && $cellA.pos >= $cellB.start(-1) && $cellA.pos <= $cellB.end(-1);
  }
  function findCell($pos) {
    return TableMap.get($pos.node(-1)).findCell($pos.pos - $pos.start(-1));
  }
  function colCount($pos) {
    return TableMap.get($pos.node(-1)).colCount($pos.pos - $pos.start(-1));
  }
  function nextCell($pos, axis, dir) {
    const table = $pos.node(-1);
    const map = TableMap.get(table);
    const tableStart = $pos.start(-1);
    const moved = map.nextCell($pos.pos - tableStart, axis, dir);
    return moved == null ? null : $pos.node(0).resolve(tableStart + moved);
  }
  function removeColSpan(attrs, pos, n = 1) {
    const result = { ...attrs, colspan: attrs.colspan - n };
    if (result.colwidth) {
      result.colwidth = result.colwidth.slice();
      result.colwidth.splice(pos, n);
      if (!result.colwidth.some((w) => w > 0))
        result.colwidth = null;
    }
    return result;
  }
  function addColSpan(attrs, pos, n = 1) {
    const result = { ...attrs, colspan: attrs.colspan + n };
    if (result.colwidth) {
      result.colwidth = result.colwidth.slice();
      for (let i = 0; i < n; i++)
        result.colwidth.splice(pos, 0, 0);
    }
    return result;
  }
  function columnIsHeader(map, table, col) {
    const headerCell = tableNodeTypes(table.type.schema).header_cell;
    for (let row = 0; row < map.height; row++)
      if (table.nodeAt(map.map[col + row * map.width]).type != headerCell)
        return false;
    return true;
  }

  // src/cellselection.ts
  var CellSelection = class _CellSelection extends Selection {
    // A table selection is identified by its anchor and head cells. The
    // positions given to this constructor should point _before_ two
    // cells in the same table. They may be the same, to select a single
    // cell.
    constructor($anchorCell, $headCell = $anchorCell) {
      const table = $anchorCell.node(-1);
      const map = TableMap.get(table);
      const tableStart = $anchorCell.start(-1);
      const rect = map.rectBetween(
        $anchorCell.pos - tableStart,
        $headCell.pos - tableStart
      );
      const doc = $anchorCell.node(0);
      const cells = map.cellsInRect(rect).filter((p) => p != $headCell.pos - tableStart);
      cells.unshift($headCell.pos - tableStart);
      const ranges = cells.map((pos) => {
        const cell = table.nodeAt(pos);
        if (!cell) {
          throw RangeError(`No cell with offset ${pos} found`);
        }
        const from = tableStart + pos + 1;
        return new SelectionRange(
          doc.resolve(from),
          doc.resolve(from + cell.content.size)
        );
      });
      super(ranges[0].$from, ranges[0].$to, ranges);
      this.$anchorCell = $anchorCell;
      this.$headCell = $headCell;
    }
    map(doc, mapping) {
      const $anchorCell = doc.resolve(mapping.map(this.$anchorCell.pos));
      const $headCell = doc.resolve(mapping.map(this.$headCell.pos));
      if (pointsAtCell($anchorCell) && pointsAtCell($headCell) && inSameTable($anchorCell, $headCell)) {
        const tableChanged = this.$anchorCell.node(-1) != $anchorCell.node(-1);
        if (tableChanged && this.isRowSelection())
          return _CellSelection.rowSelection($anchorCell, $headCell);
        else if (tableChanged && this.isColSelection())
          return _CellSelection.colSelection($anchorCell, $headCell);
        else
          return new _CellSelection($anchorCell, $headCell);
      }
      return TextSelection.between($anchorCell, $headCell);
    }
    // Returns a rectangular slice of table rows containing the selected
    // cells.
    content() {
      const table = this.$anchorCell.node(-1);
      const map = TableMap.get(table);
      const tableStart = this.$anchorCell.start(-1);
      const rect = map.rectBetween(
        this.$anchorCell.pos - tableStart,
        this.$headCell.pos - tableStart
      );
      const seen = {};
      const rows = [];
      for (let row = rect.top; row < rect.bottom; row++) {
        const rowContent = [];
        for (let index = row * map.width + rect.left, col = rect.left; col < rect.right; col++, index++) {
          const pos = map.map[index];
          if (seen[pos])
            continue;
          seen[pos] = true;
          const cellRect = map.findCell(pos);
          let cell = table.nodeAt(pos);
          if (!cell) {
            throw RangeError(`No cell with offset ${pos} found`);
          }
          const extraLeft = rect.left - cellRect.left;
          const extraRight = cellRect.right - rect.right;
          if (extraLeft > 0 || extraRight > 0) {
            let attrs = cell.attrs;
            if (extraLeft > 0) {
              attrs = removeColSpan(attrs, 0, extraLeft);
            }
            if (extraRight > 0) {
              attrs = removeColSpan(
                attrs,
                attrs.colspan - extraRight,
                extraRight
              );
            }
            if (cellRect.left < rect.left) {
              cell = cell.type.createAndFill(attrs);
              if (!cell) {
                throw RangeError(
                  `Could not create cell with attrs ${JSON.stringify(attrs)}`
                );
              }
            } else {
              cell = cell.type.create(attrs, cell.content);
            }
          }
          if (cellRect.top < rect.top || cellRect.bottom > rect.bottom) {
            const attrs = {
              ...cell.attrs,
              rowspan: Math.min(cellRect.bottom, rect.bottom) - Math.max(cellRect.top, rect.top)
            };
            if (cellRect.top < rect.top) {
              cell = cell.type.createAndFill(attrs);
            } else {
              cell = cell.type.create(attrs, cell.content);
            }
          }
          rowContent.push(cell);
        }
        rows.push(table.child(row).copy(Fragment.from(rowContent)));
      }
      const fragment = this.isColSelection() && this.isRowSelection() ? table : rows;
      return new Slice(Fragment.from(fragment), 1, 1);
    }
    replace(tr, content = Slice.empty) {
      const mapFrom = tr.steps.length, ranges = this.ranges;
      for (let i = 0; i < ranges.length; i++) {
        const { $from, $to } = ranges[i], mapping = tr.mapping.slice(mapFrom);
        tr.replace(
          mapping.map($from.pos),
          mapping.map($to.pos),
          i ? Slice.empty : content
        );
      }
      const sel = Selection.findFrom(
        tr.doc.resolve(tr.mapping.slice(mapFrom).map(this.to)),
        -1
      );
      if (sel)
        tr.setSelection(sel);
    }
    replaceWith(tr, node) {
      this.replace(tr, new Slice(Fragment.from(node), 0, 0));
    }
    forEachCell(f) {
      const table = this.$anchorCell.node(-1);
      const map = TableMap.get(table);
      const tableStart = this.$anchorCell.start(-1);
      const cells = map.cellsInRect(
        map.rectBetween(
          this.$anchorCell.pos - tableStart,
          this.$headCell.pos - tableStart
        )
      );
      for (let i = 0; i < cells.length; i++) {
        f(table.nodeAt(cells[i]), tableStart + cells[i]);
      }
    }
    // True if this selection goes all the way from the top to the
    // bottom of the table.
    isColSelection() {
      const anchorTop = this.$anchorCell.index(-1);
      const headTop = this.$headCell.index(-1);
      if (Math.min(anchorTop, headTop) > 0)
        return false;
      const anchorBottom = anchorTop + this.$anchorCell.nodeAfter.attrs.rowspan;
      const headBottom = headTop + this.$headCell.nodeAfter.attrs.rowspan;
      return Math.max(anchorBottom, headBottom) == this.$headCell.node(-1).childCount;
    }
    // Returns the smallest column selection that covers the given anchor
    // and head cell.
    static colSelection($anchorCell, $headCell = $anchorCell) {
      const table = $anchorCell.node(-1);
      const map = TableMap.get(table);
      const tableStart = $anchorCell.start(-1);
      const anchorRect = map.findCell($anchorCell.pos - tableStart);
      const headRect = map.findCell($headCell.pos - tableStart);
      const doc = $anchorCell.node(0);
      if (anchorRect.top <= headRect.top) {
        if (anchorRect.top > 0)
          $anchorCell = doc.resolve(tableStart + map.map[anchorRect.left]);
        if (headRect.bottom < map.height)
          $headCell = doc.resolve(
            tableStart + map.map[map.width * (map.height - 1) + headRect.right - 1]
          );
      } else {
        if (headRect.top > 0)
          $headCell = doc.resolve(tableStart + map.map[headRect.left]);
        if (anchorRect.bottom < map.height)
          $anchorCell = doc.resolve(
            tableStart + map.map[map.width * (map.height - 1) + anchorRect.right - 1]
          );
      }
      return new _CellSelection($anchorCell, $headCell);
    }
    // True if this selection goes all the way from the left to the
    // right of the table.
    isRowSelection() {
      const table = this.$anchorCell.node(-1);
      const map = TableMap.get(table);
      const tableStart = this.$anchorCell.start(-1);
      const anchorLeft = map.colCount(this.$anchorCell.pos - tableStart);
      const headLeft = map.colCount(this.$headCell.pos - tableStart);
      if (Math.min(anchorLeft, headLeft) > 0)
        return false;
      const anchorRight = anchorLeft + this.$anchorCell.nodeAfter.attrs.colspan;
      const headRight = headLeft + this.$headCell.nodeAfter.attrs.colspan;
      return Math.max(anchorRight, headRight) == map.width;
    }
    eq(other) {
      return other instanceof _CellSelection && other.$anchorCell.pos == this.$anchorCell.pos && other.$headCell.pos == this.$headCell.pos;
    }
    // Returns the smallest row selection that covers the given anchor
    // and head cell.
    static rowSelection($anchorCell, $headCell = $anchorCell) {
      const table = $anchorCell.node(-1);
      const map = TableMap.get(table);
      const tableStart = $anchorCell.start(-1);
      const anchorRect = map.findCell($anchorCell.pos - tableStart);
      const headRect = map.findCell($headCell.pos - tableStart);
      const doc = $anchorCell.node(0);
      if (anchorRect.left <= headRect.left) {
        if (anchorRect.left > 0)
          $anchorCell = doc.resolve(
            tableStart + map.map[anchorRect.top * map.width]
          );
        if (headRect.right < map.width)
          $headCell = doc.resolve(
            tableStart + map.map[map.width * (headRect.top + 1) - 1]
          );
      } else {
        if (headRect.left > 0)
          $headCell = doc.resolve(tableStart + map.map[headRect.top * map.width]);
        if (anchorRect.right < map.width)
          $anchorCell = doc.resolve(
            tableStart + map.map[map.width * (anchorRect.top + 1) - 1]
          );
      }
      return new _CellSelection($anchorCell, $headCell);
    }
    toJSON() {
      return {
        type: "cell",
        anchor: this.$anchorCell.pos,
        head: this.$headCell.pos
      };
    }
    static fromJSON(doc, json) {
      return new _CellSelection(doc.resolve(json.anchor), doc.resolve(json.head));
    }
    static create(doc, anchorCell, headCell = anchorCell) {
      return new _CellSelection(doc.resolve(anchorCell), doc.resolve(headCell));
    }
    getBookmark() {
      return new CellBookmark(this.$anchorCell.pos, this.$headCell.pos);
    }
  };
  CellSelection.prototype.visible = false;
  Selection.jsonID("cell", CellSelection);
  var CellBookmark = class _CellBookmark {
    constructor(anchor, head) {
      this.anchor = anchor;
      this.head = head;
    }
    map(mapping) {
      return new _CellBookmark(mapping.map(this.anchor), mapping.map(this.head));
    }
    resolve(doc) {
      const $anchorCell = doc.resolve(this.anchor), $headCell = doc.resolve(this.head);
      if ($anchorCell.parent.type.spec.tableRole == "row" && $headCell.parent.type.spec.tableRole == "row" && $anchorCell.index() < $anchorCell.parent.childCount && $headCell.index() < $headCell.parent.childCount && inSameTable($anchorCell, $headCell))
        return new CellSelection($anchorCell, $headCell);
      else
        return Selection.near($headCell, 1);
    }
  };
  function drawCellSelection(state) {
    if (!(state.selection instanceof CellSelection))
      return null;
    const cells = [];
    state.selection.forEachCell((node, pos) => {
      cells.push(
        Decoration.node(pos, pos + node.nodeSize, { class: "selectedCell" })
      );
    });
    return DecorationSet.create(state.doc, cells);
  }
  function isCellBoundarySelection({ $from, $to }) {
    if ($from.pos == $to.pos || $from.pos < $from.pos - 6)
      return false;
    let afterFrom = $from.pos;
    let beforeTo = $to.pos;
    let depth = $from.depth;
    for (; depth >= 0; depth--, afterFrom++)
      if ($from.after(depth + 1) < $from.end(depth))
        break;
    for (let d = $to.depth; d >= 0; d--, beforeTo--)
      if ($to.before(d + 1) > $to.start(d))
        break;
    return afterFrom == beforeTo && /row|table/.test($from.node(depth).type.spec.tableRole);
  }
  function isTextSelectionAcrossCells({ $from, $to }) {
    let fromCellBoundaryNode;
    let toCellBoundaryNode;
    for (let i = $from.depth; i > 0; i--) {
      const node = $from.node(i);
      if (node.type.spec.tableRole === "cell" || node.type.spec.tableRole === "header_cell") {
        fromCellBoundaryNode = node;
        break;
      }
    }
    for (let i = $to.depth; i > 0; i--) {
      const node = $to.node(i);
      if (node.type.spec.tableRole === "cell" || node.type.spec.tableRole === "header_cell") {
        toCellBoundaryNode = node;
        break;
      }
    }
    return fromCellBoundaryNode !== toCellBoundaryNode && $to.parentOffset === 0;
  }
  function normalizeSelection(state, tr, allowTableNodeSelection) {
    const sel = (tr || state).selection;
    const doc = (tr || state).doc;
    let normalize;
    let role;
    if (sel instanceof NodeSelection && (role = sel.node.type.spec.tableRole)) {
      if (role == "cell" || role == "header_cell") {
        normalize = CellSelection.create(doc, sel.from);
      } else if (role == "row") {
        const $cell = doc.resolve(sel.from + 1);
        normalize = CellSelection.rowSelection($cell, $cell);
      } else if (!allowTableNodeSelection) {
        const map = TableMap.get(sel.node);
        const start = sel.from + 1;
        const lastCell = start + map.map[map.width * map.height - 1];
        normalize = CellSelection.create(doc, start + 1, lastCell);
      }
    } else if (sel instanceof TextSelection && isCellBoundarySelection(sel)) {
      normalize = TextSelection.create(doc, sel.from);
    } else if (sel instanceof TextSelection && isTextSelectionAcrossCells(sel)) {
      normalize = TextSelection.create(doc, sel.$from.start(), sel.$from.end());
    }
    if (normalize)
      (tr || (tr = state.tr)).setSelection(normalize);
    return tr;
  }
  var fixTablesKey = new PluginKey("fix-tables");
  function changedDescendants(old, cur, offset, f) {
    const oldSize = old.childCount, curSize = cur.childCount;
    outer:
      for (let i = 0, j = 0; i < curSize; i++) {
        const child = cur.child(i);
        for (let scan = j, e = Math.min(oldSize, i + 3); scan < e; scan++) {
          if (old.child(scan) == child) {
            j = scan + 1;
            offset += child.nodeSize;
            continue outer;
          }
        }
        f(child, offset);
        if (j < oldSize && old.child(j).sameMarkup(child))
          changedDescendants(old.child(j), child, offset + 1, f);
        else
          child.nodesBetween(0, child.content.size, f, offset + 1);
        offset += child.nodeSize;
      }
  }
  function fixTables(state, oldState) {
    let tr;
    const check = (node, pos) => {
      if (node.type.spec.tableRole == "table")
        tr = fixTable(state, node, pos, tr);
    };
    if (!oldState)
      state.doc.descendants(check);
    else if (oldState.doc != state.doc)
      changedDescendants(oldState.doc, state.doc, 0, check);
    return tr;
  }
  function fixTable(state, table, tablePos, tr) {
    const map = TableMap.get(table);
    if (!map.problems)
      return tr;
    if (!tr)
      tr = state.tr;
    const mustAdd = [];
    for (let i = 0; i < map.height; i++)
      mustAdd.push(0);
    for (let i = 0; i < map.problems.length; i++) {
      const prob = map.problems[i];
      if (prob.type == "collision") {
        const cell = table.nodeAt(prob.pos);
        if (!cell)
          continue;
        const attrs = cell.attrs;
        for (let j = 0; j < attrs.rowspan; j++)
          mustAdd[prob.row + j] += prob.n;
        tr.setNodeMarkup(
          tr.mapping.map(tablePos + 1 + prob.pos),
          null,
          removeColSpan(attrs, attrs.colspan - prob.n, prob.n)
        );
      } else if (prob.type == "missing") {
        mustAdd[prob.row] += prob.n;
      } else if (prob.type == "overlong_rowspan") {
        const cell = table.nodeAt(prob.pos);
        if (!cell)
          continue;
        tr.setNodeMarkup(tr.mapping.map(tablePos + 1 + prob.pos), null, {
          ...cell.attrs,
          rowspan: cell.attrs.rowspan - prob.n
        });
      } else if (prob.type == "colwidth mismatch") {
        const cell = table.nodeAt(prob.pos);
        if (!cell)
          continue;
        tr.setNodeMarkup(tr.mapping.map(tablePos + 1 + prob.pos), null, {
          ...cell.attrs,
          colwidth: prob.colwidth
        });
      }
    }
    let first, last;
    for (let i = 0; i < mustAdd.length; i++)
      if (mustAdd[i]) {
        if (first == null)
          first = i;
        last = i;
      }
    for (let i = 0, pos = tablePos + 1; i < map.height; i++) {
      const row = table.child(i);
      const end = pos + row.nodeSize;
      const add = mustAdd[i];
      if (add > 0) {
        let role = "cell";
        if (row.firstChild) {
          role = row.firstChild.type.spec.tableRole;
        }
        const nodes = [];
        for (let j = 0; j < add; j++) {
          const node = tableNodeTypes(state.schema)[role].createAndFill();
          if (node)
            nodes.push(node);
        }
        const side = (i == 0 || first == i - 1) && last == i ? pos + 1 : end - 1;
        tr.insert(tr.mapping.map(side), nodes);
      }
      pos = end;
    }
    return tr.setMeta(fixTablesKey, { fixTables: true });
  }
  function pastedCells(slice) {
    if (!slice.size)
      return null;
    let { content, openStart, openEnd } = slice;
    while (content.childCount == 1 && (openStart > 0 && openEnd > 0 || content.child(0).type.spec.tableRole == "table")) {
      openStart--;
      openEnd--;
      content = content.child(0).content;
    }
    const first = content.child(0);
    const role = first.type.spec.tableRole;
    const schema = first.type.schema, rows = [];
    if (role == "row") {
      for (let i = 0; i < content.childCount; i++) {
        let cells = content.child(i).content;
        const left = i ? 0 : Math.max(0, openStart - 1);
        const right = i < content.childCount - 1 ? 0 : Math.max(0, openEnd - 1);
        if (left || right)
          cells = fitSlice(
            tableNodeTypes(schema).row,
            new Slice(cells, left, right)
          ).content;
        rows.push(cells);
      }
    } else if (role == "cell" || role == "header_cell") {
      rows.push(
        openStart || openEnd ? fitSlice(
          tableNodeTypes(schema).row,
          new Slice(content, openStart, openEnd)
        ).content : content
      );
    } else {
      return null;
    }
    return ensureRectangular(schema, rows);
  }
  function ensureRectangular(schema, rows) {
    const widths = [];
    for (let i = 0; i < rows.length; i++) {
      const row = rows[i];
      for (let j = row.childCount - 1; j >= 0; j--) {
        const { rowspan, colspan } = row.child(j).attrs;
        for (let r = i; r < i + rowspan; r++)
          widths[r] = (widths[r] || 0) + colspan;
      }
    }
    let width = 0;
    for (let r = 0; r < widths.length; r++)
      width = Math.max(width, widths[r]);
    for (let r = 0; r < widths.length; r++) {
      if (r >= rows.length)
        rows.push(Fragment.empty);
      if (widths[r] < width) {
        const empty = tableNodeTypes(schema).cell.createAndFill();
        const cells = [];
        for (let i = widths[r]; i < width; i++) {
          cells.push(empty);
        }
        rows[r] = rows[r].append(Fragment.from(cells));
      }
    }
    return { height: rows.length, width, rows };
  }
  function fitSlice(nodeType, slice) {
    const node = nodeType.createAndFill();
    const tr = new Transform(node).replace(0, node.content.size, slice);
    return tr.doc;
  }
  function clipCells({ width, height, rows }, newWidth, newHeight) {
    if (width != newWidth) {
      const added = [];
      const newRows = [];
      for (let row = 0; row < rows.length; row++) {
        const frag = rows[row], cells = [];
        for (let col = added[row] || 0, i = 0; col < newWidth; i++) {
          let cell = frag.child(i % frag.childCount);
          if (col + cell.attrs.colspan > newWidth)
            cell = cell.type.createChecked(
              removeColSpan(
                cell.attrs,
                cell.attrs.colspan,
                col + cell.attrs.colspan - newWidth
              ),
              cell.content
            );
          cells.push(cell);
          col += cell.attrs.colspan;
          for (let j = 1; j < cell.attrs.rowspan; j++)
            added[row + j] = (added[row + j] || 0) + cell.attrs.colspan;
        }
        newRows.push(Fragment.from(cells));
      }
      rows = newRows;
      width = newWidth;
    }
    if (height != newHeight) {
      const newRows = [];
      for (let row = 0, i = 0; row < newHeight; row++, i++) {
        const cells = [], source = rows[i % height];
        for (let j = 0; j < source.childCount; j++) {
          let cell = source.child(j);
          if (row + cell.attrs.rowspan > newHeight)
            cell = cell.type.create(
              {
                ...cell.attrs,
                rowspan: Math.max(1, newHeight - cell.attrs.rowspan)
              },
              cell.content
            );
          cells.push(cell);
        }
        newRows.push(Fragment.from(cells));
      }
      rows = newRows;
      height = newHeight;
    }
    return { width, height, rows };
  }
  function growTable(tr, map, table, start, width, height, mapFrom) {
    const schema = tr.doc.type.schema;
    const types = tableNodeTypes(schema);
    let empty;
    let emptyHead;
    if (width > map.width) {
      for (let row = 0, rowEnd = 0; row < map.height; row++) {
        const rowNode = table.child(row);
        rowEnd += rowNode.nodeSize;
        const cells = [];
        let add;
        if (rowNode.lastChild == null || rowNode.lastChild.type == types.cell)
          add = empty || (empty = types.cell.createAndFill());
        else
          add = emptyHead || (emptyHead = types.header_cell.createAndFill());
        for (let i = map.width; i < width; i++)
          cells.push(add);
        tr.insert(tr.mapping.slice(mapFrom).map(rowEnd - 1 + start), cells);
      }
    }
    if (height > map.height) {
      const cells = [];
      for (let i = 0, start2 = (map.height - 1) * map.width; i < Math.max(map.width, width); i++) {
        const header = i >= map.width ? false : table.nodeAt(map.map[start2 + i]).type == types.header_cell;
        cells.push(
          header ? emptyHead || (emptyHead = types.header_cell.createAndFill()) : empty || (empty = types.cell.createAndFill())
        );
      }
      const emptyRow = types.row.create(null, Fragment.from(cells)), rows = [];
      for (let i = map.height; i < height; i++)
        rows.push(emptyRow);
      tr.insert(tr.mapping.slice(mapFrom).map(start + table.nodeSize - 2), rows);
    }
    return !!(empty || emptyHead);
  }
  function isolateHorizontal(tr, map, table, start, left, right, top, mapFrom) {
    if (top == 0 || top == map.height)
      return false;
    let found = false;
    for (let col = left; col < right; col++) {
      const index = top * map.width + col, pos = map.map[index];
      if (map.map[index - map.width] == pos) {
        found = true;
        const cell = table.nodeAt(pos);
        const { top: cellTop, left: cellLeft } = map.findCell(pos);
        tr.setNodeMarkup(tr.mapping.slice(mapFrom).map(pos + start), null, {
          ...cell.attrs,
          rowspan: top - cellTop
        });
        tr.insert(
          tr.mapping.slice(mapFrom).map(map.positionAt(top, cellLeft, table)),
          cell.type.createAndFill({
            ...cell.attrs,
            rowspan: cellTop + cell.attrs.rowspan - top
          })
        );
        col += cell.attrs.colspan - 1;
      }
    }
    return found;
  }
  function isolateVertical(tr, map, table, start, top, bottom, left, mapFrom) {
    if (left == 0 || left == map.width)
      return false;
    let found = false;
    for (let row = top; row < bottom; row++) {
      const index = row * map.width + left, pos = map.map[index];
      if (map.map[index - 1] == pos) {
        found = true;
        const cell = table.nodeAt(pos);
        const cellLeft = map.colCount(pos);
        const updatePos = tr.mapping.slice(mapFrom).map(pos + start);
        tr.setNodeMarkup(
          updatePos,
          null,
          removeColSpan(
            cell.attrs,
            left - cellLeft,
            cell.attrs.colspan - (left - cellLeft)
          )
        );
        tr.insert(
          updatePos + cell.nodeSize,
          cell.type.createAndFill(
            removeColSpan(cell.attrs, 0, left - cellLeft)
          )
        );
        row += cell.attrs.rowspan - 1;
      }
    }
    return found;
  }
  function insertCells(state, dispatch, tableStart, rect, cells) {
    let table = tableStart ? state.doc.nodeAt(tableStart - 1) : state.doc;
    if (!table) {
      throw new Error("No table found");
    }
    let map = TableMap.get(table);
    const { top, left } = rect;
    const right = left + cells.width, bottom = top + cells.height;
    const tr = state.tr;
    let mapFrom = 0;
    function recomp() {
      table = tableStart ? tr.doc.nodeAt(tableStart - 1) : tr.doc;
      if (!table) {
        throw new Error("No table found");
      }
      map = TableMap.get(table);
      mapFrom = tr.mapping.maps.length;
    }
    if (growTable(tr, map, table, tableStart, right, bottom, mapFrom))
      recomp();
    if (isolateHorizontal(tr, map, table, tableStart, left, right, top, mapFrom))
      recomp();
    if (isolateHorizontal(tr, map, table, tableStart, left, right, bottom, mapFrom))
      recomp();
    if (isolateVertical(tr, map, table, tableStart, top, bottom, left, mapFrom))
      recomp();
    if (isolateVertical(tr, map, table, tableStart, top, bottom, right, mapFrom))
      recomp();
    for (let row = top; row < bottom; row++) {
      const from = map.positionAt(row, left, table), to = map.positionAt(row, right, table);
      tr.replace(
        tr.mapping.slice(mapFrom).map(from + tableStart),
        tr.mapping.slice(mapFrom).map(to + tableStart),
        new Slice(cells.rows[row - top], 0, 0)
      );
    }
    recomp();
    tr.setSelection(
      new CellSelection(
        tr.doc.resolve(tableStart + map.positionAt(top, left, table)),
        tr.doc.resolve(tableStart + map.positionAt(bottom - 1, right - 1, table))
      )
    );
    dispatch(tr);
  }

  // src/input.ts
  var handleKeyDown = keydownHandler({
    ArrowLeft: arrow("horiz", -1),
    ArrowRight: arrow("horiz", 1),
    ArrowUp: arrow("vert", -1),
    ArrowDown: arrow("vert", 1),
    "Shift-ArrowLeft": shiftArrow("horiz", -1),
    "Shift-ArrowRight": shiftArrow("horiz", 1),
    "Shift-ArrowUp": shiftArrow("vert", -1),
    "Shift-ArrowDown": shiftArrow("vert", 1),
    Backspace: deleteCellSelection,
    "Mod-Backspace": deleteCellSelection,
    Delete: deleteCellSelection,
    "Mod-Delete": deleteCellSelection
  });
  function maybeSetSelection(state, dispatch, selection) {
    if (selection.eq(state.selection))
      return false;
    if (dispatch)
      dispatch(state.tr.setSelection(selection).scrollIntoView());
    return true;
  }
  function arrow(axis, dir) {
    return (state, dispatch, view) => {
      if (!view)
        return false;
      const sel = state.selection;
      if (sel instanceof CellSelection) {
        return maybeSetSelection(
          state,
          dispatch,
          Selection.near(sel.$headCell, dir)
        );
      }
      if (axis != "horiz" && !sel.empty)
        return false;
      const end = atEndOfCell(view, axis, dir);
      if (end == null)
        return false;
      if (axis == "horiz") {
        return maybeSetSelection(
          state,
          dispatch,
          Selection.near(state.doc.resolve(sel.head + dir), dir)
        );
      } else {
        const $cell = state.doc.resolve(end);
        const $next = nextCell($cell, axis, dir);
        let newSel;
        if ($next)
          newSel = Selection.near($next, 1);
        else if (dir < 0)
          newSel = Selection.near(state.doc.resolve($cell.before(-1)), -1);
        else
          newSel = Selection.near(state.doc.resolve($cell.after(-1)), 1);
        return maybeSetSelection(state, dispatch, newSel);
      }
    };
  }
  function shiftArrow(axis, dir) {
    return (state, dispatch, view) => {
      if (!view)
        return false;
      const sel = state.selection;
      let cellSel;
      if (sel instanceof CellSelection) {
        cellSel = sel;
      } else {
        const end = atEndOfCell(view, axis, dir);
        if (end == null)
          return false;
        cellSel = new CellSelection(state.doc.resolve(end));
      }
      const $head = nextCell(cellSel.$headCell, axis, dir);
      if (!$head)
        return false;
      return maybeSetSelection(
        state,
        dispatch,
        new CellSelection(cellSel.$anchorCell, $head)
      );
    };
  }
  function deleteCellSelection(state, dispatch) {
    const sel = state.selection;
    if (!(sel instanceof CellSelection))
      return false;
    if (dispatch) {
      const tr = state.tr;
      const baseContent = tableNodeTypes(state.schema).cell.createAndFill().content;
      sel.forEachCell((cell, pos) => {
        if (!cell.content.eq(baseContent))
          tr.replace(
            tr.mapping.map(pos + 1),
            tr.mapping.map(pos + cell.nodeSize - 1),
            new Slice(baseContent, 0, 0)
          );
      });
      if (tr.docChanged)
        dispatch(tr);
    }
    return true;
  }
  function handleTripleClick(view, pos) {
    const doc = view.state.doc, $cell = cellAround(doc.resolve(pos));
    if (!$cell)
      return false;
    view.dispatch(view.state.tr.setSelection(new CellSelection($cell)));
    return true;
  }
  function handlePaste(view, _, slice) {
    if (!isInTable(view.state))
      return false;
    let cells = pastedCells(slice);
    const sel = view.state.selection;
    if (sel instanceof CellSelection) {
      if (!cells)
        cells = {
          width: 1,
          height: 1,
          rows: [
            Fragment.from(
              fitSlice(tableNodeTypes(view.state.schema).cell, slice)
            )
          ]
        };
      const table = sel.$anchorCell.node(-1);
      const start = sel.$anchorCell.start(-1);
      const rect = TableMap.get(table).rectBetween(
        sel.$anchorCell.pos - start,
        sel.$headCell.pos - start
      );
      cells = clipCells(cells, rect.right - rect.left, rect.bottom - rect.top);
      insertCells(view.state, view.dispatch, start, rect, cells);
      return true;
    } else if (cells) {
      const $cell = selectionCell(view.state);
      const start = $cell.start(-1);
      insertCells(
        view.state,
        view.dispatch,
        start,
        TableMap.get($cell.node(-1)).findCell($cell.pos - start),
        cells
      );
      return true;
    } else {
      return false;
    }
  }
  function handleMouseDown(view, startEvent) {
    var _a;
    if (startEvent.ctrlKey || startEvent.metaKey)
      return;
    const startDOMCell = domInCell(view, startEvent.target);
    let $anchor;
    if (startEvent.shiftKey && view.state.selection instanceof CellSelection) {
      setCellSelection(view.state.selection.$anchorCell, startEvent);
      startEvent.preventDefault();
    } else if (startEvent.shiftKey && startDOMCell && ($anchor = cellAround(view.state.selection.$anchor)) != null && ((_a = cellUnderMouse(view, startEvent)) == null ? void 0 : _a.pos) != $anchor.pos) {
      setCellSelection($anchor, startEvent);
      startEvent.preventDefault();
    } else if (!startDOMCell) {
      return;
    }
    function setCellSelection($anchor2, event) {
      let $head = cellUnderMouse(view, event);
      const starting = tableEditingKey.getState(view.state) == null;
      if (!$head || !inSameTable($anchor2, $head)) {
        if (starting)
          $head = $anchor2;
        else
          return;
      }
      const selection = new CellSelection($anchor2, $head);
      if (starting || !view.state.selection.eq(selection)) {
        const tr = view.state.tr.setSelection(selection);
        if (starting)
          tr.setMeta(tableEditingKey, $anchor2.pos);
        view.dispatch(tr);
      }
    }
    function stop() {
      view.root.removeEventListener("mouseup", stop);
      view.root.removeEventListener("dragstart", stop);
      view.root.removeEventListener("mousemove", move);
      if (tableEditingKey.getState(view.state) != null)
        view.dispatch(view.state.tr.setMeta(tableEditingKey, -1));
    }
    function move(_event) {
      const event = _event;
      const anchor = tableEditingKey.getState(view.state);
      let $anchor2;
      if (anchor != null) {
        $anchor2 = view.state.doc.resolve(anchor);
      } else if (domInCell(view, event.target) != startDOMCell) {
        $anchor2 = cellUnderMouse(view, startEvent);
        if (!$anchor2)
          return stop();
      }
      if ($anchor2)
        setCellSelection($anchor2, event);
    }
    view.root.addEventListener("mouseup", stop);
    view.root.addEventListener("dragstart", stop);
    view.root.addEventListener("mousemove", move);
  }
  function atEndOfCell(view, axis, dir) {
    if (!(view.state.selection instanceof TextSelection))
      return null;
    const { $head } = view.state.selection;
    for (let d = $head.depth - 1; d >= 0; d--) {
      const parent = $head.node(d), index = dir < 0 ? $head.index(d) : $head.indexAfter(d);
      if (index != (dir < 0 ? 0 : parent.childCount))
        return null;
      if (parent.type.spec.tableRole == "cell" || parent.type.spec.tableRole == "header_cell") {
        const cellPos = $head.before(d);
        const dirStr = axis == "vert" ? dir > 0 ? "down" : "up" : dir > 0 ? "right" : "left";
        return view.endOfTextblock(dirStr) ? cellPos : null;
      }
    }
    return null;
  }
  function domInCell(view, dom) {
    for (; dom && dom != view.dom; dom = dom.parentNode) {
      if (dom.nodeName == "TD" || dom.nodeName == "TH") {
        return dom;
      }
    }
    return null;
  }
  function cellUnderMouse(view, event) {
    const mousePos = view.posAtCoords({
      left: event.clientX,
      top: event.clientY
    });
    if (!mousePos)
      return null;
    return mousePos ? cellAround(view.state.doc.resolve(mousePos.pos)) : null;
  }

  // src/tableview.ts
  var TableView = class {
    constructor(node, cellMinWidth) {
      this.node = node;
      this.cellMinWidth = cellMinWidth;
      this.dom = document.createElement("div");
      this.dom.className = "tableWrapper";
      this.table = this.dom.appendChild(document.createElement("table"));
      this.colgroup = this.table.appendChild(document.createElement("colgroup"));
      updateColumnsOnResize(node, this.colgroup, this.table, cellMinWidth);
      this.contentDOM = this.table.appendChild(document.createElement("tbody"));
    }
    update(node) {
      if (node.type != this.node.type)
        return false;
      this.node = node;
      updateColumnsOnResize(node, this.colgroup, this.table, this.cellMinWidth);
      return true;
    }
    ignoreMutation(record) {
      return record.type == "attributes" && (record.target == this.table || this.colgroup.contains(record.target));
    }
  };
  function updateColumnsOnResize(node, colgroup, table, cellMinWidth, overrideCol, overrideValue) {
    var _a;
    let totalWidth = 0;
    let fixedWidth = true;
    let nextDOM = colgroup.firstChild;
    const row = node.firstChild;
    if (!row)
      return;
    for (let i = 0, col = 0; i < row.childCount; i++) {
      const { colspan, colwidth } = row.child(i).attrs;
      for (let j = 0; j < colspan; j++, col++) {
        const hasWidth = overrideCol == col ? overrideValue : colwidth && colwidth[j];
        const cssWidth = hasWidth ? hasWidth + "px" : "";
        totalWidth += hasWidth || cellMinWidth;
        if (!hasWidth)
          fixedWidth = false;
        if (!nextDOM) {
          colgroup.appendChild(document.createElement("col")).style.width = cssWidth;
        } else {
          if (nextDOM.style.width != cssWidth)
            nextDOM.style.width = cssWidth;
          nextDOM = nextDOM.nextSibling;
        }
      }
    }
    while (nextDOM) {
      const after = nextDOM.nextSibling;
      (_a = nextDOM.parentNode) == null ? void 0 : _a.removeChild(nextDOM);
      nextDOM = after;
    }
    if (fixedWidth) {
      table.style.width = totalWidth + "px";
      table.style.minWidth = "";
    } else {
      table.style.width = "";
      table.style.minWidth = totalWidth + "px";
    }
  }

  // src/columnresizing.ts
  var columnResizingPluginKey = new PluginKey(
    "tableColumnResizing"
  );
  function columnResizing({
    handleWidth = 5,
    cellMinWidth = 25,
    View = TableView,
    lastColumnResizable = true
  } = {}) {
    const plugin = new Plugin({
      key: columnResizingPluginKey,
      state: {
        init(_, state) {
          plugin.spec.props.nodeViews[tableNodeTypes(state.schema).table.name] = (node, view) => new View(node, cellMinWidth, view);
          return new ResizeState(-1, false);
        },
        apply(tr, prev) {
          return prev.apply(tr);
        }
      },
      props: {
        attributes: (state) => {
          const pluginState = columnResizingPluginKey.getState(state);
          return pluginState && pluginState.activeHandle > -1 ? { class: "resize-cursor" } : {};
        },
        handleDOMEvents: {
          mousemove: (view, event) => {
            handleMouseMove(
              view,
              event,
              handleWidth,
              cellMinWidth,
              lastColumnResizable
            );
          },
          mouseleave: (view) => {
            handleMouseLeave(view);
          },
          mousedown: (view, event) => {
            handleMouseDown2(view, event, cellMinWidth);
          }
        },
        decorations: (state) => {
          const pluginState = columnResizingPluginKey.getState(state);
          if (pluginState && pluginState.activeHandle > -1) {
            return handleDecorations(state, pluginState.activeHandle);
          }
        },
        nodeViews: {}
      }
    });
    return plugin;
  }
  var ResizeState = class _ResizeState {
    constructor(activeHandle, dragging) {
      this.activeHandle = activeHandle;
      this.dragging = dragging;
    }
    apply(tr) {
      const state = this;
      const action = tr.getMeta(columnResizingPluginKey);
      if (action && action.setHandle != null)
        return new _ResizeState(action.setHandle, false);
      if (action && action.setDragging !== void 0)
        return new _ResizeState(state.activeHandle, action.setDragging);
      if (state.activeHandle > -1 && tr.docChanged) {
        let handle = tr.mapping.map(state.activeHandle, -1);
        if (!pointsAtCell(tr.doc.resolve(handle))) {
          handle = -1;
        }
        return new _ResizeState(handle, state.dragging);
      }
      return state;
    }
  };
  function handleMouseMove(view, event, handleWidth, cellMinWidth, lastColumnResizable) {
    const pluginState = columnResizingPluginKey.getState(view.state);
    if (!pluginState)
      return;
    if (!pluginState.dragging) {
      const target = domCellAround(event.target);
      let cell = -1;
      if (target) {
        const { left, right } = target.getBoundingClientRect();
        if (event.clientX - left <= handleWidth)
          cell = edgeCell(view, event, "left", handleWidth);
        else if (right - event.clientX <= handleWidth)
          cell = edgeCell(view, event, "right", handleWidth);
      }
      if (cell != pluginState.activeHandle) {
        if (!lastColumnResizable && cell !== -1) {
          const $cell = view.state.doc.resolve(cell);
          const table = $cell.node(-1);
          const map = TableMap.get(table);
          const tableStart = $cell.start(-1);
          const col = map.colCount($cell.pos - tableStart) + $cell.nodeAfter.attrs.colspan - 1;
          if (col == map.width - 1) {
            return;
          }
        }
        updateHandle(view, cell);
      }
    }
  }
  function handleMouseLeave(view) {
    const pluginState = columnResizingPluginKey.getState(view.state);
    if (pluginState && pluginState.activeHandle > -1 && !pluginState.dragging)
      updateHandle(view, -1);
  }
  function handleMouseDown2(view, event, cellMinWidth) {
    var _a;
    const win = (_a = view.dom.ownerDocument.defaultView) != null ? _a : window;
    const pluginState = columnResizingPluginKey.getState(view.state);
    if (!pluginState || pluginState.activeHandle == -1 || pluginState.dragging)
      return false;
    const cell = view.state.doc.nodeAt(pluginState.activeHandle);
    const width = currentColWidth(view, pluginState.activeHandle, cell.attrs);
    view.dispatch(
      view.state.tr.setMeta(columnResizingPluginKey, {
        setDragging: { startX: event.clientX, startWidth: width }
      })
    );
    function finish(event2) {
      win.removeEventListener("mouseup", finish);
      win.removeEventListener("mousemove", move);
      const pluginState2 = columnResizingPluginKey.getState(view.state);
      if (pluginState2 == null ? void 0 : pluginState2.dragging) {
        updateColumnWidth(
          view,
          pluginState2.activeHandle,
          draggedWidth(pluginState2.dragging, event2, cellMinWidth)
        );
        view.dispatch(
          view.state.tr.setMeta(columnResizingPluginKey, { setDragging: null })
        );
      }
    }
    function move(event2) {
      if (!event2.which)
        return finish(event2);
      const pluginState2 = columnResizingPluginKey.getState(view.state);
      if (!pluginState2)
        return;
      if (pluginState2.dragging) {
        const dragged = draggedWidth(pluginState2.dragging, event2, cellMinWidth);
        displayColumnWidth(view, pluginState2.activeHandle, dragged, cellMinWidth);
      }
    }
    win.addEventListener("mouseup", finish);
    win.addEventListener("mousemove", move);
    event.preventDefault();
    return true;
  }
  function currentColWidth(view, cellPos, { colspan, colwidth }) {
    const width = colwidth && colwidth[colwidth.length - 1];
    if (width)
      return width;
    const dom = view.domAtPos(cellPos);
    const node = dom.node.childNodes[dom.offset];
    let domWidth = node.offsetWidth, parts = colspan;
    if (colwidth) {
      for (let i = 0; i < colspan; i++)
        if (colwidth[i]) {
          domWidth -= colwidth[i];
          parts--;
        }
    }
    return domWidth / parts;
  }
  function domCellAround(target) {
    while (target && target.nodeName != "TD" && target.nodeName != "TH")
      target = target.classList && target.classList.contains("ProseMirror") ? null : target.parentNode;
    return target;
  }
  function edgeCell(view, event, side, handleWidth) {
    const offset = side == "right" ? -handleWidth : handleWidth;
    const found = view.posAtCoords({
      left: event.clientX + offset,
      top: event.clientY
    });
    if (!found)
      return -1;
    const { pos } = found;
    const $cell = cellAround(view.state.doc.resolve(pos));
    if (!$cell)
      return -1;
    if (side == "right")
      return $cell.pos;
    const map = TableMap.get($cell.node(-1)), start = $cell.start(-1);
    const index = map.map.indexOf($cell.pos - start);
    return index % map.width == 0 ? -1 : start + map.map[index - 1];
  }
  function draggedWidth(dragging, event, cellMinWidth) {
    const offset = event.clientX - dragging.startX;
    return Math.max(cellMinWidth, dragging.startWidth + offset);
  }
  function updateHandle(view, value) {
    view.dispatch(
      view.state.tr.setMeta(columnResizingPluginKey, { setHandle: value })
    );
  }
  function updateColumnWidth(view, cell, width) {
    const $cell = view.state.doc.resolve(cell);
    const table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1);
    const col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1;
    const tr = view.state.tr;
    for (let row = 0; row < map.height; row++) {
      const mapIndex = row * map.width + col;
      if (row && map.map[mapIndex] == map.map[mapIndex - map.width])
        continue;
      const pos = map.map[mapIndex];
      const attrs = table.nodeAt(pos).attrs;
      const index = attrs.colspan == 1 ? 0 : col - map.colCount(pos);
      if (attrs.colwidth && attrs.colwidth[index] == width)
        continue;
      const colwidth = attrs.colwidth ? attrs.colwidth.slice() : zeroes(attrs.colspan);
      colwidth[index] = width;
      tr.setNodeMarkup(start + pos, null, { ...attrs, colwidth });
    }
    if (tr.docChanged)
      view.dispatch(tr);
  }
  function displayColumnWidth(view, cell, width, cellMinWidth) {
    const $cell = view.state.doc.resolve(cell);
    const table = $cell.node(-1), start = $cell.start(-1);
    const col = TableMap.get(table).colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1;
    let dom = view.domAtPos($cell.start(-1)).node;
    while (dom && dom.nodeName != "TABLE") {
      dom = dom.parentNode;
    }
    if (!dom)
      return;
    updateColumnsOnResize(
      table,
      dom.firstChild,
      dom,
      cellMinWidth,
      col,
      width
    );
  }
  function zeroes(n) {
    return Array(n).fill(0);
  }
  function handleDecorations(state, cell) {
    const decorations = [];
    const $cell = state.doc.resolve(cell);
    const table = $cell.node(-1);
    if (!table) {
      return DecorationSet.empty;
    }
    const map = TableMap.get(table);
    const start = $cell.start(-1);
    const col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan;
    for (let row = 0; row < map.height; row++) {
      const index = col + row * map.width - 1;
      if ((col == map.width || map.map[index] != map.map[index + 1]) && (row == 0 || map.map[index] != map.map[index - map.width])) {
        const cellPos = map.map[index];
        const pos = start + cellPos + table.nodeAt(cellPos).nodeSize - 1;
        const dom = document.createElement("div");
        dom.className = "column-resize-handle";
        decorations.push(Decoration.widget(pos, dom));
      }
    }
    return DecorationSet.create(state.doc, decorations);
  }
  function selectedRect(state) {
    const sel = state.selection;
    const $pos = selectionCell(state);
    const table = $pos.node(-1);
    const tableStart = $pos.start(-1);
    const map = TableMap.get(table);
    const rect = sel instanceof CellSelection ? map.rectBetween(
      sel.$anchorCell.pos - tableStart,
      sel.$headCell.pos - tableStart
    ) : map.findCell($pos.pos - tableStart);
    return { ...rect, tableStart, map, table };
  }
  function addColumn(tr, { map, tableStart, table }, col) {
    let refColumn = col > 0 ? -1 : 0;
    if (columnIsHeader(map, table, col + refColumn)) {
      refColumn = col == 0 || col == map.width ? null : 0;
    }
    for (let row = 0; row < map.height; row++) {
      const index = row * map.width + col;
      if (col > 0 && col < map.width && map.map[index - 1] == map.map[index]) {
        const pos = map.map[index];
        const cell = table.nodeAt(pos);
        tr.setNodeMarkup(
          tr.mapping.map(tableStart + pos),
          null,
          addColSpan(cell.attrs, col - map.colCount(pos))
        );
        row += cell.attrs.rowspan - 1;
      } else {
        const type = refColumn == null ? tableNodeTypes(table.type.schema).cell : table.nodeAt(map.map[index + refColumn]).type;
        const pos = map.positionAt(row, col, table);
        tr.insert(tr.mapping.map(tableStart + pos), type.createAndFill());
      }
    }
    return tr;
  }
  function addColumnBefore(state, dispatch) {
    if (!isInTable(state))
      return false;
    if (dispatch) {
      const rect = selectedRect(state);
      dispatch(addColumn(state.tr, rect, rect.left));
    }
    return true;
  }
  function addColumnAfter(state, dispatch) {
    if (!isInTable(state))
      return false;
    if (dispatch) {
      const rect = selectedRect(state);
      dispatch(addColumn(state.tr, rect, rect.right));
    }
    return true;
  }
  function removeColumn(tr, { map, table, tableStart }, col) {
    const mapStart = tr.mapping.maps.length;
    for (let row = 0; row < map.height; ) {
      const index = row * map.width + col;
      const pos = map.map[index];
      const cell = table.nodeAt(pos);
      const attrs = cell.attrs;
      if (col > 0 && map.map[index - 1] == pos || col < map.width - 1 && map.map[index + 1] == pos) {
        tr.setNodeMarkup(
          tr.mapping.slice(mapStart).map(tableStart + pos),
          null,
          removeColSpan(attrs, col - map.colCount(pos))
        );
      } else {
        const start = tr.mapping.slice(mapStart).map(tableStart + pos);
        tr.delete(start, start + cell.nodeSize);
      }
      row += attrs.rowspan;
    }
  }
  function deleteColumn(state, dispatch) {
    if (!isInTable(state))
      return false;
    if (dispatch) {
      const rect = selectedRect(state);
      const tr = state.tr;
      if (rect.left == 0 && rect.right == rect.map.width)
        return false;
      for (let i = rect.right - 1; ; i--) {
        removeColumn(tr, rect, i);
        if (i == rect.left)
          break;
        const table = rect.tableStart ? tr.doc.nodeAt(rect.tableStart - 1) : tr.doc;
        if (!table) {
          throw RangeError("No table found");
        }
        rect.table = table;
        rect.map = TableMap.get(table);
      }
      dispatch(tr);
    }
    return true;
  }
  function rowIsHeader(map, table, row) {
    var _a;
    const headerCell = tableNodeTypes(table.type.schema).header_cell;
    for (let col = 0; col < map.width; col++)
      if (((_a = table.nodeAt(map.map[col + row * map.width])) == null ? void 0 : _a.type) != headerCell)
        return false;
    return true;
  }
  function addRow(tr, { map, tableStart, table }, row) {
    var _a;
    let rowPos = tableStart;
    for (let i = 0; i < row; i++)
      rowPos += table.child(i).nodeSize;
    const cells = [];
    let refRow = row > 0 ? -1 : 0;
    if (rowIsHeader(map, table, row + refRow))
      refRow = row == 0 || row == map.height ? null : 0;
    for (let col = 0, index = map.width * row; col < map.width; col++, index++) {
      if (row > 0 && row < map.height && map.map[index] == map.map[index - map.width]) {
        const pos = map.map[index];
        const attrs = table.nodeAt(pos).attrs;
        tr.setNodeMarkup(tableStart + pos, null, {
          ...attrs,
          rowspan: attrs.rowspan + 1
        });
        col += attrs.colspan - 1;
      } else {
        const type = refRow == null ? tableNodeTypes(table.type.schema).cell : (_a = table.nodeAt(map.map[index + refRow * map.width])) == null ? void 0 : _a.type;
        const node = type == null ? void 0 : type.createAndFill();
        if (node)
          cells.push(node);
      }
    }
    tr.insert(rowPos, tableNodeTypes(table.type.schema).row.create(null, cells));
    return tr;
  }
  function addRowBefore(state, dispatch) {
    if (!isInTable(state))
      return false;
    if (dispatch) {
      const rect = selectedRect(state);
      dispatch(addRow(state.tr, rect, rect.top));
    }
    return true;
  }
  function addRowAfter(state, dispatch) {
    if (!isInTable(state))
      return false;
    if (dispatch) {
      const rect = selectedRect(state);
      dispatch(addRow(state.tr, rect, rect.bottom));
    }
    return true;
  }
  function removeRow(tr, { map, table, tableStart }, row) {
    let rowPos = 0;
    for (let i = 0; i < row; i++)
      rowPos += table.child(i).nodeSize;
    const nextRow = rowPos + table.child(row).nodeSize;
    const mapFrom = tr.mapping.maps.length;
    tr.delete(rowPos + tableStart, nextRow + tableStart);
    const seen = /* @__PURE__ */ new Set();
    for (let col = 0, index = row * map.width; col < map.width; col++, index++) {
      const pos = map.map[index];
      if (seen.has(pos))
        continue;
      seen.add(pos);
      if (row > 0 && pos == map.map[index - map.width]) {
        const attrs = table.nodeAt(pos).attrs;
        tr.setNodeMarkup(tr.mapping.slice(mapFrom).map(pos + tableStart), null, {
          ...attrs,
          rowspan: attrs.rowspan - 1
        });
        col += attrs.colspan - 1;
      } else if (row < map.height && pos == map.map[index + map.width]) {
        const cell = table.nodeAt(pos);
        const attrs = cell.attrs;
        const copy = cell.type.create(
          { ...attrs, rowspan: cell.attrs.rowspan - 1 },
          cell.content
        );
        const newPos = map.positionAt(row + 1, col, table);
        tr.insert(tr.mapping.slice(mapFrom).map(tableStart + newPos), copy);
        col += attrs.colspan - 1;
      }
    }
  }
  function deleteRow(state, dispatch) {
    if (!isInTable(state))
      return false;
    if (dispatch) {
      const rect = selectedRect(state), tr = state.tr;
      if (rect.top == 0 && rect.bottom == rect.map.height)
        return false;
      for (let i = rect.bottom - 1; ; i--) {
        removeRow(tr, rect, i);
        if (i == rect.top)
          break;
        const table = rect.tableStart ? tr.doc.nodeAt(rect.tableStart - 1) : tr.doc;
        if (!table) {
          throw RangeError("No table found");
        }
        rect.table = table;
        rect.map = TableMap.get(rect.table);
      }
      dispatch(tr);
    }
    return true;
  }
  function isEmpty(cell) {
    const c = cell.content;
    return c.childCount == 1 && c.child(0).isTextblock && c.child(0).childCount == 0;
  }
  function cellsOverlapRectangle({ width, height, map }, rect) {
    let indexTop = rect.top * width + rect.left, indexLeft = indexTop;
    let indexBottom = (rect.bottom - 1) * width + rect.left, indexRight = indexTop + (rect.right - rect.left - 1);
    for (let i = rect.top; i < rect.bottom; i++) {
      if (rect.left > 0 && map[indexLeft] == map[indexLeft - 1] || rect.right < width && map[indexRight] == map[indexRight + 1])
        return true;
      indexLeft += width;
      indexRight += width;
    }
    for (let i = rect.left; i < rect.right; i++) {
      if (rect.top > 0 && map[indexTop] == map[indexTop - width] || rect.bottom < height && map[indexBottom] == map[indexBottom + width])
        return true;
      indexTop++;
      indexBottom++;
    }
    return false;
  }
  function mergeCells(state, dispatch) {
    const sel = state.selection;
    if (!(sel instanceof CellSelection) || sel.$anchorCell.pos == sel.$headCell.pos)
      return false;
    const rect = selectedRect(state), { map } = rect;
    if (cellsOverlapRectangle(map, rect))
      return false;
    if (dispatch) {
      const tr = state.tr;
      const seen = {};
      let content = Fragment.empty;
      let mergedPos;
      let mergedCell;
      for (let row = rect.top; row < rect.bottom; row++) {
        for (let col = rect.left; col < rect.right; col++) {
          const cellPos = map.map[row * map.width + col];
          const cell = rect.table.nodeAt(cellPos);
          if (seen[cellPos] || !cell)
            continue;
          seen[cellPos] = true;
          if (mergedPos == null) {
            mergedPos = cellPos;
            mergedCell = cell;
          } else {
            if (!isEmpty(cell))
              content = content.append(cell.content);
            const mapped = tr.mapping.map(cellPos + rect.tableStart);
            tr.delete(mapped, mapped + cell.nodeSize);
          }
        }
      }
      if (mergedPos == null || mergedCell == null) {
        return true;
      }
      tr.setNodeMarkup(mergedPos + rect.tableStart, null, {
        ...addColSpan(
          mergedCell.attrs,
          mergedCell.attrs.colspan,
          rect.right - rect.left - mergedCell.attrs.colspan
        ),
        rowspan: rect.bottom - rect.top
      });
      if (content.size) {
        const end = mergedPos + 1 + mergedCell.content.size;
        const start = isEmpty(mergedCell) ? mergedPos + 1 : end;
        tr.replaceWith(start + rect.tableStart, end + rect.tableStart, content);
      }
      tr.setSelection(
        new CellSelection(tr.doc.resolve(mergedPos + rect.tableStart))
      );
      dispatch(tr);
    }
    return true;
  }
  function splitCell(state, dispatch) {
    const nodeTypes = tableNodeTypes(state.schema);
    return splitCellWithType(({ node }) => {
      return nodeTypes[node.type.spec.tableRole];
    })(state, dispatch);
  }
  function splitCellWithType(getCellType) {
    return (state, dispatch) => {
      var _a;
      const sel = state.selection;
      let cellNode;
      let cellPos;
      if (!(sel instanceof CellSelection)) {
        cellNode = cellWrapping(sel.$from);
        if (!cellNode)
          return false;
        cellPos = (_a = cellAround(sel.$from)) == null ? void 0 : _a.pos;
      } else {
        if (sel.$anchorCell.pos != sel.$headCell.pos)
          return false;
        cellNode = sel.$anchorCell.nodeAfter;
        cellPos = sel.$anchorCell.pos;
      }
      if (cellNode == null || cellPos == null) {
        return false;
      }
      if (cellNode.attrs.colspan == 1 && cellNode.attrs.rowspan == 1) {
        return false;
      }
      if (dispatch) {
        let baseAttrs = cellNode.attrs;
        const attrs = [];
        const colwidth = baseAttrs.colwidth;
        if (baseAttrs.rowspan > 1)
          baseAttrs = { ...baseAttrs, rowspan: 1 };
        if (baseAttrs.colspan > 1)
          baseAttrs = { ...baseAttrs, colspan: 1 };
        const rect = selectedRect(state), tr = state.tr;
        for (let i = 0; i < rect.right - rect.left; i++)
          attrs.push(
            colwidth ? {
              ...baseAttrs,
              colwidth: colwidth && colwidth[i] ? [colwidth[i]] : null
            } : baseAttrs
          );
        let lastCell;
        for (let row = rect.top; row < rect.bottom; row++) {
          let pos = rect.map.positionAt(row, rect.left, rect.table);
          if (row == rect.top)
            pos += cellNode.nodeSize;
          for (let col = rect.left, i = 0; col < rect.right; col++, i++) {
            if (col == rect.left && row == rect.top)
              continue;
            tr.insert(
              lastCell = tr.mapping.map(pos + rect.tableStart, 1),
              getCellType({ node: cellNode, row, col }).createAndFill(attrs[i])
            );
          }
        }
        tr.setNodeMarkup(
          cellPos,
          getCellType({ node: cellNode, row: rect.top, col: rect.left }),
          attrs[0]
        );
        if (sel instanceof CellSelection)
          tr.setSelection(
            new CellSelection(
              tr.doc.resolve(sel.$anchorCell.pos),
              lastCell ? tr.doc.resolve(lastCell) : void 0
            )
          );
        dispatch(tr);
      }
      return true;
    };
  }
  function setCellAttr(name, value) {
    return function(state, dispatch) {
      if (!isInTable(state))
        return false;
      const $cell = selectionCell(state);
      if ($cell.nodeAfter.attrs[name] === value)
        return false;
      if (dispatch) {
        const tr = state.tr;
        if (state.selection instanceof CellSelection)
          state.selection.forEachCell((node, pos) => {
            if (node.attrs[name] !== value)
              tr.setNodeMarkup(pos, null, {
                ...node.attrs,
                [name]: value
              });
          });
        else
          tr.setNodeMarkup($cell.pos, null, {
            ...$cell.nodeAfter.attrs,
            [name]: value
          });
        dispatch(tr);
      }
      return true;
    };
  }
  function deprecated_toggleHeader(type) {
    return function(state, dispatch) {
      if (!isInTable(state))
        return false;
      if (dispatch) {
        const types = tableNodeTypes(state.schema);
        const rect = selectedRect(state), tr = state.tr;
        const cells = rect.map.cellsInRect(
          type == "column" ? {
            left: rect.left,
            top: 0,
            right: rect.right,
            bottom: rect.map.height
          } : type == "row" ? {
            left: 0,
            top: rect.top,
            right: rect.map.width,
            bottom: rect.bottom
          } : rect
        );
        const nodes = cells.map((pos) => rect.table.nodeAt(pos));
        for (let i = 0; i < cells.length; i++)
          if (nodes[i].type == types.header_cell)
            tr.setNodeMarkup(
              rect.tableStart + cells[i],
              types.cell,
              nodes[i].attrs
            );
        if (tr.steps.length == 0)
          for (let i = 0; i < cells.length; i++)
            tr.setNodeMarkup(
              rect.tableStart + cells[i],
              types.header_cell,
              nodes[i].attrs
            );
        dispatch(tr);
      }
      return true;
    };
  }
  function isHeaderEnabledByType(type, rect, types) {
    const cellPositions = rect.map.cellsInRect({
      left: 0,
      top: 0,
      right: type == "row" ? rect.map.width : 1,
      bottom: type == "column" ? rect.map.height : 1
    });
    for (let i = 0; i < cellPositions.length; i++) {
      const cell = rect.table.nodeAt(cellPositions[i]);
      if (cell && cell.type !== types.header_cell) {
        return false;
      }
    }
    return true;
  }
  function toggleHeader(type, options) {
    options = options || { useDeprecatedLogic: false };
    if (options.useDeprecatedLogic)
      return deprecated_toggleHeader(type);
    return function(state, dispatch) {
      if (!isInTable(state))
        return false;
      if (dispatch) {
        const types = tableNodeTypes(state.schema);
        const rect = selectedRect(state), tr = state.tr;
        const isHeaderRowEnabled = isHeaderEnabledByType("row", rect, types);
        const isHeaderColumnEnabled = isHeaderEnabledByType(
          "column",
          rect,
          types
        );
        const isHeaderEnabled = type === "column" ? isHeaderRowEnabled : type === "row" ? isHeaderColumnEnabled : false;
        const selectionStartsAt = isHeaderEnabled ? 1 : 0;
        const cellsRect = type == "column" ? {
          left: 0,
          top: selectionStartsAt,
          right: 1,
          bottom: rect.map.height
        } : type == "row" ? {
          left: selectionStartsAt,
          top: 0,
          right: rect.map.width,
          bottom: 1
        } : rect;
        const newType = type == "column" ? isHeaderColumnEnabled ? types.cell : types.header_cell : type == "row" ? isHeaderRowEnabled ? types.cell : types.header_cell : types.cell;
        rect.map.cellsInRect(cellsRect).forEach((relativeCellPos) => {
          const cellPos = relativeCellPos + rect.tableStart;
          const cell = tr.doc.nodeAt(cellPos);
          if (cell) {
            tr.setNodeMarkup(cellPos, newType, cell.attrs);
          }
        });
        dispatch(tr);
      }
      return true;
    };
  }
  var toggleHeaderRow = toggleHeader("row", {
    useDeprecatedLogic: true
  });
  var toggleHeaderColumn = toggleHeader("column", {
    useDeprecatedLogic: true
  });
  var toggleHeaderCell = toggleHeader("cell", {
    useDeprecatedLogic: true
  });
  function findNextCell($cell, dir) {
    if (dir < 0) {
      const before = $cell.nodeBefore;
      if (before)
        return $cell.pos - before.nodeSize;
      for (let row = $cell.index(-1) - 1, rowEnd = $cell.before(); row >= 0; row--) {
        const rowNode = $cell.node(-1).child(row);
        const lastChild = rowNode.lastChild;
        if (lastChild) {
          return rowEnd - 1 - lastChild.nodeSize;
        }
        rowEnd -= rowNode.nodeSize;
      }
    } else {
      if ($cell.index() < $cell.parent.childCount - 1) {
        return $cell.pos + $cell.nodeAfter.nodeSize;
      }
      const table = $cell.node(-1);
      for (let row = $cell.indexAfter(-1), rowStart = $cell.after(); row < table.childCount; row++) {
        const rowNode = table.child(row);
        if (rowNode.childCount)
          return rowStart + 1;
        rowStart += rowNode.nodeSize;
      }
    }
    return null;
  }
  function goToNextCell(direction) {
    return function(state, dispatch) {
      if (!isInTable(state))
        return false;
      const cell = findNextCell(selectionCell(state), direction);
      if (cell == null)
        return false;
      if (dispatch) {
        const $cell = state.doc.resolve(cell);
        dispatch(
          state.tr.setSelection(TextSelection.between($cell, moveCellForward($cell))).scrollIntoView()
        );
      }
      return true;
    };
  }
  function deleteTable(state, dispatch) {
    const $pos = state.selection.$anchor;
    for (let d = $pos.depth; d > 0; d--) {
      const node = $pos.node(d);
      if (node.type.spec.tableRole == "table") {
        if (dispatch)
          dispatch(
            state.tr.delete($pos.before(d), $pos.after(d)).scrollIntoView()
          );
        return true;
      }
    }
    return false;
  }

  // src/index.ts
  function tableEditing({
    allowTableNodeSelection = false
  } = {}) {
    return new Plugin({
      key: tableEditingKey,
      // This piece of state is used to remember when a mouse-drag
      // cell-selection is happening, so that it can continue even as
      // transactions (which might move its anchor cell) come in.
      state: {
        init() {
          return null;
        },
        apply(tr, cur) {
          const set = tr.getMeta(tableEditingKey);
          if (set != null)
            return set == -1 ? null : set;
          if (cur == null || !tr.docChanged)
            return cur;
          const { deleted, pos } = tr.mapping.mapResult(cur);
          return deleted ? null : pos;
        }
      },
      props: {
        decorations: drawCellSelection,
        handleDOMEvents: {
          mousedown: handleMouseDown
        },
        createSelectionBetween(view) {
          return tableEditingKey.getState(view.state) != null ? view.state.selection : null;
        },
        handleTripleClick,
        handleKeyDown,
        handlePaste
      },
      appendTransaction(_, oldState, state) {
        return normalizeSelection(
          state,
          fixTables(state, oldState),
          allowTableNodeSelection
        );
      }
    });
  }

  var index$1 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    CellBookmark: CellBookmark,
    CellSelection: CellSelection,
    ResizeState: ResizeState,
    TableMap: TableMap,
    TableView: TableView,
    __clipCells: clipCells,
    __insertCells: insertCells,
    __pastedCells: pastedCells,
    addColSpan: addColSpan,
    addColumn: addColumn,
    addColumnAfter: addColumnAfter,
    addColumnBefore: addColumnBefore,
    addRow: addRow,
    addRowAfter: addRowAfter,
    addRowBefore: addRowBefore,
    cellAround: cellAround,
    colCount: colCount,
    columnIsHeader: columnIsHeader,
    columnResizing: columnResizing,
    columnResizingPluginKey: columnResizingPluginKey,
    deleteColumn: deleteColumn,
    deleteRow: deleteRow,
    deleteTable: deleteTable,
    findCell: findCell,
    fixTables: fixTables,
    fixTablesKey: fixTablesKey,
    goToNextCell: goToNextCell,
    handlePaste: handlePaste,
    inSameTable: inSameTable,
    isInTable: isInTable,
    mergeCells: mergeCells,
    moveCellForward: moveCellForward,
    nextCell: nextCell,
    pointsAtCell: pointsAtCell,
    removeColSpan: removeColSpan,
    removeColumn: removeColumn,
    removeRow: removeRow,
    rowIsHeader: rowIsHeader,
    selectedRect: selectedRect,
    selectionCell: selectionCell,
    setCellAttr: setCellAttr,
    splitCell: splitCell,
    splitCellWithType: splitCellWithType,
    tableEditing: tableEditing,
    tableEditingKey: tableEditingKey,
    tableNodeTypes: tableNodeTypes,
    tableNodes: tableNodes,
    toggleHeader: toggleHeader,
    toggleHeaderCell: toggleHeaderCell,
    toggleHeaderColumn: toggleHeaderColumn,
    toggleHeaderRow: toggleHeaderRow,
    updateColumnsOnResize: updateColumnsOnResize
  });

  /**
   * A class responsible for building a menu for a ProseMirror instance.
   * @extends {ProseMirrorPlugin}
   */
  class ProseMirrorMenu extends ProseMirrorPlugin {
    /**
     * @typedef {object} ProseMirrorMenuOptions
     * @property {Function} [onSave]        A function to call when the save button is pressed.
     * @property {boolean} [destroyOnSave]  Whether this editor instance is intended to be destroyed when saved.
     * @property {boolean} [compact]        Whether to display a more compact version of the menu.
     */

    /**
     * @param {Schema} schema                     The ProseMirror schema to build a menu for.
     * @param {EditorView} view                   The editor view.
     * @param {ProseMirrorMenuOptions} [options]  Additional options to configure the plugin's behaviour.
     */
    constructor(schema, view, options={}) {
      super(schema);
      this.options = options;

      /**
       * The editor view.
       * @type {EditorView}
       */
      Object.defineProperty(this, "view", {value: view});

      /**
       * The items configured for this menu.
       * @type {ProseMirrorMenuItem[]}
       */
      Object.defineProperty(this, "items", {value: this._getMenuItems()});

      /**
       * The ID of the menu element in the DOM.
       * @type {string}
       */
      Object.defineProperty(this, "id", {value: `prosemirror-menu-${foundry.utils.randomID()}`, writable: false});

      this._createDropDowns();
      this._wrapEditor();
    }

    /* -------------------------------------------- */

    /**
     * An enumeration of editor scopes in which a menu item can appear
     * @enum {string}
     * @protected
     */
    static _MENU_ITEM_SCOPES = {
      BOTH: "",
      TEXT: "text",
      HTML: "html"
    }

    /* -------------------------------------------- */

    /**
     * Additional options to configure the plugin's behaviour.
     * @type {ProseMirrorMenuOptions}
     */
    options;

    /* -------------------------------------------- */

    /**
     * An HTML element that we write HTML to before injecting it into the DOM.
     * @type {HTMLTemplateElement}
     * @private
     */
    #renderTarget = document.createElement("template");

    /* -------------------------------------------- */

    /**
     * Track whether we are currently in a state of editing the HTML source.
     * @type {boolean}
     */
    #editingSource = false;
    get editingSource() {
      return this.#editingSource;
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    static build(schema, options={}) {
      return new Plugin({
        view: editorView => {
          return new this(schema, editorView, options).render();
        }
      });
    }

    /* -------------------------------------------- */

    /**
     * Render the menu's HTML.
     * @returns {ProseMirrorMenu}
     */
    render() {
      const scopes = this.constructor._MENU_ITEM_SCOPES;
      const scopeKey = this.editingSource ? "HTML" : "TEXT";

      // Dropdown Menus
      const dropdowns = this.dropdowns.map(d => `<li class="text">${d.render()}</li>`);

      // Button items
      const buttons = this.items.reduce((buttons, item) => {
        if ( ![scopes.BOTH, scopes[scopeKey]].includes(item.scope) ) return buttons;
        const liClass = [item.active ? "active" : "", item.cssClass, item.scope].filterJoin(" ");
        const bClass = item.active ? "active" : "";
        const tip = game.i18n.localize(item.title);
        buttons.push(`
      <li class="${liClass}">
        <button type="button" class="${bClass}" data-tooltip="${tip}" data-action="${item.action}">
          ${item.icon}
        </button>
      </li>`);
        return buttons;
      }, []);

      // Add collaboration indicator.
      const collaborating = document.getElementById(this.id)?.querySelector(".concurrent-users");
      const tooltip = collaborating?.dataset.tooltip || game.i18n.localize("EDITOR.CollaboratingUsers");
      buttons.push(`
      <li class="concurrent-users" data-tooltip="${tooltip}">
        ${collaborating?.innerHTML || ""}
      </li>
    `);

      // Replace Menu HTML
      this.#renderTarget.innerHTML = `
      <menu class="editor-menu" id="${this.id}">
        ${dropdowns.join("")}
        ${buttons.join("")}
      </menu>
    `;
      document.getElementById(this.id).replaceWith(this.#renderTarget.content.getElementById(this.id));

      // Toggle source editing state for the parent
      const editor = this.view.dom.closest(".editor");
      editor.classList.toggle("editing-source", this.editingSource);

      // Menu interactivity
      this.activateListeners(document.getElementById(this.id));
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Attach event listeners.
     * @param {HTMLMenuElement} html  The root menu element.
     */
    activateListeners(html) {
      html.querySelectorAll("button[data-action]").forEach(button => button.onclick = evt => this._onAction(evt));
      this.dropdowns.map(d => d.activateListeners(html));
    }

    /* -------------------------------------------- */

    /**
     * Called whenever the view's state is updated.
     * @param {EditorView} view       The current editor state.
     * @param {EditorView} prevState  The previous editor state.
     */
    update(view, prevState) {
      this.dropdowns.forEach(d => d.forEachItem(item => {
        item.active = this._isItemActive(item);
      }));
      this.items.forEach(item => item.active = this._isItemActive(item));
      this.render();
    }

    /* -------------------------------------------- */

    /**
     * Called when the view is destroyed or receives a state with different plugins.
     */
    destroy() {
      const menu = this.view.dom.closest(".editor").querySelector("menu");
      menu.nextElementSibling.remove();
      menu.remove();
    }

    /* -------------------------------------------- */

    /**
     * Instantiate the ProseMirrorDropDown instances and configure them with the defined menu items.
     * @protected
     */
    _createDropDowns() {
      const dropdowns = Object.values(this._getDropDownMenus()).map(({title, cssClass, icon, entries}) => {
        return new ProseMirrorDropDown(title, entries, { cssClass, icon, onAction: this._onAction.bind(this) });
      });

      /**
       * The dropdowns configured for this menu.
       * @type {ProseMirrorDropDown[]}
       */
      Object.defineProperty(this, "dropdowns", {value: dropdowns});
    }

    /* -------------------------------------------- */

    /**
     * @typedef {object} ProseMirrorMenuItem
     * @property {string} action             A string identifier for this menu item.
     * @property {string} title              The description of the menu item.
     * @property {string} [class]            An optional class to apply to the menu item.
     * @property {string} [style]            An optional style to apply to the title text.
     * @property {string} [icon]             The menu item's icon HTML.
     * @property {MarkType} [mark]           The mark to apply to the selected text.
     * @property {NodeType} [node]           The node to wrap the selected text in.
     * @property {object} [attrs]            An object of attributes for the node or mark.
     * @property {number} [group]            Entries with the same group number will be grouped together in the drop-down.
     *                                       Lower-numbered groups appear higher in the list.
     * @property {number} [priority]         A numeric priority which determines whether this item is displayed as the
     *                                       dropdown title. Lower priority takes precedence.
     * @property {ProseMirrorCommand} [cmd]  The command to run when the menu item is clicked.
     * @property {boolean} [active=false]    Whether the current item is active under the given selection or cursor.
     */

    /**
     * @typedef {ProseMirrorMenuItem} ProseMirrorDropDownEntry
     * @property {ProseMirrorDropDownEntry[]} [children]  Any child entries.
     */

    /**
     * @typedef {object} ProseMirrorDropDownConfig
     * @property {string} title                        The default title of the drop-down.
     * @property {string} cssClass                     The menu CSS class.
     * @property {string} [icon]                       An optional icon to use instead of a text label.
     * @property {ProseMirrorDropDownEntry[]} entries  The drop-down entries.
     */

    /**
     * Configure dropdowns for this menu. Each entry in the top-level array corresponds to a separate drop-down.
     * @returns {Record<string, ProseMirrorDropDownConfig>}
     * @protected
     */
    _getDropDownMenus() {
      const menus = {
        format: {
          title: "EDITOR.Format",
          cssClass: "format",
          entries: [
            {
              action: "block",
              title: "EDITOR.Block",
              children: [{
                action: "paragraph",
                title: "EDITOR.Paragraph",
                priority: 3,
                node: this.schema.nodes.paragraph
              }, {
                action: "blockquote",
                title: "EDITOR.Blockquote",
                priority: 1,
                node: this.schema.nodes.blockquote,
                cmd: () => this._toggleBlock(this.schema.nodes.blockquote, wrapIn)
              }, {
                action: "code-block",
                title: "EDITOR.CodeBlock",
                priority: 1,
                node: this.schema.nodes.code_block,
                cmd: () => this._toggleTextBlock(this.schema.nodes.code_block)
              }, {
                action: "secret",
                title: "EDITOR.Secret",
                priority: 1,
                node: this.schema.nodes.secret,
                cmd: () => {
                  this._toggleBlock(this.schema.nodes.secret, wrapIn, {
                    attrs: {
                      id: `secret-${foundry.utils.randomID()}`
                    }
                  });
                }
              }]
            }, {
              action: "inline",
              title: "EDITOR.Inline",
              children: [{
                action: "bold",
                title: "EDITOR.Bold",
                priority: 2,
                style: "font-weight: bold;",
                mark: this.schema.marks.strong,
                cmd: toggleMark(this.schema.marks.strong)
              }, {
                action: "italic",
                title: "EDITOR.Italic",
                priority: 2,
                style: "font-style: italic;",
                mark: this.schema.marks.em,
                cmd: toggleMark(this.schema.marks.em)
              }, {
                action: "code",
                title: "EDITOR.Code",
                priority: 2,
                style: "font-family: monospace;",
                mark: this.schema.marks.code,
                cmd: toggleMark(this.schema.marks.code)
              }, {
                action: "underline",
                title: "EDITOR.Underline",
                priority: 2,
                style: "text-decoration: underline;",
                mark: this.schema.marks.underline,
                cmd: toggleMark(this.schema.marks.underline)
              }, {
                action: "strikethrough",
                title: "EDITOR.Strikethrough",
                priority: 2,
                style: "text-decoration: line-through;",
                mark: this.schema.marks.strikethrough,
                cmd: toggleMark(this.schema.marks.strikethrough)
              }, {
                action: "superscript",
                title: "EDITOR.Superscript",
                priority: 2,
                mark: this.schema.marks.superscript,
                cmd: toggleMark(this.schema.marks.superscript)
              }, {
                action: "subscript",
                title: "EDITOR.Subscript",
                priority: 2,
                mark: this.schema.marks.subscript,
                cmd: toggleMark(this.schema.marks.subscript)
              }]
            }, {
              action: "alignment",
              title: "EDITOR.Alignment",
              children: [{
                action: "align-left",
                title: "EDITOR.AlignmentLeft",
                priority: 4,
                node: this.schema.nodes.paragraph,
                attrs: {alignment: "left"},
                cmd: () => this.#toggleAlignment("left")
              }, {
                action: "align-center",
                title: "EDITOR.AlignmentCenter",
                priority: 4,
                node: this.schema.nodes.paragraph,
                attrs: {alignment: "center"},
                cmd: () => this.#toggleAlignment("center")
              }, {
                action: "align-justify",
                title: "EDITOR.AlignmentJustify",
                priority: 4,
                node: this.schema.nodes.paragraph,
                attrs: {alignment: "justify"},
                cmd: () => this.#toggleAlignment("justify")
              }, {
                action: "align-right",
                title: "EDITOR.AlignmentRight",
                priority: 4,
                node: this.schema.nodes.paragraph,
                attrs: {alignment: "right"},
                cmd: () => this.#toggleAlignment("right")
              }]
            }
          ]
        }
      };

      const headings = Array.fromRange(6, 1).map(level => ({
        action: `h${level}`,
        title: game.i18n.format("EDITOR.Heading", {level}),
        priority: 1,
        class: `level${level}`,
        node: this.schema.nodes.heading,
        attrs: {level},
        cmd: () => this._toggleTextBlock(this.schema.nodes.heading, {attrs: {level}})
      }));

      menus.format.entries.unshift({
        action: "headings",
        title: "EDITOR.Headings",
        children: headings
      });

      const fonts = FontConfig.getAvailableFonts().sort().map(family => ({
        action: `font-family-${family.slugify()}`,
        title: family,
        priority: 2,
        style: `font-family: '${family}';`,
        mark: this.schema.marks.font,
        attrs: {family},
        cmd: toggleMark(this.schema.marks.font, {family})
      }));

      if ( this.options.compact ) {
        menus.format.entries.push({
          action: "fonts",
          title: "EDITOR.Font",
          children: fonts
        });
      } else {
        menus.fonts = {
          title: "EDITOR.Font",
          cssClass: "fonts",
          entries: fonts
        };
      }

      menus.table = {
        title: "EDITOR.Table",
        cssClass: "tables",
        icon: '<i class="fas fa-table"></i>',
        entries: [{
          action: "insert-table",
          title: "EDITOR.TableInsert",
          group: 1,
          cmd: this._insertTablePrompt.bind(this)
        }, {
          action: "delete-table",
          title: "EDITOR.TableDelete",
          group: 1,
          cmd: deleteTable
        }, {
          action: "add-col-after",
          title: "EDITOR.TableAddColumnAfter",
          group: 2,
          cmd: addColumnAfter
        }, {
          action: "add-col-before",
          title: "EDITOR.TableAddColumnBefore",
          group: 2,
          cmd: addColumnBefore
        }, {
          action: "delete-col",
          title: "EDITOR.TableDeleteColumn",
          group: 2,
          cmd: deleteColumn
        }, {
          action: "add-row-after",
          title: "EDITOR.TableAddRowAfter",
          group: 3,
          cmd: addRowAfter
        }, {
          action: "add-row-before",
          title: "EDITOR.TableAddRowBefore",
          group: 3,
          cmd: addRowBefore
        }, {
          action: "delete-row",
          title: "EDITOR.TableDeleteRow",
          group: 3,
          cmd: deleteRow
        }, {
          action: "merge-cells",
          title: "EDITOR.TableMergeCells",
          group: 4,
          cmd: mergeCells
        }, {
          action: "split-cell",
          title: "EDITOR.TableSplitCell",
          group: 4,
          cmd: splitCell
        }]
      };

      Hooks.callAll("getProseMirrorMenuDropDowns", this, menus);
      return menus;
    }

    /* -------------------------------------------- */

    /**
     * Configure the items for this menu.
     * @returns {ProseMirrorMenuItem[]}
     * @protected
     */
    _getMenuItems() {
      const scopes = this.constructor._MENU_ITEM_SCOPES;
      const items = [
        {
          action: "bullet-list",
          title: "EDITOR.BulletList",
          icon: '<i class="fa-solid fa-list-ul"></i>',
          node: this.schema.nodes.bullet_list,
          scope: scopes.TEXT,
          cmd: () => this._toggleBlock(this.schema.nodes.bullet_list, wrapInList)
        },
        {
          action: "number-list",
          title: "EDITOR.NumberList",
          icon: '<i class="fa-solid fa-list-ol"></i>',
          node: this.schema.nodes.ordered_list,
          scope: scopes.TEXT,
          cmd: () => this._toggleBlock(this.schema.nodes.ordered_list, wrapInList)
        },
        {
          action: "horizontal-rule",
          title: "EDITOR.HorizontalRule",
          icon: '<i class="fa-solid fa-horizontal-rule"></i>',
          scope: scopes.TEXT,
          cmd: this.#insertHorizontalRule.bind(this)
        },
        {
          action: "image",
          title: "EDITOR.InsertImage",
          icon: '<i class="fa-solid fa-image"></i>',
          scope: scopes.TEXT,
          node: this.schema.nodes.image,
          cmd: this._insertImagePrompt.bind(this)
        },
        {
          action: "link",
          title: "EDITOR.Link",
          icon: '<i class="fa-solid fa-link"></i>',
          scope: scopes.TEXT,
          mark: this.schema.marks.link,
          cmd: this._insertLinkPrompt.bind(this)
        },
        {
          action: "clear-formatting",
          title: "EDITOR.ClearFormatting",
          icon: '<i class="fa-solid fa-text-slash"></i>',
          scope: scopes.TEXT,
          cmd: this._clearFormatting.bind(this)
        },
        {
          action: "cancel-html",
          title: "EDITOR.DiscardHTML",
          icon: '<i class="fa-solid fa-times"></i>',
          scope: scopes.HTML,
          cmd: this.#clearSourceTextarea.bind(this)
        }
      ];

      if ( this.view.state.plugins.some(p => p.spec.isHighlightMatchesPlugin) ) {
        items.push({
          action: "toggle-matches",
          title: "EDITOR.EnableHighlightDocumentMatches",
          icon: '<i class="fa-solid fa-wand-magic-sparkles"></i>',
          scope: scopes.TEXT,
          cssClass: "toggle-matches",
          cmd: this._toggleMatches.bind(this),
          active: game.settings.get("core", "pmHighlightDocumentMatches")
        });
      }

      if ( this.options.onSave ) {
        items.push({
          action: "save",
          title: `EDITOR.${this.options.destroyOnSave ? "SaveAndClose" : "Save"}`,
          icon: `<i class="fa-solid fa-${this.options.destroyOnSave ? "floppy-disk-circle-arrow-right" : "save"}"></i>`,
          scope: scopes.BOTH,
          cssClass: "right",
          cmd: this._handleSave.bind(this)
        });
      }

      items.push({
        action: "source-code",
        title: "EDITOR.SourceHTML",
        icon: '<i class="fa-solid fa-code"></i>',
        scope: scopes.BOTH,
        cssClass: "source-code-edit right",
        cmd: this.#toggleSource.bind(this)
      });

      Hooks.callAll("getProseMirrorMenuItems", this, items);
      return items;
    }

    /* -------------------------------------------- */

    /**
     * Determine whether the given menu item is currently active or not.
     * @param {ProseMirrorMenuItem} item  The menu item.
     * @returns {boolean}                 Whether the cursor or selection is in a state represented by the given menu
     *                                    item.
     * @protected
     */
    _isItemActive(item) {
      if ( item.action === "source-code" ) return !!this.#editingSource;
      if ( item.action === "toggle-matches" ) return game.settings.get("core", "pmHighlightDocumentMatches");
      if ( item.mark ) return this._isMarkActive(item);
      if ( item.node ) return this._isNodeActive(item);
      return false;
    }

    /* -------------------------------------------- */

    /**
     * Determine whether the given menu item representing a mark is active or not.
     * @param {ProseMirrorMenuItem} item  The menu item representing a {@link MarkType}.
     * @returns {boolean}                 Whether the cursor or selection is in a state represented by the given mark.
     * @protected
     */
    _isMarkActive(item) {
      const state = this.view.state;
      const {from, $from, to, empty} = state.selection;
      const markCompare = mark => {
        if ( mark.type !== item.mark ) return false;
        const attrs = foundry.utils.deepClone(mark.attrs);
        delete attrs._preserve;
        if ( item.attrs ) return foundry.utils.objectsEqual(attrs, item.attrs);
        return true;
      };
      if ( empty ) return $from.marks().some(markCompare);
      let active = false;
      state.doc.nodesBetween(from, to, node => {
        if ( node.marks.some(markCompare) ) active = true;
        return !active;
      });
      return active;
    }

    /* -------------------------------------------- */

    /**
     * Determine whether the given menu item representing a node is active or not.
     * @param {ProseMirrorMenuItem} item  The menu item representing a {@link NodeType}.
     * @returns {boolean}                 Whether the cursor or selection is currently within a block of this menu item's
     *                                    node type.
     * @protected
     */
    _isNodeActive(item) {
      const state = this.view.state;
      const {$from, $to, empty} = state.selection;
      const sameParent = empty || $from.sameParent($to);
      // If the selection spans multiple nodes, give up on detecting whether we're in a given block.
      // TODO: Add more complex logic for detecting if all selected nodes belong to the same parent.
      if ( !sameParent ) return false;
      return (state.doc.nodeAt($from.pos)?.type === item.node) || $from.hasAncestor(item.node, item.attrs);
    }

    /* -------------------------------------------- */

    /**
     * Handle a button press.
     * @param {MouseEvent} event  The click event.
     * @protected
     */
    _onAction(event) {
      event.preventDefault();
      const action = event.currentTarget.dataset.action;
      let item;

      // Check dropdowns first
      this.dropdowns.forEach(d => d.forEachItem(i => {
        if ( i.action !== action ) return;
        item = i;
        return false;
      }));

      // Menu items
      if ( !item ) item = this.items.find(i => i.action === action);
      item?.cmd?.(this.view.state, this.view.dispatch, this.view);

      // Destroy the dropdown, if present, & refocus the editor.
      document.getElementById("prosemirror-dropdown")?.remove();
      this.view.focus();
    }

    /* -------------------------------------------- */

    /**
     * Wrap the editor view element and inject our template ready to be rendered into.
     * @protected
     */
    _wrapEditor() {
      const wrapper = document.createElement("div");
      const template = document.createElement("template");
      wrapper.classList.add("editor-container");
      template.setAttribute("id", this.id);
      this.view.dom.before(template);
      this.view.dom.replaceWith(wrapper);
      wrapper.appendChild(this.view.dom);
    }

    /* -------------------------------------------- */

    /**
     * Handle requests to save the editor contents
     * @protected
     */
    _handleSave() {
      if ( this.#editingSource ) this.#commitSourceTextarea();
      return this.options.onSave?.();
    }

    /* -------------------------------------------- */

    /**
     * Global listeners for the drop-down menu.
     */
    static eventListeners() {
      document.addEventListener("pointerdown", event => {
        if ( !event.target.closest("#prosemirror-dropdown") ) {
          document.getElementById("prosemirror-dropdown")?.remove();
        }
      }, { passive: true, capture: true });
    }

    /* -------------------------------------------- */
    /*  Source Code Textarea Management             */
    /* -------------------------------------------- */

    /**
     * Handle a request to edit the source HTML directly.
     * @protected
     */
    #toggleSource () {
      if ( this.editingSource ) return this.#commitSourceTextarea();
      this.#activateSourceTextarea();
    }

    /* -------------------------------------------- */

    /**
     * Conclude editing the source HTML textarea. Clear its contents and return the HTML which was contained.
     * @returns {string}      The HTML text contained within the textarea before it was cleared
     */
    #clearSourceTextarea() {
      const editor = this.view.dom.closest(".editor");
      const textarea = editor.querySelector(":scope > textarea");
      const html = textarea.value;
      textarea.remove();
      this.#editingSource = false;
      this.items.find(i => i.action === "source-code").active = false;
      this.render();
      return html;
    }

    /* -------------------------------------------- */

    /**
     * Create and activate the source code editing textarea
     */
    #activateSourceTextarea() {
      const editor = this.view.dom.closest(".editor");
      const original = ProseMirror.dom.serializeString(this.view.state.doc.content, {spaces: 4});
      const textarea = document.createElement("textarea");
      textarea.value = original;
      editor.appendChild(textarea);
      textarea.addEventListener("keydown", event => this.#handleSourceKeydown(event));
      this.#editingSource = true;
      this.items.find(i => i.action === "source-code").active = true;
      this.render();
    }

    /* -------------------------------------------- */

    /**
     * Commit changes from the source textarea to the view.
     */
    #commitSourceTextarea() {
      const html = this.#clearSourceTextarea();
      const newDoc = ProseMirror.dom.parseString(html);
      const selection = new ProseMirror.AllSelection(this.view.state.doc);
      this.view.dispatch(this.view.state.tr.setSelection(selection).replaceSelectionWith(newDoc));
    }

    /* -------------------------------------------- */

    /**
     * Handle keypresses while editing editor source.
     * @param {KeyboardEvent} event  The keyboard event.
     */
    #handleSourceKeydown(event) {
      if ( game.keyboard.isModifierActive(KeyboardManager.MODIFIER_KEYS.CONTROL) && (event.key === "s") ) {
        event.preventDefault();
        this._handleSave();
      }
    }

    /* -------------------------------------------- */

    /**
     * Display the insert image prompt.
     * @protected
     */
    async _insertImagePrompt() {
      const state = this.view.state;
      const { $from, empty } = state.selection;
      const image = this.schema.nodes.image;
      const data = { src: "", alt: "", width: "", height: "" };
      if ( !empty ) {
        const selected = state.doc.nodeAt($from.pos);
        Object.assign(data, selected?.attrs ?? {});
      }
      const dialog = await this._showDialog("image", "templates/journal/insert-image.html", { data });
      const form = dialog.querySelector("form");
      const src = form.elements.src;
      form.elements.save.addEventListener("click", () => {
        if ( !src.value ) return;
        this.view.dispatch(this.view.state.tr.replaceSelectionWith(image.create({
          src: src.value,
          alt: form.elements.alt.value,
          width: form.elements.width.value,
          height: form.elements.height.value
        })).scrollIntoView());
      });
    }

    /* -------------------------------------------- */

    /**
     * Display the insert link prompt.
     * @protected
     */
    async _insertLinkPrompt() {
      const state = this.view.state;
      const {$from, $to, $cursor} = state.selection;

      // Capture the selected text.
      const selection = state.selection.content().content;
      const data = {text: selection.textBetween(0, selection.size), href: "", title: ""};

      // Check if the user has placed the cursor within a single link, or has selected a single link.
      let links = [];
      state.doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
        if ( node.marks.some(m => m.type === this.schema.marks.link) ) links.push([node, pos]);
      });
      const existing = links.length === 1 && links[0];
      if ( existing ) {
        const [node] = existing;
        if ( $cursor ) data.text = node.text;
        // Pre-fill the dialog with the existing link's attributes.
        const link = node.marks.find(m => m.type === this.schema.marks.link);
        data.href = link.attrs.href;
        data.title = link.attrs.title;
      }

      const dialog = await this._showDialog("link", "templates/journal/insert-link.html", {data});
      const form = dialog.querySelector("form");
      form.elements.save.addEventListener("click", () => {
        const href = form.elements.href.value;
        const text = form.elements.text.value || href;
        if ( !href ) return;
        const link = this.schema.marks.link.create({href, title: form.elements.title.value});
        const tr = state.tr;

        // The user has placed the cursor within a link they wish to edit.
        if ( existing && $cursor ) {
          const [node, pos] = existing;
          const selection = TextSelection.create(state.doc, pos, pos + node.nodeSize);
          tr.setSelection(selection);
        }

        tr.addStoredMark(link).replaceSelectionWith(this.schema.text(text)).scrollIntoView();
        this.view.dispatch(tr);
      });
    }

    /* -------------------------------------------- */

    /**
     * Display the insert table prompt.
     * @protected
     */
    async _insertTablePrompt() {
      const dialog = await this._showDialog("insert-table", "templates/journal/insert-table.html");
      const form = dialog.querySelector("form");
      form.elements.save.addEventListener("click", () => {
        const rows = Number(form.elements.rows.value) || 1;
        const cols = Number(form.elements.cols.value) || 1;
        const html = `
        <table>
          ${Array.fromRange(rows).reduce(row => row + `
            <tr>${Array.fromRange(cols).reduce(col => col + "<td></td>", "")}</tr>
          `, "")}
        </table>
      `;
        const table = ProseMirror.dom.parseString(html, this.schema);
        this.view.dispatch(this.view.state.tr.replaceSelectionWith(table).scrollIntoView());
      });
    }

    /* -------------------------------------------- */

    /**
     * Create a dialog for a menu button.
     * @param {string} action                      The unique menu button action.
     * @param {string} template                    The dialog's template.
     * @param {object} [options]                   Additional options to configure the dialog's behaviour.
     * @param {object} [options.data={}]           Data to pass to the template.
     * @returns {HTMLDialogElement}
     * @protected
     */
    async _showDialog(action, template, {data={}}={}) {
      let button = document.getElementById("prosemirror-dropdown")?.querySelector(`[data-action="${action}"]`);
      button ??= this.view.dom.closest(".editor").querySelector(`[data-action="${action}"]`);
      button.classList.add("active");
      const rect = button.getBoundingClientRect();
      const dialog = document.createElement("dialog");
      dialog.classList.add("menu-dialog", "prosemirror");
      dialog.innerHTML = await renderTemplate(template, data);
      document.body.appendChild(dialog);
      dialog.addEventListener("click", event => {
        if ( event.target.closest("form") ) return;
        button.classList.remove("active");
        dialog.remove();
      });
      const form = dialog.querySelector("form");
      form.style.top = `${rect.top + 30}px`;
      form.style.left = `${rect.left - 200 + 15}px`;
      dialog.style.zIndex = ++_maxZ;
      form.elements.save?.addEventListener("click", () => {
        button.classList.remove("active");
        dialog.remove();
        this.view.focus();
      });
      dialog.open = true;
      return dialog;
    }

    /* -------------------------------------------- */

    /**
     * Clear any marks from the current selection.
     * @protected
     */
    _clearFormatting() {
      const state = this.view.state;
      const {empty, $from, $to} = state.selection;
      if ( empty ) return;
      const tr = this.view.state.tr;
      for ( const markType of Object.values(this.schema.marks) ) {
        if ( state.doc.rangeHasMark($from.pos, $to.pos, markType) ) tr.removeMark($from.pos, $to.pos, markType);
      }
      const range = $from.blockRange($to);
      const nodePositions = [];
      // Capture any nodes that are completely encompassed by the selection, or ones that begin and end exactly at the
      // selection boundaries (i.e., the user has selected all text inside the node).
      tr.doc.nodesBetween($from.pos, $to.pos, (node, pos) => {
        if ( node.isText ) return false;
        // Node is entirely contained within the selection.
        if ( (pos >= range.start) && (pos + node.nodeSize <= range.end) ) nodePositions.push(pos);
      });
      // Clear marks and attributes from all eligible nodes.
      nodePositions.forEach(pos => {
        const node = state.doc.nodeAt(pos);
        const attrs = {...node.attrs};
        for ( const [attr, spec] of Object.entries(node.type.spec.attrs) ) {
          if ( spec.formatting ) delete attrs[attr];
        }
        tr.setNodeMarkup(pos, null, attrs);
      });
      this.view.dispatch(tr);
    }

    /* -------------------------------------------- */

    /**
     * Toggle link recommendations
     * @protected
     */
    async _toggleMatches() {
      const enabled = game.settings.get("core", "pmHighlightDocumentMatches");
      await game.settings.set("core", "pmHighlightDocumentMatches", !enabled);
      this.items.find(i => i.action === "toggle-matches").active = !enabled;
      this.render();
    }

    /* -------------------------------------------- */

    /**
     * Inserts a horizontal rule at the cursor.
     */
    #insertHorizontalRule() {
      const hr = this.schema.nodes.horizontal_rule;
      this.view.dispatch(this.view.state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
    }

    /* -------------------------------------------- */

    /**
     * Toggle a particular alignment for the given selection.
     * @param {string} alignment  The text alignment to toggle.
     */
    #toggleAlignment(alignment) {
      const state = this.view.state;
      const {$from, $to} = state.selection;
      const range = $from.blockRange($to);
      if ( !range ) return;
      const {paragraph, image} = this.schema.nodes;
      const positions = [];
      // The range positions are absolute, so we need to convert them to be relative to the parent node.
      const blockStart = range.parent.eq(state.doc) ? 0 : range.start;
      // Calculate the positions of all the paragraph nodes that are direct descendents of the blockRange parent node.
      range.parent.nodesBetween(range.start - blockStart, range.end - blockStart, (node, pos) => {
        if ( ![paragraph, image].includes(node.type) ) return false;
        positions.push({pos: blockStart + pos, attrs: node.attrs});
      });
      const tr = state.tr;
      positions.forEach(({pos, attrs}) => {
        const node = state.doc.nodeAt(pos);
        tr.setNodeMarkup(pos, null, {
          ...attrs, alignment: attrs.alignment === alignment ? node.type.attrs.alignment.default : alignment
        });
      });
      this.view.dispatch(tr);
    }

    /* -------------------------------------------- */

    /**
     * @callback MenuToggleBlockWrapCommand
     * @param {NodeType} node   The node to wrap the selection in.
     * @param {object} [attrs]  Attributes for the node.
     * @returns ProseMirrorCommand
     */

    /**
     * Toggle the given selection by wrapping it in a given block or lifting it out of one.
     * @param {NodeType} node                    The type of node being interacted with.
     * @param {MenuToggleBlockWrapCommand} wrap  The wrap command specific to the given node.
     * @param {object} [options]                 Additional options to configure behaviour.
     * @param {object} [options.attrs]           Attributes for the node.
     * @protected
     */
    _toggleBlock(node, wrap, {attrs=null}={}) {
      const state = this.view.state;
      const {$from, $to} = state.selection;
      const range = $from.blockRange($to);
      if ( !range ) return;
      const inBlock = $from.hasAncestor(node);
      if ( inBlock ) {
        // FIXME: This will lift out of the closest block rather than only the given one, and doesn't work on multiple
        // list elements.
        const target = liftTarget(range);
        if ( target != null ) this.view.dispatch(state.tr.lift(range, target));
      } else autoJoin(wrap(node, attrs), [node.name])(state, this.view.dispatch);
    }

    /* -------------------------------------------- */

    /**
     * Toggle the given selection by wrapping it in a given text block, or reverting to a paragraph block.
     * @param {NodeType} node           The type of node being interacted with.
     * @param {object} [options]        Additional options to configure behaviour.
     * @param {object} [options.attrs]  Attributes for the node.
     * @protected
     */
    _toggleTextBlock(node, {attrs=null}={}) {
      const state = this.view.state;
      const {$from, $to} = state.selection;
      const range = $from.blockRange($to);
      if ( !range ) return;
      const inBlock = $from.hasAncestor(node, attrs);
      if ( inBlock ) node = this.schema.nodes.paragraph;
      this.view.dispatch(state.tr.setBlockType(range.start, range.end, node, attrs));
    }
  }

  /**
   * Determine whether a given position has an ancestor node of the given type.
   * @param {NodeType} other  The other node type.
   * @param {object} [attrs]  An object of attributes that must also match, if provided.
   * @returns {boolean}
   */
  ResolvedPos.prototype.hasAncestor = function(other, attrs) {
    if ( !this.depth ) return false;
    for ( let i = this.depth; i > 0; i-- ) { // Depth 0 is the root document, so we don't need to test that.
      const node = this.node(i);
      if ( node.type === other ) {
        const nodeAttrs = foundry.utils.deepClone(node.attrs);
        delete nodeAttrs._preserve; // Do not include our internal attributes in the comparison.
        if ( attrs ) return foundry.utils.objectsEqual(nodeAttrs, attrs);
        return true;
      }
    }
    return false;
  };

  class Rebaseable {
      constructor(step, inverted, origin) {
          this.step = step;
          this.inverted = inverted;
          this.origin = origin;
      }
  }
  /**
  Undo a given set of steps, apply a set of other steps, and then
  redo them @internal
  */
  function rebaseSteps(steps, over, transform) {
      for (let i = steps.length - 1; i >= 0; i--)
          transform.step(steps[i].inverted);
      for (let i = 0; i < over.length; i++)
          transform.step(over[i]);
      let result = [];
      for (let i = 0, mapFrom = steps.length; i < steps.length; i++) {
          let mapped = steps[i].step.map(transform.mapping.slice(mapFrom));
          mapFrom--;
          if (mapped && !transform.maybeStep(mapped).failed) {
              transform.mapping.setMirror(mapFrom, transform.steps.length - 1);
              result.push(new Rebaseable(mapped, mapped.invert(transform.docs[transform.docs.length - 1]), steps[i].origin));
          }
      }
      return result;
  }
  // This state field accumulates changes that have to be sent to the
  // central authority in the collaborating group and makes it possible
  // to integrate changes made by peers into our local document. It is
  // defined by the plugin, and will be available as the `collab` field
  // in the resulting editor state.
  class CollabState {
      constructor(
      // The version number of the last update received from the central
      // authority. Starts at 0 or the value of the `version` property
      // in the option object, for the editor's value when the option
      // was enabled.
      version, 
      // The local steps that havent been successfully sent to the
      // server yet.
      unconfirmed) {
          this.version = version;
          this.unconfirmed = unconfirmed;
      }
  }
  function unconfirmedFrom(transform) {
      let result = [];
      for (let i = 0; i < transform.steps.length; i++)
          result.push(new Rebaseable(transform.steps[i], transform.steps[i].invert(transform.docs[i]), transform));
      return result;
  }
  const collabKey = new PluginKey("collab");
  /**
  Creates a plugin that enables the collaborative editing framework
  for the editor.
  */
  function collab(config = {}) {
      let conf = {
          version: config.version || 0,
          clientID: config.clientID == null ? Math.floor(Math.random() * 0xFFFFFFFF) : config.clientID
      };
      return new Plugin({
          key: collabKey,
          state: {
              init: () => new CollabState(conf.version, []),
              apply(tr, collab) {
                  let newState = tr.getMeta(collabKey);
                  if (newState)
                      return newState;
                  if (tr.docChanged)
                      return new CollabState(collab.version, collab.unconfirmed.concat(unconfirmedFrom(tr)));
                  return collab;
              }
          },
          config: conf,
          // This is used to notify the history plugin to not merge steps,
          // so that the history can be rebased.
          historyPreserveItems: true
      });
  }
  /**
  Create a transaction that represents a set of new steps received from
  the authority. Applying this transaction moves the state forward to
  adjust to the authority's view of the document.
  */
  function receiveTransaction(state, steps, clientIDs, options = {}) {
      // Pushes a set of steps (received from the central authority) into
      // the editor state (which should have the collab plugin enabled).
      // Will recognize its own changes, and confirm unconfirmed steps as
      // appropriate. Remaining unconfirmed steps will be rebased over
      // remote steps.
      let collabState = collabKey.getState(state);
      let version = collabState.version + steps.length;
      let ourID = collabKey.get(state).spec.config.clientID;
      // Find out which prefix of the steps originated with us
      let ours = 0;
      while (ours < clientIDs.length && clientIDs[ours] == ourID)
          ++ours;
      let unconfirmed = collabState.unconfirmed.slice(ours);
      steps = ours ? steps.slice(ours) : steps;
      // If all steps originated with us, we're done.
      if (!steps.length)
          return state.tr.setMeta(collabKey, new CollabState(version, unconfirmed));
      let nUnconfirmed = unconfirmed.length;
      let tr = state.tr;
      if (nUnconfirmed) {
          unconfirmed = rebaseSteps(unconfirmed, steps, tr);
      }
      else {
          for (let i = 0; i < steps.length; i++)
              tr.step(steps[i]);
          unconfirmed = [];
      }
      let newCollabState = new CollabState(version, unconfirmed);
      if (options && options.mapSelectionBackward && state.selection instanceof TextSelection) {
          tr.setSelection(TextSelection.between(tr.doc.resolve(tr.mapping.map(state.selection.anchor, -1)), tr.doc.resolve(tr.mapping.map(state.selection.head, -1)), -1));
          tr.updated &= ~1;
      }
      return tr.setMeta("rebased", nUnconfirmed).setMeta("addToHistory", false).setMeta(collabKey, newCollabState);
  }
  /**
  Provides data describing the editor's unconfirmed steps, which need
  to be sent to the central authority. Returns null when there is
  nothing to send.

  `origins` holds the _original_ transactions that produced each
  steps. This can be useful for looking up time stamps and other
  metadata for the steps, but note that the steps may have been
  rebased, whereas the origin transactions are still the old,
  unchanged objects.
  */
  function sendableSteps(state) {
      let collabState = collabKey.getState(state);
      if (collabState.unconfirmed.length == 0)
          return null;
      return {
          version: collabState.version,
          steps: collabState.unconfirmed.map(s => s.step),
          clientID: collabKey.get(state).spec.config.clientID,
          get origins() {
              return this._origins || (this._origins = collabState.unconfirmed.map(s => s.origin));
          }
      };
  }
  /**
  Get the version up to which the collab plugin has synced with the
  central authority.
  */
  function getVersion(state) {
      return collabKey.getState(state).version;
  }

  var index = /*#__PURE__*/Object.freeze({
    __proto__: null,
    collab: collab,
    getVersion: getVersion,
    rebaseSteps: rebaseSteps,
    receiveTransaction: receiveTransaction,
    sendableSteps: sendableSteps
  });

  class DOMParser extends DOMParser$1 {
    /** @inheritdoc */
    parse(dom, options) {
      this.#unwrapImages(dom);
      return super.parse(dom, options);
    }

    /* -------------------------------------------- */

    /**
     * Unwrap any image tags that may have been wrapped in <p></p> tags in earlier iterations of the schema.
     * @param {HTMLElement} dom  The root HTML element to parse.
     */
    #unwrapImages(dom) {
      dom.querySelectorAll("img").forEach(img => {
        const paragraph = img.parentElement;
        if ( paragraph?.tagName !== "P" ) return;
        const parent = paragraph.parentElement || dom;
        parent.insertBefore(img, paragraph);
        // If the paragraph element was purely holding the image element and is now empty, we can remove it.
        if ( !paragraph.childNodes.length ) paragraph.remove();
      });
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    static fromSchema(schema) {
      if ( schema.cached.domParser ) return schema.cached.domParser;
      return schema.cached.domParser = new this(schema, this.schemaRules(schema));
    }
  }

  /**
   * @callback ProseMirrorNodeOutput
   * @param {Node} node        The ProseMirror node.
   * @returns {DOMOutputSpec}  The specification to build a DOM node for this ProseMirror node.
   */

  /**
   * @callback ProseMirrorMarkOutput
   * @param {Mark} mark        The ProseMirror mark.
   * @param {boolean} inline   Is the mark appearing in an inline context?
   * @returns {DOMOutputSpec}  The specification to build a DOM node for this ProseMirror mark.
   */

  /**
   * A class responsible for serializing a ProseMirror document into a string of HTML.
   */
  class StringSerializer {
    /**
     * @param {Record<string, ProseMirrorNodeOutput>} nodes  The node output specs.
     * @param {Record<string, ProseMirrorMarkOutput>} marks  The mark output specs.
     */
    constructor(nodes, marks) {
      this.#nodes = nodes;
      this.#marks = marks;
    }

    /* -------------------------------------------- */

    /**
     * The node output specs.
     * @type {Record<string, ProseMirrorNodeOutput>}
     */
    #nodes;

    /* -------------------------------------------- */

    /**
     * The mark output specs.
     * @type {Record<string, ProseMirrorMarkOutput>}
     */
    #marks;

    /* -------------------------------------------- */

    /**
     * Build a serializer for the given schema.
     * @param {Schema} schema  The ProseMirror schema.
     * @returns {StringSerializer}
     */
    static fromSchema(schema) {
      if ( schema.cached.stringSerializer ) return schema.cached.stringSerializer;
      return schema.cached.stringSerializer =
        new StringSerializer(DOMSerializer.nodesFromSchema(schema), DOMSerializer.marksFromSchema(schema));
    }

    /* -------------------------------------------- */

    /**
     * Create a StringNode from a ProseMirror DOMOutputSpec.
     * @param {DOMOutputSpec} spec                            The specification.
     * @param {boolean} inline                                Whether this is a block or inline node.
     * @returns {{outer: StringNode, [content]: StringNode}}  An object describing the outer node, and a reference to the
     *                                                        child node where content should be appended, if applicable.
     * @protected
     */
    _specToStringNode(spec, inline) {
      if ( typeof spec === "string" ) {
        // This is raw text content.
        const node = new StringNode();
        node.appendChild(spec);
        return {outer: node};
      }

      // Our schema only uses the array type of DOMOutputSpec so we don't need to support the other types here.
      // Array specs take the form of [tagName, ...tail], where the tail elements may be an object of attributes, another
      // array representing a child spec, or the value 0 (read 'hole').
      let attrs = {};
      let [tagName, ...tail] = spec;
      if ( getType(tail[0]) === "Object" ) attrs = tail.shift();
      const outer = new StringNode(tagName, attrs, inline);
      let content;

      for ( const innerSpec of tail ) {
        if ( innerSpec === 0 ) {
          if ( tail.length > 1 ) throw new RangeError("Content hole must be the only child of its parent node.");
          // The outer node and the node to append content to are the same node. The vast majority of our output specs
          // are like this.
          return {outer, content: outer};
        }

        // Otherwise, recursively build any inner specifications and update our content reference to point to wherever the
        // hole is found.
        const {outer: inner, content: innerContent} = this._specToStringNode(innerSpec, true);
        outer.appendChild(inner);
        if ( innerContent ) {
          if ( content ) throw new RangeError("Multiple content holes.");
          content = innerContent;
        }
      }
      return {outer, content};
    }

    /* -------------------------------------------- */

    /**
     * Serialize a ProseMirror fragment into an HTML string.
     * @param {Fragment} fragment    The ProseMirror fragment, a collection of ProseMirror nodes.
     * @param {StringNode} [target]  The target to append to. Not required for the top-level invocation.
     * @returns {StringNode}         A DOM tree representation as a StringNode.
     */
    serializeFragment(fragment, target) {
      target = target ?? new StringNode();
      const stack = [];
      let parent = target;
      fragment.forEach(node => {
        /**
         * Handling marks is a little complicated as ProseMirror stores them in a 'flat' structure, rather than a
         * nested structure that is more natural for HTML. For example, the following HTML:
         *   <em>Almost before <strong>we knew it</strong>, we had left the ground.</em>
         * is represented in ProseMirror's internal structure as:
         *   {marks: [ITALIC], content: "Almost before "}, {marks: [ITALIC, BOLD], content: "we knew it"},
         *   {marks: [ITALIC], content: ", we had left the ground"}
         * In order to translate from the latter back into the former, we maintain a stack. When we see a new mark, we
         * push it onto the stack so that content is appended to that mark. When the mark stops appearing in subsequent
         * nodes, we pop off the stack until we find a mark that does exist, and start appending to that one again.
         *
         * The order that marks appear in the node.marks array is guaranteed to be the order that they were declared in
         * the schema.
         */
        if ( stack.length || node.marks.length ) {
          // Walk along the stack to find a mark that is not already pending (i.e. we haven't seen it yet).
          let pos = 0;
          while ( (pos < stack.length) && (pos < node.marks.length) ) {
            const next = node.marks[pos];
            // If the mark does not span multiple nodes, we can serialize it now rather than waiting.
            if ( !next.eq(stack[pos].mark) || (next.type.spec.spanning === false) ) break;
            pos++;
          }

          // Pop off the stack to reach the position of our mark.
          while ( pos < stack.length ) parent = stack.pop().parent;

          // Add the marks from this point.
          for ( let i = pos; i < node.marks.length; i++ ) {
            const mark = node.marks[i];
            const {outer, content} = this._serializeMark(mark, node.isInline);
            stack.push({mark, parent});
            parent.appendChild(outer);
            parent = content ?? outer;
          }
        }

        // Finally append the content to whichever parent node we've arrived at.
        parent.appendChild(this._toStringNode(node));
      });
      return target;
    }

    /* -------------------------------------------- */

    /**
     * Convert a ProseMirror node representation to a StringNode.
     * @param {Node} node  The ProseMirror node.
     * @returns {StringNode}
     * @protected
     */
    _toStringNode(node) {
      const {outer, content} = this._specToStringNode(this.#nodes[node.type.name](node), node.type.inlineContent);
      if ( content ) {
        if ( node.isLeaf ) throw new RangeError("Content hole not allowed in a leaf node spec.");
        this.serializeFragment(node.content, content);
      }
      return outer;
    }

    /* -------------------------------------------- */

    /**
     * Convert a ProseMirror mark representation to a StringNode.
     * @param {Mark} mark       The ProseMirror mark.
     * @param {boolean} inline  Does the mark appear in an inline context?
     * @returns {{outer: StringNode, [content]: StringNode}}
     * @protected
     */
    _serializeMark(mark, inline) {
      return this._specToStringNode(this.#marks[mark.type.name](mark, inline), true);
    }
  }

  /**
   * A class that behaves like a lightweight DOM node, allowing children to be appended. Serializes to an HTML string.
   */
  class StringNode {
    /**
     * @param {string} [tag]            The tag name. If none is provided, this node's children will not be wrapped in an
     *                                  outer tag.
     * @param {Record<string, string>} [attrs]  The tag attributes.
     * @param {boolean} [inline=false]  Whether the node appears inline or as a block.
     */
    constructor(tag, attrs={}, inline=true) {
      /**
       * The tag name.
       * @type {string}
       */
      Object.defineProperty(this, "tag", {value: tag, writable: false});

      /**
       * The tag attributes.
       * @type {Record<string, string>}
       */
      Object.defineProperty(this, "attrs", {value: attrs, writable: false});

      this.#inline = inline;
    }

    /* -------------------------------------------- */

    /**
     * A list of HTML void elements that do not have a closing tag.
     * @type {Set<string>}
     */
    static #VOID = new Set([
      "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
    ]);

    /* -------------------------------------------- */

    /**
     * A list of children. Either other StringNodes, or plain strings.
     * @type {Array<StringNode|string>}
     * @private
     */
    #children = [];

    /* -------------------------------------------- */

    /**
     * @ignore
     */
    #inline;

    /**
     * Whether the node appears inline or as a block.
     */
    get inline() {
      if ( !this.tag || StringNode.#VOID.has(this.tag) || !this.#children.length ) return true;
      return this.#inline;
    }

    /* -------------------------------------------- */

    /**
     * Append a child to this string node.
     * @param {StringNode|string} child  The child node or string.
     * @throws If attempting to append a child to a void element.
     */
    appendChild(child) {
      if ( StringNode.#VOID.has(this.tag) ) throw new Error("Void elements cannot contain children.");
      this.#children.push(child);
    }

    /* -------------------------------------------- */

    /**
     * Serialize the StringNode structure into a single string.
     * @param {string|number} spaces  The number of spaces to use for indentation (maximum 10). If this value is a string,
     *                                that string is used as indentation instead (or the first 10 characters if it is
     *                                longer).
     */
    toString(spaces=0, {_depth=0, _inlineParent=false}={}) {
      let indent = "";
      const isRoot = _depth < 1;
      if ( !_inlineParent ) {
        if ( typeof spaces === "number" ) indent = " ".repeat(Math.min(10, spaces));
        else if ( typeof spaces === "string" ) indent = spaces.substring(0, 10);
        indent = indent.repeat(Math.max(0, _depth - 1));
      }
      const attrs = isEmpty$1(this.attrs) ? "" : " " + Object.entries(this.attrs).map(([k, v]) => `${k}="${v}"`).join(" ");
      const open = this.tag ? `${indent}<${this.tag}${attrs}>` : "";
      if ( StringNode.#VOID.has(this.tag) ) return open;
      const close = this.tag ? `${this.inline && !isRoot ? "" : indent}</${this.tag}>` : "";
      const children = this.#children.map(c => {
        let content = c.toString(spaces, {_depth: _depth + 1, _inlineParent: this.inline});
        if ( !isRoot && !this.tag ) content = StringNode.#escapeHTML(content);
        return content;
      });
      const lineBreak = (this.inline && !isRoot) || !spaces ? "" : "\n";
      return [open, ...children, close].filterJoin(lineBreak);
    }

    /* -------------------------------------------- */

    /**
     * Escape HTML tags within string content.
     * @param {string} content  The string content.
     * @returns {string}
     */
    static #escapeHTML(content) {
      return content.replace(/[<>]/g, char => {
        switch ( char ) {
          case "<": return "&lt;";
          case ">": return "&gt;";
        }
        return char;
      });
    }
  }

  /**
   * Use the DOM and ProseMirror's DOMParser to construct a ProseMirror document state from an HTML string. This cannot be
   * used server-side.
   * @param {string} htmlString  A string of HTML.
   * @param {Schema} [schema]    The ProseMirror schema to use instead of the default one.
   * @returns {Node}             The document node.
   */
  function parseHTMLString(htmlString, schema$1) {
    const target = document.createElement("template");
    target.innerHTML = htmlString;
    return DOMParser.fromSchema(schema$1 ?? schema).parse(target.content);
  }

  /**
   * Use the StringSerializer to convert a ProseMirror document into an HTML string. This can be used server-side.
   * @param {Node} doc                        The ProseMirror document.
   * @param {object} [options]                Additional options to configure serialization behavior.
   * @param {Schema} [options.schema]         The ProseMirror schema to use instead of the default one.
   * @param {string|number} [options.spaces]  The number of spaces to use for indentation. See {@link StringNode#toString}
   *                                          for details.
   * @returns {string}
   */
  function serializeHTMLString(doc, {schema: schema$1, spaces}={}) {
    schema$1 = schema$1 ?? schema;
    // If the only content is an empty <p></p> tag, return an empty string.
    if ( (doc.size < 3) && (doc.content[0].type === schema$1.nodes.paragraph) ) return "";
    return StringSerializer.fromSchema(schema$1).serializeFragment(doc.content).toString(spaces);
  }

  /**
   * @callback ProseMirrorSliceTransformer
   * @param {Node} node    The candidate node.
   * @returns {Node|void}  A new node to replace the candidate node, or nothing if a replacement should not be made.
   */

  /**
   * Apply a transformation to some nodes in a slice, and return the new slice.
   * @param {Slice} slice           The slice to transform.
   * @param {function} transformer  The transformation function.
   * @returns {Slice}               Either the original slice if no changes were made, or the newly-transformed slice.
   */
  function transformSlice(slice, transformer) {
    const nodeTree = new Map();
    slice.content.nodesBetween(0, slice.content.size, (node, start, parent, index) => {
      nodeTree.set(node, { parent, index });
    });
    let newSlice;
    const replaceNode = (node, { parent, index }) => {
      // If there is a parent, make the replacement, then recurse up the tree to the root, creating new nodes as we go.
      if ( parent ) {
        const newContent = parent.content.replaceChild(index, node);
        const newParent = parent.copy(newContent);
        replaceNode(newParent, nodeTree.get(parent));
        return;
      }

      // Otherwise, handle replacing the root slice's content.
      const targetSlice = newSlice ?? slice;
      const fragment = targetSlice.content;
      const newFragment = fragment.replaceChild(index, node);
      newSlice = new Slice(newFragment, targetSlice.openStart, targetSlice.openEnd);
    };
    for ( const [node, treeInfo] of nodeTree.entries() ) {
      const newNode = transformer(node);
      if ( newNode ) replaceNode(newNode, treeInfo);
    }
    return newSlice ?? slice;
  }

  const paragraph = {
    attrs: {alignment: {default: "left", formatting: true}},
    managed: {styles: ["text-align"]},
    content: "inline*",
    group: "block",
    parseDOM: [{tag: "p", getAttrs: el => ({alignment: el.style.textAlign || "left"})}],
    toDOM: node => {
      const {alignment} = node.attrs;
      if ( alignment === "left" ) return ["p", 0];
      return ["p", {style: `text-align: ${alignment};`}, 0];
    }
  };

  /* -------------------------------------------- */

  const blockquote = {
    content: "block+",
    group: "block",
    defining: true,
    parseDOM: [{tag: "blockquote"}],
    toDOM: () => ["blockquote", 0]
  };

  /* -------------------------------------------- */

  const hr = {
    group: "block",
    parseDOM: [{tag: "hr"}],
    toDOM: () => ["hr"]
  };

  /* -------------------------------------------- */

  const heading = {
    attrs: {level: {default: 1}},
    content: "inline*",
    group: "block",
    defining: true,
    parseDOM: [
      {tag: "h1", attrs: {level: 1}},
      {tag: "h2", attrs: {level: 2}},
      {tag: "h3", attrs: {level: 3}},
      {tag: "h4", attrs: {level: 4}},
      {tag: "h5", attrs: {level: 5}},
      {tag: "h6", attrs: {level: 6}}
    ],
    toDOM: node => [`h${node.attrs.level}`, 0]
  };

  /* -------------------------------------------- */

  const pre = {
    content: "text*",
    marks: "",
    group: "block",
    code: true,
    defining: true,
    parseDOM: [{tag: "pre", preserveWhitespace: "full"}],
    toDOM: () => ["pre", ["code", 0]]
  };

  /* -------------------------------------------- */

  const br = {
    inline: true,
    group: "inline",
    selectable: false,
    parseDOM: [{tag: "br"}],
    toDOM: () => ["br"]
  };

  // A list of tag names that are considered allowable inside a node that only supports inline content.
  const INLINE_TAGS = new Set(["A", "EM", "I", "STRONG", "B", "CODE", "U", "S", "DEL", "SUP", "SUB", "SPAN"]);

  /**
   * Determine if an HTML element contains purely inline content, i.e. only text nodes and 'mark' elements.
   * @param {HTMLElement} element  The element.
   * @returns {boolean}
   */
  function onlyInlineContent(element) {
    for ( const child of element.children ) {
      if ( !INLINE_TAGS.has(child.tagName) ) return false;
    }
    return true;
  }

  /* -------------------------------------------- */

  /**
   * Determine if an HTML element is empty.
   * @param {HTMLElement} element  The element.
   * @returns {boolean}
   */
  function isElementEmpty(element) {
    return !element.childNodes.length;
  }

  /* -------------------------------------------- */

  /**
   * Convert an element's style attribute string into an object.
   * @param {string} str  The style string.
   * @returns {object}
   */
  function stylesFromString(str) {
    return Object.fromEntries(str.split(/;\s*/g).map(prop => prop.split(/:\s*/)));
  }

  /* -------------------------------------------- */

  /**
   * Merge two style attribute strings.
   * @param {string} a  The first style string.
   * @param {string} b  The second style string.
   * @returns {string}
   */
  function mergeStyle(a, b) {
    const allStyles = mergeObject(stylesFromString(a), stylesFromString(b));
    return Object.entries(allStyles).map(([k, v]) => v ? `${k}: ${v}` : null).filterJoin("; ");
  }

  /* -------------------------------------------- */

  /**
   * Convert an element's class attribute string into an array of class names.
   * @param {string} str  The class string.
   * @returns {string[]}
   */
  function classesFromString(str) {
    return str.split(/\s+/g);
  }

  /* -------------------------------------------- */

  /**
   * Merge two class attribute strings.
   * @param {string} a  The first class string.
   * @param {string} b  The second class string.
   * @returns {string}
   */
  function mergeClass(a, b) {
    const allClasses = classesFromString(a).concat(classesFromString(b));
    return Array.from(new Set(allClasses)).join(" ");
  }

  const ol = {
    content: "(list_item | list_item_text)+",
    managed: {attributes: ["start"]},
    group: "block",
    attrs: {order: {default: 1}},
    parseDOM: [{tag: "ol", getAttrs: el => ({order: el.hasAttribute("start") ? Number(el.start) : 1})}],
    toDOM: node => node.attrs.order === 1 ? ["ol", 0] : ["ol", {start: node.attrs.order}, 0]
  };

  /* -------------------------------------------- */

  const ul = {
    content: "(list_item | list_item_text)+",
    group: "block",
    parseDOM: [{tag: "ul"}],
    toDOM: () => ["ul", 0]
  };

  /* -------------------------------------------- */

  /**
   * ProseMirror enforces a stricter subset of HTML where block and inline content cannot be mixed. For example, the
   * following is valid HTML:
   * <ul>
   *   <li>
   *     The first list item.
   *     <ul>
   *       <li>An embedded list.</li>
   *     </ul>
   *   </li>
   * </ul>
   *
   * But, since the contents of the <li> would mix inline content (the text), with block content (the inner <ul>), the
   * schema is defined to only allow block content, and would transform the items to look like this:
   * <ul>
   *   <li>
   *     <p>The first list item.</p>
   *     <ul>
   *       <li><p>An embedded list.</p></li>
   *     </ul>
   *   </li>
   * </ul>
   *
   * We can address this by hooking into the DOM parsing and 'tagging' the extra paragraph elements inserted this way so
   * that when the contents are serialized again, they can be removed. This is left as a TODO for now.
   */

  // In order to preserve existing HTML we define two types of list nodes. One that contains block content, and one that
  // contains text content. We default to block content if the element is empty, in order to make integration with the
  // wrapping and lifting helpers simpler.
  const li = {
    content: "paragraph block*",
    defining: true,
    parseDOM: [{tag: "li", getAttrs: el => {
        // If this contains only inline content and no other elements, do not use this node type.
        if ( !isElementEmpty(el) && onlyInlineContent(el) ) return false;
      }}],
    toDOM: () => ["li", 0]
  };

  /* -------------------------------------------- */

  const liText = {
    content: "text*",
    defining: true,
    parseDOM: [{tag: "li", getAttrs: el => {
        // If this contains any non-inline elements, do not use this node type.
        if ( isElementEmpty(el) || !onlyInlineContent(el) ) return false;
      }}],
    toDOM: () => ["li", 0]
  };

  const CELL_ATTRS = {
    colspan: {default: 1},
    rowspan: {default: 1},
    colwidth: {default: null}
  };

  const MANAGED_CELL_ATTRS = {
    attributes: ["colspan", "rowspan", "data-colwidth"]
  };

  // If any of these elements are part of a table, consider it a 'complex' table and do not attempt to make it editable.
  const COMPLEX_TABLE_ELEMENTS = new Set(["CAPTION", "COLGROUP", "THEAD", "TFOOT"]);

  /* -------------------------------------------- */
  /*  Utilities                                   */
  /* -------------------------------------------- */

  /**
   * Determine node attributes for a table cell when parsing the DOM.
   * @param {HTMLTableCellElement} cell  The table cell DOM node.
   * @returns {{colspan: number, rowspan: number}}
   */
  function getTableCellAttrs(cell) {
    const colspan = cell.getAttribute("colspan") || 1;
    const rowspan = cell.getAttribute("rowspan") || 1;
    return {
      colspan: Number(colspan),
      rowspan: Number(rowspan)
    };
  }

  /**
   * Determine the HTML attributes to be set on the table cell DOM node based on its ProseMirror node attributes.
   * @param {Node} node  The table cell ProseMirror node.
   * @returns {object}   An object of attribute name -> attribute value.
   */
  function setTableCellAttrs(node) {
    const attrs = {};
    const {colspan, rowspan} = node.attrs;
    if ( colspan !== 1 ) attrs.colspan = colspan;
    if ( rowspan !== 1 ) attrs.rowspan = rowspan;
    return attrs;
  }

  /**
   * Whether this element exists as part of a 'complex' table.
   * @param {HTMLElement} el  The element to test.
   * @returns {boolean|void}
   */
  function inComplexTable(el) {
    const table = el.closest("table");
    if ( !table ) return;
    return Array.from(table.children).some(child => COMPLEX_TABLE_ELEMENTS.has(child.tagName));
  }

  /* -------------------------------------------- */
  /*  Built-in Tables                             */
  /* -------------------------------------------- */

  const builtInTableNodes = tableNodes({
    tableGroup: "block",
    cellContent: "block+"
  });

  /* -------------------------------------------- */
  /*  'Complex' Tables                            */
  /* -------------------------------------------- */

  const tableComplex = {
    content: "(caption | caption_block)? colgroup? thead? tbody tfoot?",
    isolating: true,
    group: "block",
    parseDOM: [{tag: "table", getAttrs: el => {
        if ( inComplexTable(el) === false ) return false;
      }}],
    toDOM: () => ["table", 0]
  };

  /* -------------------------------------------- */

  const colgroup = {
    content: "col*",
    isolating: true,
    parseDOM: [{tag: "colgroup"}],
    toDOM: () => ["colgroup", 0]
  };

  /* -------------------------------------------- */

  const col = {
    tableRole: "col",
    parseDOM: [{tag: "col"}],
    toDOM: () => ["col"]
  };

  /* -------------------------------------------- */

  const thead = {
    content: "table_row_complex+",
    isolating: true,
    parseDOM: [{tag: "thead"}],
    toDOM: () => ["thead", 0]
  };

  /* -------------------------------------------- */

  const tbody = {
    content: "table_row_complex+",
    isolating: true,
    parseDOM: [{tag: "tbody", getAttrs: el => {
        if ( inComplexTable(el) === false ) return false;
      }}],
    toDOM: () => ["tbody", 0]
  };

  /* -------------------------------------------- */

  const tfoot = {
    content: "table_row_complex+",
    isolating: true,
    parseDOM: [{tag: "tfoot"}],
    toDOM: () => ["tfoot", 0]
  };

  /* -------------------------------------------- */

  const caption = {
    content: "text*",
    isolating: true,
    parseDOM: [{tag: "caption", getAttrs: el => {
        if ( !isElementEmpty(el) && !onlyInlineContent(el) ) return false;
      }}],
    toDOM: () => ["caption", 0]
  };

  /* -------------------------------------------- */

  const captionBlock = {
    content: "block*",
    isolating: true,
    parseDOM: [{tag: "caption", getAttrs: el => {
        if ( isElementEmpty(el) || onlyInlineContent(el) ) return false;
      }}],
    toDOM: () => ["caption", 0]
  };

  /* -------------------------------------------- */

  const tableRowComplex = {
    content: "(table_cell_complex | table_header_complex | table_cell_complex_block | table_header_complex_block)*",
    parseDOM: [{tag: "tr", getAttrs: el => {
        if ( inComplexTable(el) === false ) return false;
      }}],
    toDOM: () => ["tr", 0]
  };

  /* -------------------------------------------- */

  const tableCellComplex = {
    content: "text*",
    attrs: CELL_ATTRS,
    managed: MANAGED_CELL_ATTRS,
    isolating: true,
    parseDOM: [{tag: "td", getAttrs: el => {
        if ( inComplexTable(el) === false ) return false;
        if ( !isElementEmpty(el) && !onlyInlineContent(el) ) return false;
        return getTableCellAttrs(el);
      }}],
    toDOM: node => ["td", setTableCellAttrs(node), 0]
  };

  /* -------------------------------------------- */

  const tableCellComplexBlock = {
    content: "block*",
    attrs: CELL_ATTRS,
    managed: MANAGED_CELL_ATTRS,
    isolating: true,
    parseDOM: [{tag: "td", getAttrs: el => {
        if ( inComplexTable(el) === false ) return false;
        if ( isElementEmpty(el) || onlyInlineContent(el) ) return false;
        return getTableCellAttrs(el);
      }}],
    toDOM: node => ["td", setTableCellAttrs(node), 0]
  };

  /* -------------------------------------------- */

  const tableHeaderComplex = {
    content: "text*",
    attrs: CELL_ATTRS,
    managed: MANAGED_CELL_ATTRS,
    isolating: true,
    parseDOM: [{tag: "th", getAttrs: el => {
        if ( inComplexTable(el) === false ) return false;
        if ( !isElementEmpty(el) && !onlyInlineContent(el) ) return false;
        return getTableCellAttrs(el);
      }}],
    toDOM: node => ["th", setTableCellAttrs(node), 0]
  };

  /* -------------------------------------------- */

  const tableHeaderComplexBlock = {
    content: "block*",
    attrs: CELL_ATTRS,
    managed: MANAGED_CELL_ATTRS,
    isolating: true,
    parseDOM: [{tag: "th", getAttrs: el => {
        if ( inComplexTable(el) === false ) return false;
        if ( isElementEmpty(el) || onlyInlineContent(el) ) return false;
        return getTableCellAttrs(el);
      }}],
    toDOM: node => ["th", setTableCellAttrs(node), 0]
  };

  // These nodes are supported for HTML preservation purposes, but do not have robust editing support for now.

  const details = {
    content: "(summary | summary_block) block*",
    group: "block",
    defining: true,
    parseDOM: [{tag: "details"}],
    toDOM: () => ["details", 0]
  };

  /* -------------------------------------------- */

  const summary = {
    content: "text*",
    defining: true,
    parseDOM: [{tag: "summary", getAttrs: el => {
        // If this contains any non-inline elements, do not use this node type.
        if ( !isElementEmpty(el) && !onlyInlineContent(el) ) return false;
      }}],
    toDOM: () => ["summary", 0]
  };

  /* -------------------------------------------- */

  const summaryBlock = {
    content: "block+",
    defining: true,
    parseDOM: [{tag: "summary", getAttrs: el => {
        // If this contains only text nodes and no elements, do not use this node type.
        if ( isElementEmpty(el) || onlyInlineContent(el) ) return false;
      }}],
    toDOM: () => ["summary", 0]
  };

  /* -------------------------------------------- */

  const dl = {
    content: "(block|dt|dd)*",
    group: "block",
    defining: true,
    parseDOM: [{tag: "dl"}],
    toDOM: () => ["dl", 0]
  };

  /* -------------------------------------------- */

  const dt = {
    content: "block+",
    defining: true,
    parseDOM: [{tag: "dt"}],
    toDOM: () => ["dt", 0]
  };

  /* -------------------------------------------- */

  const dd = {
    content: "block+",
    defining: true,
    parseDOM: [{tag: "dd"}],
    toDOM: () => ["dd", 0]
  };

  /* -------------------------------------------- */

  const fieldset = {
    content: "legend block*",
    group: "block",
    defining: true,
    parseDOM: [{tag: "fieldset"}],
    toDOM: () => ["fieldset", 0]
  };

  /* -------------------------------------------- */

  const legend = {
    content: "inline+",
    defining: true,
    parseDOM: [{tag: "legend"}],
    toDOM: () => ["legend", 0]
  };

  /* -------------------------------------------- */

  const picture = {
    content: "source* image",
    group: "block",
    defining: true,
    parseDOM: [{tag: "picture"}],
    toDOM: () => ["picture", 0]
  };

  /* -------------------------------------------- */

  const audio$1 = {
    content: "source* track*",
    group: "block",
    parseDOM: [{tag: "audio"}],
    toDOM: () => ["audio", 0]
  };

  /* -------------------------------------------- */

  const video = {
    content: "source* track*",
    group: "block",
    parseDOM: [{tag: "video"}],
    toDOM: () => ["video", 0]
  };

  /* -------------------------------------------- */

  const track = {
    parseDOM: [{tag: "track"}],
    toDOM: () => ["track"]
  };

  /* -------------------------------------------- */

  const source = {
    parseDOM: [{tag: "source"}],
    toDOM: () => ["source"]
  };

  /* -------------------------------------------- */

  const object = {
    inline: true,
    group: "inline",
    parseDOM: [{tag: "object"}],
    toDOM: () => ["object"]
  };

  /* -------------------------------------------- */

  const figure = {
    content: "(figcaption|block)*",
    group: "block",
    defining: true,
    parseDOM: [{tag: "figure"}],
    toDOM: () => ["figure", 0]
  };

  /* -------------------------------------------- */

  const figcaption = {
    content: "inline+",
    defining: true,
    parseDOM: [{tag: "figcaption"}],
    toDOM: () => ["figcaption", 0]
  };

  /* -------------------------------------------- */

  const small = {
    content: "paragraph block*",
    group: "block",
    defining: true,
    parseDOM: [{tag: "small"}],
    toDOM: () => ["small", 0]
  };

  /* -------------------------------------------- */

  const ruby = {
    content: "(rp|rt|block)+",
    group: "block",
    defining: true,
    parseDOM: [{tag: "ruby"}],
    toDOM: () => ["ruby", 0]
  };

  /* -------------------------------------------- */

  const rp = {
    content: "inline+",
    parseDOM: [{tag: "rp"}],
    toDOM: () => ["rp", 0]
  };

  /* -------------------------------------------- */

  const rt = {
    content: "inline+",
    parseDOM: [{tag: "rt"}],
    toDOM: () => ["rt", 0]
  };

  /* -------------------------------------------- */

  const iframe = {
    attrs: { sandbox: { default: "allow-scripts allow-forms" } },
    managed: { attributes: ["sandbox"] },
    group: "block",
    defining: true,
    parseDOM: [{tag: "iframe", getAttrs: el => {
      let sandbox = "allow-scripts allow-forms";
      const url = URL.parseSafe(el.src);
      const host = url?.hostname;
      const isTrusted = CONST.TRUSTED_IFRAME_DOMAINS.some(domain => (host === domain) || host?.endsWith(`.${domain}`));
      if ( isTrusted ) sandbox = null;
      return { sandbox };
    }}],
    toDOM: node => {
      const attrs = {};
      if ( node.attrs.sandbox ) attrs.sandbox = node.attrs.sandbox;
      return ["iframe", attrs];
    }
  };

  const em = {
    parseDOM: [{tag: "i"}, {tag: "em"}, {style: "font-style=italic"}],
    toDOM: () => ["em", 0]
  };

  /* -------------------------------------------- */

  const strong = {
    parseDOM: [
      {tag: "strong"},
      {tag: "b"},
      {style: "font-weight", getAttrs: weight => /^(bold(er)?|[5-9]\d{2})$/.test(weight) && null}
    ],
    toDOM: () => ["strong", 0]
  };

  /* -------------------------------------------- */

  const code = {
    parseDOM: [{tag: "code"}],
    toDOM: () => ["code", 0]
  };

  /* -------------------------------------------- */

  const underline = {
    parseDOM: [{tag: "u"}, {style: "text-decoration=underline"}],
    toDOM: () => ["span", {style: "text-decoration: underline;"}, 0]
  };

  /* -------------------------------------------- */

  const strikethrough = {
    parseDOM: [{tag: "s"}, {tag: "del"}, {style: "text-decoration=line-through"}],
    toDOM: () => ["s", 0]
  };

  /* -------------------------------------------- */

  const superscript = {
    parseDOM: [{tag: "sup"}, {style: "vertical-align=super"}],
    toDOM: () => ["sup", 0]
  };

  /* -------------------------------------------- */

  const subscript = {
    parseDOM: [{tag: "sub"}, {style: "vertical-align=sub"}],
    toDOM: () => ["sub", 0]
  };

  /* -------------------------------------------- */

  const span = {
    parseDOM: [{tag: "span", getAttrs: el => {
        if ( el.style.fontFamily ) return false;
        return {};
      }}],
    toDOM: () => ["span", 0]
  };

  /* -------------------------------------------- */

  const font = {
    attrs: {
      family: {}
    },
    parseDOM: [{style: "font-family", getAttrs: family => ({family})}],
    toDOM: node => ["span", {style: `font-family: ${node.attrs.family.replaceAll('"', "'")}`}]
  };

  /**
   * An abstract interface for a ProseMirror schema definition.
   * @abstract
   */
  class SchemaDefinition {
    /* -------------------------------------------- */
    /*  Properties                                  */
    /* -------------------------------------------- */

    /**
     * The HTML tag selector this node is associated with.
     * @type {string}
     */
    static tag = "";

    /* -------------------------------------------- */
    /*  Methods                                     */
    /* -------------------------------------------- */

    /**
     * Schema attributes.
     * @returns {Record<string, AttributeSpec>}
     * @abstract
     */
    static get attrs() {
      throw new Error("SchemaDefinition subclasses must implement the attrs getter.");
    }

    /* -------------------------------------------- */

    /**
     * Check if an HTML element is appropriate to represent as this node, and if so, extract its schema attributes.
     * @param {HTMLElement} el    The HTML element.
     * @returns {object|boolean}  Returns false if the HTML element is not appropriate for this schema node, otherwise
     *                            returns its attributes.
     * @abstract
     */
    static getAttrs(el) {
      throw new Error("SchemaDefinition subclasses must implement the getAttrs method.");
    }

    /* -------------------------------------------- */

    /**
     * Convert a ProseMirror Node back into an HTML element.
     * @param {Node} node  The ProseMirror node.
     * @returns {[string, any]}
     * @abstract
     */
    static toDOM(node) {
      throw new Error("SchemaDefinition subclasses must implement the toDOM method.");
    }

    /* -------------------------------------------- */

    /**
     * Create the ProseMirror schema specification.
     * @returns {NodeSpec|MarkSpec}
     * @abstract
     */
    static make() {
      return {
        attrs: this.attrs,
        parseDOM: [{tag: this.tag, getAttrs: this.getAttrs.bind(this)}],
        toDOM: this.toDOM.bind(this)
      };
    }
  }

  /**
   * A class responsible for encapsulating logic around image nodes in the ProseMirror schema.
   * @extends {SchemaDefinition}
   */
  class ImageNode extends SchemaDefinition {
    /** @override */
    static tag = "img[src]";

    /* -------------------------------------------- */

    /** @override */
    static get attrs() {
      return {
        src: {},
        alt: {default: null},
        title: {default: null},
        width: {default: ""},
        height: {default: ""},
        alignment: {default: "", formatting: true}
      };
    }

    /* -------------------------------------------- */

    /** @override */
    static getAttrs(el) {
      const attrs = {
        src: el.getAttribute("src"),
        title: el.title,
        alt: el.alt
      };
      if ( el.classList.contains("centered") ) attrs.alignment = "center";
      else if ( el.style.float ) attrs.alignment = el.style.float;
      if ( el.hasAttribute("width") ) attrs.width = el.width;
      if ( el.hasAttribute("height") ) attrs.height = el.height;
      return attrs;
    }

    /* -------------------------------------------- */

    /** @override */
    static toDOM(node) {
      const {src, alt, title, width, height, alignment} = node.attrs;
      const attrs = {src};
      if ( alignment === "center" ) attrs.class = "centered";
      else if ( alignment ) attrs.style = `float: ${alignment};`;
      if ( alt ) attrs.alt = alt;
      if ( title ) attrs.title = title;
      if ( width ) attrs.width = width;
      if ( height ) attrs.height = height;
      return ["img", attrs];
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    static make() {
      return mergeObject(super.make(), {
        managed: {styles: ["float"], classes: ["centered"]},
        group: "block",
        draggable: true
      });
    }
  }

  /**
   * A class responsible for encapsulating logic around link marks in the ProseMirror schema.
   * @extends {SchemaDefinition}
   */
  class LinkMark extends SchemaDefinition {
    /** @override */
    static tag = "a";

    /* -------------------------------------------- */

    /** @override */
    static get attrs() {
      return {
        href: { default: null },
        title: { default: null }
      }
    }

    /* -------------------------------------------- */

    /** @override */
    static getAttrs(el) {
      if ( (el.children.length === 1) && (el.children[0]?.tagName === "IMG") ) return false;
      return { href: el.href, title: el.title };
    }

    /* -------------------------------------------- */

    /** @override */
    static toDOM(node) {
      const { href, title } = node.attrs;
      const attrs = {};
      if ( href ) attrs.href = href;
      if ( title ) attrs.title = title;
      return ["a", attrs];
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    static make() {
      return mergeObject(super.make(), {
        inclusive: false
      });
    }

    /* -------------------------------------------- */

    /**
     * Handle clicks on link marks while editing.
     * @param {EditorView} view     The ProseMirror editor view.
     * @param {number} pos          The position in the ProseMirror document that the click occurred at.
     * @param {PointerEvent} event  The click event.
     * @param {Mark} mark           The Mark instance.
     * @returns {boolean|void}      Returns true to indicate the click was handled here and should not be propagated to
     *                              other plugins.
     */
    static onClick(view, pos, event, mark) {
      if ( (event.ctrlKey || event.metaKey) && mark.attrs.href ) window.open(mark.attrs.href, "_blank");
      return true;
    }
  }

  /**
   * A class responsible for encapsulating logic around image-link nodes in the ProseMirror schema.
   * @extends {SchemaDefinition}
   */
  class ImageLinkNode extends SchemaDefinition {
    /** @override */
    static tag = "a";

    /* -------------------------------------------- */

    /** @override */
    static get attrs() {
      return mergeObject(ImageNode.attrs, LinkMark.attrs);
    }

    /* -------------------------------------------- */

    /** @override */
    static getAttrs(el) {
      if ( (el.children.length !== 1) || (el.children[0].tagName !== "IMG") ) return false;
      const attrs = ImageNode.getAttrs(el.children[0]);
      attrs.href = el.href;
      attrs.title = el.title;
      return attrs;
    }

    /* -------------------------------------------- */

    /** @override */
    static toDOM(node) {
      const spec = LinkMark.toDOM(node);
      spec.push(ImageNode.toDOM(node));
      return spec;
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    static make() {
      return mergeObject(super.make(), {
        group: "block",
        draggable: true,
        managed: { styles: ["float"], classes: ["centered"] }
      });
    }

    /* -------------------------------------------- */

    /**
     * Handle clicking on image links while editing.
     * @param {EditorView} view     The ProseMirror editor view.
     * @param {number} pos          The position in the ProseMirror document that the click occurred at.
     * @param {PointerEvent} event  The click event.
     * @param {Node} node           The Node instance.
     */
    static onClick(view, pos, event, node) {
      if ( (event.ctrlKey || event.metaKey) && node.attrs.href ) window.open(node.attrs.href, "_blank");
      // For some reason, calling event.preventDefault in this (mouseup) handler is not enough to cancel the default click
      // behaviour. It seems to be related to the outer anchor being set to contenteditable="false" by ProseMirror.
      // This workaround seems to prevent the click.
      const parent = event.target.parentElement;
      if ( (parent.tagName === "A") && !parent.isContentEditable ) parent.contentEditable = "true";
      return true;
    }
  }

  /**
   * A class responsible for encapsulating logic around secret nodes in the ProseMirror schema.
   * @extends {SchemaDefinition}
   */
  class SecretNode extends SchemaDefinition {
    /** @override */
    static tag = "section";

    /* -------------------------------------------- */

    /** @override */
    static get attrs() {
      return {
        revealed: { default: false },
        id: {}
      };
    }

    /* -------------------------------------------- */

    /** @override */
    static getAttrs(el) {
      if ( !el.classList.contains("secret") ) return false;
      return {
        revealed: el.classList.contains("revealed"),
        id: el.id || `secret-${randomID()}`
      };
    }

    /* -------------------------------------------- */

    /** @override */
    static toDOM(node) {
      const attrs = {
        id: node.attrs.id,
        class: `secret${node.attrs.revealed ? " revealed" : ""}`
      };
      return ["section", attrs, 0];
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    static make() {
      return mergeObject(super.make(), {
        content: "block+",
        group: "block",
        defining: true,
        managed: { attributes: ["id"], classes: ["revealed"] }
      });
    }

    /* -------------------------------------------- */

    /**
     * Handle splitting a secret block in two, making sure the new block gets a unique ID.
     * @param {EditorState} state                   The ProseMirror editor state.
     * @param {(tr: Transaction) => void} dispatch  The editor dispatch function.
     */
    static split(state, dispatch) {
      const secret = state.schema.nodes.secret;
      const { $cursor } = state.selection;
      // Check we are actually on a blank line and not splitting text content.
      if ( !$cursor || $cursor.parent.content.size ) return false;
      // Check that we are actually in a secret block.
      if ( $cursor.node(-1).type !== secret ) return false;
      // Check that the block continues past the cursor.
      if ( $cursor.after() === $cursor.end(-1) ) return false;
      const before = $cursor.before(); // The previous line.
      // Ensure a new ID assigned to the new secret block.
      dispatch(state.tr.split(before, 1, [{type: secret, attrs: {id: `secret-${randomID()}`}}]));
      return true;
    }
  }

  /**
   * @typedef {object} AllowedAttributeConfiguration
   * @property {Set<string>} attrs   The set of exactly-matching attribute names.
   * @property {string[]} wildcards  A list of wildcard allowed prefixes for attributes.
   */

  /**
   * @typedef {object} ManagedAttributesSpec
   * @property {string[]} attributes  A list of managed attributes.
   * @property {string[]} styles      A list of CSS property names that are managed as inline styles.
   * @property {string[]} classes     A list of managed class names.
   */

  /**
   * A class responsible for injecting attribute capture logic into the ProseMirror schema.
   */
  class AttributeCapture {
    constructor() {
      this.#parseAllowedAttributesConfig(ALLOWED_HTML_ATTRIBUTES ?? {});
    }

    /* -------------------------------------------- */

    /**
     * The configuration of attributes that are allowed on HTML elements.
     * @type {Record<string, AllowedAttributeConfiguration>}
     */
    #allowedAttrs = {};

    /* -------------------------------------------- */

    /**
     * Augments the schema definition to allow each node or mark to capture all the attributes on an element and preserve
     * them when re-serialized back into the DOM.
     * @param {NodeSpec|MarkSpec} spec  The schema specification.
     */
    attributeCapture(spec) {
      if ( !spec.parseDOM ) return;
      if ( !spec.attrs ) spec.attrs = {};
      spec.attrs._preserve = { default: {}, formatting: true };
      spec.parseDOM.forEach(rule => {
        if ( rule.style ) return; // This doesn't work for style rules. We need a different solution there.
        const getAttrs = rule.getAttrs;
        rule.getAttrs = el => {
          let attrs = getAttrs?.(el);
          if ( attrs === false ) return false;
          if ( typeof attrs !== "object" ) attrs = {};
          mergeObject(attrs, rule.attrs);
          mergeObject(attrs, { _preserve: this.#captureAttributes(el, spec.managed) });
          return attrs;
        };
      });
      const toDOM = spec.toDOM;
      spec.toDOM = node => {
        const domSpec = toDOM(node);
        const attrs = domSpec[1];
        const preserved = node.attrs._preserve ?? {};
        if ( preserved.style ) preserved.style = preserved.style.replaceAll('"', "'");
        if ( getType(attrs) === "Object" ) {
          domSpec[1] = mergeObject(preserved, attrs, { inplace: false });
          if ( ("style" in preserved) && ("style" in attrs) ) domSpec[1].style = mergeStyle(preserved.style, attrs.style);
          if ( ("class" in preserved) && ("class" in attrs) ) domSpec[1].class = mergeClass(preserved.class, attrs.class);
        }
        else domSpec.splice(1, 0, { ...preserved });
        return domSpec;
      };
    }

    /* -------------------------------------------- */

    /**
     * Capture all allowable attributes present on an HTML element and store them in an object for preservation in the
     * schema.
     * @param {HTMLElement} el                 The element.
     * @param {ManagedAttributesSpec} managed  An object containing the attributes, styles, and classes that are managed
     *                                         by the ProseMirror node and should not be preserved.
     * @returns {Attrs}
     */
    #captureAttributes(el, managed={}) {
      const allowed = this.#allowedAttrs[el.tagName.toLowerCase()] ?? this.#allowedAttrs["*"];
      return Array.from(el.attributes).reduce((obj, attr) => {
        if ( attr.name.startsWith("data-pm-") ) return obj; // Ignore attributes managed by the ProseMirror editor itself.
        if ( managed.attributes?.includes(attr.name) ) return obj; // Ignore attributes managed by the node.
        // Ignore attributes that are not allowed.
        if ( !allowed.wildcards.some(prefix => attr.name.startsWith(prefix)) && !allowed.attrs.has(attr.name) ) {
          return obj;
        }
        if ( (attr.name === "class") && managed.classes?.length ) {
          obj.class = classesFromString(attr.value).filter(cls => !managed.classes.includes(cls)).join(" ");
          return obj;
        }
        if ( (attr.name === "style") && managed.styles?.length ) {
          const styles = stylesFromString(attr.value);
          managed.styles.forEach(style => delete styles[style]);
          obj.style = Object.entries(styles).map(([k, v]) => v ? `${k}: ${v}` : null).filterJoin("; ");
          return obj;
        }
        obj[attr.name] = attr.value;
        return obj;
      }, {});
    }

    /* -------------------------------------------- */

    /**
     * Parse the configuration of allowed attributes into a more performant structure.
     * @param {Record<string, string[]>} config  The allowed attributes configuration.
     */
    #parseAllowedAttributesConfig(config) {
      const all = this.#allowedAttrs["*"] = this.#parseAllowedAttributes(config["*"] ?? []);
      for ( const [tag, attrs] of Object.entries(config ?? {}) ) {
        if ( tag === "*" ) continue;
        const allowed = this.#allowedAttrs[tag] = this.#parseAllowedAttributes(attrs);
        all.attrs.forEach(allowed.attrs.add, allowed.attrs);
        allowed.wildcards.push(...all.wildcards);
      }
    }

    /* -------------------------------------------- */

    /**
     * Parse an allowed attributes configuration into a more efficient structure.
     * @param {string[]} attrs  The list of allowed attributes.
     * @returns {AllowedAttributeConfiguration}
     */
    #parseAllowedAttributes(attrs) {
      const allowed = { wildcards: [], attrs: new Set() };
      for ( const attr of attrs ) {
        const wildcard = attr.indexOf("*");
        if ( wildcard < 0 ) allowed.attrs.add(attr);
        else allowed.wildcards.push(attr.substring(0, wildcard));
      }
      return allowed;
    }
  }

  const doc = {
    content: "block+"
  };

  const text = {
    group: "inline"
  };

  const secret = SecretNode.make();
  const link = LinkMark.make();
  const image = ImageNode.make();
  const imageLink = ImageLinkNode.make();

  const nodes = {
    // Core Nodes.
    doc, text, paragraph, blockquote, secret, horizontal_rule: hr, heading, code_block: pre, image_link: imageLink, image,
    hard_break: br,

    // Lists.
    ordered_list: ol, bullet_list: ul, list_item: li, list_item_text: liText,

    // Tables
    table_complex: tableComplex, tbody, thead, tfoot, caption, caption_block: captionBlock, colgroup, col, table_row_complex: tableRowComplex, table_cell_complex: tableCellComplex,
    table_header_complex: tableHeaderComplex, table_cell_complex_block: tableCellComplexBlock, table_header_complex_block: tableHeaderComplexBlock,
    ...builtInTableNodes,

    // Misc.
    details, summary, summary_block: summaryBlock, dl, dt, dd, fieldset, legend, picture, audio: audio$1, video, track, source, object, figure,
    figcaption, small, ruby, rp, rt, iframe
  };

  const marks = {superscript, subscript, span, font, link, em, strong, underline, strikethrough, code};

  // Auto-generated specifications for HTML preservation.
  ["header", "main", "section", "article", "aside", "nav", "footer", "div", "address"].forEach(tag => {
    nodes[tag] = {
      content: "block+",
      group: "block",
      defining: true,
      parseDOM: [{tag}],
      toDOM: () => [tag, 0]
    };
  });

  ["abbr", "cite", "mark", "q", "time", "ins"].forEach(tag => {
    marks[tag] = {
      parseDOM: [{tag}],
      toDOM: () => [tag, 0]
    };
  });

  const all = Object.values(nodes).concat(Object.values(marks));
  const capture = new AttributeCapture();
  all.forEach(capture.attributeCapture.bind(capture));

  const schema = new Schema({nodes, marks});

  /* -------------------------------------------- */
  /*  Handlers                                    */
  /* -------------------------------------------- */

  schema.nodes.list_item.split = splitListItem(schema.nodes.list_item);
  schema.nodes.secret.split = SecretNode.split;
  schema.marks.link.onClick = LinkMark.onClick;
  schema.nodes.image_link.onClick = ImageLinkNode.onClick;

  /**
   * A class responsible for handle drag-and-drop and pasting of image content. Ensuring no base64 data is injected
   * directly into the journal content and it is instead uploaded to the user's data directory.
   * @extends {ProseMirrorPlugin}
   */
  class ProseMirrorImagePlugin extends ProseMirrorPlugin {
    /**
     * @param {Schema} schema                    The ProseMirror schema.
     * @param {object} options                   Additional options to configure the plugin's behaviour.
     * @param {ClientDocument} options.document  A related Document to store extract base64 images for.
     */
    constructor(schema, {document}={}) {
      super(schema);

      if ( !document ) {
        throw new Error("The image drop and pasting plugin requires a reference to a related Document to function.");
      }

      /**
       * The related Document to store extracted base64 images for.
       * @type {ClientDocument}
       */
      Object.defineProperty(this, "document", {value: document, writable: false});
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    static build(schema, options={}) {
      const plugin = new ProseMirrorImagePlugin(schema, options);
      return new Plugin({
        props: {
          handleDrop: plugin._onDrop.bind(plugin),
          handlePaste: plugin._onPaste.bind(plugin)
        }
      });
    }

    /* -------------------------------------------- */

    /**
     * Handle a drop onto the editor.
     * @param {EditorView} view  The ProseMirror editor view.
     * @param {DragEvent} event  The drop event.
     * @param {Slice} slice      A slice of editor content.
     * @param {boolean} moved    Whether the slice has been moved from a different part of the editor.
     * @protected
     */
    _onDrop(view, event, slice, moved) {
      // This is a drag-drop of internal editor content which we do not need to handle specially.
      if ( moved ) return;
      const pos = view.posAtCoords({left: event.clientX, top: event.clientY});
      if ( !pos ) return; // This was somehow dropped outside the editor content.

      if ( event.dataTransfer.types.some(t => t === "text/uri-list") ) {
        const uri = event.dataTransfer.getData("text/uri-list");
        if ( !isBase64Data(uri) ) return; // This is a direct URL hotlink which we can just embed without issue.
      }

      // Handle image drops.
      if ( event.dataTransfer.files.length ) {
        this._uploadImages(view, event.dataTransfer.files, pos.pos);
        return true;
      }
    }

    /* -------------------------------------------- */

    /**
     * Handle a paste into the editor.
     * @param {EditorView} view       The ProseMirror editor view.
     * @param {ClipboardEvent} event  The paste event.
     * @protected
     */
    _onPaste(view, event) {
      if ( event.clipboardData.files.length ) {
        this._uploadImages(view, event.clipboardData.files);
        return true;
      }
      const html = event.clipboardData.getData("text/html");
      if ( !html ) return; // We only care about handling rich content.
      const images = this._extractBase64Images(html);
      if ( !images.length ) return; // If there were no base64 images, defer to the default paste handler.
      this._replaceBase64Images(view, html, images);
      return true;
    }

    /* -------------------------------------------- */

    /**
     * Upload any image files encountered in the drop.
     * @param {EditorView} view  The ProseMirror editor view.
     * @param {FileList} files   The files to upload.
     * @param {number} [pos]     The position in the document to insert at. If not provided, the current selection will be
     *                           replaced instead.
     * @protected
     */
    async _uploadImages(view, files, pos) {
      const image = this.schema.nodes.image;
      const imageExtensions = Object.keys(CONST.IMAGE_FILE_EXTENSIONS);
      for ( const file of files ) {
        if ( !hasFileExtension(file.name, imageExtensions) ) continue;
        const src = await TextEditor._uploadImage(this.document.uuid, file);
        if ( !src ) continue;
        const node = image.create({src});
        if ( pos === undefined ) {
          pos = view.state.selection.from;
          view.dispatch(view.state.tr.replaceSelectionWith(node));
        } else view.dispatch(view.state.tr.insert(pos, node));
        pos += 2; // Advance the position past the just-inserted image so the next image is inserted below it.
      }
    }

    /* -------------------------------------------- */

    /**
     * Capture any base64-encoded images embedded in the rich text paste and upload them.
     * @param {EditorView} view                                      The ProseMirror editor view.
     * @param {string} html                                          The HTML data as a string.
     * @param {[full: string, mime: string, data: string][]} images  An array of extracted base64 image data.
     * @protected
     */
    async _replaceBase64Images(view, html, images) {
      const byMimetype = Object.fromEntries(Object.entries(CONST.IMAGE_FILE_EXTENSIONS).map(([k, v]) => [v, k]));
      let cleaned = html;
      for ( const [full, mime, data] of images ) {
        const file = this.constructor.base64ToFile(data, `pasted-image.${byMimetype[mime]}`, mime);
        const path = await TextEditor._uploadImage(this.document.uuid, file) ?? "";
        cleaned = cleaned.replace(full, path);
      }
      const doc = dom.parseString(cleaned);
      view.dispatch(view.state.tr.replaceSelectionWith(doc));
    }

    /* -------------------------------------------- */

    /**
     * Detect base64 image data embedded in an HTML string and extract it.
     * @param {string} html  The HTML data as a string.
     * @returns {[full: string, mime: string, data: string][]}
     * @protected
     */
    _extractBase64Images(html) {
      const images = Object.values(CONST.IMAGE_FILE_EXTENSIONS);
      const rgx = new RegExp(`data:(${images.join("|")});base64,([^"']+)`, "g");
      return [...html.matchAll(rgx)];
    }

    /* -------------------------------------------- */

    /**
     * Convert a base64 string into a File object.
     * @param {string} data      Base64 encoded data.
     * @param {string} filename  The filename.
     * @param {string} mimetype  The file's mimetype.
     * @returns {File}
     */
    static base64ToFile(data, filename, mimetype) {
      const bin = atob(data);
      let n = bin.length;
      const buf = new ArrayBuffer(n);
      const bytes = new Uint8Array(buf);
      while ( n-- ) bytes[n] = bin.charCodeAt(n);
      return new File([bytes], filename, {type: mimetype});
    }
  }

  /**
   * A simple plugin that records the dirty state of the editor.
   * @extends {ProseMirrorPlugin}
   */
  class ProseMirrorDirtyPlugin extends ProseMirrorPlugin {
    /** @inheritdoc */
    static build(schema, options={}) {
      return new Plugin({
        state: {
          init() {
            return false;
          },
          apply() {
            return true; // If any transaction is applied to the state, we mark the editor as dirty.
          }
        }
      });
    }
  }

  /**
   * A class responsible for handling the dropping of Documents onto the editor and creating content links for them.
   * @extends {ProseMirrorPlugin}
   */
  class ProseMirrorContentLinkPlugin extends ProseMirrorPlugin {
    /**
     * @typedef {object} ProseMirrorContentLinkOptions
     * @property {ClientDocument} [document]      The parent document housing this editor.
     * @property {boolean} [relativeLinks=false]  Whether to generate links relative to the parent document.
     */

    /**
     * @param {Schema} schema                          The ProseMirror schema.
     * @param {ProseMirrorContentLinkOptions} options  Additional options to configure the plugin's behaviour.
     */
    constructor(schema, {document, relativeLinks=false}={}) {
      super(schema);

      if ( relativeLinks && !document ) {
        throw new Error("A document must be provided in order to generate relative links.");
      }

      /**
       * The parent document housing this editor.
       * @type {ClientDocument}
       */
      Object.defineProperty(this, "document", {value: document, writable: false});

      /**
       * Whether to generate links relative to the parent document.
       * @type {boolean}
       */
      Object.defineProperty(this, "relativeLinks", {value: relativeLinks, writable: false});
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    static build(schema, options={}) {
      const plugin = new ProseMirrorContentLinkPlugin(schema, options);
      return new Plugin({
        props: {
          handleDrop: plugin._onDrop.bind(plugin)
        }
      });
    }

    /* -------------------------------------------- */

    /**
     * Handle a drop onto the editor.
     * @param {EditorView} view  The ProseMirror editor view.
     * @param {DragEvent} event  The drop event.
     * @param {Slice} slice      A slice of editor content.
     * @param {boolean} moved    Whether the slice has been moved from a different part of the editor.
     * @protected
     */
    _onDrop(view, event, slice, moved) {
      if ( moved ) return;
      const pos = view.posAtCoords({left: event.clientX, top: event.clientY});
      const data = TextEditor.getDragEventData(event);
      if ( !data.type ) return;
      const options = {};
      if ( this.relativeLinks ) options.relativeTo = this.document;
      const selection = view.state.selection;
      if ( !selection.empty ) {
        const content = selection.content().content;
        options.label = content.textBetween(0, content.size);
      }
      TextEditor.getContentLink(data, options).then(link => {
        if ( !link ) return;
        const tr = view.state.tr;
        if ( selection.empty ) tr.insertText(link, pos.pos);
        else tr.replaceSelectionWith(this.schema.text(link));
        view.dispatch(tr);
        // Focusing immediately only seems to work in Chrome. In Firefox we must yield execution before attempting to
        // focus, otherwise the cursor becomes invisible until the user manually unfocuses and refocuses.
        setTimeout(view.focus.bind(view), 0);
      });
      event.stopPropagation();
      return true;
    }
  }

  /**
   * A class responsible for handling the display of automated link recommendations when a user highlights text in a
   * ProseMirror editor.
   * @param {EditorView} view   The editor view.
   */
  class PossibleMatchesTooltip {

    /**
     * @param {EditorView} view   The editor view.
     */
    constructor(view) {
      this.update(view, null);
    }

    /* -------------------------------------------- */

    /**
     * A reference to any existing tooltip that has been generated as part of a highlight match.
     * @type {HTMLElement}
     */
    tooltip;

    /* -------------------------------------------- */

    /**
     * Update the tooltip based on changes to the selected text.
     * @param {EditorView} view   The editor view.
     * @param {State} lastState   The previous state of the document.
     */
    async update(view, lastState) {
      if ( !game.settings.get("core", "pmHighlightDocumentMatches") ) return;
      const state = view.state;

      // Deactivate tooltip if the document/selection didn't change or is empty
      const stateUnchanged = lastState && (lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection));
      if ( stateUnchanged || state.selection.empty ) return this._deactivateTooltip();

      const selection = state.selection.content().content;
      const highlighted = selection.textBetween(0, selection.size);

      // If the user selected fewer than a certain amount of characters appropriate for the language, we bail out.
      if ( highlighted.length < CONFIG.i18n.searchMinimumCharacterLength ) return this._deactivateTooltip();

      // Look for any matches based on the contents of the selection
      let html = this._findMatches(highlighted);

      // If html is an empty string bail out and deactivate tooltip
      if ( !html ) return this._deactivateTooltip();

      // Enrich the matches HTML to get proper content links
      html = await TextEditor.enrichHTML(html);
      html = html.replace(/data-tooltip="[^"]+"/g, "");
      const {from, to} = state.selection;

      // In-screen coordinates
      const start = view.coordsAtPos(from);
      view.coordsAtPos(to);

      // Position the tooltip. This needs to be very close to the user's cursor, otherwise the locked tooltip will be
      // immediately dismissed for being too far from the tooltip.
      // TODO: We use the selection endpoints here which works fine for single-line selections, but not multi-line.
      const left = (start.left + 3) + "px";
      const bottom = window.innerHeight - start.bottom + 25 + "px";
      const position = {bottom, left};

      if ( this.tooltip ) this._updateTooltip(html);
      else this._createTooltip(position, html, {cssClass: "link-matches"});
    }

    /* -------------------------------------------- */

    /**
     * Create a locked tooltip at the given position.
     * @param {object} position             A position object with coordinates for where the tooltip should be placed
     * @param {string} position.top         Explicit top position for the tooltip
     * @param {string} position.right       Explicit right position for the tooltip
     * @param {string} position.bottom      Explicit bottom position for the tooltip
     * @param {string} position.left        Explicit left position for the tooltip
     * @param {string} text                 Explicit tooltip text or HTML to display.
     * @param {object} [options={}]         Additional options which can override tooltip behavior.
     * @param {array} [options.cssClass]    An optional, space-separated list of CSS classes to apply to the activated
     *                                      tooltip.
     */
    _createTooltip(position, text, options) {
      this.tooltip = game.tooltip.createLockedTooltip(position, text, options);
    }

    /* -------------------------------------------- */

    /**
     * Update the tooltip with new HTML
     * @param {string} html      The HTML to be included in the tooltip
     */
    _updateTooltip(html) {
      this.tooltip.innerHTML = html;
    }

    /* -------------------------------------------- */

    /**
     * Dismiss all locked tooltips and set this tooltip to undefined.
     */
    _deactivateTooltip() {
      if ( !this.tooltip ) return;
      game.tooltip.dismissLockedTooltip(this.tooltip);
      this.tooltip = undefined;
    }

    /* -------------------------------------------- */

    /**
     * Find all Documents in the world/compendia with names that match the selection insensitive to case.
     * @param {string} text      A string which will be matched against document names
     * @returns {string}
     */
    _findMatches(text) {
      let html = "";
      const matches = game.documentIndex.lookup(text, { ownership: "OBSERVER" });
      for ( const [type, collection] of Object.entries(matches) ) {
        if ( collection.length === 0 ) continue;
        html += `<section><h4>${type}</h4><p>`;
        for ( const document of collection ) {
          html += document.entry?.link ? document.entry.link : `@UUID[${document.uuid}]{${document.entry.name}}`;
        }
        html += "</p></section>";
      }
      return html;
    }
  }

  /**
   * A ProseMirrorPlugin wrapper around the {@link PossibleMatchesTooltip} class.
   * @extends {ProseMirrorPlugin}
   */
  class ProseMirrorHighlightMatchesPlugin extends ProseMirrorPlugin {
    /**
     * @param {Schema} schema                     The ProseMirror schema.
     * @param {ProseMirrorMenuOptions} [options]  Additional options to configure the plugin's behaviour.
     */
    constructor(schema, options={}) {
      super(schema);
      this.options = options;
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    static build(schema, options={}) {
      return new Plugin({
        view(editorView) {
          return new PossibleMatchesTooltip(editorView);
        },
        isHighlightMatchesPlugin: true
      });
    }
  }

  /**
   * A class responsible for managing click events inside a ProseMirror editor.
   * @extends {ProseMirrorPlugin}
   */
  class ProseMirrorClickHandler extends ProseMirrorPlugin {
    /** @override */
    static build(schema, options={}) {
      const plugin = new ProseMirrorClickHandler(schema);
      return new Plugin({
        props: {
          handleClickOn: plugin._onClick.bind(plugin)
        }
      });
    }

    /* -------------------------------------------- */

    /**
     * Handle a click on the editor.
     * @param {EditorView} view     The ProseMirror editor view.
     * @param {number} pos          The position in the ProseMirror document that the click occurred at.
     * @param {Node} node           The current ProseMirror Node that the click has bubbled to.
     * @param {number} nodePos      The position of the click within this Node.
     * @param {PointerEvent} event  The click event.
     * @param {boolean} direct      Whether this Node is the one that was directly clicked on.
     * @returns {boolean|void}      A return value of true indicates the event has been handled, it will not propagate to
     *                              other plugins, and ProseMirror will call preventDefault on it.
     * @protected
     */
    _onClick(view, pos, node, nodePos, event, direct) {
      // If this is the inner-most click bubble, check marks for onClick handlers.
      if ( direct ) {
        const $pos = view.state.doc.resolve(pos);
        for ( const mark of $pos.marks() ) {
          if ( mark.type.onClick?.(view, pos, event, mark) === true ) return true;
        }
      }

      // Check the current Node for onClick handlers.
      return node.type.onClick?.(view, pos, event, node);
    }
  }

  /**
   * A class responsible for applying transformations to content pasted inside the editor.
   */
  class ProseMirrorPasteTransformer extends ProseMirrorPlugin {
    /** @override */
    static build(schema, options={}) {
      const plugin = new ProseMirrorPasteTransformer(schema);
      return new Plugin({
        props: {
          transformPasted: plugin._onPaste.bind(plugin)
        }
      });
    }

    /* -------------------------------------------- */

    /**
     * Transform content before it is injected into the ProseMirror document.
     * @param {Slice} slice      The content slice.
     * @param {EditorView} view  The ProseMirror editor view.
     * @returns {Slice}          The transformed content.
     */
    _onPaste(slice, view) {
      // Give pasted secret blocks new IDs.
      const secret = view.state.schema.nodes.secret;
      return transformSlice(slice, node => {
        if ( node.type === secret ) {
          return secret.create({ ...node.attrs, id: `secret-${randomID()}` }, node.content, node.marks);
        }
      });
    }
  }

  /** @module prosemirror */


  const dom = {
    parser: DOMParser.fromSchema(schema),
    serializer: DOMSerializer.fromSchema(schema),
    parseString: parseHTMLString,
    serializeString: serializeHTMLString
  };

  const defaultPlugins = {
    inputRules: ProseMirrorInputRules.build(schema),
    keyMaps: ProseMirrorKeyMaps.build(schema),
    menu: ProseMirrorMenu.build(schema),
    isDirty: ProseMirrorDirtyPlugin.build(schema),
    clickHandler: ProseMirrorClickHandler.build(schema),
    pasteTransformer: ProseMirrorPasteTransformer.build(schema),
    baseKeyMap: keymap(baseKeymap),
    dropCursor: dropCursor(),
    gapCursor: gapCursor(),
    history: history(),
    columnResizing: columnResizing(),
    tables: tableEditing()
  };

  var prosemirror = /*#__PURE__*/Object.freeze({
    __proto__: null,
    AllSelection: AllSelection,
    DOMParser: DOMParser,
    DOMSerializer: DOMSerializer,
    EditorState: EditorState,
    EditorView: EditorView,
    Plugin: Plugin,
    PluginKey: PluginKey,
    ProseMirrorClickHandler: ProseMirrorClickHandler,
    ProseMirrorContentLinkPlugin: ProseMirrorContentLinkPlugin,
    ProseMirrorDirtyPlugin: ProseMirrorDirtyPlugin,
    ProseMirrorHighlightMatchesPlugin: ProseMirrorHighlightMatchesPlugin,
    ProseMirrorImagePlugin: ProseMirrorImagePlugin,
    ProseMirrorInputRules: ProseMirrorInputRules,
    ProseMirrorKeyMaps: ProseMirrorKeyMaps,
    ProseMirrorMenu: ProseMirrorMenu,
    ProseMirrorPlugin: ProseMirrorPlugin,
    Schema: Schema,
    Step: Step,
    TextSelection: TextSelection,
    collab: index,
    commands: index$3,
    defaultPlugins: defaultPlugins,
    defaultSchema: schema,
    dom: dom,
    input: index$4,
    keymap: keymap,
    list: index$2,
    state: index$5,
    tables: index$1,
    transform: index$6
  });

  /**
   * @typedef {object} GridConfiguration
   * @property {number} size            The size of a grid space in pixels (a positive number)
   * @property {number} [distance=1]    The distance of a grid space in units (a positive number)
   * @property {string} [units=""]      The units of measurement
   * @property {string} [style="solidLines"] The style of the grid
   * @property {ColorSource} [color=0]  The color of the grid
   * @property {number} [alpha=1]       The alpha of the grid
   * @property {number} [thickness=1]   The line thickness of the grid
   */

  /**
   * A pair of row and column coordinates of a grid space.
   * @typedef {object} GridOffset
   * @property {number} i    The row coordinate
   * @property {number} j    The column coordinate
   */

  /**
   * An offset of a grid space or a point with pixel coordinates.
   * @typedef {GridOffset|Point} GridCoordinates
   */

  /**
   * Snapping behavior is defined by the snapping mode at the given resolution of the grid.
   * @typedef {object} GridSnappingBehavior
   * @property {number} mode              The snapping mode (a union of {@link CONST.GRID_SNAPPING_MODES})
   * @property {number} [resolution=1]    The resolution (a positive integer)
   */

  /**
   * The base grid class.
   * @abstract
   */
  class BaseGrid {
    /**
     * The base grid constructor.
     * @param {GridConfiguration} config                        The grid configuration
     */
    constructor({size, distance=1, units="", style="solidLines", thickness=1, color, alpha=1}) {
      /** @deprecated since v12 */
      if ( "dimensions" in arguments[0] ) {
        const msg = "The constructor BaseGrid({dimensions, color, alpha}) is deprecated "
          + "in favor of BaseGrid({size, distance, units, style, thickness, color, alpha}).";
        logCompatibilityWarning(msg, {since: 12, until: 14});
        const dimensions = arguments[0].dimensions;
        size = dimensions.size;
        distance = dimensions.distance || 1;
      }

      if ( size === undefined ) throw new Error(`${this.constructor.name} cannot be constructed without a size`);

      // Convert the color to a CSS string
      if ( color ) color = Color$1.from(color);
      if ( !color?.valid ) color = new Color$1(0);

      /**
       * The size of a grid space in pixels.
       * @type {number}
       */
      this.size = size;

      /**
       * The width of a grid space in pixels.
       * @type {number}
       */
      this.sizeX = size;

      /**
       * The height of a grid space in pixels.
       * @type {number}
       */
      this.sizeY = size;

      /**
       * The distance of a grid space in units.
       * @type {number}
       */
      this.distance = distance;

      /**
       * The distance units used in this grid.
       * @type {string}
       */
      this.units = units;

      /**
       * The style of the grid.
       * @type {string}
       */
      this.style = style;

      /**
       * The thickness of the grid.
       * @type {number}
       */
      this.thickness = thickness;

      /**
       * The color of the grid.
       * @type {Color}
       */
      this.color = color;

      /**
       * The opacity of the grid.
       * @type {number}
       */
      this.alpha = alpha;
    }

    /* -------------------------------------------- */

    /**
     * The grid type (see {@link CONST.GRID_TYPES}).
     * @type {number}
     */
    type;

    /* -------------------------------------------- */

    /**
     * Is this a gridless grid?
     * @type {boolean}
     */
    get isGridless() {
      return this.type === GRID_TYPES.GRIDLESS;
    }

    /* -------------------------------------------- */

    /**
     * Is this a square grid?
     * @type {boolean}
     */
    get isSquare() {
      return this.type === GRID_TYPES.SQUARE;
    }

    /* -------------------------------------------- */

    /**
     * Is this a hexagonal grid?
     * @type {boolean}
     */
    get isHexagonal() {
      return (this.type >= GRID_TYPES.HEXODDR) && (this.type <= GRID_TYPES.HEXEVENQ);
    }

    /* -------------------------------------------- */

    /**
     * Calculate the total size of the canvas with padding applied, as well as the top-left coordinates of the inner
     * rectangle that houses the scene.
     * @param {number} sceneWidth         The width of the scene.
     * @param {number} sceneHeight        The height of the scene.
     * @param {number} padding            The percentage of padding.
     * @returns {{width: number, height: number, x: number, y: number, rows: number, columns: number}}
     * @abstract
     */
    calculateDimensions(sceneWidth, sceneHeight, padding) {
      throw new Error("A subclass of the BaseGrid must implement the calculateDimensions method");
    }

    /* -------------------------------------------- */

    /**
     * Returns the offset of the grid space corresponding to the given coordinates.
     * @param {GridCoordinates} coords    The coordinates
     * @returns {GridOffset}              The offset
     * @abstract
     */
    getOffset(coords) {
      throw new Error("A subclass of the BaseGrid must implement the getOffset method");
    }

    /* -------------------------------------------- */

    /**
     * Returns the smallest possible range containing the offsets of all grid spaces that intersect the given bounds.
     * If the bounds are empty (nonpositive width or height), then the offset range is empty.
     * @example
     * ```js
     * const [i0, j0, i1, j1] = grid.getOffsetRange(bounds);
     * for ( let i = i0; i < i1; i++ ) {
     *   for ( let j = j0; j < j1; j++ ) {
     *     const offset = {i, j};
     *     // ...
     *   }
     * }
     * ```
     * @param {Rectangle} bounds                                      The bounds
     * @returns {[i0: number, j0: number, i1: number, j1: number]}    The offset range
     * @abstract
     */
    getOffsetRange({x, y, width, height}) {
      throw new Error("A subclass of the BaseGrid must implement the getOffsetRange method");
    }

    /* -------------------------------------------- */

    /**
     * Returns the offsets of the grid spaces adjacent to the one corresponding to the given coordinates.
     * Returns an empty array in gridless grids.
     * @param {GridCoordinates} coords    The coordinates
     * @returns {GridOffset[]}            The adjacent offsets
     * @abstract
     */
    getAdjacentOffsets(coords) {
      throw new Error("A subclass of the BaseGrid must implement the getAdjacentOffsets method");
    }

    /* -------------------------------------------- */

    /**
     * Returns true if the grid spaces corresponding to the given coordinates are adjacent to each other.
     * In square grids with illegal diagonals the diagonally neighboring grid spaces are not adjacent.
     * Returns false in gridless grids.
     * @param {GridCoordinates} coords1    The first coordinates
     * @param {GridCoordinates} coords2    The second coordinates
     * @returns {boolean}
     * @abstract
     */
    testAdjacency(coords1, coords2) {
      throw new Error("A subclass of the BaseGrid must implement the testAdjacency method");
    }

    /* -------------------------------------------- */

    /**
     * Returns the offset of the grid space corresponding to the given coordinates
     * shifted by one grid space in the given direction.
     * In square grids with illegal diagonals the offset of the given coordinates is returned
     * if the direction is diagonal.
     * @param {GridCoordinates} coords    The coordinates
     * @param {number} direction          The direction (see {@link CONST.MOVEMENT_DIRECTIONS})
     * @returns {GridOffset}              The offset
     * @abstract
     */
    getShiftedOffset(coords, direction) {
      throw new Error("A subclass of the BaseGrid must implement the getShiftedOffset method");
    }

    /* -------------------------------------------- */

    /**
     * Returns the point shifted by the difference between the grid space corresponding to the given coordinates
     * and the shifted grid space in the given direction.
     * In square grids with illegal diagonals the point is not shifted if the direction is diagonal.
     * In gridless grids the point coordinates are shifted by the grid size.
     * @param {Point} point         The point that is to be shifted
     * @param {number} direction    The direction (see {@link CONST.MOVEMENT_DIRECTIONS})
     * @returns {Point}             The shifted point
     * @abstract
     */
    getShiftedPoint(point, direction) {
      throw new Error("A subclass of the BaseGrid must implement the getShiftedPoint method");
    }

    /* -------------------------------------------- */

    /**
     * Returns the top-left point of the grid space corresponding to the given coordinates.
     * If given a point, the top-left point of the grid space that contains it is returned.
     * In gridless grids a point with the same coordinates as the given point is returned.
     * @param {GridCoordinates} coords    The coordinates
     * @returns {Point}                   The top-left point
     * @abstract
     */
    getTopLeftPoint(coords) {
      throw new Error("A subclass of the BaseGrid must implement the getTopLeftPoint method");
    }

    /* -------------------------------------------- */

    /**
     * Returns the center point of the grid space corresponding to the given coordinates.
     * If given a point, the center point of the grid space that contains it is returned.
     * In gridless grids a point with the same coordinates as the given point is returned.
     * @param {GridCoordinates} coords    The coordinates
     * @returns {Point}                   The center point
     * @abstract
     */
    getCenterPoint(coords) {
      throw new Error("A subclass of the BaseGrid must implement the getCenterPoint method");
    }

    /* -------------------------------------------- */

    /**
     * Returns the points of the grid space shape relative to the center point.
     * The points are returned in the same order as in {@link BaseGrid#getVertices}.
     * In gridless grids an empty array is returned.
     * @returns {Point[]}    The points of the polygon
     * @abstract
     */
    getShape() {
      throw new Error("A subclass of the BaseGrid must implement the getShape method");
    }

    /* -------------------------------------------- */

    /**
     * Returns the vertices of the grid space corresponding to the given coordinates.
     * The vertices are returned ordered in positive orientation with the first vertex
     * being the top-left vertex in square grids, the top vertex in row-oriented
     * hexagonal grids, and the left vertex in column-oriented hexagonal grids.
     * In gridless grids an empty array is returned.
     * @param {GridCoordinates} coords    The coordinates
     * @returns {Point[]}                 The vertices
     * @abstract
     */
    getVertices(coords) {
      throw new Error("A subclass of the BaseGrid must implement the getVertices method");
    }

    /* -------------------------------------------- */

    /**
     * Snaps the given point to the grid.
     * @param {Point} point                      The point that is to be snapped
     * @param {GridSnappingBehavior} behavior    The snapping behavior
     * @returns {Point}                          The snapped point
     * @abstract
     */
    getSnappedPoint({x, y}, behavior) {
      throw new Error("A subclass of the BaseGrid must implement the getSnappedPoint method");
    }

    /* -------------------------------------------- */

    /**
     * @typedef {GridCoordinates | (GridCoordinates & {teleport: boolean})} GridMeasurePathWaypoint
     */

    /**
     * The measurements of a waypoint.
     * @typedef {object} GridMeasurePathResultWaypoint
     * @property {GridMeasurePathResultSegment|null} backward    The segment from the previous waypoint to this waypoint.
     * @property {GridMeasurePathResultSegment|null} forward     The segment from this waypoint to the next waypoint.
     * @property {number} distance    The total distance travelled along the path up to this waypoint.
     * @property {number} spaces      The total number of spaces moved along a direct path up to this waypoint.
     * @property {number} cost    The total cost of the direct path ({@link BaseGrid#getDirectPath}) up to this waypoint.
     */

    /**
     * The measurements of a segment.
     * @typedef {object} GridMeasurePathResultSegment
     * @property {GridMeasurePathResultWaypoint} from    The waypoint that this segment starts from.
     * @property {GridMeasurePathResultWaypoint} to      The waypoint that this segment goes to.
     * @property {boolean} teleport   Is teleporation?
     * @property {number} distance    The distance travelled in grid units along this segment.
     * @property {number} spaces      The number of spaces moved along this segment.
     * @property {number} cost    The cost of the direct path ({@link BaseGrid#getDirectPath}) between the two waypoints.
     */

    /**
     * The measurements result of {@link BaseGrid#measurePath}.
     * @typedef {object} GridMeasurePathResult
     * @property {GridMeasurePathResultWaypoint[]} waypoints    The measurements at each waypoint.
     * @property {GridMeasurePathResultSegment[]} segments      The measurements at each segment.
     * @property {number} distance    The total distance travelled along the path through all waypoints.
     * @property {number} spaces      The total number of spaces moved along a direct path through all waypoints.
     *                                Moving from a grid space to any of its neighbors counts as 1 step.
     *                                Always 0 in gridless grids.
     * @property {number} cost   The total cost of the direct path ({@link BaseGrid#getDirectPath}) through all waypoints.
     */

    /**
     * A function that returns the cost for a given move between grid spaces.
     * In square and hexagonal grids the grid spaces are always adjacent unless teleported.
     * The distance is 0 if and only if teleported. The function is never called with the same offsets.
     * @callback GridMeasurePathCostFunction
     * @param {GridOffset} from    The offset that is moved from.
     * @param {GridOffset} to      The offset that is moved to.
     * @param {number} distance    The distance between the grid spaces, or 0 if teleported.
     * @returns {number}           The cost of the move between the grid spaces.
     */

    /**
     * Measure a shortest, direct path through the given waypoints.
     * @param {GridMeasurePathWaypoint[]} waypoints           The waypoints the path must pass through
     * @param {object} [options]                              Additional measurement options
     * @param {GridMeasurePathCostFunction} [options.cost]    The function that returns the cost
     *   for a given move between grid spaces (default is the distance travelled along the direct path)
     * @returns {GridMeasurePathResult}        The measurements a shortest, direct path through the given waypoints.
     */
    measurePath(waypoints, options={}) {
      const result = {
        waypoints: [],
        segments: []
      };
      if ( waypoints.length !== 0 ) {
        let from = {backward: null, forward: null};
        result.waypoints.push(from);
        for ( let i = 1; i < waypoints.length; i++ ) {
          const to = {backward: null, forward: null};
          const segment = {from, to};
          from.forward = to.backward = segment;
          result.waypoints.push(to);
          result.segments.push(segment);
          from = to;
        }
      }
      this._measurePath(waypoints, options, result);
      return result;
    }

    /* -------------------------------------------- */

    /**
     * Measures the path and writes the measurements into `result`.
     * Called by {@link BaseGrid#measurePath}.
     * @param {GridMeasurePathWaypoint[]} waypoints           The waypoints the path must pass through
     * @param {object} options                                Additional measurement options
     * @param {GridMeasurePathCostFunction} [options.cost]    The function that returns the cost
     *   for a given move between grid spaces (default is the distance travelled)
     * @param {GridMeasurePathResult} result    The measurement result that the measurements need to be written to
     * @protected
     * @abstract
     */
    _measurePath(waypoints, options, result) {
      throw new Error("A subclass of the BaseGrid must implement the _measurePath method");
    }

    /* -------------------------------------------- */

    /**
     * Returns the sequence of grid offsets of a shortest, direct path passing through the given waypoints.
     * @param {GridCoordinates[]} waypoints    The waypoints the path must pass through
     * @returns {GridOffset[]}                 The sequence of grid offsets of a shortest, direct path
     * @abstract
     */
    getDirectPath(waypoints) {
      throw new Error("A subclass of the BaseGrid must implement the getDirectPath method");
    }

    /* -------------------------------------------- */

    /**
     * Get the point translated in a direction by a distance.
     * @param {Point} point         The point that is to be translated.
     * @param {number} direction    The angle of direction in degrees.
     * @param {number} distance     The distance in grid units.
     * @returns {Point}             The translated point.
     * @abstract
     */
    getTranslatedPoint(point, direction, distance) {
      throw new Error("A subclass of the BaseGrid must implement the getTranslatedPoint method");
    }

    /* -------------------------------------------- */

    /**
     * Get the circle polygon given the radius in grid units for this grid.
     * The points of the polygon are returned ordered in positive orientation.
     * In gridless grids an approximation of the true circle with a deviation of less than 0.25 pixels is returned.
     * @param {Point} center     The center point of the circle.
     * @param {number} radius    The radius in grid units.
     * @returns {Point[]}        The points of the circle polygon.
     * @abstract
     */
    getCircle(center, radius) {
      throw new Error("A subclass of the BaseGrid must implement the getCircle method");
    }

    /* -------------------------------------------- */

    /**
     * Get the cone polygon given the radius in grid units and the angle in degrees for this grid.
     * The points of the polygon are returned ordered in positive orientation.
     * In gridless grids an approximation of the true cone with a deviation of less than 0.25 pixels is returned.
     * @param {Point} origin        The origin point of the cone.
     * @param {number} radius       The radius in grid units.
     * @param {number} direction    The direction in degrees.
     * @param {number} angle        The angle in degrees.
     * @returns {Point[]}           The points of the cone polygon.
     */
    getCone(origin, radius, direction, angle) {
      if ( (radius <= 0) || (angle <= 0) ) return [];
      const circle = this.getCircle(origin, radius);
      if ( angle >= 360 ) return circle;
      const n = circle.length;
      const aMin = Math.normalizeRadians(Math.toRadians(direction - (angle / 2)));
      const aMax = aMin + Math.toRadians(angle);
      const pMin = {x: origin.x + (Math.cos(aMin) * this.size), y: origin.y + (Math.sin(aMin) * this.size)};
      const pMax = {x: origin.x + (Math.cos(aMax) * this.size), y: origin.y + (Math.sin(aMax) * this.size)};
      const angles = circle.map(p => {
        const a = Math.atan2(p.y - origin.y, p.x - origin.x);
        return a >= aMin ? a : a + (2 * Math.PI);
      });
      const points = [{x: origin.x, y: origin.y}];
      for ( let i = 0, c0 = circle[n - 1], a0 = angles[n - 1]; i < n; i++ ) {
        let c1 = circle[i];
        let a1 = angles[i];
        if ( a0 > a1 ) {
          const {x: x1, y: y1} = lineLineIntersection(c0, c1, origin, pMin);
          points.push({x: x1, y: y1});
          while ( a1 < aMax ) {
            points.push(c1);
            i = (i + 1) % n;
            c0 = c1;
            c1 = circle[i];
            a0 = a1;
            a1 = angles[i];
            if ( a0 > a1 ) break;
          }
          const {x: x2, y: y2} = lineLineIntersection(c0, c1, origin, pMax);
          points.push({x: x2, y: y2});
          break;
        }
        c0 = c1;
        a0 = a1;
      }
      return points;
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getRect(w, h) {
      const msg = "BaseGrid#getRect is deprecated. If you need the size of a Token, use Token#getSize instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return new PIXI.Rectangle(0, 0, w * this.sizeX, h * this.sizeY);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    static calculatePadding(gridType, width, height, size, padding, options) {
      const msg = "BaseGrid.calculatePadding is deprecated in favor of BaseGrid#calculateDimensions.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      let grid;
      if ( gridType === GRID_TYPES.GRIDLESS ) {
        grid = new foundry.grid.GridlessGrid({size});
      } else if ( gridType === GRID_TYPES.SQUARE ) {
        grid = new foundry.grid.SquareGrid({size});
      } else if ( gridType.between(GRID_TYPES.HEXODDR, GRID_TYPES.HEXEVENQ) ) {
        const columns = (gridType === GRID_TYPES.HEXODDQ) || (gridType === GRID_TYPES.HEXEVENQ);
        if ( options?.legacy ) return HexagonalGrid._calculatePreV10Dimensions(columns, size,
          sceneWidth, sceneHeight, padding);
        grid = new foundry.grid.HexagonalGrid({
          columns,
          even: (gridType === GRID_TYPES.HEXEVENR) || (gridType === GRID_TYPES.HEXEVENQ),
          size
        });
      } else {
        throw new Error("Invalid grid type");
      }
      return grid.calculateDimensions(width, height, padding);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated
     * @ignore
     */
    get w() {
      const msg = "BaseGrid#w is deprecated in favor of BaseGrid#sizeX.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return this.sizeX;
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    set w(value) {
      const msg = "BaseGrid#w is deprecated in favor of BaseGrid#sizeX.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      this.sizeX = value;
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get h() {
      const msg = "BaseGrid#h is deprecated in favor of BaseGrid#sizeY.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return this.sizeY;
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    set h(value) {
      const msg = "BaseGrid#h is deprecated in favor of BaseGrid#sizeY.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      this.sizeY = value;
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getTopLeft(x, y) {
      const msg = "BaseGrid#getTopLeft is deprecated. Use BaseGrid#getTopLeftPoint instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      let [row, col] = this.getGridPositionFromPixels(x, y);
      return this.getPixelsFromGridPosition(row, col);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getCenter(x, y) {
      const msg = "BaseGrid#getCenter is deprecated. Use BaseGrid#getCenterPoint instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return [x, y];
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getNeighbors(row, col) {
      const msg = "BaseGrid#getNeighbors is deprecated. Use BaseGrid#getAdjacentOffsets instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return this.getAdjacentOffsets({i: row, j: col}).map(({i, j}) => [i, j]);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getGridPositionFromPixels(x, y) {
      const msg = "BaseGrid#getGridPositionFromPixels is deprecated. Use BaseGrid#getOffset instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return [y, x].map(Math.round);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getPixelsFromGridPosition(row, col) {
      const msg = "BaseGrid#getPixelsFromGridPosition is deprecated. Use BaseGrid#getTopLeftPoint instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return [col, row].map(Math.round);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    shiftPosition(x, y, dx, dy, options={}) {
      const msg = "BaseGrid#shiftPosition is deprecated. Use BaseGrid#getShiftedPoint instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return [x + (dx * this.size), y + (dy * this.size)];
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    measureDistances(segments, options={}) {
      const msg = "BaseGrid#measureDistances is deprecated. Use BaseGrid#measurePath instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return segments.map(s => {
        return (s.ray.distance / this.size) * this.distance;
      });
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getSnappedPosition(x, y, interval=null, options={}) {
      const msg = "BaseGrid#getSnappedPosition is deprecated. Use BaseGrid#getSnappedPoint instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      if ( interval === 0 ) return {x: Math.round(x), y: Math.round(y)};
      interval = interval ?? 1;
      return {
        x: Math.round(x.toNearest(this.sizeX / interval)),
        y: Math.round(y.toNearest(this.sizeY / interval))
      };
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    highlightGridPosition(layer, options) {
      const msg = "BaseGrid#highlightGridPosition is deprecated. Use GridLayer#highlightPosition instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      canvas.interface.grid.highlightPosition(layer.name, options);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get grid() {
      const msg = "canvas.grid.grid is deprecated. Use canvas.grid instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return this;
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    isNeighbor(r0, c0, r1, c1) {
      const msg = "canvas.grid.isNeighbor is deprecated. Use canvas.grid.testAdjacency instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return this.testAdjacency({i: r0, j: c0}, {i: r1, j: c1});
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get isHex() {
      const msg = "canvas.grid.isHex is deprecated. Use of canvas.grid.isHexagonal instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return this.isHexagonal;
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    measureDistance(origin, target, options={}) {
      const msg = "canvas.grid.measureDistance is deprecated. "
        + "Use canvas.grid.measurePath instead for non-Euclidean measurements.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      const ray = new Ray(origin, target);
      const segments = [{ray}];
      return this.measureDistances(segments, options)[0];
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get highlight() {
      const msg = "canvas.grid.highlight is deprecated. Use canvas.interface.grid.highlight instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return canvas.interface.grid.highlight;
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get highlightLayers() {
      const msg = "canvas.grid.highlightLayers is deprecated. Use canvas.interface.grid.highlightLayers instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return canvas.interface.grid.highlightLayers;
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    addHighlightLayer(name) {
      const msg = "canvas.grid.addHighlightLayer is deprecated. Use canvas.interface.grid.addHighlightLayer instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return canvas.interface.grid.addHighlightLayer(name);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    clearHighlightLayer(name) {
      const msg = "canvas.grid.clearHighlightLayer is deprecated. Use canvas.interface.grid.clearHighlightLayer instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      canvas.interface.grid.clearHighlightLayer(name);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    destroyHighlightLayer(name) {
      const msg = "canvas.grid.destroyHighlightLayer is deprecated. Use canvas.interface.grid.destroyHighlightLayer instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      canvas.interface.grid.destroyHighlightLayer(name);
    }

    /* -------------------------------------------- */


    /**
     * @deprecated since v12
     * @ignore
     */
    getHighlightLayer(name) {
      const msg = "canvas.grid.getHighlightLayer is deprecated. Use canvas.interface.grid.getHighlightLayer instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return canvas.interface.grid.getHighlightLayer(name);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    highlightPosition(name, options) {
      const msg = "canvas.grid.highlightPosition is deprecated. Use canvas.interface.grid.highlightPosition instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      canvas.interface.grid.highlightPosition(name, options);
    }
  }

  /**
   * @typedef {object} _HexagonalGridConfiguration
   * @property {boolean} [columns=false]  Is this grid column-based (flat-topped) or row-based (pointy-topped)?
   * @property {boolean} [even=false]     Is this grid even or odd?
   */

  /**
   * @typedef {GridConfiguration&_HexagonalGridConfiguration} HexagonalGridConfiguration
   */

  /**
   * Cube coordinates in a hexagonal grid. q + r + s = 0.
   * @typedef {object} HexagonalGridCube
   * @property {number} q    The coordinate along the E-W (columns) or SW-NE (rows) axis.
   *                         Equal to the offset column coordinate if column orientation.
   * @property {number} r    The coordinate along the NE-SW (columns) or N-S (rows) axis.
   *                         Equal to the offset row coordinate if row orientation.
   * @property {number} s    The coordinate along the SE-NW axis.
   */

  /**
   * Hex cube coordinates, an offset of a grid space, or a point with pixel coordinates.
   * @typedef {GridCoordinates|HexagonalGridCube} HexagonalGridCoordinates
   */

  /* -------------------------------------------- */

  /**
   * The hexagonal grid class.
   */
  let HexagonalGrid$1 = class HexagonalGrid extends BaseGrid {
    /**
     * The hexagonal grid constructor.
     * @param {HexagonalGridConfiguration} config   The grid configuration
     */
    constructor(config) {
      super(config);
      const {columns, even} = config;

      /**
       * Is this grid column-based (flat-topped) or row-based (pointy-topped)?
       * @type {boolean}
       */
      this.columns = !!columns;

      /**
       * Is this grid even or odd?
       * @type {boolean}
       */
      this.even = !!even;

      // Set the type and size of the grid
      if ( columns ) {
        if ( even ) this.type = GRID_TYPES.HEXEVENQ;
        else this.type = GRID_TYPES.HEXODDQ;
        this.sizeX *= (2 * Math.SQRT1_3);
      } else {
        if ( even ) this.type = GRID_TYPES.HEXEVENR;
        else this.type = GRID_TYPES.HEXODDR;
        this.sizeY *= (2 * Math.SQRT1_3);
      }
    }

    /* -------------------------------------------- */

    /**
     * Returns the offset of the grid space corresponding to the given coordinates.
     * @param {HexagonalGridCoordinates} coords    The coordinates
     * @returns {GridOffset}                       The offset
     */
    getOffset(coords) {
      if ( coords.i !== undefined ) return {i: coords.i, j: coords.j};
      const cube = coords.q !== undefined ? coords : this.pointToCube(coords);
      return this.cubeToOffset(HexagonalGrid.cubeRound(cube));
    }

    /* -------------------------------------------- */

    /** @override */
    getOffsetRange({x, y, width, height}) {
      const x0 = x;
      const y0 = y;
      const {i: i00, j: j00} = this.getOffset({x: x0, y: y0});
      if ( !((width > 0) && (height > 0)) ) return [i00, j00, i00, j00];
      const x1 = x + width;
      const y1 = y + height;
      const {i: i01, j: j01} = this.getOffset({x: x1, y: y0});
      const {i: i10, j: j10} = this.getOffset({x: x0, y: y1});
      const {i: i11, j: j11} = this.getOffset({x: x1, y: y1});
      let i0 = Math.min(i00, i01, i10, i11);
      let j0 = Math.min(j00, j01, j10, j11);
      let i1 = Math.max(i00, i01, i10, i11) + 1;
      let j1 = Math.max(j00, j01, j10, j11) + 1;
      // While the corners of the rectangle are included in this range, the edges of the rectangle might
      // intersect rows or columns outside of the range. So we need to expand the range if necessary.
      if ( this.columns ) {
        if ( (i00 === i01) && (j00 < j01) && (!(j00 % 2) !== this.even) && (y0 < i00 * this.sizeY) ) i0--;
        if ( (i10 === i11) && (j10 < j11) && (!(j00 % 2) === this.even) && (y1 > (i10 + 0.5) * this.sizeY) ) i1++;
        if ( (j00 === j10) && (i00 < i10) && (x0 < ((j00 * 0.75) + 0.25) * this.sizeX) ) j0--;
        if ( (j01 === j11) && (i01 < i11) && (x1 > ((j01 * 0.75) + 0.75) * this.sizeX) ) j1++;
      } else {
        if ( (j00 === j10) && (i00 < i10) && (!(i00 % 2) !== this.even) && (x0 < j00 * this.sizeX) ) j0--;
        if ( (j01 === j11) && (i01 < i11) && (!(i00 % 2) === this.even) && (x1 > (j01 + 0.5) * this.sizeX) ) j1++;
        if ( (i00 === i01) && (j00 < j01) && (y0 < ((i00 * 0.75) + 0.25) * this.sizeY) ) i0--;
        if ( (i10 === i11) && (j10 < j11) && (y1 > ((i10 * 0.75) + 0.75) * this.sizeY) ) i1++;
      }
      return [i0, j0, i1, j1];
    }

    /* -------------------------------------------- */

    /** @override */
    getAdjacentOffsets(coords) {
      return this.getAdjacentCubes(coords).map(cube => this.getOffset(cube));
    }

    /* -------------------------------------------- */

    /** @override */
    testAdjacency(coords1, coords2) {
      return HexagonalGrid.cubeDistance(this.getCube(coords1), this.getCube(coords2)) === 1;
    }

    /* -------------------------------------------- */

    /** @override */
    getShiftedOffset(coords, direction) {
      const offset = this.getOffset(coords);
      if ( this.columns ) {
        if ( !(direction & MOVEMENT_DIRECTIONS.LEFT) !== !(direction & MOVEMENT_DIRECTIONS.RIGHT) ) {
          const even = (offset.j % 2 === 0) === this.even;
          if ( (even && (direction & MOVEMENT_DIRECTIONS.UP)) || (!even && (direction & MOVEMENT_DIRECTIONS.DOWN)) ) {
            direction &= ~(MOVEMENT_DIRECTIONS.UP | MOVEMENT_DIRECTIONS.DOWN);
          }
        }
      } else {
        if ( !(direction & MOVEMENT_DIRECTIONS.UP) !== !(direction & MOVEMENT_DIRECTIONS.DOWN) ) {
          const even = (offset.i % 2 === 0) === this.even;
          if ( (even && (direction & MOVEMENT_DIRECTIONS.LEFT)) || (!even && (direction & MOVEMENT_DIRECTIONS.RIGHT)) ) {
            direction &= ~(MOVEMENT_DIRECTIONS.LEFT | MOVEMENT_DIRECTIONS.RIGHT);
          }
        }
      }
      if ( direction & MOVEMENT_DIRECTIONS.UP ) offset.i--;
      if ( direction & MOVEMENT_DIRECTIONS.DOWN ) offset.i++;
      if ( direction & MOVEMENT_DIRECTIONS.LEFT ) offset.j--;
      if ( direction & MOVEMENT_DIRECTIONS.RIGHT ) offset.j++;
      return offset;
    }

    /* -------------------------------------------- */

    /** @override */
    getShiftedPoint(point, direction) {
      const center = this.getCenterPoint(point);
      const shifted = this.getCenterPoint(this.getShiftedOffset(center, direction));
      shifted.x = point.x + (shifted.x - center.x);
      shifted.y = point.y + (shifted.y - center.y);
      return shifted;
    }

    /* -------------------------------------------- */

    /**
     * Returns the cube coordinates of the grid space corresponding to the given coordinates.
     * @param {HexagonalGridCoordinates} coords    The coordinates
     * @returns {HexagonalGridCube}                The cube coordinates
     */
    getCube(coords) {
      if ( coords.i !== undefined ) return this.offsetToCube(coords);
      const cube = coords.q !== undefined ? coords : this.pointToCube(coords);
      return HexagonalGrid.cubeRound(cube);
    }

    /* -------------------------------------------- */

    /**
     * Returns the cube coordinates of grid spaces adjacent to the one corresponding to the given coordinates.
     * @param {HexagonalGridCoordinates} coords   The coordinates
     * @returns {HexagonalGridCube[]}             The adjacent cube coordinates
     */
    getAdjacentCubes(coords) {
      const {q, r, s} = this.getCube(coords);
      return [
        {q, r: r - 1, s: s + 1},
        {q: q + 1, r: r - 1, s},
        {q: q + 1, r, s: s - 1},
        {q, r: r + 1, s: s - 1},
        {q: q - 1, r: r + 1, s},
        {q: q - 1, r, s: s + 1}
      ];
    }

    /* -------------------------------------------- */

    /**
     * Returns the cube coordinates of the grid space corresponding to the given coordinates
     * shifted by one grid space in the given direction.
     * @param {GridCoordinates} coords    The coordinates
     * @param {number} direction          The direction (see {@link CONST.MOVEMENT_DIRECTIONS})
     * @returns {HexagonalGridCube}       The cube coordinates
     */
    getShiftedCube(coords, direction) {
      return this.getCube(this.getShiftedOffset(coords, direction));
    }

    /* -------------------------------------------- */

    /**
     * Returns the top-left point of the grid space corresponding to the given coordinates.
     * If given a point, the top-left point of the grid space that contains it is returned.
     * @param {HexagonalGridCoordinates} coords    The coordinates
     * @returns {Point}                            The top-left point
     */
    getTopLeftPoint(coords) {
      const point = this.getCenterPoint(coords);
      point.x -= (this.sizeX / 2);
      point.y -= (this.sizeY / 2);
      return point;
    }

    /* -------------------------------------------- */

    /**
     * Returns the center point of the grid space corresponding to the given coordinates.
     * If given a point, the center point of the grid space that contains it is returned.
     * @param {HexagonalGridCoordinates} coords    The coordinates
     * @returns {Point}                            The center point
     */
    getCenterPoint(coords) {
      if ( coords.i !== undefined ) {
        const {i, j} = coords;
        let x;
        let y;
        if ( this.columns ) {
          x = (2 * Math.SQRT1_3) * ((0.75 * j) + 0.5);
          const even = (j + 1) % 2 === 0;
          y = i + (this.even === even ? 0 : 0.5);
        } else {
          y = (2 * Math.SQRT1_3) * ((0.75 * i) + 0.5);
          const even = (i + 1) % 2 === 0;
          x = j + (this.even === even ? 0 : 0.5);
        }
        const size = this.size;
        x *= size;
        y *= size;
        return {x, y};
      }
      const cube = coords.q !== undefined ? coords : this.pointToCube(coords);
      return this.cubeToPoint(HexagonalGrid.cubeRound(cube));
    }

    /* -------------------------------------------- */

    /** @override */
    getShape() {
      const scaleX = this.sizeX / 4;
      const scaleY = this.sizeY / 4;
      if ( this.columns ) {
        const x0 = -2 * scaleX;
        const x1 = -scaleX;
        const x2 = scaleX;
        const x3 = 2 * scaleX;
        const y0 = -2 * scaleY;
        const y1 = 2 * scaleY;
        return [{x: x0, y: 0}, {x: x1, y: y0}, {x: x2, y: y0}, {x: x3, y: 0}, {x: x2, y: y1}, {x: x1, y: y1}];
      } else {
        const y0 = -2 * scaleY;
        const y1 = -scaleY;
        const y2 = scaleY;
        const y3 = 2 * scaleY;
        const x0 = -2 * scaleX;
        const x1 = 2 * scaleX;
        return [{x: 0, y: y0}, {x: x1, y: y1}, {x: x1, y: y2}, {x: 0, y: y3}, {x: x0, y: y2}, {x: x0, y: y1}];
      }
    }

    /* -------------------------------------------- */

    /** @override */
    getVertices(coords) {
      const {i, j} = this.getOffset(coords);
      const scaleX = this.sizeX / 4;
      const scaleY = this.sizeY / 4;
      if ( this.columns ) {
        const x = 3 * j;
        const x0 = x * scaleX;
        const x1 = (x + 1) * scaleX;
        const x2 = (x + 3) * scaleX;
        const x3 = (x + 4) * scaleX;
        const even = (j + 1) % 2 === 0;
        const y = (4 * i) - (this.even === even ? 2 : 0);
        const y0 = y * scaleY;
        const y1 = (y + 2) * scaleY;
        const y2 = (y + 4) * scaleY;
        return [{x: x0, y: y1}, {x: x1, y: y0}, {x: x2, y: y0}, {x: x3, y: y1}, {x: x2, y: y2}, {x: x1, y: y2}];
      } else {
        const y = 3 * i;
        const y0 = y * scaleY;
        const y1 = (y + 1) * scaleY;
        const y2 = (y + 3) * scaleY;
        const y3 = (y + 4) * scaleY;
        const even = (i + 1) % 2 === 0;
        const x = (4 * j) - (this.even === even ? 2 : 0);
        const x0 = x * scaleX;
        const x1 = (x + 2) * scaleX;
        const x2 = (x + 4) * scaleX;
        return [{x: x1, y: y0}, {x: x2, y: y1}, {x: x2, y: y2}, {x: x1, y: y3}, {x: x0, y: y2}, {x: x0, y: y1}];
      }
    }

    /* -------------------------------------------- */

    /** @override */
    getSnappedPoint(point, {mode, resolution=1}) {
      if ( mode & ~0xFFF3 ) throw new Error("Invalid snapping mode");
      if ( mode === 0 ) return {x: point.x, y: point.y};

      let nearest;
      let distance;
      const keepNearest = candidate => {
        if ( !nearest ) return nearest = candidate;
        const {x, y} = point;
        distance ??= ((nearest.x - x) ** 2) + ((nearest.y - y) ** 2);
        const d = ((candidate.x - x) ** 2) + ((candidate.y - y) ** 2);
        if ( d < distance ) {
          nearest = candidate;
          distance = d;
        }
        return nearest;
      };

      // Symmetries and identities
      if ( this.columns ) {
        // Top-Left = Bottom-Left
        if ( mode & 0x50 ) mode |= 0x50; // Vertex
        if ( mode & 0x500 ) mode |= 0x500; // Corner
        // Top-Right = Bottom-Right
        if ( mode & 0xA0 ) mode |= 0xA0; // Vertex
        if ( mode & 0xA00 ) mode |= 0xA00; // Corner
        // Left Side = Right Vertex
        if ( mode & 0x4000 ) mode |= 0xA0;
        // Right Side = Left Vertex
        if ( mode & 0x8000 ) mode |= 0x50;
      } else {
        // Top-Left = Top-Right
        if ( mode & 0x30 ) mode |= 0x30; // Vertex
        if ( mode & 0x300 ) mode |= 0x300; // Corner
        // Bottom-Left = Bottom-Right
        if ( mode & 0xC0 ) mode |= 0xC0; // Vertex
        if ( mode & 0xC00 ) mode |= 0xC00; // Corner
        // Top Side = Bottom Vertex
        if ( mode & 0x1000 ) mode |= 0xC0;
        // Bottom Side = Top Vertex
        if ( mode & 0x2000 ) mode |= 0x30;
      }

      // Only top/bottom or left/right edges
      if ( !(mode & 0x2) ) {
        if ( this.columns ) {
          // Top/Left side (= edge)
          if ( mode & 0x3000 ) keepNearest(this.#snapToTopOrBottom(point, resolution));
        } else {
          // Left/Right side (= edge)
          if ( mode & 0xC000 ) keepNearest(this.#snapToLeftOrRight(point, resolution));
        }
      }

      // Any vertex (plus edge/center)
      if ( (mode & 0xF0) === 0xF0 ) {
        switch ( mode & 0x3 ) {
          case 0x0: keepNearest(this.#snapToVertex(point, resolution)); break;
          case 0x1: keepNearest(this.#snapToVertexOrCenter(point, resolution)); break;
          case 0x2: keepNearest(this.#snapToEdgeOrVertex(point, resolution)); break;
          case 0x3: keepNearest(this.#snapToEdgeOrVertexOrCenter(point, resolution)); break;
        }
      }
      // A specific vertex
      else if ( mode & 0xF0 ) {
        // Center
        if ( (mode & 0x3) === 0x1 ) {
          keepNearest(this.#snapToSpecificVertexOrCenter(point, !(mode & 0x10), resolution));
        } else {
          // Edge and/or center
          switch ( mode & 0x3 ) {
            case 0x2: keepNearest(this.#snapToEdge(point, resolution)); break;
            case 0x3: keepNearest(this.#snapToEdgeOrCenter(point, resolution)); break;
          }

          // A combination of specific vertices and corners that results in a rectangular grid
          if ( ((mode & 0xF0) ^ ((mode & 0xF00) >> 4)) === 0xF0 ) {
            return keepNearest(this.#snapToRectangularGrid(point, !(mode & 0x100), resolution));
          }

          keepNearest(this.#snapToSpecificVertex(point, !(mode & 0x10), resolution));
        }
      }
      // Edges and/or centers
      else {
        switch ( mode & 0x3 ) {
          case 0x1: keepNearest(this.#snapToCenter(point, resolution)); break;
          case 0x2: keepNearest(this.#snapToEdge(point, resolution)); break;
          case 0x3: keepNearest(this.#snapToEdgeOrCenter(point, resolution)); break;
        }
      }

      // Any corner
      if ( (mode & 0xF00) === 0xF00 ) {
        keepNearest(this.#snapToCorner(point, resolution));
      }
      // A specific corner
      else if ( mode & 0xF00 ) {
        keepNearest(this.#snapToSpecificCorner(point, !(mode & 0x100), resolution));
      }

      return nearest;
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest center of a hexagon.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @param {number} [dx=0]        The x-translation of the grid
     * @param {number} [dy=0]        The y-translation of the grid
     * @param {boolean} [columns]    Flat-top instead of pointy-top?
     * @param {boolean} [even]       Start at a full grid space?
     * @param {number} [size]        The size of a grid space
     * @returns {Point}              The snapped point
     */
    #snapToCenter({x, y}, resolution, dx=0, dy=0, columns=this.columns, even=this.even, size=this.size) {

      // Subdivide the hex grid
      const grid = HexagonalGrid.#TEMP_GRID;
      grid.columns = columns;
      grid.size = size / resolution;

      // Align the subdivided grid with this hex grid
      if ( columns ) {
        dx += ((size - grid.size) * Math.SQRT1_3);
        if ( even ) dy += (size / 2);
      } else {
        if ( even ) dx += (size / 2);
        dy += ((size - grid.size) * Math.SQRT1_3);
      }

      // Get the snapped center point for the subdivision
      const point = HexagonalGrid.#TEMP_POINT;
      point.x = x - dx;
      point.y = y - dy;
      const snapped = grid.getCenterPoint(point);
      snapped.x += dx;
      snapped.y += dy;
      return snapped;
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest vertex of a hexagon.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @param {number} [dx=0]        The x-offset of the grid
     * @param {number} [dy=0]        The y-offset of the grid
     * @returns {Point}              The snapped point
     */
    #snapToVertex(point, resolution, dx, dy) {
      const center = this.#snapToCenter(point, resolution, dx, dy);
      const {x: x0, y: y0} = center;
      let angle = Math.atan2(point.y - y0, point.x - x0);
      if ( this.columns ) angle = Math.round(angle / (Math.PI / 3)) * (Math.PI / 3);
      else angle = (Math.floor(angle / (Math.PI / 3)) + 0.5) * (Math.PI / 3);
      const radius = Math.max(this.sizeX, this.sizeY) / (2 * resolution);
      const vertex = center; // Reuse the object
      vertex.x = x0 + (Math.cos(angle) * radius);
      vertex.y = y0 + (Math.sin(angle) * radius);
      return vertex;
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest vertex or center of a hexagon.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToVertexOrCenter(point, resolution) {
      let size;
      let dx = 0;
      let dy = 0;
      if ( this.columns ) {
        size = this.sizeX / 2;
        dy = size * (Math.SQRT1_3 / 2);
      } else {
        size = this.sizeY / 2;
        dx = size * (Math.SQRT1_3 / 2);
      }
      return this.#snapToCenter(point, resolution, dx, dy, !this.columns, !this.even, size);
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest edge of a hexagon.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToEdge(point, resolution) {
      const center = this.#snapToCenter(point, resolution);
      const {x: x0, y: y0} = center;
      let angle = Math.atan2(point.y - y0, point.x - x0);
      if ( this.columns ) angle = (Math.floor(angle / (Math.PI / 3)) + 0.5) * (Math.PI / 3);
      else angle = Math.round(angle / (Math.PI / 3)) * (Math.PI / 3);
      const radius = Math.min(this.sizeX, this.sizeY) / (2 * resolution);
      const vertex = center; // Reuse the object
      vertex.x = x0 + (Math.cos(angle) * radius);
      vertex.y = y0 + (Math.sin(angle) * radius);
      return vertex;
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest edge or center of a hexagon.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToEdgeOrCenter(point, resolution) {
      let size;
      let dx = 0;
      let dy = 0;
      if ( this.columns ) {
        size = this.sizeY / 2;
        dx = size * Math.SQRT1_3;
      } else {
        size = this.sizeX / 2;
        dy = size * Math.SQRT1_3;
      }
      return this.#snapToCenter(point, resolution, dx, dy, this.columns, false, size);
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest edge or vertex of a hexagon.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToEdgeOrVertex(point, resolution) {
      const {x, y} = point;
      point = this.#snapToCenter(point, resolution);
      const {x: x0, y: y0} = point;
      const dx = x - x0;
      const dy = y - y0;
      let angle = Math.atan2(dy, dx);
      if ( this.columns ) angle = (Math.floor(angle / (Math.PI / 3)) + 0.5) * (Math.PI / 3);
      else angle = Math.round(angle / (Math.PI / 3)) * (Math.PI / 3);
      const s = 2 * resolution;
      let radius1 = this.sizeX / s;
      let radius2 = this.sizeY / s;
      if ( radius1 > radius2 ) [radius1, radius2] = [radius2, radius1];
      const cos = Math.cos(angle);
      const sin = Math.sin(angle);
      const d = (cos * dy) - (sin * dx);
      if ( Math.abs(d) <= radius2 / 4 ) {
        point.x = x0 + (cos * radius1);
        point.y = y0 + (sin * radius1);
      } else {
        angle += ((Math.PI / 6) * Math.sign(d));
        point.x = x0 + (Math.cos(angle) * radius2);
        point.y = y0 + (Math.sin(angle) * radius2);
      }
      return point;
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest edge, vertex, center of a hexagon.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToEdgeOrVertexOrCenter(point, resolution) {
      const {x, y} = point;
      point = this.#snapToCenter(point, resolution);
      const {x: x0, y: y0} = point;
      const dx = x - x0;
      const dy = y - y0;
      let angle = Math.atan2(dy, dx);
      if ( this.columns ) angle = (Math.floor(angle / (Math.PI / 3)) + 0.5) * (Math.PI / 3);
      else angle = Math.round(angle / (Math.PI / 3)) * (Math.PI / 3);
      const s = 2 * resolution;
      let radius1 = this.sizeX / s;
      let radius2 = this.sizeY / s;
      if ( radius1 > radius2 ) [radius1, radius2] = [radius2, radius1];
      const cos = Math.cos(angle);
      const sin = Math.sin(angle);
      const d1 = (cos * dx) + (sin * dy);
      if ( d1 <= radius1 / 2 ) return point;
      const d2 = (cos * dy) - (sin * dx);
      if ( Math.abs(d2) <= radius2 / 4 ) {
        point.x = x0 + (cos * radius1);
        point.y = y0 + (sin * radius1);
      } else {
        angle += ((Math.PI / 6) * Math.sign(d2));
        point.x = x0 + (Math.cos(angle) * radius2);
        point.y = y0 + (Math.sin(angle) * radius2);
      }
      return point;
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest corner of a hexagon.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToCorner(point, resolution) {
      let dx = 0;
      let dy = 0;
      const s = 2 * resolution;
      if ( this.columns ) dy = this.sizeY / s;
      else dx = this.sizeX / s;
      return this.#snapToVertex(point, resolution, dx, dy);
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest top/bottom-left/right vertex of a hexagon.
     * @param {Point} point          The point
     * @param {boolean} other        Bottom-right instead of top-left vertex?
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToSpecificVertex(point, other, resolution) {
      let dx = 0;
      let dy = 0;
      const s = (other ? -2 : 2) * resolution;
      if ( this.columns ) dx = this.sizeX / s;
      else dy = this.sizeY / s;
      return this.#snapToCenter(point, resolution, dx, dy);
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest top/bottom-left/right vertex or center of a hexagon.
     * @param {Point} point          The point
     * @param {boolean} other        Bottom-right instead of top-left vertex?
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToSpecificVertexOrCenter(point, other, resolution) {
      let dx = 0;
      let dy = 0;
      const s = (other ? 2 : -2) * resolution;
      if ( this.columns ) dx = this.sizeX / s;
      else dy = this.sizeY / s;
      return this.#snapToVertex(point, resolution, dx, dy);
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest top/bottom-left/right corner of a hexagon.
     * @param {Point} point          The point
     * @param {boolean} other        Bottom-right instead of top-left corner?
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToSpecificCorner(point, other, resolution) {
      let dx = 0;
      let dy = 0;
      const s = (other ? -4 : 4) * resolution;
      if ( this.columns ) dx = this.sizeX / s;
      else dy = this.sizeY / s;
      return this.#snapToCenter(point, resolution, dx, dy);
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest grid intersection of the rectanglar grid.
     * @param {Point} point          The point
     * @param {boolean} other        Align rectangles with top-left vertices instead of top-left corners?
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToRectangularGrid(point, other, resolution) {
      const tx = this.sizeX / 2;
      const ty = this.sizeY / 2;
      let sx = tx;
      let sy = ty;
      let dx = 0;
      let dy = 0;
      const d = other ? 1 / 3 : 2 / 3;
      if ( this.columns ) {
        sx *= 1.5;
        dx = d;
      } else {
        sy *= 1.5;
        dy = d;
      }
      sx /= resolution;
      sy /= resolution;
      return {
        x: ((Math.round(((point.x - tx) / sx) + dx) - dx) * sx) + tx,
        y: ((Math.round(((point.y - ty) / sy) + dy) - dy) * sy) + ty
      };
    }

    /**
     * Snap the point to the nearest top/bottom side of the bounds of a hexagon.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToTopOrBottom(point, resolution) {
      return this.#snapToCenter(point, resolution, 0, this.sizeY / (2 * resolution));
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest left/right side of the bounds of a hexagon.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToLeftOrRight(point, resolution) {
      return this.#snapToCenter(point, resolution, this.sizeX / (2 * resolution), 0);
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    calculateDimensions(sceneWidth, sceneHeight, padding) {
      const {columns, size} = this;
      const sizeX = columns ? (2 * size) / Math.SQRT3 : size;
      const sizeY = columns ? size : (2 * size) / Math.SQRT3;
      const strideX = columns ? 0.75 * sizeX : sizeX;
      const strideY = columns ? sizeY : 0.75 * sizeY;

      // Skip padding computation for Scenes which do not include padding
      if ( !padding ) {
        const cols = Math.ceil(((sceneWidth + (columns ? -sizeX / 4 : sizeX / 2)) / strideX) - 1e-6);
        const rows = Math.ceil(((sceneHeight + (columns ? sizeY / 2 : -sizeY / 4)) / strideY) - 1e-6);
        return {width: sceneWidth, height: sceneHeight, x: 0, y: 0, rows, columns: cols};
      }

      // The grid size is equal to the short diagonal of the hexagon, so padding in that axis will divide evenly by the
      // grid size. In the cross-axis, however, the hexagons do not stack but instead interleave. Multiplying the long
      // diagonal by 75% gives us the amount of space each hexagon takes up in that axis without overlapping.
      // Note: Do not replace `* (1 / strideX)` by `/ strideX` and `* (1 / strideY)` by `/ strideY`!
      // It could change the result and therefore break certain scenes.
      let x = Math.ceil((padding * sceneWidth) * (1 / strideX)) * strideX;
      let y = Math.ceil((padding * sceneHeight) * (1 / strideY)) * strideY;
      // Note: The width and height calculation needs rounded x/y. If we were to remove the rounding here,
      // the result of the rounding of the width and height below would change in certain scenes.
      let width = sceneWidth + (2 * Math.round(Math.ceil((padding * sceneWidth) * (1 / strideX)) / (1 / strideX)));
      let height = sceneHeight + (2 * Math.round(Math.ceil((padding * sceneHeight) * (1 / strideY)) / (1 / strideY)));

      // Ensure that the top-left hexagon of the scene rectangle is always a full hexagon for even grids and always a
      // half hexagon for odd grids, by shifting the padding in the main axis by half a hex if the number of hexagons in
      // the cross-axis is odd.
      const crossEven = Math.round(columns ? x / strideX : y / strideY) % 2 === 0;
      if ( !crossEven ) {
        if ( columns ) {
          y += (sizeY / 2);
          height += sizeY;
        } else {
          x += (sizeX / 2);
          width += sizeX;
        }
      }

      // The height (if column orientation) or width (if row orientation) must be a multiple of the grid size, and
      // the last column (if column orientation) or row (if row orientation) must be fully within the bounds.
      // Note: Do not replace `* (1 / strideX)` by `/ strideX` and `* (1 / strideY)` by `/ strideY`!
      // It could change the result and therefore break certain scenes.
      let cols = Math.round(width * (1 / strideX));
      let rows = Math.round(height * (1 / strideY));
      width = cols * strideX;
      height = rows * strideY;
      if ( columns ) {
        rows++;
        width += (sizeX / 4);
      } else {
        cols++;
        height += (sizeY / 4);
      }
      return {width, height, x, y, rows, columns: cols};
    }

    /* -------------------------------------------- */

    /**
     * Calculate the total size of the canvas with padding applied, as well as the top-left coordinates of the inner
     * rectangle that houses the scene. (Legacy)
     * @param {number} columns            Column or row orientation?
     * @param {number} legacySize         The legacy size of the grid.
     * @param {number} sceneWidth         The width of the scene.
     * @param {number} sceneHeight        The height of the scene.
     * @param {number} padding            The percentage of padding.
     * @returns {{width: number, height: number, x: number, y: number, rows: number, columns: number}}
     * @internal
     */
    static _calculatePreV10Dimensions(columns, legacySize, sceneWidth, sceneHeight, padding) {
      // Note: Do not replace `* (1 / legacySize)` by `/ legacySize`!
      // It could change the result and therefore break certain scenes.
      const x = Math.ceil((padding * sceneWidth) * (1 / legacySize)) * legacySize;
      const y = Math.ceil((padding * sceneHeight) * (1 / legacySize)) * legacySize;
      const width = sceneWidth + (2 * x);
      const height = sceneHeight + (2 * y);
      const size = legacySize * (Math.SQRT3 / 2);
      const sizeX = columns ? legacySize : size;
      const sizeY = columns ? size : legacySize;
      const strideX = columns ? 0.75 * sizeX : sizeX;
      const strideY = columns ? sizeY : 0.75 * sizeY;
      const cols = Math.floor(((width + (columns ? sizeX / 4 : sizeX)) / strideX) + 1e-6);
      const rows = Math.floor(((height + (columns ? sizeY : sizeY / 4)) / strideY) + 1e-6);
      return {width, height, x, y, rows, columns: cols};
    }

    /* -------------------------------------------- */

    /** @override */
    _measurePath(waypoints, {cost}, result) {
      result.distance = 0;
      result.spaces = 0;
      result.cost = 0;

      if ( waypoints.length === 0 ) return;

      const from = result.waypoints[0];
      from.distance = 0;
      from.spaces = 0;
      from.cost = 0;

      // Convert to (fractional) cube coordinates
      const toCube = coords => {
        if ( coords.x !== undefined ) return this.pointToCube(coords);
        if ( coords.i !== undefined ) return this.offsetToCube(coords);
        return coords;
      };

      // Prepare data for the starting point
      const w0 = waypoints[0];
      let o0 = this.getOffset(w0);
      let c0 = this.offsetToCube(o0);
      let d0 = toCube(w0);

      // Iterate over additional path points
      for ( let i = 1; i < waypoints.length; i++ ) {
        const w1 = waypoints[i];
        const o1 = this.getOffset(w1);
        const c1 = this.offsetToCube(o1);
        const d1 = toCube(w1);

        // Measure segment
        const to = result.waypoints[i];
        const segment = to.backward;
        if ( !w1.teleport ) {

          // Determine the number of hexes and cube distance
          const c = HexagonalGrid.cubeDistance(c0, c1);
          let d = HexagonalGrid.cubeDistance(d0, d1);
          if ( d.almostEqual(c) ) d = c;

          // Calculate the distance based on the cube distance
          segment.distance = d * this.distance;
          segment.spaces = c;
          segment.cost = cost ? this.#calculateCost(c0, c1, cost) : c * this.distance;
        } else {
          segment.distance = 0;
          segment.spaces = 0;
          segment.cost = cost && ((o0.i !== o1.i) || (o0.j !== o1.j)) ? cost(o0, o1, 0) : 0;
        }

        // Accumulate measurements
        result.distance += segment.distance;
        result.spaces += segment.spaces;
        result.cost += segment.cost;

        // Set waypoint measurements
        to.distance = result.distance;
        to.spaces = result.spaces;
        to.cost = result.cost;

        o0 = o1;
        c0 = c1;
        d0 = d1;
      }
    }

    /* -------------------------------------------- */

    /**
     * Calculate the cost of the direct path segment.
     * @param {HexagonalGridCube} from    The coordinates the segment starts from
     * @param {HexagonalGridCube} to      The coordinates the segment goes to
     * @param {GridMeasurePathCostFunction} cost    The cost function
     * @returns {number}                  The cost of the path segment
     */
    #calculateCost(from, to, cost) {
      const path = this.getDirectPath([from, to]);
      if ( path.length <= 1 ) return 0;

      // Prepare data for the starting point
      let o0 = path[0];
      let c = 0;

      // Iterate over additional path points
      for ( let i = 1; i < path.length; i++ ) {
        const o1 = path[i];

        // Calculate and accumulate the cost
        c += cost(o0, o1, this.distance);

        o0 = o1;
      }

      return c;
    }

    /* -------------------------------------------- */

    /**
     * @see {@link https://www.redblobgames.com/grids/hexagons/#line-drawing}
     * @override
     */
    getDirectPath(waypoints) {
      if ( waypoints.length === 0 ) return [];

      // Prepare data for the starting point
      let c0 = this.getCube(waypoints[0]);
      let {q: q0, r: r0} = c0;
      const path = [this.getOffset(c0)];

      // Iterate over additional path points
      for ( let i = 1; i < waypoints.length; i++ ) {
        const c1 = this.getCube(waypoints[i]);
        const {q: q1, r: r1} = c1;
        if ( (q0 === q1) && (r0 === r1) ) continue;

        // Walk from (q0, r0, s0) to (q1, r1, s1)
        const dq = q0 - q1;
        const dr = r0 - r1;
        // If the path segment is collinear with some hexagon edge, we need to nudge
        // the cube coordinates in the right direction so that we get a consistent, clean path.
        const EPS = 1e-6;
        let eq = 0;
        let er = 0;
        if ( this.columns ) {
          // Collinear with SE-NW edges
          if ( dq === dr ) {
            // Prefer movement such that we have symmetry with the E-W case
            er = !(q0 & 1) === this.even ? EPS : -EPS;
            eq = -er;
          }
          // Collinear with SW-NE edges
          else if ( -2 * dq === dr ) {
            // Prefer movement such that we have symmetry with the E-W case
            eq = !(q0 & 1) === this.even ? EPS : -EPS;
          }
          // Collinear with E-W edges
          else if ( dq === -2 * dr ) {
            // Move such we don't leave the row that we're in
            er = !(q0 & 1) === this.even ? -EPS : EPS;
          }
        } else {
          // Collinear with SE-NW edges
          if ( dq === dr ) {
            // Prefer movement such that we have symmetry with the S-N case
            eq = !(r0 & 1) === this.even ? EPS : -EPS;
            er = -eq;
          }
          // Collinear with SW-NE edges
          else if ( dq === -2 * dr ) {
            // Prefer movement such that we have symmetry with the S-N case
            er = !(r0 & 1) === this.even ? EPS : -EPS;
          }
          // Collinear with S-N edges
          else if ( -2 * dq === dr ) {
            // Move such we don't leave the column that we're in
            eq = !(r0 & 1) === this.even ? -EPS : EPS;
          }
        }
        const n = HexagonalGrid.cubeDistance(c0, c1);
        for ( let j = 1; j < n; j++ ) {
          // Break tries on E-W (if columns) / S-N (if rows) edges
          const t = (j + EPS) / n;
          const q = Math.mix(q0, q1, t) + eq;
          const r = Math.mix(r0, r1, t) + er;
          const s = 0 - q - r;
          path.push(this.getOffset({q, r, s}));
        }
        path.push(this.getOffset(c1));

        c0 = c1;
        q0 = q1;
        r0 = r1;
      }

      return path;
    }

    /* -------------------------------------------- */

    /** @override */
    getTranslatedPoint(point, direction, distance) {
      direction = Math.toRadians(direction);
      const dx = Math.cos(direction);
      const dy = Math.sin(direction);
      let q;
      let r;
      if ( this.columns ) {
        q = (2 * Math.SQRT1_3) * dx;
        r = (-0.5 * q) + dy;
      } else {
        r = (2 * Math.SQRT1_3) * dy;
        q = (-0.5 * r) + dx;
      }
      const s = distance / this.distance * this.size / ((Math.abs(r) + Math.abs(q) + Math.abs(q + r)) / 2);
      return {x: point.x + (dx * s), y: point.y + (dy * s)};
    }

    /* -------------------------------------------- */

    /** @override */
    getCircle({x, y}, radius) {
      // TODO: Move to BaseGrid once BaseGrid -> GridlessGrid
      if ( radius <= 0 ) return [];
      const r = radius / this.distance * this.size;
      if ( this.columns ) {
        const x0 = r * (Math.SQRT3 / 2);
        const x1 = -x0;
        const y0 = r;
        const y1 = y0 / 2;
        const y2 = -y1;
        const y3 = -y0;
        return [{x: x, y: y + y0}, {x: x + x1, y: y + y1}, {x: x + x1, y: y + y2},
          {x: x, y: y + y3}, {x: x + x0, y: y + y2}, {x: x + x0, y: y + y1}];
      } else {
        const y0 = r * (Math.SQRT3 / 2);
        const y1 = -y0;
        const x0 = r;
        const x1 = x0 / 2;
        const x2 = -x1;
        const x3 = -x0;
        return [{x: x + x0, y: y}, {x: x + x1, y: y + y0}, {x: x + x2, y: y + y0},
          {x: x + x3, y: y}, {x: x + x2, y: y + y1}, {x: x + x1, y: y + y1}];
      }
    }

    /* -------------------------------------------- */
    /*  Conversion Functions                        */
    /* -------------------------------------------- */

    /**
     * Round the fractional cube coordinates (q, r, s).
     * @see {@link https://www.redblobgames.com/grids/hexagons/}
     * @param {HexagonalGridCube} cube    The fractional cube coordinates
     * @returns {HexagonalGridCube}       The rounded integer cube coordinates
     */
    static cubeRound({q, r, s}) {
      let iq = Math.round(q);
      let ir = Math.round(r);
      let is = Math.round(s);
      const dq = Math.abs(iq - q);
      const dr = Math.abs(ir - r);
      const ds = Math.abs(is - s);

      if ( (dq > dr) && (dq > ds) ) {
        iq = -ir - is;
      } else if ( dr > ds ) {
        ir = -iq - is;
      } else {
        is = -iq - ir;
      }

      return {q: iq | 0, r: ir | 0, s: is | 0};
    }

    /* -------------------------------------------- */

    /**
     * Convert point coordinates (x, y) into cube coordinates (q, r, s).
     * Inverse of {@link HexagonalGrid#cubeToPoint}.
     * @see {@link https://www.redblobgames.com/grids/hexagons/}
     * @param {Point} point            The point
     * @returns {HexagonalGridCube}    The (fractional) cube coordinates
     */
    pointToCube({x, y}) {
      let q;
      let r;

      const size = this.size;
      x /= size;
      y /= size;

      if ( this.columns ) {
        q = ((2 * Math.SQRT1_3) * x) - (2 / 3);
        r = (-0.5 * (q + (this.even ? 1 : 0))) + y;
      } else {
        r = ((2 * Math.SQRT1_3) * y) - (2 / 3);
        q = (-0.5 * (r + (this.even ? 1 : 0))) + x;
      }

      return {q, r, s: 0 - q - r};
    }

    /* -------------------------------------------- */

    /**
     * Convert cube coordinates (q, r, s) into point coordinates (x, y).
     * Inverse of {@link HexagonalGrid#pointToCube}.
     * @see {@link https://www.redblobgames.com/grids/hexagons/}
     * @param {HexagonalGridCube} cube    The cube coordinates
     * @returns {Point}                   The point coordinates
     */
    cubeToPoint({q, r}) {
      let x;
      let y;

      if ( this.columns ) {
        x = (Math.SQRT3 / 2) * (q + (2 / 3));
        y = (0.5 * (q + (this.even ? 1 : 0))) + r;
      } else {
        y = (Math.SQRT3 / 2) * (r + (2 / 3));
        x = (0.5 * (r + (this.even ? 1 : 0))) + q;
      }

      const size = this.size;
      x *= size;
      y *= size;

      return {x, y};
    }

    /* -------------------------------------------- */

    /**
     * Convert offset coordinates (i, j) into integer cube coordinates (q, r, s).
     * Inverse of {@link HexagonalGrid#cubeToOffset}.
     * @see {@link https://www.redblobgames.com/grids/hexagons/}
     * @param {GridOffset} offset      The offset coordinates
     * @returns {HexagonalGridCube}    The integer cube coordinates
     */
    offsetToCube({i, j}) {
      let q;
      let r;
      if ( this.columns ) {
        q = j;
        r = i - ((j + ((this.even ? 1 : -1) * (j & 1))) >> 1);
      } else {
        q = j - ((i + ((this.even ? 1 : -1) * (i & 1))) >> 1);
        r = i;
      }
      return {q, r, s: 0 - q - r};
    }

    /* -------------------------------------------- */

    /**
     * Convert integer cube coordinates (q, r, s) into offset coordinates (i, j).
     * Inverse of {@link HexagonalGrid#offsetToCube}.
     * @see {@link https://www.redblobgames.com/grids/hexagons/}
     * @param {HexagonalGridCube} cube    The cube coordinates
     * @returns {GridOffset}              The offset coordinates
     */
    cubeToOffset({q, r}) {
      let i;
      let j;
      if ( this.columns ) {
        j = q;
        i = r + ((q + ((this.even ? 1 : -1) * (q & 1))) >> 1);
      } else {
        i = r;
        j = q + ((r + ((this.even ? 1 : -1) * (r & 1))) >> 1);
      }
      return {i, j};
    }

    /* -------------------------------------------- */

    /**
     * Measure the distance in hexagons between two cube coordinates.
     * @see {@link https://www.redblobgames.com/grids/hexagons/}
     * @param {HexagonalGridCube} a    The first cube coordinates
     * @param {HexagonalGridCube} b    The second cube coordinates
     * @returns {number}               The distance between the two cube coordinates in hexagons
     */
    static cubeDistance(a, b) {
      const dq = a.q - b.q;
      const dr = a.r - b.r;
      return (Math.abs(dq) + Math.abs(dr) + Math.abs(dq + dr)) / 2;
    }

    /* -------------------------------------------- */

    /**
     * Used by {@link HexagonalGrid#snapToCenter}.
     * @type {Point}
     */
    static #TEMP_POINT = {x: 0, y: 0};

    /* -------------------------------------------- */

    /**
     * Used by {@link HexagonalGrid#snapToCenter}.
     * Always an odd grid!
     * @type {HexagonalGrid}
     */
    static #TEMP_GRID = new HexagonalGrid({size: 1});

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    static get POINTY_HEX_BORDERS() {
      const msg = "HexagonalGrid.POINTY_HEX_BORDERS is deprecated without replacement.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return this.#POINTY_HEX_BORDERS;
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    static #POINTY_HEX_BORDERS = {
      0.5: [[0, 0.25], [0.5, 0], [1, 0.25], [1, 0.75], [0.5, 1], [0, 0.75]],
      1: [[0, 0.25], [0.5, 0], [1, 0.25], [1, 0.75], [0.5, 1], [0, 0.75]],
      2: [
        [.5, 0], [.75, 1/7], [.75, 3/7], [1, 4/7], [1, 6/7], [.75, 1], [.5, 6/7], [.25, 1], [0, 6/7], [0, 4/7],
        [.25, 3/7], [.25, 1/7]
      ],
      3: [
        [.5, .1], [2/3, 0], [5/6, .1], [5/6, .3], [1, .4], [1, .6], [5/6, .7], [5/6, .9], [2/3, 1], [.5, .9], [1/3, 1],
        [1/6, .9], [1/6, .7], [0, .6], [0, .4], [1/6, .3], [1/6, .1], [1/3, 0]
      ],
      4: [
        [.5, 0], [5/8, 1/13], [.75, 0], [7/8, 1/13], [7/8, 3/13], [1, 4/13], [1, 6/13], [7/8, 7/13], [7/8, 9/13],
        [.75, 10/13], [.75, 12/13], [5/8, 1], [.5, 12/13], [3/8, 1], [.25, 12/13], [.25, 10/13], [1/8, 9/13],
        [1/8, 7/13], [0, 6/13], [0, 4/13], [1/8, 3/13], [1/8, 1/13], [.25, 0], [3/8, 1/13]
      ]
    };

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    static get FLAT_HEX_BORDERS() {
      const msg = "HexagonalGrid.FLAT_HEX_BORDERS is deprecated without replacement.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return this.#FLAT_HEX_BORDERS;
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    static #FLAT_HEX_BORDERS = {
      0.5: [[0, 0.5], [0.25, 0], [0.75, 0], [1, 0.5], [0.75, 1], [0.25, 1]],
      1: [[0, 0.5], [0.25, 0], [0.75, 0], [1, 0.5], [0.75, 1], [0.25, 1]],
      2: [
        [3/7, .25], [4/7, 0], [6/7, 0], [1, .25], [6/7, .5], [1, .75], [6/7, 1], [4/7, 1], [3/7, .75], [1/7, .75],
        [0, .5], [1/7, .25]
      ],
      3: [
        [.4, 0], [.6, 0], [.7, 1/6], [.9, 1/6], [1, 1/3], [.9, .5], [1, 2/3], [.9, 5/6], [.7, 5/6], [.6, 1], [.4, 1],
        [.3, 5/6], [.1, 5/6], [0, 2/3], [.1, .5], [0, 1/3], [.1, 1/6], [.3, 1/6]
      ],
      4: [
        [6/13, 0], [7/13, 1/8], [9/13, 1/8], [10/13, .25], [12/13, .25], [1, 3/8], [12/13, .5], [1, 5/8], [12/13, .75],
        [10/13, .75], [9/13, 7/8], [7/13, 7/8], [6/13, 1], [4/13, 1], [3/13, 7/8], [1/13, 7/8], [0, .75], [1/13, 5/8],
        [0, .5], [1/13, 3/8], [0, .25], [1/13, 1/8], [3/13, 1/8], [4/13, 0]
      ]
    };

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    static get pointyHexPoints() {
      const msg = "HexagonalGrid.pointyHexPoints is deprecated without replacement.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return this.#POINTY_HEX_BORDERS[1];
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    static get flatHexPoints() {
      const msg = "HexagonalGrid.flatHexPoints is deprecated without replacement.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return this.#FLAT_HEX_BORDERS[1];
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get hexPoints() {
      const msg = "HexagonalGrid#hexPoints is deprecated without replacement.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return this.columns ? this.constructor.flatHexPoints : this.constructor.pointyHexPoints;
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getPolygon(x, y, w, h, points) {
      const msg = "HexagonalGrid#getPolygon is deprecated. You can get the shape of the hex with HexagonalGrid#getShape "
        + "and the polygon of any hex with HexagonalGrid#getVertices.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      w = w ?? this.sizeX;
      h = h ?? this.sizeY;
      points ??= this.hexPoints;
      const poly = [];
      for ( let i=0; i < points.length; i++ ) {
        poly.push(x + (w * points[i][0]), y + (h * points[i][1]));
      }
      return poly;
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getBorderPolygon(w, h, p) {
      const msg = "HexagonalGrid#getBorderPolygon is deprecated. "
        + "If you need the shape of a Token, use Token#shape/getShape instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      const points = this.columns ? this.constructor.FLAT_HEX_BORDERS[w] : this.constructor.POINTY_HEX_BORDERS[w];
      if ( (w !== h) || !points ) return null;
      const p2 = p / 2;
      const p4 = p / 4;
      const r = this.getRect(w, h);
      return this.getPolygon(-p4, -p4, r.width + p2, r.height + p2, points);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getRect(w, h) {
      const msg = "HexagonalGrid#getRect is deprecated. If you need the size of a Token, use Token#getSize instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      if ( !this.columns || (w < 1) ) w *= this.sizeX;
      else w = (this.sizeX * .75 * (w - 1)) + this.sizeX;
      if ( this.columns || (h < 1) ) h *= this.sizeY;
      else h = (this.sizeY * .75 * (h - 1)) + this.sizeY;
      return new PIXI.Rectangle(0, 0, w, h);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    _adjustSnapForTokenSize(x, y, token) {
      const msg = "HexagonalGrid#_adjustSnapForTokenSize is deprecated.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      if ( (token.document.width <= 1) && (token.document.height <= 1) ) {
        const [row, col] = this.getGridPositionFromPixels(x, y);
        const [x0, y0] = this.getPixelsFromGridPosition(row, col);
        return [x0 + (this.sizeX / 2) - (token.w / 2), y0 + (this.sizeY / 2) - (token.h / 2)];
      }

      if ( this.columns && (token.document.height > 1) ) y -= this.sizeY / 2;
      if ( !this.columns && (token.document.width > 1) ) x -= this.sizeX / 2;
      return [x, y];
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    static computeDimensions({columns, size, legacy}) {
      const msg = "HexagonalGrid.computeDimensions is deprecated without replacement.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});

      // Legacy dimensions (deprecated)
      if ( legacy ) {
        if ( columns ) return { width: size, height: (Math.SQRT3 / 2) * size };
        return { width: (Math.SQRT3 / 2) * size, height: size };
      }

      // Columnar orientation
      if ( columns ) return { width: (2 * size) / Math.SQRT3, height: size };

      // Row orientation
      return { width: size, height: (2 * size) / Math.SQRT3 };
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get columnar() {
      const msg = "HexagonalGrid#columnar is deprecated in favor of HexagonalGrid#columns.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return this.columns;
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    set columnar(value) {
      const msg = "HexagonalGrid#columnar is deprecated in favor of HexagonalGrid#columns.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      this.columns = value;
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getCenter(x, y) {
      const msg = "HexagonalGrid#getCenter is deprecated. Use HexagonalGrid#getCenterPoint instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      let [x0, y0] = this.getTopLeft(x, y);
      return [x0 + (this.sizeX / 2), y0 + (this.sizeY / 2)];
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getSnappedPosition(x, y, interval=1, {token}={}) {
      const msg = "HexagonalGrid#getSnappedPosition is deprecated. Use HexagonalGrid#getSnappedPoint instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      if ( interval === 0 ) return {x: Math.round(x), y: Math.round(y)};

      // At precision 5, return the center or nearest vertex
      if ( interval === 5) {
        const w4 = this.w / 4;
        const h4 = this.h / 4;

        // Distance relative to center
        let [xc, yc] = this.getCenter(x, y);
        let dx = x - xc;
        let dy = y - yc;
        let ox = dx.between(-w4, w4) ? 0 : Math.sign(dx);
        let oy = dy.between(-h4, h4) ? 0 : Math.sign(dy);

        // Closest to the center
        if ( (ox === 0) && (oy === 0) ) return {x: xc, y: yc};

        // Closest vertex based on offset
        if ( this.columns && (ox === 0) ) ox = Math.sign(dx) ?? -1;
        if ( !this.columns && (oy === 0) ) oy = Math.sign(dy) ?? -1;
        const {x: x0, y: y0 } = this.#getClosestVertex(xc, yc, ox, oy);
        return {x: Math.round(x0), y: Math.round(y0)};
      }

      // Start with the closest top-left grid position
      if ( token ) {
        if ( this.columns && (token.document.height > 1) ) y += this.sizeY / 2;
        if ( !this.columns && (token.document.width > 1) ) x += this.sizeX / 2;
      }
      const options = {
        columns: this.columns,
        even: this.even,
        size: this.size,
        width: this.sizeX,
        height: this.sizeY
      };
      const offset = HexagonalGrid.pixelsToOffset({x, y}, options, "round");
      const point = HexagonalGrid.offsetToPixels(offset, options);

      // Adjust pixel coordinate for token size
      let x0 = point.x;
      let y0 = point.y;
      if ( token ) [x0, y0] = this._adjustSnapForTokenSize(x0, y0, token);

      // Snap directly at interval 1
      if ( interval === 1 ) return {x: x0, y: y0};

      // Round the remainder
      const dx = (x - x0).toNearest(this.w / interval);
      const dy = (y - y0).toNearest(this.h / interval);
      return {x: Math.round(x0 + dx), y: Math.round(y0 + dy)};
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    #getClosestVertex(xc, yc, ox, oy) {
      const b = ox + (oy << 2); // Bit shift to make a unique reference
      const vertices = this.columns
        ? {"-1": 0, "-5": 1, "-3": 2, 1: 3, 5: 4, 3: 5}   // Flat hex vertices
        : {"-5": 0, "-4": 1, "-3": 2, 5: 3, 4: 4, 3: 5};  // Pointy hex vertices
      const idx = vertices[b];
      const pt = this.hexPoints[idx];
      return {
        x: (xc - (this.sizeX / 2)) + (pt[0] * this.sizeX),
        y: (yc - (this.sizeY / 2)) + (pt[1] * this.sizeY)
      };
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    #measureDistance(p0, p1) {
      const [i0, j0] = this.getGridPositionFromPixels(p0.x, p0.y);
      const [i1, j1] = this.getGridPositionFromPixels(p1.x, p1.y);
      const c0 = this.getCube({i: i0, j: j0});
      const c1 = this.getCube({i: i1, j: j1});
      return HexagonalGrid.cubeDistance(c0, c1);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getGridPositionFromPixels(x, y) {
      const msg = "HexagonalGrid#getGridPositionFromPixels is deprecated. This function is based on the \"brick wall\" grid. "
      + " For getting the offset coordinates of the hex containing the given point use HexagonalGrid#getOffset.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      let {row, col} = HexagonalGrid.pixelsToOffset({x, y}, {
        columns: this.columns,
        even: this.even,
        size: this.size,
        width: this.sizeX,
        height: this.sizeY
      });
      return [row, col];
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getPixelsFromGridPosition(row, col) {
      const msg = "HexagonalGrid#getPixelsFromGridPosition is deprecated. This function is based on the \"brick wall\" grid. "
      + " For getting the top-left coordinates of the hex at the given offset coordinates use HexagonalGrid#getTopLeftPoint.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      const {x, y} = HexagonalGrid.offsetToPixels({row, col}, {
        columns: this.columns,
        even: this.even,
        size: this.size,
        width: this.sizeX,
        height: this.sizeY
      });
      return [Math.ceil(x), Math.ceil(y)];
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    shiftPosition(x, y, dx, dy, {token}={}) {
      const msg = "BaseGrid#shiftPosition is deprecated. Use BaseGrid#getShiftedPoint instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      let [row, col] = this.getGridPositionFromPixels(x, y);

      // Adjust diagonal moves for offset
      let isDiagonal = (dx !== 0) && (dy !== 0);
      if ( isDiagonal ) {

        // Column orientation
        if ( this.columns ) {
          let isEven = ((col+1) % 2 === 0) === this.even;
          if ( isEven && (dy > 0)) dy--;
          else if ( !isEven && (dy < 0)) dy++;
        }

        // Row orientation
        else {
          let isEven = ((row + 1) % 2 === 0) === this.even;
          if ( isEven && (dx > 0) ) dx--;
          else if ( !isEven && (dx < 0 ) ) dx++;
        }
      }
      const [shiftX, shiftY] = this.getPixelsFromGridPosition(row+dy, col+dx);
      if ( token ) return this._adjustSnapForTokenSize(shiftX, shiftY, token);
      return [shiftX, shiftY];
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    measureDistances(segments, options={}) {
      const msg = "HexagonalGrid#measureDistances is deprecated. "
        + "Use BaseGrid#measurePath instead for non-Euclidean measurements.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      if ( !options.gridSpaces ) return super.measureDistances(segments, options);
      return segments.map(s => {
        let r = s.ray;
        return this.#measureDistance(r.A, r.B) * this.distance;
      });
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    _adjustPositionForTokenSize(row, col, token) {
      const msg = "HexagonalGrid#_adjustPositionForTokenSize is deprecated.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      if ( this.columns && (token.document.height > 1) ) row++;
      if ( !this.columns && (token.document.width > 1) ) col++;
      return [row, col];
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    static getConfig(type, size) {
      const msg = "HexagonalGrid.getConfig is deprecated without replacement.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      const config = {
        columns: [GRID_TYPES.HEXODDQ, GRID_TYPES.HEXEVENQ].includes(type),
        even: [GRID_TYPES.HEXEVENR, GRID_TYPES.HEXEVENQ].includes(type),
        size: size
      };
      const {width, height} = HexagonalGrid.computeDimensions(config);
      config.width = width;
      config.height = height;
      return config;
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    static offsetToCube({row, col}={}, {columns=true, even=false}={}) {
      const msg = "HexagonalGrid.offsetToCube is deprecated. Use HexagonalGrid#offsetToCube instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return new HexagonalGrid({size: 100, columns, even}).offsetToCube({i: row, j: col});
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    static cubeToOffset(cube={}, {columns=true, even=false}={}) {
      const msg = "HexagonalGrid.cubeToOffset is deprecated. Use HexagonalGrid#cubeToOffset instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      const {i: row, j: col} = new HexagonalGrid({size: 100, columns, even}).cubeToOffset(cube);
      return {row, col};
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    static pixelToCube({x, y}={}, config) {
      const msg = "HexagonalGrid.pixelToCube is deprecated. Use HexagonalGrid#pointToCube instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      const {size} = config;
      const cx = x / (size / 2);
      const cy = y / (size / 2);

      // Fractional hex coordinates, might not satisfy (fx + fy + fz = 0) due to rounding
      const fr = (2/3) * cx;
      const fq = ((-1/3) * cx) + ((1 / Math.sqrt(3)) * cy);
      const fs = ((-1/3) * cx) - ((1 / Math.sqrt(3)) * cy);

      // Convert to integer triangle coordinates
      const a = Math.ceil(fr - fq);
      const b = Math.ceil(fq - fs);
      const c = Math.ceil(fs - fr);

      // Convert back to cube coordinates
      return {
        q: Math.round((a - c) / 3),
        r: Math.round((c - b) / 3),
        s: Math.round((b - a) / 3)
      };
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    static offsetToPixels({row, col}, {columns, even, width, height}) {
      const msg = "HexagonalGrid.offsetToPixels is deprecated. Use HexagonalGrid#getTopLeftPoint instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      let x;
      let y;

      // Flat-topped hexes
      if ( columns ) {
        x = Math.ceil(col * (width * 0.75));
        const isEven = (col + 1) % 2 === 0;
        y = Math.ceil((row - (even === isEven ? 0.5 : 0)) * height);
      }

      // Pointy-topped hexes
      else {
        y = Math.ceil(row * (height * 0.75));
        const isEven = (row + 1) % 2 === 0;
        x = Math.ceil((col - (even === isEven ? 0.5 : 0)) * width);
      }

      // Return the pixel coordinate
      return {x, y};
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    static pixelsToOffset({x, y}, config, method="floor") {
      const msg = "HexagonalGrid.pixelsToOffset is deprecated without replacement. This function is based on the \"brick wall\" grid. "
        + " For getting the offset coordinates of the hex containing the given point use HexagonalGrid#getOffset.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      const {columns, even, width, height} = config;
      const fn = Math[method];
      let row;
      let col;

      // Columnar orientation
      if ( columns ) {
        col = fn(x / (width * 0.75));
        const isEven = (col + 1) % 2 === 0;
        row = fn((y / height) + (even === isEven ? 0.5 : 0));
      }

      // Row orientation
      else {
        row = fn(y / (height * 0.75));
        const isEven = (row + 1) % 2 === 0;
        col = fn((x / width) + (even === isEven ? 0.5 : 0));
      }
      return {row, col};
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getAStarPath(start, goal, options) {
      const msg = "HexagonalGrid#getAStarPath is deprecated without replacement.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      const costs = new Map();

      // Create a prioritized frontier sorted by increasing cost
      const frontier = [];
      const explore = (hex, from, cost) => {
        const idx = frontier.findIndex(l => l.cost > cost);
        if ( idx === -1 ) frontier.push({hex, cost, from});
        else frontier.splice(idx, 0, {hex, cost, from});
        costs.set(hex, cost);
      };
      explore(start, null, 0);

      // Expand the frontier, exploring towards the goal
      let current;
      let solution;
      while ( frontier.length ) {
        current = frontier.shift();
        if ( current.cost === Infinity ) break;
        if ( current.hex.equals(goal) ) {
          solution = current;
          break;
        }
        for ( const next of current.hex.getNeighbors() ) {
          const deltaCost = next.getTravelCost instanceof Function ? next.getTravelCost(current.hex, options) : 1;
          const newCost = current.cost + deltaCost;     // Total cost of reaching this hex
          if ( costs.get(next) <= newCost ) continue;   // We already made it here in the lowest-cost way
          explore(next, current, newCost);
        }
      }

      // Ensure a path was achieved
      if ( !solution ) {
        throw new Error("No valid path between these positions exists");
      }

      // Return the optimal path and cost
      const path = [];
      let c = solution;
      while ( c.from ) {
        path.unshift(c.hex);
        c = c.from;
      }
      return {from: start, to: goal, cost: solution.cost, path};
    }
  };

  /**
   * A helper class which represents a single hexagon as part of a HexagonalGrid.
   * This class relies on having an active canvas scene in order to know the configuration of the hexagonal grid.
   */
  class GridHex {
    /**
     * Construct a GridHex instance by providing a hex coordinate.
     * @param {HexagonalGridCoordinates} coordinates  The coordinates of the hex to construct
     * @param {HexagonalGrid} grid                    The hexagonal grid instance to which this hex belongs
     */
    constructor(coordinates, grid) {
      if ( !(grid instanceof HexagonalGrid$1) ) {
        grid = new HexagonalGrid$1(grid);
        foundry.utils.logCompatibilityWarning("The GridHex class now requires a HexagonalGrid instance to be passed to "
          + "its constructor, rather than a HexagonalGridConfiguration", {since: 12, until: 14});
      }
      if ( "row" in coordinates ) {
        coordinates = {i: coordinates.row, j: coordinates.col};
        foundry.utils.logCompatibilityWarning("The coordinates used to construct the GridHex class are now a GridOffset"
          + " with format {i, j}.", {since: 12, until: 14});
      }

      /**
       * The hexagonal grid to which this hex belongs.
       * @type {HexagonalGrid}
       */
      this.grid = grid;

      /**
       * The cube coordinate of this hex
       * @type {HexagonalGridCube}
       */
      this.cube = this.grid.getCube(coordinates);

      /**
       * The offset coordinate of this hex
       * @type {GridOffset}
       */
      this.offset = this.grid.cubeToOffset(this.cube);
    }

    /* -------------------------------------------- */

    /**
     * Return a reference to the pixel point in the center of this hexagon.
     * @type {Point}
     */
    get center() {
      return this.grid.getCenterPoint(this.cube);
    }

    /* -------------------------------------------- */

    /**
     * Return a reference to the pixel point of the top-left corner of this hexagon.
     * @type {Point}
     */
    get topLeft() {
      return this.grid.getTopLeftPoint(this.cube);
    }

    /* -------------------------------------------- */

    /**
     * Return the array of hexagons which are neighbors of this one.
     * This result is un-bounded by the confines of the game canvas and may include hexes which are off-canvas.
     * @returns {GridHex[]}
     */
    getNeighbors() {
      return this.grid.getAdjacentCubes(this.cube).map(c => new this.constructor(c, this.grid));
    }

    /* -------------------------------------------- */

    /**
     * Get a neighboring hex by shifting along cube coordinates
     * @param {number} dq     A number of hexes to shift along the q axis
     * @param {number} dr     A number of hexes to shift along the r axis
     * @param {number} ds     A number of hexes to shift along the s axis
     * @returns {GridHex}     The shifted hex
     */
    shiftCube(dq, dr, ds) {
      const {q, r, s} = this.cube;
      return new this.constructor({q: q + dq, r: r + dr, s: s + ds}, this.grid);
    }

    /* -------------------------------------------- */

    /**
     * Return whether this GridHex equals the same position as some other GridHex instance.
     * @param {GridHex} other     Some other GridHex
     * @returns {boolean}         Are the positions equal?
     */
    equals(other) {
      return (this.offset.i === other.offset.i) && (this.offset.j === other.offset.j);
    }
  }

  /**
   * The gridless grid class.
   */
  class GridlessGrid extends BaseGrid {

    /** @override */
    type = GRID_TYPES.GRIDLESS;

    /* -------------------------------------------- */

    /** @override */
    calculateDimensions(sceneWidth, sceneHeight, padding) {
      // Note: Do not replace `* (1 / this.size)` by `/ this.size`!
      // It could change the result and therefore break certain scenes.
      const x = Math.ceil((padding * sceneWidth) * (1 / this.size)) * this.size;
      const y = Math.ceil((padding * sceneHeight) * (1 / this.size)) * this.size;
      const width = sceneWidth + (2 * x);
      const height = sceneHeight + (2 * y);
      return {width, height, x, y, rows: Math.ceil(height), columns: Math.ceil(width)};
    }

    /* -------------------------------------------- */

    /** @override */
    getOffset(coords) {
      const i = coords.i;
      if ( i !== undefined ) return {i, j: coords.j};
      return {i: Math.round(coords.y) | 0, j: Math.round(coords.x) | 0};
    }

    /* -------------------------------------------- */

    /** @override */
    getOffsetRange({x, y, width, height}) {
      const i0 = Math.floor(y);
      const j0 = Math.floor(x);
      if ( !((width > 0) && (height > 0)) ) return [i0, j0, i0, j0];
      return [i0, j0, Math.ceil(y + height) | 0, Math.ceil(x + width) | 0];
    }

    /* -------------------------------------------- */

    /** @override */
    getAdjacentOffsets(coords) {
      return [];
    }

    /* -------------------------------------------- */

    /** @override */
    testAdjacency(coords1, coords2) {
      return false;
    }

    /* -------------------------------------------- */

    /** @override */
    getShiftedOffset(coords, direction) {
      const i = coords.i;
      if ( i !== undefined ) coords = {x: coords.j, y: i};
      return this.getOffset(this.getShiftedPoint(coords, direction));
    }

    /* -------------------------------------------- */

    /** @override */
    getShiftedPoint(point, direction) {
      let di = 0;
      let dj = 0;
      if ( direction & MOVEMENT_DIRECTIONS.UP ) di--;
      if ( direction & MOVEMENT_DIRECTIONS.DOWN ) di++;
      if ( direction & MOVEMENT_DIRECTIONS.LEFT ) dj--;
      if ( direction & MOVEMENT_DIRECTIONS.RIGHT ) dj++;
      return {x: point.x + (dj * this.size), y: point.y + (di * this.size)};
    }

    /* -------------------------------------------- */

    /** @override */
    getTopLeftPoint(coords) {
      const i = coords.i;
      if ( i !== undefined ) return {x: coords.j, y: i};
      return {x: coords.x, y: coords.y};
    }

    /* -------------------------------------------- */

    /** @override */
    getCenterPoint(coords) {
      const i = coords.i;
      if ( i !== undefined ) return {x: coords.j, y: i};
      return {x: coords.x, y: coords.y};
    }

    /* -------------------------------------------- */

    /** @override */
    getShape() {
      return [];
    }

    /* -------------------------------------------- */

    /** @override */
    getVertices(coords) {
      return [];
    }

    /* -------------------------------------------- */

    /** @override */
    getSnappedPoint({x, y}, behavior) {
      return {x, y};
    }

    /* -------------------------------------------- */

    /** @override */
    _measurePath(waypoints, {cost}, result) {
      result.distance = 0;
      result.spaces = 0;
      result.cost = 0;

      if ( waypoints.length === 0 ) return;

      const from = result.waypoints[0];
      from.distance = 0;
      from.spaces = 0;
      from.cost = 0;

      // Prepare data for the starting point
      const w0 = waypoints[0];
      let o0 = this.getOffset(w0);
      let p0 = this.getCenterPoint(w0);

      // Iterate over additional path points
      for ( let i = 1; i < waypoints.length; i++ ) {
        const w1 = waypoints[i];
        const o1 = this.getOffset(w1);
        const p1 = this.getCenterPoint(w1);

        // Measure segment
        const to = result.waypoints[i];
        const segment = to.backward;
        if ( !w1.teleport ) {

          // Calculate the Euclidean distance
          segment.distance = Math.hypot(p0.x - p1.x, p0.y - p1.y) / this.size * this.distance;
          segment.spaces = 0;
          const offsetDistance = Math.hypot(o0.i - o1.i, o0.j - o1.j) / this.size * this.distance;
          segment.cost = cost && (offsetDistance !== 0) ? cost(o0, o1, offsetDistance) : offsetDistance;
        } else {
          segment.distance = 0;
          segment.spaces = 0;
          segment.cost = cost && ((o0.i !== o1.i) || (o0.j !== o1.j)) ? cost(o0, o1, 0) : 0;
        }

        // Accumulate measurements
        result.distance += segment.distance;
        result.cost += segment.cost;

        // Set waypoint measurements
        to.distance = result.distance;
        to.spaces = 0;
        to.cost = result.cost;

        o0 = o1;
        p0 = p1;
      }
    }

    /* -------------------------------------------- */

    /** @override */
    getDirectPath(waypoints) {
      if ( waypoints.length === 0 ) return [];
      let o0 = this.getOffset(waypoints[0]);
      const path = [o0];
      for ( let i = 1; i < waypoints.length; i++ ) {
        const o1 = this.getOffset(waypoints[i]);
        if ( (o0.i === o1.i) && (o0.j === o1.j) ) continue;
        path.push(o1);
        o0 = o1;
      }
      return path;
    }

    /* -------------------------------------------- */

    /** @override */
    getTranslatedPoint(point, direction, distance) {
      direction = Math.toRadians(direction);
      const dx = Math.cos(direction);
      const dy = Math.sin(direction);
      const s = distance / this.distance * this.size;
      return {x: point.x + (dx * s), y: point.y + (dy * s)};
    }

    /* -------------------------------------------- */

    /** @override */
    getCircle({x, y}, radius) {
      if ( radius <= 0 ) return [];
      const r = radius / this.distance * this.size;
      const n = Math.max(Math.ceil(Math.PI / Math.acos(Math.max(r - 0.25, 0) / r)), 4);
      const points = new Array(n);
      for ( let i = 0; i < n; i++ ) {
        const a = 2 * Math.PI * (i / n);
        points[i] = {x: x + (Math.cos(a) * r), y: y + (Math.sin(a) * r)};
      }
      return points;
    }

    /* -------------------------------------------- */

    /** @override */
    getCone(origin, radius, direction, angle) {
      if ( (radius <= 0) || (angle <= 0) ) return [];
      if ( angle >= 360 ) return this.getCircle(origin, radius);
      const r = radius / this.distance * this.size;
      const n = Math.max(Math.ceil(Math.PI / Math.acos(Math.max(r - 0.25, 0) / r) * (angle / 360)), 4);
      const a0 = Math.toRadians(direction - (angle / 2));
      const a1 = Math.toRadians(direction + (angle / 2));
      const points = new Array(n + 1);
      const {x, y} = origin;
      points[0] = {x, y};
      for ( let i = 0; i <= n; i++ ) {
        const a = Math.mix(a0, a1, i / n);
        points[i + 1] = {x: x + (Math.cos(a) * r), y: y + (Math.sin(a) * r)};
      }
      return points;
    }
  }

  /**
   * @typedef {object} _SquareGridConfiguration
   * @property {number} [diagonals=CONST.GRID_DIAGONALS.EQUIDISTANT]  The rule for diagonal measurement
   *                                                                  (see {@link CONST.GRID_DIAGONALS})
   */

  /**
   * @typedef {GridConfiguration&_SquareGridConfiguration} SquareGridConfiguration
   */

  /**
   * An offset of a grid space or a point with pixel coordinates.
   * @typedef {GridCoordinates} SquareGridCoordinates
   */

  /**
   * The square grid class.
   */
  class SquareGrid extends BaseGrid {
    /**
     * The square grid constructor.
     * @param {SquareGridConfiguration} config   The grid configuration
     */
    constructor(config) {
      super(config);

      this.type = GRID_TYPES.SQUARE;

      /**
       * The rule for diagonal measurement (see {@link CONST.GRID_DIAGONALS}).
       * @type {number}
       */
      this.diagonals = config.diagonals ?? GRID_DIAGONALS.EQUIDISTANT;
    }

    /* -------------------------------------------- */

    /**
     * Returns the offset of the grid space corresponding to the given coordinates.
     * @param {SquareGridCoordinates} coords    The coordinates
     * @returns {GridOffset}                    The offset
     */
    getOffset(coords) {
      let i = coords.i;
      let j;
      if ( i !== undefined ) {
        j = coords.j;
      } else {
        j = Math.floor(coords.x / this.size);
        i = Math.floor(coords.y / this.size);
      }
      return {i, j};
    }

    /* -------------------------------------------- */

    /** @override */
    getOffsetRange({x, y, width, height}) {
      const i0 = Math.floor(y / this.size);
      const j0 = Math.floor(x / this.size);
      if ( !((width > 0) && (height > 0)) ) return [i0, j0, i0, j0];
      return [i0, j0, Math.ceil((y + height) / this.size) | 0, Math.ceil((x + width) / this.size) | 0];
    }

    /* -------------------------------------------- */

    /** @override */
    getAdjacentOffsets(coords) {
      const {i, j} = this.getOffset(coords);

      // Non-diagonals
      const offsets = [
        {i: i - 1, j},
        {i, j: j + 1},
        {i: i + 1, j},
        {i, j: j - 1}
      ];
      if ( this.diagonals === GRID_DIAGONALS.ILLEGAL ) return offsets;

      // Diagonals
      offsets.push(
        {i: i - 1, j: j - 1},
        {i: i - 1, j: j + 1},
        {i: i + 1, j: j + 1},
        {i: i + 1, j: j - 1}
      );
      return offsets;
    }

    /* -------------------------------------------- */

    /** @override */
    testAdjacency(coords1, coords2) {
      const {i: i1, j: j1} = this.getOffset(coords1);
      const {i: i2, j: j2} = this.getOffset(coords2);
      const di = Math.abs(i1 - i2);
      const dj = Math.abs(j1 - j2);
      const diagonals = this.diagonals !== GRID_DIAGONALS.ILLEGAL;
      return diagonals ? Math.max(di, dj) === 1 : (di + dj) === 1;
    }

    /* -------------------------------------------- */

    /** @override */
    getShiftedOffset(coords, direction) {
      let di = 0;
      let dj = 0;
      if ( direction & MOVEMENT_DIRECTIONS.UP ) di--;
      if ( direction & MOVEMENT_DIRECTIONS.DOWN ) di++;
      if ( direction & MOVEMENT_DIRECTIONS.LEFT ) dj--;
      if ( direction & MOVEMENT_DIRECTIONS.RIGHT ) dj++;
      if ( di && dj && (this.diagonals === GRID_DIAGONALS.ILLEGAL) ) {
        // Diagonal movement is not allowed
        di = 0;
        dj = 0;
      }
      const offset = this.getOffset(coords);
      offset.i += di;
      offset.j += dj;
      return offset;
    }

    /* -------------------------------------------- */

    /** @override */
    getShiftedPoint(point, direction) {
      const topLeft = this.getTopLeftPoint(point);
      const shifted = this.getTopLeftPoint(this.getShiftedOffset(topLeft, direction));
      shifted.x = point.x + (shifted.x - topLeft.x);
      shifted.y = point.y + (shifted.y - topLeft.y);
      return shifted;
    }

    /* -------------------------------------------- */

    /**
     * Returns the top-left point of the grid space corresponding to the given coordinates.
     * If given a point, the top-left point of the grid space that contains it is returned.
     * @param {SquareGridCoordinates} coords    The coordinates
     * @returns {Point}                         The top-left point
     */
    getTopLeftPoint(coords) {
      let i = coords.i;
      let j;
      if ( i !== undefined ) {
        j = coords.j;
      } else {
        j = Math.floor(coords.x / this.size);
        i = Math.floor(coords.y / this.size);
      }
      return {x: j * this.size, y: i * this.size};
    }

    /* -------------------------------------------- */

    /**
     * Returns the center point of the grid space corresponding to the given coordinates.
     * If given a point, the center point of the grid space that contains it is returned.
     * @param {SquareGridCoordinates} coords    The coordinates
     * @returns {Point}                         The center point
     */
    getCenterPoint(coords) {
      const point = this.getTopLeftPoint(coords);
      const halfSize = this.size / 2;
      point.x += halfSize;
      point.y += halfSize;
      return point;
    }

    /* -------------------------------------------- */

    /** @override */
    getShape() {
      const s = this.size / 2;
      return [{x: -s, y: -s}, {x: s, y: -s}, {x: s, y: s}, {x: -s, y: s}];
    }

    /* -------------------------------------------- */

    /** @override */
    getVertices(coords) {
      const {i, j} = this.getOffset(coords);
      const x0 = j * this.size;
      const x1 = (j + 1) * this.size;
      const y0 = i * this.size;
      const y1 = (i + 1) * this.size;
      return [{x: x0, y: y0}, {x: x1, y: y0}, {x: x1, y: y1}, {x: x0, y: y1}];
    }

    /* -------------------------------------------- */

    /** @override */
    getSnappedPoint(point, {mode, resolution=1}) {
      if ( mode & ~0xFFF3 ) throw new Error("Invalid snapping mode");
      if ( mode === 0 ) return {x: point.x, y: point.y};

      let nearest;
      let distance;
      const keepNearest = candidate => {
        if ( !nearest ) return nearest = candidate;
        const {x, y} = point;
        distance ??= ((nearest.x - x) ** 2) + ((nearest.y - y) ** 2);
        const d = ((candidate.x - x) ** 2) + ((candidate.y - y) ** 2);
        if ( d < distance ) {
          nearest = candidate;
          distance = d;
        }
        return nearest;
      };

      // Any edge = Any side
      if ( !(mode & 0x2) ) {
        // Horizontal (Top/Bottom) side + Vertical (Left/Right) side = Any edge
        if ( (mode & 0x3000) && (mode & 0xC000) ) mode |= 0x2;
        // Horizontal (Top/Bottom) side
        else if ( mode & 0x3000 ) keepNearest(this.#snapToTopOrBottom(point, resolution));
        // Vertical (Left/Right) side
        else if ( mode & 0xC000 ) keepNearest(this.#snapToLeftOrRight(point, resolution));
      }

      // With vertices (= corners)
      if ( mode & 0xFF0 ) {
        switch ( mode & ~0xFFF0 ) {
          case 0x0: keepNearest(this.#snapToVertex(point, resolution)); break;
          case 0x1: keepNearest(this.#snapToVertexOrCenter(point, resolution)); break;
          case 0x2: keepNearest(this.#snapToEdgeOrVertex(point, resolution)); break;
          case 0x3: keepNearest(this.#snapToEdgeOrVertexOrCenter(point, resolution)); break;
        }
      }
      // Without vertices
      else {
        switch ( mode & ~0xFFF0 ) {
          case 0x1: keepNearest(this.#snapToCenter(point, resolution)); break;
          case 0x2: keepNearest(this.#snapToEdge(point, resolution)); break;
          case 0x3: keepNearest(this.#snapToEdgeOrCenter(point, resolution)); break;
        }
      }

      return nearest;
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest center of a square.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToCenter({x, y}, resolution) {
      const s = this.size / resolution;
      const t = this.size / 2;
      return {
        x: (Math.round((x - t) / s) * s) + t,
        y: (Math.round((y - t) / s) * s) + t
      };
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest vertex of a square.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToVertex({x, y}, resolution) {
      const s = this.size / resolution;
      const t = this.size / 2;
      return {
        x: ((Math.ceil((x - t) / s) - 0.5) * s) + t,
        y: ((Math.ceil((y - t) / s) - 0.5) * s) + t
      };
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest vertex or center of a square.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToVertexOrCenter({x, y}, resolution) {
      const s = this.size / resolution;
      const t = this.size / 2;
      const c0 = (x - t) / s;
      const r0 = (y - t) / s;
      const c1 = Math.round(c0 + r0);
      const r1 = Math.round(r0 - c0);
      return {
        x: ((c1 - r1) * s / 2) + t,
        y: ((c1 + r1) * s / 2) + t
      };
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest edge of a square.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToEdge({x, y}, resolution) {
      const s = this.size / resolution;
      const t = this.size / 2;
      const c0 = (x - t) / s;
      const r0 = (y - t) / s;
      const c1 = Math.floor(c0 + r0);
      const r1 = Math.floor(r0 - c0);
      return {
        x: ((c1 - r1) * s / 2) + t,
        y: ((c1 + r1 + 1) * s / 2) + t
      };
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest edge or center of a square.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToEdgeOrCenter({x, y}, resolution) {
      const s = this.size / resolution;
      const t = this.size / 2;
      const c0 = (x - t) / s;
      const r0 = (y - t) / s;
      const x0 = (Math.round(c0) * s) + t;
      const y0 = (Math.round(r0) * s) + t;
      if ( Math.max(Math.abs(x - x0), Math.abs(y - y0)) <= s / 4 ) {
        return {x: x0, y: y0};
      }
      const c1 = Math.floor(c0 + r0);
      const r1 = Math.floor(r0 - c0);
      return {
        x: ((c1 - r1) * s / 2) + t,
        y: ((c1 + r1 + 1) * s / 2) + t
      };
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest edge or vertex of a square.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToEdgeOrVertex({x, y}, resolution) {
      const s = this.size / resolution;
      const t = this.size / 2;
      const c0 = (x - t) / s;
      const r0 = (y - t) / s;
      const x0 = ((Math.floor(c0) + 0.5) * s) + t;
      const y0 = ((Math.floor(r0) + 0.5) * s) + t;
      if ( Math.max(Math.abs(x - x0), Math.abs(y - y0)) <= s / 4 ) {
        return {x: x0, y: y0};
      }
      const c1 = Math.floor(c0 + r0);
      const r1 = Math.floor(r0 - c0);
      return {
        x: ((c1 - r1) * s / 2) + t,
        y: ((c1 + r1 + 1) * s / 2) + t
      };
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest edge, vertex, or center of a square.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToEdgeOrVertexOrCenter({x, y}, resolution) {
      const s = this.size / (resolution * 2);
      return {
        x: Math.round(x / s) * s,
        y: Math.round(y / s) * s
      };
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest top/bottom side of a square.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToTopOrBottom({x, y}, resolution) {
      const s = this.size / resolution;
      const t = this.size / 2;
      return {
        x: (Math.round((x - t) / s) * s) + t,
        y: ((Math.ceil((y - t) / s) - 0.5) * s) + t
      };
    }

    /* -------------------------------------------- */

    /**
     * Snap the point to the nearest left/right side of a square.
     * @param {Point} point          The point
     * @param {number} resolution    The grid resolution
     * @returns {Point}              The snapped point
     */
    #snapToLeftOrRight({x, y}, resolution) {
      const s = this.size / resolution;
      const t = this.size / 2;
      return {
        x: ((Math.ceil((x - t) / s) - 0.5) * s) + t,
        y: (Math.round((y - t) / s) * s) + t
      };
    }

    /* -------------------------------------------- */

    /**
     * @typedef {object} _SquareGridMeasurePathResultWaypoint
     * @property {number} diagonals    The total number of diagonals moved along a direct path up to this waypoint.
     */

    /**
     * @typedef {GridMeasurePathResultWaypoint & _SquareGridMeasurePathResultWaypoint} SquareGridMeasurePathResultWaypoint
     */

    /**
     * @typedef {object} _SquareGridMeasurePathResultWaypoint
     * @property {number} diagonals    The number of diagonals moved along this segment.
     */

    /**
     * @typedef {GridMeasurePathResultWaypoint & _SquareGridMeasurePathResultWaypoint} SquareGridMeasurePathResultWaypoint
     */

    /**
     * @typedef {object} _SquareGridMeasurePathResult
     * @property {number} diagonals    The total number of diagonals moved along a direct path through all waypoints.
     */

    /**
     * @typedef {GridMeasurePathResult & _SquareGridMeasurePathResult} SquareGridMeasurePathResult
     */

    /**
     * Measure a shortest, direct path through the given waypoints.
     * @function measurePath
     * @memberof SquareGrid
     * @instance
     *
     * @param {GridMeasurePathWaypoint[]} waypoints           The waypoints the path must pass through
     * @param {object} [options]                              Additional measurement options
     * @param {GridMeasurePathCostFunction} [options.cost]    The function that returns the cost
     *   for a given move between grid spaces (default is the distance travelled)
     * @returns {SquareGridMeasurePathResult}    The measurements a shortest, direct path through the given waypoints.
     */

    /** @override */
    _measurePath(waypoints, {cost}, result) {
      result.distance = 0;
      result.spaces = 0;
      result.cost = 0;
      result.diagonals = 0;

      if ( waypoints.length === 0 ) return;

      const from = result.waypoints[0];
      from.distance = 0;
      from.spaces = 0;
      from.cost = 0;
      from.diagonals = 0;

      // Convert to point coordiantes
      const toPoint = coords => {
        if ( coords.x !== undefined ) return coords;
        return this.getCenterPoint(coords);
      };

      // Prepare data for the starting point
      const w0 = waypoints[0];
      let o0 = this.getOffset(w0);
      let p0 = toPoint(w0);

      // Iterate over additional path points
      const diagonals = this.diagonals;
      let da = 0;
      let db = 0;
      let l0 = 0;
      for ( let i = 1; i < waypoints.length; i++ ) {
        const w1 = waypoints[i];
        const o1 = this.getOffset(w1);
        const p1 = toPoint(w1);

        // Measure segment
        const to = result.waypoints[i];
        const segment = to.backward;
        if ( !w1.teleport ) {
          const di = Math.abs(o0.i - o1.i);
          const dj = Math.abs(o0.j - o1.j);
          const ns = Math.abs(di - dj); // The number of straight moves
          let nd = Math.min(di, dj); // The number of diagonal moves
          let n = ns + nd; // The number of moves total

          // Determine the offset distance of the diagonal moves
          let cd;
          switch ( diagonals ) {
            case GRID_DIAGONALS.EQUIDISTANT: cd = nd; break;
            case GRID_DIAGONALS.EXACT: cd = Math.SQRT2 * nd; break;
            case GRID_DIAGONALS.APPROXIMATE: cd = 1.5 * nd; break;
            case GRID_DIAGONALS.RECTILINEAR: cd = 2 * nd; break;
            case GRID_DIAGONALS.ALTERNATING_1:
              if ( result.diagonals & 1 ) cd = ((nd + 1) & -2) + (nd >> 1);
              else cd = (nd & -2) + ((nd + 1) >> 1);
              break;
            case GRID_DIAGONALS.ALTERNATING_2:
              if ( result.diagonals & 1 ) cd = (nd & -2) + ((nd + 1) >> 1);
              else cd = ((nd + 1) & -2) + (nd >> 1);
              break;
            case GRID_DIAGONALS.ILLEGAL:
              cd = 2 * nd;
              nd = 0;
              n = di + dj;
              break;
          }

          // Determine the distance of the segment
          const dx = Math.abs(p0.x - p1.x) / this.size;
          const dy = Math.abs(p0.y - p1.y) / this.size;
          let l;
          switch ( diagonals ) {
            case GRID_DIAGONALS.EQUIDISTANT: l = Math.max(dx, dy); break;
            case GRID_DIAGONALS.EXACT: l = Math.max(dx, dy) + ((Math.SQRT2 - 1) * Math.min(dx, dy)); break;
            case GRID_DIAGONALS.APPROXIMATE: l = Math.max(dx, dy) + (0.5 * Math.min(dx, dy)); break;
            case GRID_DIAGONALS.ALTERNATING_1:
            case GRID_DIAGONALS.ALTERNATING_2:
              {
                const a = da += Math.max(dx, dy);
                const b = db += Math.min(dx, dy);
                const c = Math.floor(b / 2);
                const d = b - (2 * c);
                const e = Math.min(d, 1);
                const f = Math.max(d, 1) - 1;
                const l1 = a - b + (3 * c) + e + f + (diagonals === GRID_DIAGONALS.ALTERNATING_1 ? f : e);
                l = l1 - l0;
                l0 = l1;
              }
              break;
            case GRID_DIAGONALS.RECTILINEAR:
            case GRID_DIAGONALS.ILLEGAL: l = dx + dy; break;
          }
          if ( l.almostEqual(ns + cd) ) l = ns + cd;

          // Calculate the distance: the cost of the straight moves plus the cost of the diagonal moves
          segment.distance = l * this.distance;
          segment.spaces = n;
          segment.cost = cost ? this.#calculateCost(o0, o1, cost, result.diagonals) : (ns + cd) * this.distance;
          segment.diagonals = nd;
        } else {
          segment.distance = 0;
          segment.spaces = 0;
          segment.cost = cost && ((o0.i !== o1.i) || (o0.j !== o1.j)) ? cost(o0, o1, 0) : 0;
          segment.diagonals = 0;
        }

        // Accumulate measurements
        result.distance += segment.distance;
        result.spaces += segment.spaces;
        result.cost += segment.cost;
        result.diagonals += segment.diagonals;

        // Set waypoint measurements
        to.distance = result.distance;
        to.spaces = result.spaces;
        to.cost = result.cost;
        to.diagonals = result.diagonals;

        o0 = o1;
        p0 = p1;
      }
    }

    /* -------------------------------------------- */

    /**
     * Calculate the cost of the direct path segment.
     * @param {GridOffset} from     The coordinates the segment starts from
     * @param {GridOffset} to       The coordinates the segment goes to
     * @param {GridMeasurePathCostFunction} cost    The cost function
     * @param {number} diagonals    The number of diagonal moves that have been performed already
     * @returns {number}            The cost of the path segment
     */
    #calculateCost(from, to, cost, diagonals) {
      const path = this.getDirectPath([from, to]);
      if ( path.length <= 1 ) return 0;

      // Prepare data for the starting point
      let o0 = path[0];
      let c = 0;

      // Iterate over additional path points
      for ( let i = 1; i < path.length; i++ ) {
        const o1 = path[i];

        // Determine the normalized distance
        let k;
        if ( (o0.i === o1.i) || (o0.j === o1.j) ) k = 1;
        else {
          switch ( this.diagonals ) {
            case GRID_DIAGONALS.EQUIDISTANT: k = 1; break;
            case GRID_DIAGONALS.EXACT: k = Math.SQRT2; break;
            case GRID_DIAGONALS.APPROXIMATE: k = 1.5; break;
            case GRID_DIAGONALS.RECTILINEAR: k = 2; break;
            case GRID_DIAGONALS.ALTERNATING_1: k = diagonals & 1 ? 2 : 1; break;
            case GRID_DIAGONALS.ALTERNATING_2: k = diagonals & 1 ? 1 : 2; break;
          }
          diagonals++;
        }

        // Calculate and accumulate the cost
        c += cost(o0, o1, k * this.distance);

        o0 = o1;
      }

      return c;
    }

    /* -------------------------------------------- */

    /**
     * @see {@link https://en.wikipedia.org/wiki/Bresenham's_line_algorithm}
     * @override
     */
    getDirectPath(waypoints) {
      if ( waypoints.length === 0 ) return [];

      // Prepare data for the starting point
      const o0 = this.getOffset(waypoints[0]);
      let {i: i0, j: j0} = o0;
      const path = [o0];

      // Iterate over additional path points
      const diagonals = this.diagonals !== GRID_DIAGONALS.ILLEGAL;
      for ( let i = 1; i < waypoints.length; i++ ) {
        const o1 = this.getOffset(waypoints[i]);
        const {i: i1, j: j1} = o1;
        if ( (i0 === i1) && (j0 === j1) ) continue;

        // Walk from (r0, c0) to (r1, c1)
        const di = Math.abs(i0 - i1);
        const dj = 0 - Math.abs(j0 - j1);
        const si = i0 < i1 ? 1 : -1;
        const sj = j0 < j1 ? 1 : -1;
        let e = di + dj;
        for ( ;; ) {
          const e2 = e * 2;
          if ( diagonals ) {
            if ( e2 >= dj ) {
              e += dj;
              i0 += si;
            }
            if ( e2 <= di ) {
              e += di;
              j0 += sj;
            }
          } else {
            if ( e2 - dj >= di - e2 ) {
              e += dj;
              i0 += si;
            } else {
              e += di;
              j0 += sj;
            }
          }
          if ( (i0 === i1) && (j0 === j1) ) break;
          path.push({i: i0, j: j0});
        }
        path.push(o1);

        i0 = i1;
        j0 = j1;
      }

      return path;
    }

    /* -------------------------------------------- */

    /** @override */
    getTranslatedPoint(point, direction, distance) {
      direction = Math.toRadians(direction);
      const dx = Math.cos(direction);
      const dy = Math.sin(direction);
      const adx = Math.abs(dx);
      const ady = Math.abs(dy);
      let s = distance / this.distance;
      switch ( this.diagonals ) {
        case GRID_DIAGONALS.EQUIDISTANT: s /= Math.max(adx, ady); break;
        case GRID_DIAGONALS.EXACT: s /= (Math.max(adx, ady) + ((Math.SQRT2 - 1) * Math.min(adx, ady))); break;
        case GRID_DIAGONALS.APPROXIMATE: s /= (Math.max(adx, ady) + (0.5 * Math.min(adx, ady))); break;
        case GRID_DIAGONALS.ALTERNATING_1:
          {
            let a = Math.max(adx, ady);
            const b = Math.min(adx, ady);
            const t = (2 * a) + b;
            let k = Math.floor(s * b / t);
            if ( (s * b) - (k * t) > a ) {
              a += b;
              k = -1 - k;
            }
            s = (s - k) / a;
          }
          break;
        case GRID_DIAGONALS.ALTERNATING_2:
          {
            let a = Math.max(adx, ady);
            const b = Math.min(adx, ady);
            const t = (2 * a) + b;
            let k = Math.floor(s * b / t);
            if ( (s * b) - (k * t) > a + b ) {
              k += 1;
            } else {
              a += b;
              k = -k;
            }
            s = (s - k) / a;
          }
          break;
        case GRID_DIAGONALS.RECTILINEAR:
        case GRID_DIAGONALS.ILLEGAL: s /= (adx + ady); break;
      }
      s *= this.size;
      return {x: point.x + (dx * s), y: point.y + (dy * s)};
    }

    /* -------------------------------------------- */

    /** @override */
    getCircle(center, radius) {
      if ( radius <= 0 ) return [];
      switch ( this.diagonals ) {
        case GRID_DIAGONALS.EQUIDISTANT: return this.#getCircleEquidistant(center, radius);
        case GRID_DIAGONALS.EXACT: return this.#getCircleExact(center, radius);
        case GRID_DIAGONALS.APPROXIMATE: return this.#getCircleApproximate(center, radius);
        case GRID_DIAGONALS.ALTERNATING_1: return this.#getCircleAlternating(center, radius, false);
        case GRID_DIAGONALS.ALTERNATING_2: return this.#getCircleAlternating(center, radius, true);
        case GRID_DIAGONALS.RECTILINEAR:
        case GRID_DIAGONALS.ILLEGAL: return this.#getCircleRectilinear(center, radius);
      }
    }

    /* -------------------------------------------- */

    /**
     * Get the circle polygon given the radius in grid units (EQUIDISTANT).
     * @param {Point} center     The center point of the circle.
     * @param {number} radius    The radius in grid units (positive).
     * @returns {Point[]}        The points of the circle polygon.
     */
    #getCircleEquidistant({x, y}, radius) {
      const r = radius / this.distance * this.size;
      const x0 = x + r;
      const x1 = x - r;
      const y0 = y + r;
      const y1 = y - r;
      return [{x: x0, y: y0}, {x: x1, y: y0}, {x: x1, y: y1}, {x: x0, y: y1}];
    }

    /* -------------------------------------------- */

    /**
     * Get the circle polygon given the radius in grid units (EXACT).
     * @param {Point} center     The center point of the circle.
     * @param {number} radius    The radius in grid units (positive).
     * @returns {Point[]}        The points of the circle polygon.
     */
    #getCircleExact({x, y}, radius) {
      const r = radius / this.distance * this.size;
      const s = r / Math.SQRT2;
      return [
        {x: x + r, y},
        {x: x + s, y: y + s},
        {x: x, y: y + r },
        {x: x - s, y: y + s},
        {x: x - r, y},
        {x: x - s, y: y - s},
        {x: x, y: y - r},
        {x: x + s, y: y - s}
      ];
    }

    /* -------------------------------------------- */

    /**
     * Get the circle polygon given the radius in grid units (APPROXIMATE).
     * @param {Point} center     The center point of the circle.
     * @param {number} radius    The radius in grid units (positive).
     * @returns {Point[]}        The points of the circle polygon.
     */
    #getCircleApproximate({x, y}, radius) {
      const r = radius / this.distance * this.size;
      const s = r / 1.5;
      return [
        {x: x + r, y},
        {x: x + s, y: y + s},
        {x: x, y: y + r },
        {x: x - s, y: y + s},
        {x: x - r, y},
        {x: x - s, y: y - s},
        {x: x, y: y - r},
        {x: x + s, y: y - s}
      ];
    }

    /* -------------------------------------------- */

    /**
     * Get the circle polygon given the radius in grid units (ALTERNATING_1/2).
     * @param {Point} center           The center point of the circle.
     * @param {number} radius          The radius in grid units (positive).
     * @param {boolean} firstDouble    2/1/2 instead of 1/2/1?
     * @returns {Point[]}              The points of the circle polygon.
     */
    #getCircleAlternating(center, radius, firstDouble) {
      const r = radius / this.distance;
      const points = [];
      let dx = 0;
      let dy = 0;

      // Generate points of the first quarter
      if ( firstDouble ) {
        points.push({x: r - dx, y: dy});
        dx++;
        dy++;
      }
      for ( ;; ) {
        if ( r - dx < dy ) {
          [dx, dy] = [dy - 1, dx - 1];
          break;
        }
        points.push({x: r - dx, y: dy});
        dy++;
        if ( r - dx < dy ) {
          points.push({x: r - dx, y: r - dx});
          if ( dx === 0 ) dy = 0;
          else {
            points.push({x: dy - 1, y: r - dx});
            [dx, dy] = [dy - 2, dx - 1];
          }
          break;
        }
        points.push({x: r - dx, y: dy});
        dx++;
        dy++;
      }
      for ( ;; ) {
        if ( dx === 0 ) break;
        points.push({x: dx, y: r - dy});
        dx--;
        if ( dx === 0 ) break;
        points.push({x: dx, y: r - dy});
        dx--;
        dy--;
      }

      // Generate the points of the other three quarters by mirroring the first
      const n = points.length;
      for ( let i = 0; i < n; i++ ) {
        const p = points[i];
        points.push({x: -p.y, y: p.x});
      }
      for ( let i = 0; i < n; i++ ) {
        const p = points[i];
        points.push({x: -p.x, y: -p.y});
      }
      for ( let i = 0; i < n; i++ ) {
        const p = points[i];
        points.push({x: p.y, y: -p.x});
      }

      // Scale and center the polygon points
      for ( let i = 0; i < 4 * n; i++ ) {
        const p = points[i];
        p.x = (p.x * this.size) + center.x;
        p.y = (p.y * this.size) + center.y;
      }
      return points;
    }

    /* -------------------------------------------- */

    /**
     * Get the circle polygon given the radius in grid units (RECTILINEAR/ILLEGAL).
     * @param {Point} center     The center point of the circle.
     * @param {number} radius    The radius in grid units (positive).
     * @returns {Point[]}        The points of the circle polygon.
     */
    #getCircleRectilinear({x, y}, radius) {
      const r = radius / this.distance * this.size;
      return [{x: x + r, y}, {x, y: y + r}, {x: x - r, y}, {x, y: y - r}];
    }

    /* -------------------------------------------- */

    /** @override */
    calculateDimensions(sceneWidth, sceneHeight, padding) {
      // Note: Do not replace `* (1 / this.size)` by `/ this.size`!
      // It could change the result and therefore break certain scenes.
      const x = Math.ceil((padding * sceneWidth) * (1 / this.size)) * this.size;
      const y = Math.ceil((padding * sceneHeight) * (1 / this.size)) * this.size;
      const width = sceneWidth + (2 * x);
      const height = sceneHeight + (2 * y);
      const rows = Math.ceil((height / this.size) - 1e-6);
      const columns = Math.ceil((width / this.size) - 1e-6);
      return {width, height, x, y, rows, columns};
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getCenter(x, y) {
      const msg = "SquareGrid#getCenter is deprecated. Use SquareGrid#getCenterPoint instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return this.getTopLeft(x, y).map(c => c + (this.size / 2));
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getSnappedPosition(x, y, interval=1, options={}) {
      const msg = "SquareGrid#getSnappedPosition is deprecated. "
        + "Use BaseGrid#getSnappedPoint instead for non-Euclidean measurements.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      if ( interval === 0 ) return {x: Math.round(x), y: Math.round(y)};
      let [x0, y0] = this.#getNearestVertex(x, y);
      let dx = 0;
      let dy = 0;
      if ( interval !== 1 ) {
        let delta = this.size / interval;
        dx = Math.round((x - x0) / delta) * delta;
        dy = Math.round((y - y0) / delta) * delta;
      }
      return {
        x: Math.round(x0 + dx),
        y: Math.round(y0 + dy)
      };
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    #getNearestVertex(x, y) {
      return [x.toNearest(this.size), y.toNearest(this.size)];
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getGridPositionFromPixels(x, y) {
      const msg = "BaseGrid#getGridPositionFromPixels is deprecated. Use BaseGrid#getOffset instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return [Math.floor(y / this.size), Math.floor(x / this.size)];
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getPixelsFromGridPosition(row, col) {
      const msg = "BaseGrid#getPixelsFromGridPosition is deprecated. Use BaseGrid#getTopLeftPoint instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      return [col * this.size, row * this.size];
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    shiftPosition(x, y, dx, dy, options={}) {
      const msg = "BaseGrid#shiftPosition is deprecated. Use BaseGrid#getShiftedPoint instead.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      let [row, col] = this.getGridPositionFromPixels(x, y);
      return this.getPixelsFromGridPosition(row+dy, col+dx);
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    measureDistances(segments, options={}) {
      const msg = "SquareGrid#measureDistances is deprecated. "
        + "Use BaseGrid#measurePath instead for non-Euclidean measurements.";
      logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
      if ( !options.gridSpaces ) return super.measureDistances(segments, options);
      return segments.map(s => {
        let r = s.ray;
        let nx = Math.abs(Math.ceil(r.dx / this.size));
        let ny = Math.abs(Math.ceil(r.dy / this.size));

        // Determine the number of straight and diagonal moves
        let nd = Math.min(nx, ny);
        let ns = Math.abs(ny - nx);

        // Linear distance for all moves
        return (nd + ns) * this.distance;
      });
    }
  }

  /** @module foundry.grid */

  var grid = /*#__PURE__*/Object.freeze({
    __proto__: null,
    BaseGrid: BaseGrid,
    GridHex: GridHex,
    GridlessGrid: GridlessGrid,
    HexagonalGrid: HexagonalGrid$1,
    SquareGrid: SquareGrid
  });

  /**
   * @typedef {Object} ApplicationConfiguration
   * @property {string} id                        An HTML element identifier used for this Application instance
   * @property {string} uniqueId                  An string discriminator substituted for {id} in the default
   *                                              HTML element identifier for the class
   * @property {string[]} classes                 An array of CSS classes to apply to the Application
   * @property {string} tag                       The HTMLElement tag type used for the outer Application frame
   * @property {ApplicationWindowConfiguration} window  Configuration of the window behaviors for this Application
   * @property {Record<string, ApplicationClickAction|{handler: ApplicationClickAction, buttons: number[]}>} actions
   *                                              Click actions supported by the Application and their event handler
   *                                              functions. A handler function can be defined directly which only
   *                                              responds to left-click events. Otherwise, an object can be declared
   *                                              containing both a handler function and an array of buttons which are
   *                                              matched against the PointerEvent#button property.
   * @property {ApplicationFormConfiguration} [form] Configuration used if the application top-level element is a form or
   *                                                 dialog
   * @property {Partial<ApplicationPosition>} position  Default positioning data for the application
   */

  /**
   * @typedef {Object} ApplicationPosition
   * @property {number} top                       Window offset pixels from top
   * @property {number} left                      Window offset pixels from left
   * @property {number|"auto"} width              Un-scaled pixels in width or "auto"
   * @property {number|"auto"} height             Un-scaled pixels in height or "auto"
   * @property {number} scale                     A numeric scaling factor applied to application dimensions
   * @property {number} zIndex                    A z-index of the application relative to siblings
   */

  /**
   * @typedef {Object} ApplicationWindowConfiguration
   * @property {boolean} [frame=true]             Is this Application rendered inside a window frame?
   * @property {boolean} [positioned=true]        Can this Application be positioned via JavaScript or only by CSS
   * @property {string} [title]                   The window title. Displayed only if the application is framed
   * @property {string|false} [icon]              An optional Font Awesome icon class displayed left of the window title
   * @property {ApplicationHeaderControlsEntry[]} [controls]  An array of window control entries
   * @property {boolean} [minimizable=true]       Can the window app be minimized by double-clicking on the title
   * @property {boolean} [resizable=false]        Is this window resizable?
   * @property {string} [contentTag="section"]    A specific tag name to use for the .window-content element
   * @property {string[]} [contentClasses]        Additional CSS classes to apply to the .window-content element
   */

  /**
   * @typedef {Object} ApplicationFormConfiguration
   * @property {ApplicationFormSubmission} handler
   * @property {boolean} submitOnChange
   * @property {boolean} closeOnSubmit
   */

  /**
   * @typedef {Object} ApplicationHeaderControlsEntry
   * @property {string} icon                      A font-awesome icon class which denotes the control button
   * @property {string} label                     The text label for the control button. This label will be automatically
   *                                              localized when the button is rendered
   * @property {string} action                    The action name triggered by clicking the control button
   * @property {boolean} [visible]                Is the control button visible for the current client?
   * @property {string|number} [ownership]        A key or value in CONST.DOCUMENT_OWNERSHIP_LEVELS that restricts
   *                                              visibility of this option for the current user. This option only
   *                                              applies to DocumentSheetV2 instances.
   */

  /**
   * @typedef {Object} ApplicationConstructorParams
   * @property {ApplicationPosition} position
   */

  /**
   * @typedef {Object} ApplicationRenderOptions
   * @property {boolean} [force=false]            Force application rendering. If true, an application which does not
   *                                              yet exist in the DOM is added. If false, only applications which
   *                                              already exist are rendered.
   * @property {ApplicationPosition} [position]   A specific position at which to render the Application
   * @property {ApplicationWindowRenderOptions} [window]  Updates to the Application window frame
   * @property {string[]} [parts]                 Some Application classes, for example the HandlebarsApplication,
   *                                              support re-rendering a subset of application parts instead of the full
   *                                              Application HTML.
   * @property {boolean} [isFirstRender]          Is this render the first one for the application? This property is
   *                                              populated automatically.
   */

  /**
   * @typedef {Object} ApplicationWindowRenderOptions
   * @property {string} title                     Update the window title with a new value?
   * @property {string|false} icon                Update the window icon with a new value?
   * @property {boolean} controls                 Re-render the window controls menu?
   */

  /**
   * @typedef {Object} ApplicationRenderContext   Context data provided to the renderer
   */

  /**
   * @typedef {Object} ApplicationClosingOptions
   * @property {boolean} animate                  Whether to animate the close, or perform it instantaneously
   * @property {boolean} closeKey                 Whether the application was closed via keypress.
   */

  /**
   * @callback ApplicationClickAction             An on-click action supported by the Application. Run in the context of
   *                                              a {@link HandlebarsApplication}.
   * @param {PointerEvent} event                  The originating click event
   * @param {HTMLElement} target                  The capturing HTML element which defines the [data-action]
   * @returns {Promise<void>}
   */

  /**
   * @callback ApplicationFormSubmission          A form submission handler method. Run in the context of a
   *                                              {@link HandlebarsApplication}.
   * @param {SubmitEvent|Event} event             The originating form submission or input change event
   * @param {HTMLFormElement} form                The form element that was submitted
   * @param {FormDataExtended} formData           Processed data for the submitted form
   * @returns {Promise<void>}
   */


  /**
   * @typedef {Object} ApplicationTab
   * @property {string} id
   * @property {string} group
   * @property {string} icon
   * @property {string} label
   * @property {boolean} active
   * @property {string} cssClass
   */

  /**
   * @typedef {Object} FormNode
   * @property {boolean} fieldset
   * @property {string} [legend]
   * @property {FormNode[]} [fields]
   * @property {DataField} [field]
   * @property {any} [value]
   */

  /**
   * @typedef {Object} FormFooterButton
   * @property {string} type
   * @property {string} [name]
   * @property {string} [icon]
   * @property {string} [label]
   * @property {string} [action]
   * @property {string} [cssClass]
   * @property {boolean} [disabled=false]
   */

  var _types$3 = /*#__PURE__*/Object.freeze({
    __proto__: null
  });

  /**
   * @typedef {import("../_types.mjs").ApplicationConfiguration} ApplicationConfiguration
   * @typedef {import("../_types.mjs").ApplicationRenderOptions} ApplicationRenderOptions
   * @typedef {import("../_types.mjs").ApplicationRenderContext} ApplicationRenderContext
   * @typedef {import("../_types.mjs").ApplicationClosingOptions} ApplicationClosingOptions
   * @typedef {import("../_types.mjs").ApplicationPosition} ApplicationPosition
   * @typedef {import("../_types.mjs").ApplicationHeaderControlsEntry} ApplicationHeaderControlsEntry
   */

  /**
   * The Application class is responsible for rendering an HTMLElement into the Foundry Virtual Tabletop user interface.
   * @template {ApplicationConfiguration} Configuration
   * @template {ApplicationRenderOptions} RenderOptions
   * @alias ApplicationV2
   */
  class ApplicationV2 extends EventEmitterMixin(Object) {

    /**
     * Applications are constructed by providing an object of configuration options.
     * @param {Partial<Configuration>} [options]     Options used to configure the Application instance
     */
    constructor(options={}) {
      super();

      // Configure Application Options
      this.options = Object.freeze(this._initializeApplicationOptions(options));
      this.#id = this.options.id.replace("{id}", this.options.uniqueId);
      Object.assign(this.#position, this.options.position);

      // Verify the Application class is renderable
      this.#renderable = (this._renderHTML !== ApplicationV2.prototype._renderHTML)
        && (this._replaceHTML !== ApplicationV2.prototype._replaceHTML);
    }

    /**
     * Designates which upstream Application class in this class' inheritance chain is the base application.
     * Any DEFAULT_OPTIONS of super-classes further upstream of the BASE_APPLICATION are ignored.
     * Hook events for super-classes further upstream of the BASE_APPLICATION are not dispatched.
     * @type {typeof ApplicationV2}
     */
    static BASE_APPLICATION = ApplicationV2;

    /**
     * The default configuration options which are assigned to every instance of this Application class.
     * @type {Partial<Configuration>}
     */
    static DEFAULT_OPTIONS = {
      id: "app-{id}",
      classes: [],
      tag: "div",
      window: {
        frame: true,
        positioned: true,
        title: "",
        icon: "",
        controls: [],
        minimizable: true,
        resizable: false,
        contentTag: "section",
        contentClasses: []
      },
      actions: {},
      form: {
        handler: undefined,
        submitOnChange: false,
        closeOnSubmit: false
      },
      position: {}
    }

    /**
     * The sequence of rendering states that describe the Application life-cycle.
     * @enum {number}
     */
    static RENDER_STATES = Object.freeze({
      ERROR: -3,
      CLOSING: -2,
      CLOSED: -1,
      NONE: 0,
      RENDERING: 1,
      RENDERED: 2
    });

    /**
     * Which application is currently "in front" with the maximum z-index
     * @type {ApplicationV2}
     */
    static #frontApp;

    /** @override */
    static emittedEvents = Object.freeze(["render", "close", "position"]);

    /**
     * Application instance configuration options.
     * @type {Configuration}
     */
    options;

    /**
     * @type {string}
     */
    #id;

    /**
     * Flag that this Application instance is renderable.
     * Applications are not renderable unless a subclass defines the _renderHTML and _replaceHTML methods.
     */
    #renderable = true;

    /**
     * The outermost HTMLElement of this rendered Application.
     * For window applications this is ApplicationV2##frame.
     * For non-window applications this ApplicationV2##content.
     * @type {HTMLDivElement}
     */
    #element;

    /**
     * The HTMLElement within which inner HTML is rendered.
     * For non-window applications this is the same as ApplicationV2##element.
     * @type {HTMLElement}
     */
    #content;

    /**
     * Data pertaining to the minimization status of the Application.
     * @type {{
     *  active: boolean,
     *  [priorWidth]: number,
     *  [priorHeight]: number,
     *  [priorBoundingWidth]: number,
     *  [priorBoundingHeight]: number
     * }}
     */
    #minimization = Object.seal({
      active: false,
      priorWidth: undefined,
      priorHeight: undefined,
      priorBoundingWidth: undefined,
      priorBoundingHeight: undefined
    });

    /**
     * The rendered position of the Application.
     * @type {ApplicationPosition}
     */
    #position = Object.seal({
      top: undefined,
      left: undefined,
      width: undefined,
      height: "auto",
      scale: 1,
      zIndex: _maxZ
    });

    /**
     * @type {ApplicationV2.RENDER_STATES}
     */
    #state = ApplicationV2.RENDER_STATES.NONE;

    /**
     * A Semaphore used to enqueue asynchronous operations.
     * @type {Semaphore}
     */
    #semaphore = new Semaphore(1);

    /**
     * Convenience references to window header elements.
     * @type {{
     *  header: HTMLElement,
     *  resize: HTMLElement,
     *  title: HTMLHeadingElement,
     *  icon: HTMLElement,
     *  close: HTMLButtonElement,
     *  controls: HTMLButtonElement,
     *  controlsDropdown: HTMLDivElement,
     *  onDrag: Function,
     *  onResize: Function,
     *  pointerStartPosition: ApplicationPosition,
     *  pointerMoveThrottle: boolean
     * }}
     */
    get window() {
      return this.#window;
    }
    #window = {
      title: undefined,
      icon: undefined,
      close: undefined,
      controls: undefined,
      controlsDropdown: undefined,
      onDrag: this.#onWindowDragMove.bind(this),
      onResize: this.#onWindowResizeMove.bind(this),
      pointerStartPosition: undefined,
      pointerMoveThrottle: false
    };

    /**
     * If this Application uses tabbed navigation groups, this mapping is updated whenever the changeTab method is called.
     * Reports the active tab for each group.
     * Subclasses may override this property to define default tabs for each group.
     * @type {Record<string, string>}
     */
    tabGroups = {};

    /* -------------------------------------------- */
    /*  Application Properties                      */
    /* -------------------------------------------- */

    /**
     * The CSS class list of this Application instance
     * @type {DOMTokenList}
     */
    get classList() {
      return this.#element?.classList;
    }

    /**
     * The HTML element ID of this Application instance.
     * @type {string}
     */
    get id() {
      return this.#id;
    }

    /**
     * A convenience reference to the title of the Application window.
     * @type {string}
     */
    get title() {
      return game.i18n.localize(this.options.window.title);
    }

    /**
     * The HTMLElement which renders this Application into the DOM.
     * @type {HTMLElement}
     */
    get element() {
      return this.#element;
    }

    /**
     * Is this Application instance currently minimized?
     * @type {boolean}
     */
    get minimized() {
      return this.#minimization.active;
    }

    /**
     * The current position of the application with respect to the window.document.body.
     * @type {ApplicationPosition}
     */
    position = new Proxy(this.#position, {
      set: (obj, prop, value) => {
        if ( prop in obj ) {
          obj[prop] = value;
          this._updatePosition(this.#position);
          return value;
        }
      }
    });

    /**
     * Is this Application instance currently rendered?
     * @type {boolean}
     */
    get rendered() {
      return this.#state === ApplicationV2.RENDER_STATES.RENDERED;
    }

    /**
     * The current render state of the Application.
     * @type {ApplicationV2.RENDER_STATES}
     */
    get state() {
      return this.#state;
    }

    /**
     * Does this Application instance render within an outer window frame?
     * @type {boolean}
     */
    get hasFrame() {
      return this.options.window.frame;
    }

    /* -------------------------------------------- */
    /*  Initialization                              */
    /* -------------------------------------------- */

    /**
     * Iterate over the inheritance chain of this Application.
     * The chain includes this Application itself and all parents until the base application is encountered.
     * @see ApplicationV2.BASE_APPLICATION
     * @generator
     * @yields {typeof ApplicationV2}
     */
    static *inheritanceChain() {
      let cls = this;
      while ( cls ) {
        yield cls;
        if ( cls === this.BASE_APPLICATION ) return;
        cls = Object.getPrototypeOf(cls);
      }
    }

    /* -------------------------------------------- */

    /**
     * Initialize configuration options for the Application instance.
     * The default behavior of this method is to intelligently merge options for each class with those of their parents.
     * - Array-based options are concatenated
     * - Inner objects are merged
     * - Otherwise, properties in the subclass replace those defined by a parent
     * @param {Partial<ApplicationConfiguration>} options      Options provided directly to the constructor
     * @returns {ApplicationConfiguration}                     Configured options for the application instance
     * @protected
     */
    _initializeApplicationOptions(options) {

      // Options initialization order
      const order = [options];
      for ( const cls of this.constructor.inheritanceChain() ) {
        order.unshift(cls.DEFAULT_OPTIONS);
      }

      // Intelligently merge with parent class options
      const applicationOptions = {};
      for ( const opts of order ) {
        for ( const [k, v] of Object.entries(opts) ) {
          if ( (k in applicationOptions) ) {
            const v0 = applicationOptions[k];
            if ( Array.isArray(v0) ) applicationOptions[k].push(...v);                // Concatenate arrays
            else if ( foundry.utils.getType(v0) === "Object") Object.assign(v0, v);   // Merge objects
            else applicationOptions[k] = foundry.utils.deepClone(v);                  // Override option
          }
          else applicationOptions[k] = foundry.utils.deepClone(v);
        }
      }

      // Unique application ID
      applicationOptions.uniqueId = String(++globalThis._appId);

      // Special handling for classes
      if ( applicationOptions.window.frame ) applicationOptions.classes.unshift("application");
      applicationOptions.classes = Array.from(new Set(applicationOptions.classes));
      return applicationOptions;
    }

    /* -------------------------------------------- */
    /*  Rendering                                   */
    /* -------------------------------------------- */

    /**
     * Render the Application, creating its HTMLElement and replacing its innerHTML.
     * Add it to the DOM if it is not currently rendered and rendering is forced. Otherwise, re-render its contents.
     * @param {boolean|RenderOptions} [options]             Options which configure application rendering behavior.
     *                                                      A boolean is interpreted as the "force" option.
     * @param {RenderOptions} [_options]                    Legacy options for backwards-compatibility with the original
     *                                                      ApplicationV1#render signature.
     * @returns {Promise<ApplicationV2>}            A Promise which resolves to the rendered Application instance
     */
    async render(options={}, _options={}) {
      if ( typeof options === "boolean" ) options = Object.assign(_options, {force: options});
      return this.#semaphore.add(this.#render.bind(this), options);
    }

    /* -------------------------------------------- */

    /**
     * Manage the rendering step of the Application life-cycle.
     * This private method delegates out to several protected methods which can be defined by the subclass.
     * @param {RenderOptions} [options]             Options which configure application rendering behavior
     * @returns {Promise<ApplicationV2>}            A Promise which resolves to the rendered Application instance
     */
    async #render(options) {
      const states = ApplicationV2.RENDER_STATES;
      if ( !this.#renderable ) throw new Error(`The ${this.constructor.name} Application class is not renderable because`
        + " it does not define the _renderHTML and _replaceHTML methods which are required.");

      // Verify that the Application is allowed to be rendered
      try {
        const canRender = this._canRender(options);
        if ( canRender === false ) return this;
      } catch(err) {
        ui.notifications.warn(err.message);
        return this;
      }
      options.isFirstRender = this.#state <= states.NONE;

      // Prepare rendering context data
      this._configureRenderOptions(options);
      const context = await this._prepareContext(options);

      // Pre-render life-cycle events (awaited)
      if ( options.isFirstRender ) {
        if ( !options.force ) return this;
        await this.#doEvent(this._preFirstRender, {async: true, handlerArgs: [context, options],
          debugText: "Before first render"});
      }
      await this.#doEvent(this._preRender, {async: true, handlerArgs: [context, options],
        debugText: "Before render"});

      // Render the Application frame
      this.#state = states.RENDERING;
      if ( options.isFirstRender ) {
        this.#element = await this._renderFrame(options);
        this.#content = this.hasFrame ? this.#element.querySelector(".window-content") : this.#element;
        this._attachFrameListeners();
      }

      // Render Application content
      try {
        const result = await this._renderHTML(context, options);
        this._replaceHTML(result, this.#content, options);
      }
      catch(err) {
        if ( this.#element ) {
          this.#element.remove();
          this.#element = null;
        }
        this.#state = states.ERROR;
        throw new Error(`Failed to render Application "${this.id}":\n${err.message}`, { cause: err });
      }

      // Register the rendered Application
      if ( options.isFirstRender ) {
        foundry.applications.instances.set(this.#id, this);
        this._insertElement(this.#element);
      }
      if ( this.hasFrame ) this._updateFrame(options);
      this.#state = states.RENDERED;

      // Post-render life-cycle events (not awaited)
      if ( options.isFirstRender ) {
        // noinspection ES6MissingAwait
        this.#doEvent(this._onFirstRender, {handlerArgs: [context, options], debugText: "After first render"});
      }
      // noinspection ES6MissingAwait
      this.#doEvent(this._onRender, {handlerArgs: [context, options], debugText: "After render", eventName: "render",
          hookName: "render", hookArgs: [this.#element]});

      // Update application position
      if ( "position" in options ) this.setPosition(options.position);
      if ( options.force && this.minimized ) this.maximize();
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Modify the provided options passed to a render request.
     * @param {RenderOptions} options                 Options which configure application rendering behavior
     * @protected
     */
    _configureRenderOptions(options) {
      const isFirstRender = this.#state <= ApplicationV2.RENDER_STATES.NONE;
      const {window, position} = this.options;

      // Initial frame options
      if ( isFirstRender ) {
        if ( this.hasFrame ) {
          options.window ||= {};
          options.window.title ||= this.title;
          options.window.icon ||= window.icon;
          options.window.controls = true;
          options.window.resizable = window.resizable;
        }
      }

      // Automatic repositioning
      if ( isFirstRender ) options.position = Object.assign(this.#position, options.position);
      else {
        if ( position.width === "auto" ) options.position = Object.assign({width: "auto"}, options.position);
        if ( position.height === "auto" ) options.position = Object.assign({height: "auto"}, options.position);
      }
    }

    /* -------------------------------------------- */

    /**
     * Prepare application rendering context data for a given render request.
     * @param {RenderOptions} options                 Options which configure application rendering behavior
     * @returns {Promise<ApplicationRenderContext>}   Context data for the render operation
     * @protected
     */
    async _prepareContext(options) {
      return {};
    }

    /* -------------------------------------------- */

    /**
     * Configure the array of header control menu options
     * @returns {ApplicationHeaderControlsEntry[]}
     * @protected
     */
    _getHeaderControls() {
      return this.options.window.controls || [];
    }

    /* -------------------------------------------- */

    /**
     * Iterate over header control buttons, filtering for controls which are visible for the current client.
     * @returns {Generator<ApplicationHeaderControlsEntry>}
     * @yields {ApplicationHeaderControlsEntry}
     * @protected
     */
    *_headerControlButtons() {
      for ( const control of this._getHeaderControls() ) {
        if ( control.visible === false ) continue;
        yield control;
      }
    }

    /* -------------------------------------------- */

    /**
     * Render an HTMLElement for the Application.
     * An Application subclass must implement this method in order for the Application to be renderable.
     * @param {ApplicationRenderContext} context      Context data for the render operation
     * @param {RenderOptions} options                 Options which configure application rendering behavior
     * @returns {Promise<any>}                        The result of HTML rendering may be implementation specific.
     *                                                Whatever value is returned here is passed to _replaceHTML
     * @abstract
     */
    async _renderHTML(context, options) {}

    /* -------------------------------------------- */

    /**
     * Replace the HTML of the application with the result provided by the rendering backend.
     * An Application subclass should implement this method in order for the Application to be renderable.
     * @param {any} result                            The result returned by the application rendering backend
     * @param {HTMLElement} content                   The content element into which the rendered result must be inserted
     * @param {RenderOptions} options                 Options which configure application rendering behavior
     * @protected
     */
    _replaceHTML(result, content, options) {}

    /* -------------------------------------------- */

    /**
     * Render the outer framing HTMLElement which wraps the inner HTML of the Application.
     * @param {RenderOptions} options                 Options which configure application rendering behavior
     * @returns {Promise<HTMLElement>}
     * @protected
     */
    async _renderFrame(options) {
      const frame = document.createElement(this.options.tag);
      frame.id = this.#id;
      if ( this.options.classes.length ) frame.className = this.options.classes.join(" ");
      if ( !this.hasFrame ) return frame;

      // Window applications
      const labels = {
        controls: game.i18n.localize("APPLICATION.TOOLS.ControlsMenu"),
        toggleControls: game.i18n.localize("APPLICATION.TOOLS.ToggleControls"),
        close: game.i18n.localize("APPLICATION.TOOLS.Close")
      };
      const contentClasses = ["window-content", ...this.options.window.contentClasses].join(" ");
      frame.innerHTML = `<header class="window-header">
      <i class="window-icon hidden"></i>
      <h1 class="window-title"></h1>
      <button type="button" class="header-control fa-solid fa-ellipsis-vertical"
              data-tooltip="${labels.toggleControls}" aria-label="${labels.toggleControls}"
              data-action="toggleControls"></button>
      <button type="button" class="header-control fa-solid fa-times"
              data-tooltip="${labels.close}" aria-label="${labels.close}" data-action="close"></button>
    </header>
    <menu class="controls-dropdown"></menu>
    <${this.options.window.contentTag} class="${contentClasses}"></section>
    ${this.options.window.resizable ? `<div class="window-resize-handle"></div>` : ""}`;

      // Reference elements
      this.#window.header = frame.querySelector(".window-header");
      this.#window.title = frame.querySelector(".window-title");
      this.#window.icon = frame.querySelector(".window-icon");
      this.#window.resize = frame.querySelector(".window-resize-handle");
      this.#window.close = frame.querySelector("button[data-action=close]");
      this.#window.controls = frame.querySelector("button[data-action=toggleControls]");
      this.#window.controlsDropdown = frame.querySelector(".controls-dropdown");
      return frame;
    }

    /* -------------------------------------------- */

    /**
     * Render a header control button.
     * @param {ApplicationHeaderControlsEntry} control
     * @returns {HTMLLIElement}
     * @protected
     */
    _renderHeaderControl(control) {
      const li = document.createElement("li");
      li.className = "header-control";
      li.dataset.action = control.action;
      const label = game.i18n.localize(control.label);
      li.innerHTML = `<button type="button" class="control">
        <i class="control-icon fa-fw ${control.icon}"></i><span class="control-label">${label}</span>
    </button>`;
      return li;
    }

    /* -------------------------------------------- */

    /**
     * When the Application is rendered, optionally update aspects of the window frame.
     * @param {RenderOptions} options               Options provided at render-time
     * @protected
     */
    _updateFrame(options) {
      const window = options.window;
      if ( !window ) return;
      if ( "title" in window ) this.#window.title.innerText = window.title;
      if ( "icon" in window ) this.#window.icon.className = `window-icon fa-fw ${window.icon || "hidden"}`;

      // Window header controls
      if ( "controls" in window ) {
        const controls = [];
        for ( const c of this._headerControlButtons() ) {
          controls.push(this._renderHeaderControl(c));
        }
        this.#window.controlsDropdown.replaceChildren(...controls);
        this.#window.controls.classList.toggle("hidden", !controls.length);
      }
    }

    /* -------------------------------------------- */

    /**
     * Insert the application HTML element into the DOM.
     * Subclasses may override this method to customize how the application is inserted.
     * @param {HTMLElement} element                 The element to insert
     * @protected
     */
    _insertElement(element) {
      const existing = document.getElementById(element.id);
      if ( existing ) existing.replaceWith(element);
      else document.body.append(element);
      element.querySelector("[autofocus]")?.focus();
    }

    /* -------------------------------------------- */
    /*  Closing                                     */
    /* -------------------------------------------- */

    /**
     * Close the Application, removing it from the DOM.
     * @param {ApplicationClosingOptions} [options] Options which modify how the application is closed.
     * @returns {Promise<ApplicationV2>}            A Promise which resolves to the closed Application instance
     */
    async close(options={}) {
      return this.#semaphore.add(this.#close.bind(this), options);
    }

    /* -------------------------------------------- */

    /**
     * Manage the closing step of the Application life-cycle.
     * This private method delegates out to several protected methods which can be defined by the subclass.
     * @param {ApplicationClosingOptions} [options] Options which modify how the application is closed
     * @returns {Promise<ApplicationV2>}            A Promise which resolves to the rendered Application instance
     */
    async #close(options) {
      const states = ApplicationV2.RENDER_STATES;
      if ( !this.#element ) {
        this.#state = states.CLOSED;
        return this;
      }

      // Pre-close life-cycle events (awaited)
      await this.#doEvent(this._preClose, {async: true, handlerArgs: [options], debugText: "Before close"});

      // Set explicit dimensions for the transition.
      if ( options.animate !== false ) {
        const { width, height } = this.#element.getBoundingClientRect();
        this.#applyPosition({ ...this.#position, width, height });
      }

      // Remove the application element
      this.#element.classList.add("minimizing");
      this.#element.style.maxHeight = "0px";
      this.#state = states.CLOSING;
      if ( options.animate !== false ) await this._awaitTransition(this.#element, 1000);

      // Remove the closed element
      this._removeElement(this.#element);
      this.#element = null;
      this.#state = states.CLOSED;
      foundry.applications.instances.delete(this.#id);

      // Reset minimization state
      this.#minimization.active = false;

      // Post-close life-cycle events (not awaited)
      // noinspection ES6MissingAwait
      this.#doEvent(this._onClose, {handlerArgs: [options], debugText: "After close", eventName: "close",
        hookName: "close"});
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Remove the application HTML element from the DOM.
     * Subclasses may override this method to customize how the application element is removed.
     * @param {HTMLElement} element                 The element to be removed
     * @protected
     */
    _removeElement(element) {
      element.remove();
    }

    /* -------------------------------------------- */
    /*  Positioning                                 */
    /* -------------------------------------------- */

    /**
     * Update the Application element position using provided data which is merged with the prior position.
     * @param {Partial<ApplicationPosition>} [position] New Application positioning data
     * @returns {ApplicationPosition}                   The updated application position
     */
    setPosition(position) {
      if ( !this.options.window.positioned ) return;
      position = Object.assign(this.#position, position);
      this.#doEvent(this._prePosition, {handlerArgs: [position], debugText: "Before reposition"});

      // Update resolved position
      const updated = this._updatePosition(position);
      Object.assign(this.#position, updated);

      // Assign CSS styles
      this.#applyPosition(updated);
      this.#doEvent(this._onPosition, {handlerArgs: [position], debugText: "After reposition", eventName: "position"});
      return position;
    }

    /* -------------------------------------------- */

    /**
     * Translate a requested application position updated into a resolved allowed position for the Application.
     * Subclasses may override this method to implement more advanced positioning behavior.
     * @param {ApplicationPosition} position        Requested Application positioning data
     * @returns {ApplicationPosition}               Resolved Application positioning data
     * @protected
     */
    _updatePosition(position) {
      if ( !this.#element ) return position;
      const el = this.#element;
      let {width, height, left, top, scale} = position;
      scale ??= 1.0;
      const computedStyle = getComputedStyle(el);
      let minWidth = ApplicationV2.parseCSSDimension(computedStyle.minWidth, el.parentElement.offsetWidth) || 0;
      let maxWidth = ApplicationV2.parseCSSDimension(computedStyle.maxWidth, el.parentElement.offsetWidth) || Infinity;
      let minHeight = ApplicationV2.parseCSSDimension(computedStyle.minHeight, el.parentElement.offsetHeight) || 0;
      let maxHeight = ApplicationV2.parseCSSDimension(computedStyle.maxHeight, el.parentElement.offsetHeight) || Infinity;
      let bounds = el.getBoundingClientRect();
      const {clientWidth, clientHeight} = document.documentElement;

      // Explicit width
      const autoWidth = width === "auto";
      if ( !autoWidth ) {
        const targetWidth = Number(width || bounds.width);
        minWidth = parseInt(minWidth) || 0;
        maxWidth = parseInt(maxWidth) || (clientWidth / scale);
        width = Math.clamp(targetWidth, minWidth, maxWidth);
      }

      // Explicit height
      const autoHeight = height === "auto";
      if ( !autoHeight ) {
        const targetHeight = Number(height || bounds.height);
        minHeight = parseInt(minHeight) || 0;
        maxHeight = parseInt(maxHeight) || (clientHeight / scale);
        height = Math.clamp(targetHeight, minHeight, maxHeight);
      }

      // Implicit height
      if ( autoHeight ) {
        Object.assign(el.style, {width: `${width}px`, height: ""});
        bounds = el.getBoundingClientRect();
        height = bounds.height;
      }

      // Implicit width
      if ( autoWidth ) {
        Object.assign(el.style, {height: `${height}px`, width: ""});
        bounds = el.getBoundingClientRect();
        width = bounds.width;
      }

      // Left Offset
      const scaledWidth = width * scale;
      const targetLeft = left ?? ((clientWidth - scaledWidth) / 2);
      const maxLeft = Math.max(clientWidth - scaledWidth, 0);
      left = Math.clamp(targetLeft, 0, maxLeft);

      // Top Offset
      const scaledHeight = height * scale;
      const targetTop = top ?? ((clientHeight - scaledHeight) / 2);
      const maxTop = Math.max(clientHeight - scaledHeight, 0);
      top = Math.clamp(targetTop, 0, maxTop);

      // Scale
      scale ??= 1.0;
      return {width: autoWidth ? "auto" : width, height: autoHeight ? "auto" : height, left, top, scale};
    }

    /* -------------------------------------------- */

    /**
     * Apply validated position changes to the element.
     * @param {ApplicationPosition} position  The new position data to apply.
     */
    #applyPosition(position) {
      Object.assign(this.#element.style, {
        width: position.width === "auto" ? "" : `${position.width}px`,
        height: position.height === "auto" ? "" : `${position.height}px`,
        left: `${position.left}px`,
        top: `${position.top}px`,
        transform: position.scale === 1 ? "" : `scale(${position.scale})`
      });
    }

    /* -------------------------------------------- */
    /*  Other Public Methods                        */
    /* -------------------------------------------- */

    /**
     * Is the window control buttons menu currently expanded?
     * @type {boolean}
     */
    #controlsExpanded = false;

    /**
     * Toggle display of the Application controls menu.
     * Only applicable to window Applications.
     * @param {boolean} [expanded]      Set the controls visibility to a specific state.
     *                                  Otherwise, the visible state is toggled from its current value
     */
    toggleControls(expanded) {
      expanded ??= !this.#controlsExpanded;
      if ( expanded === this.#controlsExpanded ) return;
      const dropdown = this.#element.querySelector(".controls-dropdown");
      dropdown.classList.toggle("expanded", expanded);
      this.#controlsExpanded = expanded;
      game.tooltip.deactivate();
    }

    /* -------------------------------------------- */

    /**
     * Minimize the Application, collapsing it to a minimal header.
     * @returns {Promise<void>}
     */
    async minimize() {
      if ( this.minimized || !this.hasFrame || !this.options.window.minimizable ) return;
      this.#minimization.active = true;

      // Set explicit dimensions for the transition.
      const { width, height } = this.#element.getBoundingClientRect();
      this.#applyPosition({ ...this.#position, width, height });

      // Record pre-minimization data
      this.#minimization.priorWidth = this.#position.width;
      this.#minimization.priorHeight = this.#position.height;
      this.#minimization.priorBoundingWidth = width;
      this.#minimization.priorBoundingHeight = height;

      // Animate to collapsed size
      this.#element.classList.add("minimizing");
      this.#element.style.maxWidth = "var(--minimized-width)";
      this.#element.style.maxHeight = "var(--header-height)";
      await this._awaitTransition(this.#element, 1000);
      this.#element.classList.add("minimized");
      this.#element.classList.remove("minimizing");
    }

    /* -------------------------------------------- */

    /**
     * Restore the Application to its original dimensions.
     * @returns {Promise<void>}
     */
    async maximize() {
      if ( !this.minimized ) return;
      this.#minimization.active = false;

      // Animate back to full size
      const { priorBoundingWidth: width, priorBoundingHeight: height } = this.#minimization;
      this.#element.classList.remove("minimized");
      this.#element.classList.add("maximizing");
      this.#element.style.maxWidth = "";
      this.#element.style.maxHeight = "";
      this.#applyPosition({ ...this.#position, width, height });
      await this._awaitTransition(this.#element, 1000);
      this.#element.classList.remove("maximizing");

      // Restore the application position
      this._updatePosition(Object.assign(this.#position, {
        width: this.#minimization.priorWidth,
        height: this.#minimization.priorHeight
      }));
    }

    /* -------------------------------------------- */

    /**
     * Bring this Application window to the front of the rendering stack by increasing its z-index.
     * Once ApplicationV1 is deprecated we should switch from _maxZ to ApplicationV2#maxZ
     * We should also eliminate ui.activeWindow in favor of only ApplicationV2#frontApp
     */
    bringToFront() {
      if ( !((ApplicationV2.#frontApp === this) && (ui.activeWindow === this)) ) this.#position.zIndex = ++_maxZ;
      this.#element.style.zIndex = String(this.#position.zIndex);
      ApplicationV2.#frontApp = this;
      ui.activeWindow = this; // ApplicationV1 compatibility
    }

    /* -------------------------------------------- */

    /**
     * Change the active tab within a tab group in this Application instance.
     * @param {string} tab        The name of the tab which should become active
     * @param {string} group      The name of the tab group which defines the set of tabs
     * @param {object} [options]  Additional options which affect tab navigation
     * @param {Event} [options.event]                 An interaction event which caused the tab change, if any
     * @param {HTMLElement} [options.navElement]      An explicit navigation element being modified
     * @param {boolean} [options.force=false]         Force changing the tab even if the new tab is already active
     * @param {boolean} [options.updatePosition=true] Update application position after changing the tab?
     */
    changeTab(tab, group, {event, navElement, force=false, updatePosition=true}={}) {
      if ( !tab || !group ) throw new Error("You must pass both the tab and tab group identifier");
      if ( (this.tabGroups[group] === tab) && !force ) return;  // No change necessary
      const tabElement = this.#content.querySelector(`.tabs > [data-group="${group}"][data-tab="${tab}"]`);
      if ( !tabElement ) throw new Error(`No matching tab element found for group "${group}" and tab "${tab}"`);

      // Update tab navigation
      for ( const t of this.#content.querySelectorAll(`.tabs > [data-group="${group}"]`) ) {
        t.classList.toggle("active", t.dataset.tab === tab);
      }

      // Update tab contents
      for ( const section of this.#content.querySelectorAll(`.tab[data-group="${group}"]`) ) {
        section.classList.toggle("active", section.dataset.tab === tab);
      }
      this.tabGroups[group] = tab;

      // Update automatic width or height
      if ( !updatePosition ) return;
      const positionUpdate = {};
      if ( this.options.position.width === "auto" ) positionUpdate.width = "auto";
      if ( this.options.position.height === "auto" ) positionUpdate.height = "auto";
      if ( !foundry.utils.isEmpty(positionUpdate) ) this.setPosition(positionUpdate);
    }

    /* -------------------------------------------- */
    /*  Life-Cycle Handlers                         */
    /* -------------------------------------------- */

    /**
     * Perform an event in the application life-cycle.
     * Await an internal life-cycle method defined by the class.
     * Optionally dispatch an event for any registered listeners.
     * @param {Function} handler        A handler function to call
     * @param {object} options          Options which configure event handling
     * @param {boolean} [options.async]         Await the result of the handler function?
     * @param {any[]} [options.handlerArgs]     Arguments passed to the handler function
     * @param {string} [options.debugText]      Debugging text to log for the event
     * @param {string} [options.eventName]      An event name to dispatch for registered listeners
     * @param {string} [options.hookName]       A hook name to dispatch for this and all parent classes
     * @param {any[]} [options.hookArgs]        Arguments passed to the requested hook function
     * @returns {Promise<void>}         A promise which resoles once the handler is complete
     */
    async #doEvent(handler, {async=false, handlerArgs, debugText, eventName, hookName, hookArgs=[]}={}) {

      // Debug logging
      if ( debugText && CONFIG.debug.applications ) {
        console.debug(`${this.constructor.name} | ${debugText}`);
      }

      // Call handler function
      const response = handler.call(this, ...handlerArgs);
      if ( async ) await response;

      // Dispatch event for this Application instance
      if ( eventName ) this.dispatchEvent(new Event(eventName, { bubbles: true, cancelable: true }));

      // Call hooks for this Application class
      if ( hookName ) {
        for ( const cls of this.constructor.inheritanceChain() ) {
          if ( !cls.name ) continue;
          Hooks.callAll(`${hookName}${cls.name}`, this, ...hookArgs);
        }
      }
      return response;
    }

    /* -------------------------------------------- */
    /*  Rendering Life-Cycle Methods                */
    /* -------------------------------------------- */

    /**
     * Test whether this Application is allowed to be rendered.
     * @param {RenderOptions} options                 Provided render options
     * @returns {false|void}                          Return false to prevent rendering
     * @throws {Error}                                An Error to display a warning message
     * @protected
     */
    _canRender(options) {}

    /**
     * Actions performed before a first render of the Application.
     * @param {ApplicationRenderContext} context      Prepared context data
     * @param {RenderOptions} options                 Provided render options
     * @returns {Promise<void>}
     * @protected
     */
    async _preFirstRender(context, options) {}

    /**
     * Actions performed after a first render of the Application.
     * Post-render steps are not awaited by the render process.
     * @param {ApplicationRenderContext} context      Prepared context data
     * @param {RenderOptions} options                 Provided render options
     * @protected
     */
    _onFirstRender(context, options) {}

    /**
     * Actions performed before any render of the Application.
     * Pre-render steps are awaited by the render process.
     * @param {ApplicationRenderContext} context      Prepared context data
     * @param {RenderOptions} options                 Provided render options
     * @returns {Promise<void>}
     * @protected
     */
    async _preRender(context, options) {}

    /**
     * Actions performed after any render of the Application.
     * Post-render steps are not awaited by the render process.
     * @param {ApplicationRenderContext} context      Prepared context data
     * @param {RenderOptions} options                 Provided render options
     * @protected
     */
    _onRender(context, options) {}

    /**
     * Actions performed before closing the Application.
     * Pre-close steps are awaited by the close process.
     * @param {RenderOptions} options                 Provided render options
     * @returns {Promise<void>}
     * @protected
     */
    async _preClose(options) {}

    /**
     * Actions performed after closing the Application.
     * Post-close steps are not awaited by the close process.
     * @param {RenderOptions} options                 Provided render options
     * @protected
     */
    _onClose(options) {}

    /**
     * Actions performed before the Application is re-positioned.
     * Pre-position steps are not awaited because setPosition is synchronous.
     * @param {ApplicationPosition} position          The requested application position
     * @protected
     */
    _prePosition(position) {}

    /**
     * Actions performed after the Application is re-positioned.
     * @param {ApplicationPosition} position          The requested application position
     * @protected
     */
    _onPosition(position) {}

    /* -------------------------------------------- */
    /*  Event Listeners and Handlers                */
    /* -------------------------------------------- */

    /**
     * Attach event listeners to the Application frame.
     * @protected
     */
    _attachFrameListeners() {

      // Application Click Events
      this.#element.addEventListener("pointerdown", this.#onPointerDown.bind(this), {capture: true});
      const click = this.#onClick.bind(this);
      this.#element.addEventListener("click", click);
      this.#element.addEventListener("contextmenu", click);

      if ( this.hasFrame ) {
        this.bringToFront();
        this.#window.header.addEventListener("pointerdown", this.#onWindowDragStart.bind(this));
        this.#window.header.addEventListener("dblclick", this.#onWindowDoubleClick.bind(this));
        this.#window.resize?.addEventListener("pointerdown", this.#onWindowResizeStart.bind(this));
      }

      // Form handlers
      if ( this.options.tag === "form" ) {
        this.#element.addEventListener("submit", this._onSubmitForm.bind(this, this.options.form));
        this.#element.addEventListener("change", this._onChangeForm.bind(this, this.options.form));
      }
    }

    /* -------------------------------------------- */

    /**
     * Handle initial pointerdown events inside a rendered Application.
     * @param {PointerEvent} event
     */
    async #onPointerDown(event) {
      if ( this.hasFrame ) this.bringToFront();
    }

    /* -------------------------------------------- */

    /**
     * Centralized handling of click events which occur on or within the Application frame.
     * @param {PointerEvent} event
     */
    async #onClick(event) {
      const target = event.target;
      const actionButton = target.closest("[data-action]");
      if ( actionButton ) return this.#onClickAction(event, actionButton);
    }

    /* -------------------------------------------- */

    /**
     * Handle a click event on an element which defines a [data-action] handler.
     * @param {PointerEvent} event      The originating click event
     * @param {HTMLElement} target      The capturing HTML element which defined a [data-action]
     */
    #onClickAction(event, target) {
      const action = target.dataset.action;
      switch ( action ) {
        case "close":
          event.stopPropagation();
          if ( event.button === 0 ) this.close();
          break;
        case "tab":
          if ( event.button === 0 ) this.#onClickTab(event);
          break;
        case "toggleControls":
          event.stopPropagation();
          if ( event.button === 0 ) this.toggleControls();
          break;
        default:
          let handler = this.options.actions[action];

          // No defined handler
          if ( !handler ) {
            this._onClickAction(event, target);
            break;
          }

          // Defined handler
          let buttons = [0];
          if ( typeof handler === "object" ) {
            buttons = handler.buttons;
            handler = handler.handler;
          }
          if ( buttons.includes(event.button) ) handler?.call(this, event, target);
          break;
      }
    }

    /* -------------------------------------------- */

    /**
     * Handle click events on a tab within the Application.
     * @param {PointerEvent} event
     */
    #onClickTab(event) {
      const button = event.target;
      const tab = button.dataset.tab;
      if ( !tab || button.classList.contains("active") ) return;
      const group = button.dataset.group;
      const navElement = button.closest(".tabs");
      this.changeTab(tab, group, {event, navElement});
    }

    /* -------------------------------------------- */

    /**
     * A generic event handler for action clicks which can be extended by subclasses.
     * Action handlers defined in DEFAULT_OPTIONS are called first. This method is only called for actions which have
     * no defined handler.
     * @param {PointerEvent} event      The originating click event
     * @param {HTMLElement} target      The capturing HTML element which defined a [data-action]
     * @protected
     */
    _onClickAction(event, target) {}

    /* -------------------------------------------- */

    /**
     * Begin capturing pointer events on the application frame.
     * @param {PointerEvent} event  The triggering event.
     * @param {function} callback   The callback to attach to pointer move events.
     */
    #startPointerCapture(event, callback) {
      this.#window.pointerStartPosition = Object.assign(foundry.utils.deepClone(this.#position), {
        clientX: event.clientX, clientY: event.clientY
      });
      this.#element.addEventListener("pointermove", callback, { passive: true });
      this.#element.addEventListener("pointerup", event => this.#endPointerCapture(event, callback), {
        capture: true, once: true
      });
    }

    /* -------------------------------------------- */

    /**
     * End capturing pointer events on the application frame.
     * @param {PointerEvent} event  The triggering event.
     * @param {function} callback   The callback to remove from pointer move events.
     */
    #endPointerCapture(event, callback) {
      this.#element.releasePointerCapture(event.pointerId);
      this.#element.removeEventListener("pointermove", callback);
      delete this.#window.pointerStartPosition;
      this.#window.pointerMoveThrottle = false;
    }

    /* -------------------------------------------- */

    /**
     * Handle a pointer move event while dragging or resizing the window frame.
     * @param {PointerEvent} event
     * @returns {{dx: number, dy: number}|void}  The amount the cursor has moved since the last frame, or undefined if
     *                                           the movement occurred between frames.
     */
    #onPointerMove(event) {
      if ( this.#window.pointerMoveThrottle ) return;
      this.#window.pointerMoveThrottle = true;
      const dx = event.clientX - this.#window.pointerStartPosition.clientX;
      const dy = event.clientY - this.#window.pointerStartPosition.clientY;
      requestAnimationFrame(() => this.#window.pointerMoveThrottle = false);
      return { dx, dy };
    }

    /* -------------------------------------------- */

    /**
     * Begin dragging the Application position.
     * @param {PointerEvent} event
     */
    #onWindowDragStart(event) {
      if ( event.target.closest(".header-control") ) return;
      this.#endPointerCapture(event, this.#window.onDrag);
      this.#startPointerCapture(event, this.#window.onDrag);
    }

    /* -------------------------------------------- */

    /**
     * Begin resizing the Application.
     * @param {PointerEvent} event
     */
    #onWindowResizeStart(event) {
      this.#endPointerCapture(event, this.#window.onResize);
      this.#startPointerCapture(event, this.#window.onResize);
    }

    /* -------------------------------------------- */

    /**
     * Drag the Application position during mouse movement.
     * @param {PointerEvent} event
     */
    #onWindowDragMove(event) {
      if ( !this.#window.header.hasPointerCapture(event.pointerId) ) {
        this.#window.header.setPointerCapture(event.pointerId);
      }
      const delta = this.#onPointerMove(event);
      if ( !delta ) return;
      const { pointerStartPosition } = this.#window;
      let { top, left, height, width } = pointerStartPosition;
      left += delta.dx;
      top += delta.dy;
      this.setPosition({ top, left, height, width });
    }

    /* -------------------------------------------- */

    /**
     * Resize the Application during mouse movement.
     * @param {PointerEvent} event
     */
    #onWindowResizeMove(event) {
      if ( !this.#window.resize.hasPointerCapture(event.pointerId) ) {
        this.#window.resize.setPointerCapture(event.pointerId);
      }
      const delta = this.#onPointerMove(event);
      if ( !delta ) return;
      const { scale } = this.#position;
      const { pointerStartPosition } = this.#window;
      let { top, left, height, width } = pointerStartPosition;
      if ( width !== "auto" ) width += delta.dx / scale;
      if ( height !== "auto" ) height += delta.dy / scale;
      this.setPosition({ top, left, width, height });
    }

    /* -------------------------------------------- */

    /**
     * Double-click events on the window title are used to minimize or maximize the application.
     * @param {PointerEvent} event
     */
    #onWindowDoubleClick(event) {
      event.preventDefault();
      if ( event.target.dataset.action ) return; // Ignore double clicks on buttons which perform an action
      if ( !this.options.window.minimizable ) return;
      if ( this.minimized ) this.maximize();
      else this.minimize();
    }

    /* -------------------------------------------- */

    /**
     * Handle submission for an Application which uses the form element.
     * @param {ApplicationFormConfiguration} formConfig     The form configuration for which this handler is bound
     * @param {Event|SubmitEvent} event                     The form submission event
     * @returns {Promise<void>}
     * @protected
     */
    async _onSubmitForm(formConfig, event) {
      event.preventDefault();
      const form = event.currentTarget;
      const {handler, closeOnSubmit} = formConfig;
      const formData = new FormDataExtended(form);
      if ( handler instanceof Function ) {
        try {
          await handler.call(this, event, form, formData);
        } catch(err){
          ui.notifications.error(err, {console: true});
          return; // Do not close
        }
      }
      if ( closeOnSubmit ) await this.close();
    }

    /* -------------------------------------------- */

    /**
     * Handle changes to an input element within the form.
     * @param {ApplicationFormConfiguration} formConfig     The form configuration for which this handler is bound
     * @param {Event} event                                 An input change event within the form
     */
    _onChangeForm(formConfig, event) {
      if ( formConfig.submitOnChange ) this._onSubmitForm(formConfig, event);
    }

    /* -------------------------------------------- */
    /*  Helper Methods                              */
    /* -------------------------------------------- */

    /**
     * Parse a CSS style rule into a number of pixels which apply to that dimension.
     * @param {string} style            The CSS style rule
     * @param {number} parentDimension  The relevant dimension of the parent element
     * @returns {number}                The parsed style dimension in pixels
     */
    static parseCSSDimension(style, parentDimension) {
      if ( style.includes("px") ) return parseInt(style.replace("px", ""));
      if ( style.includes("%") ) {
        const p = parseInt(style.replace("%", "")) / 100;
        return parentDimension * p;
      }
    }

    /* -------------------------------------------- */

    /**
     * Wait for a CSS transition to complete for an element.
     * @param {HTMLElement} element         The element which is transitioning
     * @param {number} timeout              A timeout in milliseconds in case the transitionend event does not occur
     * @returns {Promise<void>}
     * @internal
     */
    async _awaitTransition(element, timeout) {
      return Promise.race([
        new Promise(resolve => element.addEventListener("transitionend", resolve, {once: true})),
        new Promise(resolve => window.setTimeout(resolve, timeout))
      ]);
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    bringToTop() {
      foundry.utils.logCompatibilityWarning(`ApplicationV2#bringToTop is not a valid function and redirects to 
      ApplicationV2#bringToFront. This shim will be removed in v14.`, {since: 12, until: 14});
      return this.bringToFront();
    }
  }

  /**
   * @typedef {import("../_types.mjs").ApplicationConfiguration} ApplicationConfiguration
   */

  /**
   * @typedef {Object} DialogV2Button
   * @property {string} action                      The button action identifier.
   * @property {string} label                       The button label. Will be localized.
   * @property {string} [icon]                      FontAwesome icon classes.
   * @property {string} [class]                     CSS classes to apply to the button.
   * @property {boolean} [default]                  Whether this button represents the default action to take if the user
   *                                                submits the form without pressing a button, i.e. with an Enter
   *                                                keypress.
   * @property {DialogV2ButtonCallback} [callback]  A function to invoke when the button is clicked. The value returned
   *                                                from this function will be used as the dialog's submitted value.
   *                                                Otherwise, the button's identifier is used.
   */

  /**
   * @callback DialogV2ButtonCallback
   * @param {PointerEvent|SubmitEvent} event        The button click event, or a form submission event if the dialog was
   *                                                submitted via keyboard.
   * @param {HTMLButtonElement} button              If the form was submitted via keyboard, this will be the default
   *                                                button, otherwise the button that was clicked.
   * @param {HTMLDialogElement} dialog              The dialog element.
   * @returns {Promise<any>}
   */

  /**
   * @typedef {Object} DialogV2Configuration
   * @property {boolean} [modal]                    Modal dialogs prevent interaction with the rest of the UI until they
   *                                                are dismissed or submitted.
   * @property {DialogV2Button[]} buttons           Button configuration.
   * @property {string} [content]                   The dialog content.
   * @property {DialogV2SubmitCallback} [submit]    A function to invoke when the dialog is submitted. This will not be
   *                                                called if the dialog is dismissed.
   */

  /**
   * @callback DialogV2RenderCallback
   * @param {Event} event                           The render event.
   * @param {HTMLDialogElement} dialog              The dialog element.
   */

  /**
   * @callback DialogV2CloseCallback
   * @param {Event} event                           The close event.
   * @param {DialogV2} dialog                       The dialog instance.
   */

  /**
   * @callback DialogV2SubmitCallback
   * @param {any} result                            Either the identifier of the button that was clicked to submit the
   *                                                dialog, or the result returned by that button's callback.
   * @returns {Promise<void>}
   */

  /**
   * @typedef {object} DialogV2WaitOptions
   * @property {DialogV2RenderCallback} [render]    A synchronous function to invoke whenever the dialog is rendered.
   * @property {DialogV2CloseCallback} [close]      A synchronous function to invoke when the dialog is closed under any
   *                                                circumstances.
   * @property {boolean} [rejectClose=true]         Throw a Promise rejection if the dialog is dismissed.
   */

  /**
   * A lightweight Application that renders a dialog containing a form with arbitrary content, and some buttons.
   * @extends {ApplicationV2<ApplicationConfiguration & DialogV2Configuration>}
   *
   * @example Prompt the user to confirm an action.
   * ```js
   * const proceed = await foundry.applications.api.DialogV2.confirm({
   *   content: "Are you sure?",
   *   rejectClose: false,
   *   modal: true
   * });
   * if ( proceed ) console.log("Proceed.");
   * else console.log("Do not proceed.");
   * ```
   *
   * @example Prompt the user for some input.
   * ```js
   * let guess;
   * try {
   *   guess = await foundry.applications.api.DialogV2.prompt({
   *     window: { title: "Guess a number between 1 and 10" },
   *     content: '<input name="guess" type="number" min="1" max="10" step="1" autofocus>',
   *     ok: {
   *       label: "Submit Guess",
   *       callback: (event, button, dialog) => button.form.elements.guess.valueAsNumber
   *     }
   *   });
   * } catch {
   *   console.log("User did not make a guess.");
   *   return;
   * }
   * const n = Math.ceil(CONFIG.Dice.randomUniform() * 10);
   * if ( n === guess ) console.log("User guessed correctly.");
   * else console.log("User guessed incorrectly.");
   * ```
   *
   * @example A custom dialog.
   * ```js
   * new foundry.applications.api.DialogV2({
   *   window: { title: "Choose an option" },
   *   content: `
   *     <label><input type="radio" name="choice" value="one" checked> Option 1</label>
   *     <label><input type="radio" name="choice" value="two"> Option 2</label>
   *     <label><input type="radio" name="choice" value="three"> Options 3</label>
   *   `,
   *   buttons: [{
   *     action: "choice",
   *     label: "Make Choice",
   *     default: true,
   *     callback: (event, button, dialog) => button.form.elements.choice.value
   *   }, {
   *     action: "all",
   *     label: "Take All"
   *   }],
   *   submit: result => {
   *     if ( result === "all" ) console.log("User picked all options.");
   *     else console.log(`User picked option: ${result}`);
   *   }
   * }).render({ force: true });
   * ```
   */
  class DialogV2 extends ApplicationV2 {

    /** @inheritDoc */
    static DEFAULT_OPTIONS = {
      id: "dialog-{id}",
      classes: ["dialog"],
      tag: "dialog",
      form: {
        closeOnSubmit: true
      },
      window: {
        frame: true,
        positioned: true,
        minimizable: false
      }
    };

    /* -------------------------------------------- */

    /** @inheritDoc */
    _initializeApplicationOptions(options) {
      options = super._initializeApplicationOptions(options);
      if ( !options.buttons?.length ) throw new Error("You must define at least one entry in options.buttons");
      options.buttons = options.buttons.reduce((obj, button) => {
        options.actions[button.action] = this.constructor._onClickButton;
        obj[button.action] = button;
        return obj;
      }, {});
      return options;
    }

    /* -------------------------------------------- */

    /** @override */
    async _renderHTML(_context, _options) {
      const form = document.createElement("form");
      form.className = "dialog-form standard-form";
      form.autocomplete = "off";
      form.innerHTML = `
      ${this.options.content ? `<div class="dialog-content standard-form">${this.options.content}</div>` : ""}
      <footer class="form-footer">${this._renderButtons()}</footer>
    `;
      form.addEventListener("submit", event => this._onSubmit(event.submitter, event));
      return form;
    }

    /* -------------------------------------------- */

    /**
     * Render configured buttons.
     * @returns {string}
     * @protected
     */
    _renderButtons() {
      return Object.values(this.options.buttons).map(button => {
        const { action, label, icon, default: isDefault, class: cls="" } = button;
        return `
        <button type="${isDefault ? "submit" : "button"}" data-action="${action}" class="${cls}"
                ${isDefault ? "autofocus" : ""}>
          ${icon ? `<i class="${icon}"></i>` : ""}
          <span>${game.i18n.localize(label)}</span>
        </button>
      `;
      }).join("");
    }

    /* -------------------------------------------- */

    /**
     * Handle submitting the dialog.
     * @param {HTMLButtonElement} target        The button that was clicked or the default button.
     * @param {PointerEvent|SubmitEvent} event  The triggering event.
     * @returns {Promise<DialogV2>}
     * @protected
     */
    async _onSubmit(target, event) {
      event.preventDefault();
      const button = this.options.buttons[target?.dataset.action];
      const result = (await button?.callback?.(event, target, this.element)) ?? button?.action;
      await this.options.submit?.(result);
      return this.options.form.closeOnSubmit ? this.close() : this;
    }

    /* -------------------------------------------- */

    /** @override */
    _onFirstRender(_context, _options) {
      if ( this.options.modal ) this.element.showModal();
      else this.element.show();
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _attachFrameListeners() {
      super._attachFrameListeners();
      this.element.addEventListener("keydown", this._onKeyDown.bind(this));
    }

    /* -------------------------------------------- */

    /** @override */
    _replaceHTML(result, content, _options) {
      content.replaceChildren(result);
    }

    /* -------------------------------------------- */

    /**
     * Handle keypresses within the dialog.
     * @param {KeyboardEvent} event  The triggering event.
     * @protected
     */
    _onKeyDown(event) {
      // Capture Escape keypresses for dialogs to ensure that close is called properly.
      if ( event.key === "Escape" ) {
        event.preventDefault(); // Prevent default browser dialog dismiss behavior.
        event.stopPropagation();
        this.close();
      }
    }

    /* -------------------------------------------- */

    /**
     * @this {DialogV2}
     * @param {PointerEvent} event        The originating click event.
     * @param {HTMLButtonElement} target  The button element that was clicked.
     * @protected
     */
    static _onClickButton(event, target) {
      this._onSubmit(target, event);
    }

    /* -------------------------------------------- */
    /*  Factory Methods                             */
    /* -------------------------------------------- */

    /**
     * A utility helper to generate a dialog with yes and no buttons.
     * @param {Partial<ApplicationConfiguration & DialogV2Configuration & DialogV2WaitOptions>} [options]
     * @param {DialogV2Button} [options.yes]  Options to overwrite the default yes button configuration.
     * @param {DialogV2Button} [options.no]   Options to overwrite the default no button configuration.
     * @returns {Promise<any>}                Resolves to true if the yes button was pressed, or false if the no button
     *                                        was pressed. If additional buttons were provided, the Promise resolves to
     *                                        the identifier of the one that was pressed, or the value returned by its
     *                                        callback. If the dialog was dismissed, and rejectClose is false, the
     *                                        Promise resolves to null.
     */
    static async confirm({ yes={}, no={}, ...options }={}) {
      options.buttons ??= [];
      options.buttons.unshift(mergeObject({
        action: "yes", label: "Yes", icon: "fas fa-check", callback: () => true
      }, yes), mergeObject({
        action: "no", label: "No", icon: "fas fa-xmark", default: true, callback: () => false
      }, no));
      return this.wait(options);
    }

    /* -------------------------------------------- */

    /**
     * A utility helper to generate a dialog with a single confirmation button.
     * @param {Partial<ApplicationConfiguration & DialogV2Configuration & DialogV2WaitOptions>} [options]
     * @param {Partial<DialogV2Button>} [options.ok]  Options to overwrite the default confirmation button configuration.
     * @returns {Promise<any>}                        Resolves to the identifier of the button used to submit the dialog,
     *                                                or the value returned by that button's callback. If the dialog was
     *                                                dismissed, and rejectClose is false, the Promise resolves to null.
     */
    static async prompt({ ok={}, ...options }={}) {
      options.buttons ??= [];
      options.buttons.unshift(mergeObject({
        action: "ok", label: "Confirm", icon: "fas fa-check", default: true
      }, ok));
      return this.wait(options);
    }

    /* -------------------------------------------- */

    /**
     * Spawn a dialog and wait for it to be dismissed or submitted.
     * @param {Partial<ApplicationConfiguration & DialogV2Configuration>} [options]
     * @param {DialogV2RenderCallback} [options.render]  A function to invoke whenever the dialog is rendered.
     * @param {DialogV2CloseCallback} [options.close]    A function to invoke when the dialog is closed under any
     *                                                   circumstances.
     * @param {boolean} [options.rejectClose=true]       Throw a Promise rejection if the dialog is dismissed.
     * @returns {Promise<any>}                           Resolves to the identifier of the button used to submit the
     *                                                   dialog, or the value returned by that button's callback. If the
     *                                                   dialog was dismissed, and rejectClose is false, the Promise
     *                                                   resolves to null.
     */
    static async wait({ rejectClose=true, close, render, ...options }={}) {
      return new Promise((resolve, reject) => {
        // Wrap submission handler with Promise resolution.
        const originalSubmit = options.submit;
        options.submit = async result => {
          await originalSubmit?.(result);
          resolve(result);
        };

        const dialog = new this(options);
        dialog.addEventListener("close", event => {
          if ( close instanceof Function ) close(event, dialog);
          if ( rejectClose ) reject(new Error("Dialog was dismissed without pressing a button."));
          else resolve(null);
        }, { once: true });
        if ( render instanceof Function ) {
          dialog.addEventListener("render", event => render(event, dialog.element));
        }
        dialog.render({ force: true });
      });
    }
  }

  /**
   * @typedef {import("../_types.mjs").ApplicationConfiguration} ApplicationConfiguration
   * @typedef {import("../_types.mjs").ApplicationRenderOptions} ApplicationRenderOptions
   */

  /**
   * @typedef {Object} DocumentSheetConfiguration
   * @property {Document} document          The Document instance associated with this sheet
   * @property {number} viewPermission      A permission level in CONST.DOCUMENT_OWNERSHIP_LEVELS
   * @property {number} editPermission      A permission level in CONST.DOCUMENT_OWNERSHIP_LEVELS
   * @property {boolean} sheetConfig        Allow sheet configuration as a header button
   */

  /**
   * @typedef {Object} DocumentSheetRenderOptions
   * @property {string} renderContext       A string with the format "{operation}{documentName}" providing context
   * @property {object} renderData          Data describing the document modification that occurred
   */

  /**
   * The Application class is responsible for rendering an HTMLElement into the Foundry Virtual Tabletop user interface.
   * @extends {ApplicationV2<
   *  ApplicationConfiguration & DocumentSheetConfiguration,
   *  ApplicationRenderOptions & DocumentSheetRenderOptions
   * >}
   * @alias DocumentSheetV2
   */
  class DocumentSheetV2 extends ApplicationV2 {
    constructor(options={}) {
      super(options);
      this.#document = options.document;
    }

    /** @inheritDoc */
    static DEFAULT_OPTIONS = {
      id: "{id}",
      classes: ["sheet"],
      tag: "form",  // Document sheets are forms by default
      document: null,
      viewPermission: DOCUMENT_OWNERSHIP_LEVELS.LIMITED,
      editPermission: DOCUMENT_OWNERSHIP_LEVELS.OWNER,
      sheetConfig: true,
      actions: {
        configureSheet: DocumentSheetV2.#onConfigureSheet,
        copyUuid: {handler: DocumentSheetV2.#onCopyUuid, buttons: [0, 2]}
      },
      form: {
        handler: this.#onSubmitDocumentForm,
        submitOnChange: false,
        closeOnSubmit: false
      }
    };

    /* -------------------------------------------- */

    /**
     * The Document instance associated with the application
     * @type {ClientDocument}
     */
    get document() {
      return this.#document;
    }

    #document;

    /* -------------------------------------------- */

    /** @override */
    get title() {
      const {constructor: cls, id, name, type} = this.document;
      const prefix = cls.hasTypeData ? CONFIG[cls.documentName].typeLabels[type] : cls.metadata.label;
      return `${game.i18n.localize(prefix)}: ${name ?? id}`;
    }

    /* -------------------------------------------- */

    /**
     * Is this Document sheet visible to the current User?
     * This is governed by the viewPermission threshold configured for the class.
     * @type {boolean}
     */
    get isVisible() {
      return this.document.testUserPermission(game.user, this.options.viewPermission);
    }

    /* -------------------------------------------- */

    /**
     * Is this Document sheet editable by the current User?
     * This is governed by the editPermission threshold configured for the class.
     * @type {boolean}
     */
    get isEditable() {
      if ( this.document.pack ) {
        const pack = game.packs.get(this.document.pack);
        if ( pack.locked ) return false;
      }
      return this.document.testUserPermission(game.user, this.options.editPermission);
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _initializeApplicationOptions(options) {
      options = super._initializeApplicationOptions(options);
      options.uniqueId = `${this.constructor.name}-${options.document.uuid}`;
      return options;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    *_headerControlButtons() {
      for ( const control of this._getHeaderControls() ) {
        if ( control.visible === false ) continue;
        if ( ("ownership" in control) && !this.document.testUserPermission(game.user, control.ownership) ) continue;
        yield control;
      }
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    async _renderFrame(options) {
      const frame = await super._renderFrame(options);

      // Add form options
      if ( this.options.tag === "form" ) frame.autocomplete = "off";

      // Add document ID copy
      const copyLabel = game.i18n.localize("SHEETS.CopyUuid");
      const copyId = `<button type="button" class="header-control fa-solid fa-passport" data-action="copyUuid"
                            data-tooltip="${copyLabel}" aria-label="${copyLabel}"></button>`;
      this.window.close.insertAdjacentHTML("beforebegin", copyId);

      // Add sheet configuration button
      if ( this.options.sheetConfig && this.isEditable && !this.document.getFlag("core", "sheetLock") ) {
        const label = game.i18n.localize("SHEETS.ConfigureSheet");
        const sheetConfig = `<button type="button" class="header-control fa-solid fa-cog" data-action="configureSheet"
                                   data-tooltip="${label}" aria-label="${label}"></button>`;
        this.window.close.insertAdjacentHTML("beforebegin", sheetConfig);
      }
      return frame;
    }

    /* -------------------------------------------- */
    /*  Application Life-Cycle Events               */
    /* -------------------------------------------- */

    /** @override */
    _canRender(_options) {
      if ( !this.isVisible ) throw new Error(game.i18n.format("SHEETS.DocumentSheetPrivate", {
        type: game.i18n.localize(this.document.constructor.metadata.label)
      }));
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _onFirstRender(context, options) {
      super._onFirstRender(context, options);
      this.document.apps[this.id] = this;
    }

    /* -------------------------------------------- */

    /** @override */
    _onClose(_options) {
      delete this.document.apps[this.id];
    }

    /* -------------------------------------------- */
    /*  Event Listeners and Handlers                */
    /* -------------------------------------------- */

    /**
     * Handle click events to configure the sheet used for this document.
     * @param {PointerEvent} event
     * @this {DocumentSheetV2}
     */
    static #onConfigureSheet(event) {
      event.stopPropagation(); // Don't trigger other events
      if ( event.detail > 1 ) return; // Ignore repeated clicks
      new DocumentSheetConfig(this.document, {
        top: this.position.top + 40,
        left: this.position.left + ((this.position.width - DocumentSheet.defaultOptions.width) / 2)
      }).render(true);
    }

    /* -------------------------------------------- */

    /**
     * Handle click events to copy the UUID of this document to clipboard.
     * @param {PointerEvent} event
     * @this {DocumentSheetV2}
     */
    static #onCopyUuid(event) {
      event.preventDefault(); // Don't open context menu
      event.stopPropagation(); // Don't trigger other events
      if ( event.detail > 1 ) return; // Ignore repeated clicks
      const id = event.button === 2 ? this.document.id : this.document.uuid;
      const type = event.button === 2 ? "id" : "uuid";
      const label = game.i18n.localize(this.document.constructor.metadata.label);
      game.clipboard.copyPlainText(id);
      ui.notifications.info(game.i18n.format("DOCUMENT.IdCopiedClipboard", {label, type, id}));
    }

    /* -------------------------------------------- */
    /*  Form Submission                             */
    /* -------------------------------------------- */

    /**
     * Process form submission for the sheet
     * @this {DocumentSheetV2}                      The handler is called with the application as its bound scope
     * @param {SubmitEvent} event                   The originating form submission event
     * @param {HTMLFormElement} form                The form element that was submitted
     * @param {FormDataExtended} formData           Processed data for the submitted form
     * @returns {Promise<void>}
     */
    static async #onSubmitDocumentForm(event, form, formData) {
      const submitData = this._prepareSubmitData(event, form, formData);
      await this._processSubmitData(event, form, submitData);
    }

    /* -------------------------------------------- */

    /**
     * Prepare data used to update the Item upon form submission.
     * This data is cleaned and validated before being returned for further processing.
     * @param {SubmitEvent} event                   The originating form submission event
     * @param {HTMLFormElement} form                The form element that was submitted
     * @param {FormDataExtended} formData           Processed data for the submitted form
     * @returns {object}                            Prepared submission data as an object
     * @throws {Error}                              Subclasses may throw validation errors here to prevent form submission
     * @protected
     */
    _prepareSubmitData(event, form, formData) {
      const submitData = this._processFormData(event, form, formData);
      const addType = this.document.constructor.hasTypeData && !("type" in submitData);
      if ( addType ) submitData.type = this.document.type;
      this.document.validate({changes: submitData, clean: true, fallback: false});
      if ( addType ) delete submitData.type;
      return submitData;
    }

    /* -------------------------------------------- */

    /**
     * Customize how form data is extracted into an expanded object.
     * @param {SubmitEvent} event                   The originating form submission event
     * @param {HTMLFormElement} form                The form element that was submitted
     * @param {FormDataExtended} formData           Processed data for the submitted form
     * @returns {object}                            An expanded object of processed form data
     * @throws {Error}                              Subclasses may throw validation errors here to prevent form submission
     */
    _processFormData(event, form, formData) {
      return foundry.utils.expandObject(formData.object);
    }

    /* -------------------------------------------- */

    /**
     * Submit a document update based on the processed form data.
     * @param {SubmitEvent} event                   The originating form submission event
     * @param {HTMLFormElement} form                The form element that was submitted
     * @param {object} submitData                   Processed and validated form data to be used for a document update
     * @returns {Promise<void>}
     * @protected
     */
    async _processSubmitData(event, form, submitData) {
      await this.document.update(submitData);
    }

    /* -------------------------------------------- */

    /**
     * Programmatically submit a DocumentSheetV2 instance, providing additional data to be merged with form data.
     * @param {object} options
     * @param {object} [options.updateData]           Additional data merged with processed form data
     * @returns {Promise<void>}
     */
    async submit({updateData}={}) {
      const formConfig = this.options.form;
      if ( !formConfig?.handler ) throw new Error(`The ${this.constructor.name} DocumentSheetV2 does not support a`
        + ` single top-level form element.`);
      const form = this.element;
      const event = new Event("submit");
      const formData = new FormDataExtended(form);
      const submitData = this._prepareSubmitData(event, form, formData);
      foundry.utils.mergeObject(submitData, updateData, {inplace: true});
      await this._processSubmitData(event, form, submitData);
    }
  }

  /**
   * @typedef {import("../types.mjs").Constructor} Constructor
   * @typedef {import("../_types.mjs").ApplicationConfiguration} ApplicationConfiguration
   * @typedef {import("./types.mjs").ApplicationFormSubmission} ApplicationFormSubmission
   */

  /**
   * @typedef {Object} HandlebarsRenderOptions
   * @property {string[]} parts                       An array of named template parts to render
   */

  /**
   * @typedef {Object} HandlebarsTemplatePart
   * @property {string} template                      The template entry-point for the part
   * @property {string} [id]                          A CSS id to assign to the top-level element of the rendered part.
   *                                                  This id string is automatically prefixed by the application id.
   * @property {string[]} [classes]                   An array of CSS classes to apply to the top-level element of the
   *                                                  rendered part.
   * @property {string[]} [templates]                 An array of templates that are required to render the part.
   *                                                  If omitted, only the entry-point is inferred as required.
   * @property {string[]} [scrollable]                An array of selectors within this part whose scroll positions should
   *                                                  be persisted during a re-render operation. A blank string is used
   *                                                  to denote that the root level of the part is scrollable.
   * @property {Record<string, ApplicationFormConfiguration>} [forms]  A registry of forms selectors and submission handlers.
   */

  /**
   * Augment an Application class with [Handlebars](https://handlebarsjs.com) template rendering behavior.
   * @param {Constructor} BaseApplication
   */
  function HandlebarsApplicationMixin(BaseApplication) {
    /**
     * The mixed application class augmented with [Handlebars](https://handlebarsjs.com) template rendering behavior.
     * @extends {ApplicationV2<ApplicationConfiguration, HandlebarsRenderOptions>}
     */
    class HandlebarsApplication extends BaseApplication {

      /**
       * Configure a registry of template parts which are supported for this application for partial rendering.
       * @type {Record<string, HandlebarsTemplatePart>}
       */
      static PARTS = {}

      /**
       * A record of all rendered template parts.
       * @returns {Record<string, HTMLElement>}
       */
      get parts() {
        return this.#parts;
      }
      #parts = {};

      /* -------------------------------------------- */

      /** @inheritDoc */
      _configureRenderOptions(options) {
        super._configureRenderOptions(options);
        options.parts ??= Object.keys(this.constructor.PARTS);
      }

      /* -------------------------------------------- */

      /** @inheritDoc */
      async _preFirstRender(context, options) {
        await super._preFirstRender(context, options);
        const allTemplates = new Set();
        for ( const part of Object.values(this.constructor.PARTS) ) {
          const partTemplates = part.templates ?? [part.template];
          for ( const template of partTemplates ) allTemplates.add(template);
        }
        await loadTemplates(Array.from(allTemplates));
      }

      /* -------------------------------------------- */

      /**
       * Render each configured application part using Handlebars templates.
       * @param {ApplicationRenderContext} context        Context data for the render operation
       * @param {HandlebarsRenderOptions} options         Options which configure application rendering behavior
       * @returns {Promise<Record<string, HTMLElement>>}  A single rendered HTMLElement for each requested part
       * @protected
       * @override
       */
      async _renderHTML(context, options) {
        const rendered = {};
        for ( const partId of options.parts ) {
          const part = this.constructor.PARTS[partId];
          if ( !part ) {
            ui.notifications.warn(`Part "${partId}" is not a supported template part for ${this.constructor.name}`);
            continue;
          }
          const partContext = await this._preparePartContext(partId, context, options);
          try {
            const htmlString = await renderTemplate(part.template, partContext);
            rendered[partId] = this.#parsePartHTML(partId, part, htmlString);
          } catch(err) {
            throw new Error(`Failed to render template part "${partId}":\n${err.message}`, {cause: err});
          }
        }
        return rendered;
      }

      /* -------------------------------------------- */

      /**
       * Prepare context that is specific to only a single rendered part.
       *
       * It is recommended to augment or mutate the shared context so that downstream methods like _onRender have
       * visibility into the data that was used for rendering. It is acceptable to return a different context object
       * rather than mutating the shared context at the expense of this transparency.
       *
       * @param {string} partId                         The part being rendered
       * @param {ApplicationRenderContext} context      Shared context provided by _prepareContext
       * @param {HandlebarsRenderOptions} options       Options which configure application rendering behavior
       * @returns {Promise<ApplicationRenderContext>}   Context data for a specific part
       * @protected
       */
      async _preparePartContext(partId, context, options) {
        context.partId = `${this.id}-${partId}`;
        return context;
      }

      /* -------------------------------------------- */

      /**
       * Parse the returned HTML string from template rendering into a uniquely identified HTMLElement for insertion.
       * @param {string} partId                   The id of the part being rendered
       * @param {HandlebarsTemplatePart} part     Configuration of the part being parsed
       * @param {string} htmlString               The string rendered for the part
       * @returns {HTMLElement}                   The parsed HTMLElement for the part
       */
      #parsePartHTML(partId, part, htmlString) {
        const t = document.createElement("template");
        t.innerHTML = htmlString;
        if ( (t.content.children.length !== 1) ) {
          throw new Error(`Template part "${partId}" must render a single HTML element.`);
        }
        const e = t.content.firstElementChild;
        e.dataset.applicationPart = partId;
        if ( part.id ) e.setAttribute("id", `${this.id}-${part.id}`);
        if ( part.classes ) e.classList.add(...part.classes);
        return e;
      }

      /* -------------------------------------------- */

      /**
       * Replace the HTML of the application with the result provided by Handlebars rendering.
       * @param {Record<string, HTMLElement>} result  The result from Handlebars template rendering
       * @param {HTMLElement} content                 The content element into which the rendered result must be inserted
       * @param {HandlebarsRenderOptions} options     Options which configure application rendering behavior
       * @protected
       * @override
       */
      _replaceHTML(result, content, options) {
        for ( const [partId, htmlElement] of Object.entries(result) ) {
          const priorElement = content.querySelector(`[data-application-part="${partId}"]`);
          const state = {};
          if ( priorElement ) {
            this._preSyncPartState(partId, htmlElement, priorElement, state);
            priorElement.replaceWith(htmlElement);
            this._syncPartState(partId, htmlElement, priorElement, state);
          }
          else content.appendChild(htmlElement);
          this._attachPartListeners(partId, htmlElement, options);
          this.#parts[partId] = htmlElement;
        }
      }

      /* -------------------------------------------- */

      /**
       * Prepare data used to synchronize the state of a template part.
       * @param {string} partId                       The id of the part being rendered
       * @param {HTMLElement} newElement              The new rendered HTML element for the part
       * @param {HTMLElement} priorElement            The prior rendered HTML element for the part
       * @param {object} state                        A state object which is used to synchronize after replacement
       * @protected
       */
      _preSyncPartState(partId, newElement, priorElement, state) {
        const part = this.constructor.PARTS[partId];

        // Focused element or field
        const focus = priorElement.querySelector(":focus");
        if ( focus?.id ) state.focus = `#${focus.id}`;
        else if ( focus?.name ) state.focus = `${focus.tagName}[name="${focus.name}"]`;
        else state.focus = undefined;

        // Scroll positions
        state.scrollPositions = [];
        for ( const selector of (part.scrollable || []) ) {
          const el0 = selector === "" ? priorElement : priorElement.querySelector(selector);
          if ( el0 ) {
            const el1 = selector === "" ? newElement : newElement.querySelector(selector);
            if ( el1 ) state.scrollPositions.push([el1, el0.scrollTop, el0.scrollLeft]);
          }
        }
      }

      /* -------------------------------------------- */

      /**
       * Synchronize the state of a template part after it has been rendered and replaced in the DOM.
       * @param {string} partId                       The id of the part being rendered
       * @param {HTMLElement} newElement              The new rendered HTML element for the part
       * @param {HTMLElement} priorElement            The prior rendered HTML element for the part
       * @param {object} state                        A state object which is used to synchronize after replacement
       * @protected
       */
      _syncPartState(partId, newElement, priorElement, state) {
        if ( state.focus ) {
          const newFocus = newElement.querySelector(state.focus);
          if ( newFocus ) newFocus.focus();
        }
        for ( const [el, scrollTop, scrollLeft] of state.scrollPositions ) Object.assign(el, {scrollTop, scrollLeft});
      }

      /* -------------------------------------------- */
      /*  Event Listeners and Handlers                */
      /* -------------------------------------------- */

      /**
       * Attach event listeners to rendered template parts.
       * @param {string} partId                       The id of the part being rendered
       * @param {HTMLElement} htmlElement             The rendered HTML element for the part
       * @param {ApplicationRenderOptions} options    Rendering options passed to the render method
       * @protected
       */
      _attachPartListeners(partId, htmlElement, options) {
        const part = this.constructor.PARTS[partId];

        // Attach form submission handlers
        if ( part.forms ) {
          for ( const [selector, formConfig] of Object.entries(part.forms) ) {
            const form = htmlElement.matches(selector) ? htmlElement : htmlElement.querySelector(selector);
            form.addEventListener("submit", this._onSubmitForm.bind(this, formConfig));
            form.addEventListener("change", this._onChangeForm.bind(this, formConfig));
          }
        }
      }
    }
    return HandlebarsApplication;
  }

  var _module$b = /*#__PURE__*/Object.freeze({
    __proto__: null,
    ApplicationV2: ApplicationV2,
    DialogV2: DialogV2,
    DocumentSheetV2: DocumentSheetV2,
    HandlebarsApplicationMixin: HandlebarsApplicationMixin
  });

  /**
   * @typedef {object} DiceTermFulfillmentDescriptor
   * @property {string} id        A unique identifier for the term.
   * @property {DiceTerm} term    The term.
   * @property {string} method    The fulfillment method.
   * @property {boolean} [isNew]  Was the term newly-added to this resolver?
   */

  /**
   * An application responsible for handling unfulfilled dice terms in a roll.
   * @extends {ApplicationV2<ApplicationConfiguration, ApplicationRenderOptions>}
   * @mixes HandlebarsApplication
   * @alias RollResolver
   */
  class RollResolver extends HandlebarsApplicationMixin(ApplicationV2) {
    constructor(roll, options={}) {
      super(options);
      this.#roll = roll;
    }

    /** @inheritDoc */
    static DEFAULT_OPTIONS = {
      id: "roll-resolver-{id}",
      tag: "form",
      classes: ["roll-resolver"],
      window: {
        title: "DICE.RollResolution",
      },
      position: {
        width: 500,
        height: "auto"
      },
      form: {
        submitOnChange: false,
        closeOnSubmit: false,
        handler: this._fulfillRoll
      }
    };

    /** @override */
    static PARTS = {
      form: {
        id: "form",
        template: "templates/dice/roll-resolver.hbs"
      }
    };

    /**
     * A collection of fulfillable dice terms.
     * @type {Map<string, DiceTermFulfillmentDescriptor>}
     */
    get fulfillable() {
      return this.#fulfillable;
    }

    #fulfillable = new Map();

    /**
     * A function to call when the first pass of fulfillment is complete.
     * @type {function}
     */
    #resolve;

    /**
     * The roll being resolved.
     * @type {Roll}
     */
    get roll() {
      return this.#roll;
    }

    #roll;

    /* -------------------------------------------- */

    /**
     * Identify any terms in this Roll that should be fulfilled externally, and prompt the user to do so.
     * @returns {Promise<void>}  Returns a Promise that resolves when the first pass of fulfillment is complete.
     */
    async awaitFulfillment() {
      const fulfillable = await this.#identifyFulfillableTerms(this.roll.terms);
      if ( !fulfillable.length ) return;
      Roll.defaultImplementation.RESOLVERS.set(this.roll, this);
      this.render(true);
      return new Promise(resolve => this.#resolve = resolve);
    }

    /* -------------------------------------------- */

    /**
     * Register a fulfilled die roll.
     * @param {string} method        The method used for fulfillment.
     * @param {string} denomination  The denomination of the fulfilled die.
     * @param {number} result        The rolled number.
     * @returns {boolean}            Whether the result was consumed.
     */
    registerResult(method, denomination, result) {
      const query = `label[data-denomination="${denomination}"][data-method="${method}"] > input:not(:disabled)`;
      const term = Array.from(this.element.querySelectorAll(query)).find(input => input.value === "");
      if ( !term ) {
        ui.notifications.warn(`${denomination} roll was not needed by the resolver.`);
        return false;
      }
      term.value = `${result}`;
      const submitTerm = term.closest(".form-fields")?.querySelector("button");
      if ( submitTerm ) submitTerm.dispatchEvent(new MouseEvent("click"));
      else this._checkDone();
      return true;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    async close(options={}) {
      if ( this.rendered ) await this.constructor._fulfillRoll.call(this, null, null, new FormDataExtended(this.element));
      Roll.defaultImplementation.RESOLVERS.delete(this.roll);
      this.#resolve?.();
      return super.close(options);
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    async _prepareContext(_options) {
      const context = {
        formula: this.roll.formula,
        groups: {}
      };
      for ( const fulfillable of this.fulfillable.values() ) {
        const { id, term, method, isNew } = fulfillable;
        fulfillable.isNew = false;
        const config = CONFIG.Dice.fulfillment.methods[method];
        const group = context.groups[id] = {
          results: [],
          label: term.expression,
          icon: config.icon ?? '<i class="fas fa-bluetooth"></i>',
          tooltip: game.i18n.localize(config.label)
        };
        const { denomination, faces } = term;
        const icon = CONFIG.Dice.fulfillment.dice[denomination]?.icon;
        for ( let i = 0; i < Math.max(term.number ?? 1, term.results.length); i++ ) {
          const result = term.results[i];
          const { result: value, exploded, rerolled } = result ?? {};
          group.results.push({
            denomination, faces, id, method, icon, exploded, rerolled, isNew,
            value: value ?? "",
            readonly: method !== "manual",
            disabled: !!result
          });
        }
      }
      return context;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    async _onSubmitForm(formConfig, event) {
      this._toggleSubmission(false);
      this.element.querySelectorAll("input").forEach(input => {
        if ( !isNaN(input.valueAsNumber) ) return;
        const { term } = this.fulfillable.get(input.name);
        input.value = `${term.randomFace()}`;
      });
      await super._onSubmitForm(formConfig, event);
      this.element?.querySelectorAll("input").forEach(input => input.disabled = true);
      this.#resolve();
    }

    /* -------------------------------------------- */

    /**
     * Handle prompting for a single extra result from a term.
     * @param {DiceTerm} term  The term.
     * @param {string} method  The method used to obtain the result.
     * @param {object} [options]
     * @returns {Promise<number|void>}
     */
    async resolveResult(term, method, { reroll=false, explode=false }={}) {
      const group = this.element.querySelector(`fieldset[data-term-id="${term._id}"]`);
      if ( !group ) {
        console.warn("Attempted to resolve a single result for an unregistered DiceTerm.");
        return;
      }
      const fields = document.createElement("div");
      fields.classList.add("form-fields");
      fields.innerHTML = `
      <label class="icon die-input new-addition" data-denomination="${term.denomination}" data-method="${method}">
        <input type="number" min="1" max="${term.faces}" step="1" name="${term._id}"
               ${method === "manual" ? "" : "readonly"} placeholder="${game.i18n.localize(term.denomination)}">
        ${reroll ? '<i class="fas fa-arrow-rotate-right"></i>' : ""}
        ${explode ? '<i class="fas fa-burst"></i>' : ""}
        ${CONFIG.Dice.fulfillment.dice[term.denomination]?.icon ?? ""}
      </label>
      <button type="button" class="submit-result" data-tooltip="DICE.SubmitRoll"
              aria-label="${game.i18n.localize("DICE.SubmitRoll")}">
        <i class="fas fa-arrow-right"></i>
      </button>
    `;
      group.appendChild(fields);
      this.setPosition({ height: "auto" });
      return new Promise(resolve => {
        const button = fields.querySelector("button");
        const input = fields.querySelector("input");
        button.addEventListener("click", () => {
          if ( !input.validity.valid ) {
            input.form.reportValidity();
            return;
          }
          let value = input.valueAsNumber;
          if ( !value ) value = term.randomFace();
          input.value = `${value}`;
          input.disabled = true;
          button.remove();
          resolve(value);
        });
      });
    }

    /* -------------------------------------------- */

    /**
     * Update the Roll instance with the fulfilled results.
     * @this {RollResolver}
     * @param {SubmitEvent} event          The originating form submission event.
     * @param {HTMLFormElement} form       The form element that was submitted.
     * @param {FormDataExtended} formData  Processed data for the submitted form.
     * @returns {Promise<void>}
     * @protected
     */
    static async _fulfillRoll(event, form, formData) {
      // Update the DiceTerms with the fulfilled values.
      for ( let [id, results] of Object.entries(formData.object) ) {
        const { term } = this.fulfillable.get(id);
        if ( !Array.isArray(results) ) results = [results];
        for ( const result of results ) {
          const roll = { result: undefined, active: true };
          // A null value indicates the user wishes to skip external fulfillment and fall back to the digital roll.
          if ( result === null ) roll.result = term.randomFace();
          else roll.result = result;
          term.results.push(roll);
        }
      }
    }

    /* -------------------------------------------- */

    /**
     * Identify any of the given terms which should be fulfilled externally.
     * @param {RollTerm[]} terms               The terms.
     * @param {object} [options]
     * @param {boolean} [options.isNew=false]  Whether this term is a new addition to the already-rendered RollResolver.
     * @returns {Promise<DiceTerm[]>}
     */
    async #identifyFulfillableTerms(terms, { isNew=false }={}) {
      const config = game.settings.get("core", "diceConfiguration");
      const fulfillable = Roll.defaultImplementation.identifyFulfillableTerms(terms);
      fulfillable.forEach(term => {
        if ( term._id ) return;
        const method = config[term.denomination] || CONFIG.Dice.fulfillment.defaultMethod;
        const id = foundry.utils.randomID();
        term._id = id;
        term.method = method;
        this.fulfillable.set(id, { id, term, method, isNew });
      });
      return fulfillable;
    }

    /* -------------------------------------------- */

    /**
     * Add a new term to the resolver.
     * @param {DiceTerm} term    The term.
     * @returns {Promise<void>}  Returns a Promise that resolves when the term's results have been externally fulfilled.
     */
    async addTerm(term) {
      if ( !(term instanceof foundry.dice.terms.DiceTerm) ) {
        throw new Error("Only DiceTerm instances may be added to the RollResolver.");
      }
      const fulfillable = await this.#identifyFulfillableTerms([term], { isNew: true });
      if ( !fulfillable.length ) return;
      this.render({ force: true, position: { height: "auto" } });
      return new Promise(resolve => this.#resolve = resolve);
    }

    /* -------------------------------------------- */

    /**
     * Check if all rolls have been fulfilled.
     * @protected
     */
    _checkDone() {
      // If the form has already in the submission state, we don't need to re-submit.
      const submitter = this.element.querySelector('button[type="submit"]');
      if ( submitter.disabled ) return;

      // If there are any manual inputs, or if there are any empty inputs, then fulfillment is not done.
      if ( this.element.querySelector("input:not([readonly], :disabled)") ) return;
      for ( const input of this.element.querySelectorAll("input[readonly]:not(:disabled)") ) {
        if ( input.value === "" ) return;
      }
      this.element.requestSubmit(submitter);
    }

    /* -------------------------------------------- */

    /**
     * Toggle the state of the submit button.
     * @param {boolean} enabled  Whether the button is enabled.
     * @protected
     */
    _toggleSubmission(enabled) {
      const submit = this.element.querySelector('button[type="submit"]');
      const icon = submit.querySelector("i");
      icon.className = `fas ${enabled ? "fa-check" : "fa-spinner fa-pulse"}`;
      submit.disabled = !enabled;
    }
  }

  var _module$a = /*#__PURE__*/Object.freeze({
    __proto__: null,
    RollResolver: RollResolver
  });

  /**
   * An abstract custom HTMLElement designed for use with form inputs.
   * @abstract
   * @template {any} FormInputValueType
   *
   * @fires {Event} input           An "input" event when the value of the input changes
   * @fires {Event} change          A "change" event when the value of the element changes
   */
  class AbstractFormInputElement extends HTMLElement {
    constructor() {
      super();
      this._internals = this.attachInternals();
    }

    /**
     * The HTML tag name used by this element.
     * @type {string}
     */
    static tagName;

    /**
     * Declare that this custom element provides form element functionality.
     * @type {boolean}
     */
    static formAssociated = true;

    /**
     * Attached ElementInternals which provides form handling functionality.
     * @type {ElementInternals}
     * @protected
     */
    _internals;

    /**
     * The primary input (if any). Used to determine what element should receive focus when an associated label is clicked
     * on.
     * @type {HTMLElement}
     * @protected
     */
    _primaryInput;

    /**
     * The form this element belongs to.
     * @type {HTMLFormElement}
     */
    get form() {
      return this._internals.form;
    }

    /* -------------------------------------------- */
    /*  Element Properties                          */
    /* -------------------------------------------- */

    /**
     * The input element name.
     * @type {string}
     */
    get name() {
      return this.getAttribute("name");
    }

    set name(value) {
      this.setAttribute("name", value);
    }

    /* -------------------------------------------- */

    /**
     * The value of the input element.
     * @type {FormInputValueType}
     */
    get value() {
      return this._getValue();
    }

    set value(value) {
      this._setValue(value);
      this.dispatchEvent(new Event("input", {bubbles: true, cancelable: true}));
      this.dispatchEvent(new Event("change", {bubbles: true, cancelable: true}));
      this._refresh();
    }

    /**
     * The underlying value of the element.
     * @type {FormInputValueType}
     * @protected
     */
    _value;

    /* -------------------------------------------- */

    /**
     * Return the value of the input element which should be submitted to the form.
     * @returns {FormInputValueType}
     * @protected
     */
    _getValue() {
      return this._value;
    }

    /* -------------------------------------------- */

    /**
     * Translate user-provided input value into the format that should be stored.
     * @param {FormInputValueType} value  A new value to assign to the element
     * @throws {Error}        An error if the provided value is invalid
     * @protected
     */
    _setValue(value) {
      this._value = value;
    }

    /* -------------------------------------------- */

    /**
     * Is this element disabled?
     * @type {boolean}
     */
    get disabled() {
      return this.hasAttribute("disabled");
    }

    set disabled(value) {
      this.toggleAttribute("disabled", value);
      this._toggleDisabled(!this.editable);
    }

    /* -------------------------------------------- */

    /**
     * Is this field editable? The field can be neither disabled nor readonly.
     * @type {boolean}
     */
    get editable() {
      return !(this.hasAttribute("disabled") || this.hasAttribute("readonly"));
    }

    /* -------------------------------------------- */

    /**
     * Special behaviors that the subclass should implement when toggling the disabled state of the input.
     * @param {boolean} disabled    The new disabled state
     * @protected
     */
    _toggleDisabled(disabled) {}

    /* -------------------------------------------- */
    /*  Element Lifecycle                           */
    /* -------------------------------------------- */

    /**
     * Initialize the custom element, constructing its HTML.
     */
    connectedCallback() {
      const elements = this._buildElements();
      this.replaceChildren(...elements);
      this._refresh();
      this._toggleDisabled(!this.editable);
      this.addEventListener("click", this._onClick.bind(this));
      this._activateListeners();
    }

    /* -------------------------------------------- */

    /**
     * Create the HTML elements that should be included in this custom element.
     * Elements are returned as an array of ordered children.
     * @returns {HTMLElement[]}
     * @protected
     */
    _buildElements() {
      return [];
    }

    /* -------------------------------------------- */

    /**
     * Refresh the active state of the custom element.
     * @protected
     */
    _refresh() {}

    /* -------------------------------------------- */

    /**
     * Apply key attributes on the containing custom HTML element to input elements contained within it.
     * @internal
     */
    _applyInputAttributes(input) {
      input.toggleAttribute("required", this.hasAttribute("required"));
      input.toggleAttribute("disabled", this.hasAttribute("disabled"));
      input.toggleAttribute("readonly", this.hasAttribute("readonly"));
    }

    /* -------------------------------------------- */

    /**
     * Activate event listeners which add dynamic behavior to the custom element.
     * @protected
     */
    _activateListeners() {}

    /* -------------------------------------------- */

    /**
     * Special handling when the custom element is clicked. This should be implemented to transfer focus to an
     * appropriate internal element.
     * @param {PointerEvent} event
     * @protected
     */
    _onClick(event) {
      if ( event.target === this ) this._primaryInput?.focus?.();
    }
  }

  /**
   * @typedef {import("../forms/fields.mjs").FormInputConfig} FormInputConfig
   */

  /**
   * @typedef {Object} StringTagsInputConfig
   * @property {boolean} slug               Automatically slugify provided strings?
   */

  /**
   * A custom HTML element which allows for arbitrary assignment of a set of string tags.
   * This element may be used directly or subclassed to impose additional validation or functionality.
   * @extends {AbstractFormInputElement<Set<string>>}
   */
  class HTMLStringTagsElement extends AbstractFormInputElement {
    constructor() {
      super();
      this.#slug = this.hasAttribute("slug");
      this._value = new Set();
      this._initializeTags();
    }

    /** @override */
    static tagName = "string-tags";

    static icons = {
      add: "fa-solid fa-tag",
      remove: "fa-solid fa-times"
    }

    static labels = {
      add: "ELEMENTS.TAGS.Add",
      remove: "ELEMENTS.TAGS.Remove",
      placeholder: ""
    }

    /**
     * The button element to add a new tag.
     * @type {HTMLButtonElement}
     */
    #button;

    /**
     * The input element to enter a new tag.
     * @type {HTMLInputElement}
     */
    #input;

    /**
     * The tags list of assigned tags.
     * @type {HTMLDivElement}
     */
    #tags;

    /**
     * Automatically slugify all strings provided to the element?
     * @type {boolean}
     */
    #slug;

    /* -------------------------------------------- */

    /**
     * Initialize innerText or an initial value attribute of the element as a comma-separated list of currently assigned
     * string tags.
     * @protected
     */
    _initializeTags() {
      const initial = this.getAttribute("value") || this.innerText || "";
      const tags = initial ? initial.split(",") : [];
      for ( let tag of tags ) {
        tag = tag.trim();
        if ( tag ) {
          if ( this.#slug ) tag = tag.slugify({strict: true});
          try {
            this._validateTag(tag);
          } catch ( err ) {
            console.warn(err.message);
            continue;
          }
          this._value.add(tag);
        }
      }
      this.innerText = "";
      this.removeAttribute("value");
    }

    /* -------------------------------------------- */

    /**
     * Subclasses may impose more strict validation on what tags are allowed.
     * @param {string} tag      A candidate tag
     * @throws {Error}          An error if the candidate tag is not allowed
     * @protected
     */
    _validateTag(tag) {
      if ( !tag ) throw new Error(game.i18n.localize("ELEMENTS.TAGS.ErrorBlank"));
    }

    /* -------------------------------------------- */

    /** @override */
    _buildElements() {

      // Create tags list
      const tags = document.createElement("div");
      tags.className = "tags input-element-tags";
      this.#tags = tags;

      // Create input element
      const input = document.createElement("input");
      input.type = "text";
      input.placeholder = game.i18n.localize(this.constructor.labels.placeholder);
      this.#input = this._primaryInput = input;

      // Create button
      const button = document.createElement("button");
      button.type = "button";
      button.className = `icon ${this.constructor.icons.add}`;
      button.dataset.tooltip = this.constructor.labels.add;
      button.ariaLabel = game.i18n.localize(this.constructor.labels.add);
      this.#button = button;
      return [this.#tags, this.#input, this.#button];
    }

    /* -------------------------------------------- */

    /** @override */
    _refresh() {
      const tags = this.value.map(tag => this.constructor.renderTag(tag, tag, this.editable));
      this.#tags.replaceChildren(...tags);
    }

    /* -------------------------------------------- */

    /**
     * Render the tagged string as an HTML element.
     * @param {string} tag        The raw tag value
     * @param {string} [label]    An optional tag label
     * @param {boolean} [editable=true]  Is the tag editable?
     * @returns {HTMLDivElement}  A rendered HTML element for the tag
     */
    static renderTag(tag, label, editable=true) {
      const div = document.createElement("div");
      div.className = "tag";
      div.dataset.key = tag;
      const span = document.createElement("span");
      span.textContent = label ?? tag;
      div.append(span);
      if ( editable ) {
        const t = game.i18n.localize(this.labels.remove);
        const a = `<a class="remove ${this.icons.remove}" data-tooltip="${t}" aria-label="${t}"></a>`;
        div.insertAdjacentHTML("beforeend", a);
      }
      return div;
    }

    /* -------------------------------------------- */

    /** @override */
    _activateListeners() {
      this.#button.addEventListener("click", this.#addTag.bind(this));
      this.#tags.addEventListener("click", this.#onClickTag.bind(this));
      this.#input.addEventListener("keydown", this.#onKeydown.bind(this));
    }

    /* -------------------------------------------- */

    /**
     * Remove a tag from the set when its removal button is clicked.
     * @param {PointerEvent} event
     */
    #onClickTag(event) {
      if ( !event.target.classList.contains("remove") ) return;
      const tag = event.target.closest(".tag");
      this._value.delete(tag.dataset.key);
      this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
      this._refresh();
    }

    /* -------------------------------------------- */

    /**
     * Add a tag to the set when the ENTER key is pressed in the text input.
     * @param {KeyboardEvent} event
     */
    #onKeydown(event) {
      if ( event.key !== "Enter" ) return;
      event.preventDefault();
      event.stopPropagation();
      this.#addTag();
    }

    /* -------------------------------------------- */

    /**
     * Add a new tag to the set upon user input.
     */
    #addTag() {
      let tag = this.#input.value.trim();
      if ( this.#slug ) tag = tag.slugify({strict: true});

      // Validate the proposed code
      try {
        this._validateTag(tag);
      } catch(err) {
        ui.notifications.error(err.message);
        this.#input.value = "";
        return;
      }

      // Ensure uniqueness
      if ( this._value.has(tag) ) {
        const message = game.i18n.format("ELEMENTS.TAGS.ErrorNonUnique", {tag});
        ui.notifications.error(message);
        this.#input.value = "";
        return;
      }

      // Add hex
      this._value.add(tag);
      this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
      this.#input.value = "";
      this._refresh();
    }

    /* -------------------------------------------- */
    /*  Form Handling                               */
    /* -------------------------------------------- */

    /** @override */
    _getValue() {
      return Array.from(this._value);
    }

    /* -------------------------------------------- */

    /** @override */
    _setValue(value) {
      this._value.clear();
      const toAdd = [];
      for ( let v of value ) {
        if ( this.#slug ) v = v.slugify({strict: true});
        this._validateTag(v);
        toAdd.push(v);
      }
      for ( const v of toAdd ) this._value.add(v);
    }

    /* -------------------------------------------- */

    /** @override */
    _toggleDisabled(disabled) {
      this.#input.toggleAttribute("disabled", disabled);
      this.#button.toggleAttribute("disabled", disabled);
    }

    /* -------------------------------------------- */

    /**
     * Create a HTMLStringTagsElement using provided configuration data.
     * @param {FormInputConfig & StringTagsInputConfig} config
     */
    static create(config) {
      const tags = document.createElement(this.tagName);
      tags.name = config.name;
      const value = Array.from(config.value || []).join(",");
      tags.toggleAttribute("slug", !!config.slug);
      tags.setAttribute("value", value);
      foundry.applications.fields.setInputAttributes(tags, config);
      return tags;
    }
  }

  /**
   * @typedef {import("../forms/fields.mjs").FormInputConfig} FormInputConfig
   */

  /**
   * @typedef {Object} DocumentTagsInputConfig
   * @property {string} [type]      A specific document type in CONST.ALL_DOCUMENT_TYPES
   * @property {boolean} [single]   Only allow referencing a single document. In this case the submitted form value will
   *                                be a single UUID string rather than an array
   * @property {number} [max]       Only allow attaching a maximum number of documents
   */

  /**
   * A custom HTMLElement used to render a set of associated Documents referenced by UUID.
   * @extends {AbstractFormInputElement<string|string[]|null>}
   */
  class HTMLDocumentTagsElement extends AbstractFormInputElement {
    constructor() {
      super();
      this._initializeTags();
    }

    /** @override */
    static tagName = "document-tags";

    /* -------------------------------------------- */

    /**
     * @override
     * @type {Record<string, string>}
     * @protected
     */
    _value = {};

    /**
     * The button element to add a new document.
     * @type {HTMLButtonElement}
     */
    #button;

    /**
     * The input element to define a Document UUID.
     * @type {HTMLInputElement}
     */
    #input;

    /**
     * The list of tagged documents.
     * @type {HTMLDivElement}
     */
    #tags;

    /* -------------------------------------------- */

    /**
     * Restrict this element to documents of a particular type.
     * @type {string|null}
     */
    get type() {
      return this.getAttribute("type");
    }

    set type(value) {
      if ( !value ) return this.removeAttribute("type");
      if ( !CONST.ALL_DOCUMENT_TYPES.includes(value) ) {
        throw new Error(`"${value}" is not a valid Document type in CONST.ALL_DOCUMENT_TYPES`);
      }
      this.setAttribute("type", value);
    }

    /* -------------------------------------------- */

    /**
     * Restrict to only allow referencing a single Document instead of an array of documents.
     * @type {boolean}
     */
    get single() {
      return this.hasAttribute("single");
    }

    set single(value) {
      this.toggleAttribute("single", value === true);
    }

    /* -------------------------------------------- */

    /**
     * Allow a maximum number of documents to be tagged to the element.
     * @type {number}
     */
    get max() {
      const max = parseInt(this.getAttribute("max"));
      return isNaN(max) ? Infinity : max;
    }

    set max(value) {
      if ( Number.isInteger(value) && (value > 0) ) this.setAttribute("max", String(value));
      else this.removeAttribute("max");
    }

    /* -------------------------------------------- */

    /**
     * Initialize innerText or an initial value attribute of the element as a serialized JSON array.
     * @protected
     */
    _initializeTags() {
      const initial = this.getAttribute("value") || this.innerText || "";
      const tags = initial ? initial.split(",") : [];
      for ( const t of tags ) {
        try {
          this.#add(t);
        } catch(err) {
          this._value[t] = `${t} [INVALID]`; // Display invalid UUIDs as a raw string
        }
      }
      this.innerText = "";
      this.removeAttribute("value");
    }

    /* -------------------------------------------- */

    /** @override */
    _buildElements() {

      // Create tags list
      this.#tags = document.createElement("div");
      this.#tags.className = "tags input-element-tags";

      // Create input element
      this.#input = this._primaryInput = document.createElement("input");
      this.#input.type = "text";
      this.#input.placeholder = game.i18n.format("HTMLDocumentTagsElement.PLACEHOLDER", {
        type: game.i18n.localize(this.type ? getDocumentClass(this.type).metadata.label : "DOCUMENT.Document")});

      // Create button
      this.#button = document.createElement("button");
      this.#button.type = "button";
      this.#button.className = "icon fa-solid fa-file-plus";
      this.#button.dataset.tooltip = game.i18n.localize("ELEMENTS.DOCUMENT_TAGS.Add");
      this.#button.setAttribute("aria-label", this.#button.dataset.tooltip);
      return [this.#tags, this.#input, this.#button];
    }

    /* -------------------------------------------- */

    /** @override */
    _refresh() {
      if ( !this.#tags ) return; // Not yet connected
      const tags = Object.entries(this._value).map(([k, v]) => this.constructor.renderTag(k, v, this.editable));
      this.#tags.replaceChildren(...tags);
    }

    /* -------------------------------------------- */

    /**
     * Create an HTML string fragment for a single document tag.
     * @param {string} uuid              The document UUID
     * @param {string} name              The document name
     * @param {boolean} [editable=true]  Is the tag editable?
     * @returns {HTMLDivElement}
     */
    static renderTag(uuid, name, editable=true) {
      const div = HTMLStringTagsElement.renderTag(uuid, TextEditor.truncateText(name, {maxLength: 32}), editable);
      div.classList.add("document-tag");
      div.querySelector("span").dataset.tooltip = uuid;
      if ( editable ) {
        const t = game.i18n.localize("ELEMENTS.DOCUMENT_TAGS.Remove");
        const a = div.querySelector("a");
        a.dataset.tooltip = t;
        a.ariaLabel = t;
      }
      return div;
    }

    /* -------------------------------------------- */

    /** @override */
    _activateListeners() {
      this.#button.addEventListener("click", () => this.#tryAdd(this.#input.value));
      this.#tags.addEventListener("click", this.#onClickTag.bind(this));
      this.#input.addEventListener("keydown", this.#onKeydown.bind(this));
      this.addEventListener("drop", this.#onDrop.bind(this));
    }

    /* -------------------------------------------- */

    /**
     * Remove a single coefficient by clicking on its tag.
     * @param {PointerEvent} event
     */
    #onClickTag(event) {
      if ( !event.target.classList.contains("remove") ) return;
      const tag = event.target.closest(".tag");
      delete this._value[tag.dataset.key];
      this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
      this._refresh();
    }

    /* -------------------------------------------- */

    /**
     * Add a new document tag by pressing the ENTER key in the UUID input field.
     * @param {KeyboardEvent} event
     */
    #onKeydown(event) {
      if ( event.key !== "Enter" ) return;
      event.preventDefault();
      event.stopPropagation();
      this.#tryAdd(this.#input.value);
    }

    /* -------------------------------------------- */

    /**
     * Handle data dropped onto the form element.
     * @param {DragEvent} event
     */
    #onDrop(event) {
      event.preventDefault();
      const dropData = TextEditor.getDragEventData(event);
      if ( dropData.uuid ) this.#tryAdd(dropData.uuid);
    }

    /* -------------------------------------------- */

    /**
     * Add a Document to the tagged set using the value of the input field.
     * @param {string} uuid     The UUID to attempt to add
     */
    #tryAdd(uuid) {
      try {
        this.#add(uuid);
        this._refresh();
      } catch(err) {
        ui.notifications.error(err.message);
      }
      this.#input.value = "";
      this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
      this.#input.focus();
    }

    /* -------------------------------------------- */

    /**
     * Validate that the tagged document is allowed to be added to this field.
     * Subclasses may impose more strict validation as to which types of documents are allowed.
     * @param {foundry.abstract.Document|object} document   A candidate document or compendium index entry to tag
     * @throws {Error}                                      An error if the candidate document is not allowed
     */
    _validateDocument(document) {
      const {type, max} = this;
      if ( type && (document.documentName !== type) ) throw new Error(`Incorrect document type "${document.documentName}"`
        + ` provided to document tag field which requires "${type}" documents.`);
      const n = Object.keys(this._value).length;
      if ( n >= max ) throw new Error(`You may only attach at most ${max} Documents to the "${this.name}" field`);
    }

    /* -------------------------------------------- */

    /**
     * Add a new UUID to the tagged set, throwing an error if the UUID is not valid.
     * @param {string} uuid   The UUID to add
     * @throws {Error}        If the UUID is not valid
     */
    #add(uuid) {

      // Require the UUID to exist
      let record;
      const {id} = foundry.utils.parseUuid(uuid);
      if ( id ) record = fromUuidSync(uuid);
      else if ( this.type ) {
        const collection = game.collections.get(this.type);
        record = collection.get(uuid);
      }
      if ( !record ) throw new Error(`Invalid document UUID "${uuid}" provided to document tag field.`);

      // Require a certain type of document
      this._validateDocument(record);

      // Replace singleton
      if ( this.single ) {
        for ( const k of Object.keys(this._value) ) delete this._value[k];
      }

      // Record the document
      this._value[uuid] = record.name;
    }

    /* -------------------------------------------- */
    /*  Form Handling                               */
    /* -------------------------------------------- */

    /** @override */
    _getValue() {
      const uuids = Object.keys(this._value);
      if ( this.single ) return uuids[0] ?? null;
      else return uuids;
    }

    /** @override */
    _setValue(value) {
      this._value = {};
      if ( !value ) return;
      if ( typeof value === "string" ) value = [value];
      for ( const uuid of value ) this.#add(uuid);
    }

    /* -------------------------------------------- */

    /** @override */
    _toggleDisabled(disabled) {
      this.#input?.toggleAttribute("disabled", disabled);
      this.#button?.toggleAttribute("disabled", disabled);
    }

    /* -------------------------------------------- */

    /**
     * Create a HTMLDocumentTagsElement using provided configuration data.
     * @param {FormInputConfig & DocumentTagsInputConfig} config
     * @returns {HTMLDocumentTagsElement}
     */
    static create(config) {
      const tags = /** @type {HTMLDocumentTagsElement} */ document.createElement(HTMLDocumentTagsElement.tagName);
      tags.name = config.name;

      // Coerce value to an array
      let values;
      if ( config.value instanceof Set ) values = Array.from(config.value);
      else if ( !Array.isArray(config.value) ) values = [config.value];
      else values = config.value;

      tags.setAttribute("value", values);
      tags.type = config.type;
      tags.max = config.max;
      tags.single = config.single;
      foundry.applications.fields.setInputAttributes(tags, config);
      return tags;
    }
  }

  /**
   * @typedef {import("../forms/fields.mjs").FormInputConfig} FormInputConfig
   */

  /**
   * @typedef {Object} FilePickerInputConfig
   * @property {FilePickerOptions.type} [type]
   * @property {string} [placeholder]
   * @property {boolean} [noupload]
   */

  /**
   * A custom HTML element responsible for rendering a file input field and associated FilePicker button.
   * @extends {AbstractFormInputElement<string>}
   */
  class HTMLFilePickerElement extends AbstractFormInputElement {

    /** @override */
    static tagName = "file-picker";

    /**
     * The file path selected.
     * @type {HTMLInputElement}
     */
    input;

    /**
     * A button to open the file picker interface.
     * @type {HTMLButtonElement}
     */
    button;

    /**
     * A reference to the FilePicker application instance originated by this element.
     * @type {FilePicker}
     */
    picker;

    /* -------------------------------------------- */

    /**
     * A type of file which can be selected in this field.
     * @see {@link FilePicker.FILE_TYPES}
     * @type {FilePickerOptions.type}
     */
    get type() {
      return this.getAttribute("type") ?? "any";
    }

    set type(value) {
      if ( !FilePicker.FILE_TYPES.includes(value) ) throw new Error(`Invalid type "${value}" provided which must be a `
        + "value in FilePicker.TYPES");
      this.setAttribute("type", value);
    }

    /* -------------------------------------------- */

    /**
     * Prevent uploading new files as part of this element's FilePicker dialog.
     * @type {boolean}
     */
    get noupload() {
      return this.hasAttribute("noupload");
    }

    set noupload(value) {
      this.toggleAttribute("noupload", value === true);
    }

    /* -------------------------------------------- */

    /** @override */
    _buildElements() {

      // Initialize existing value
      this._value ??= this.getAttribute("value") || this.innerText || "";
      this.removeAttribute("value");

      // Create an input field
      const elements = [];
      this.input = this._primaryInput = document.createElement("input");
      this.input.className = "image";
      this.input.type = "text";
      this.input.placeholder = this.getAttribute("placeholder") ?? "path/to/file.ext";
      elements.push(this.input);

      // Disallow browsing for some users
      if ( game.world && !game.user.can("FILES_BROWSE") ) return elements;

      // Create a FilePicker button
      this.button = document.createElement("button");
      this.button.className = "fa-solid fa-file-import fa-fw";
      this.button.type = "button";
      this.button.dataset.tooltip = game.i18n.localize("FILES.BrowseTooltip");
      this.button.setAttribute("aria-label", this.button.dataset.tooltip);
      this.button.tabIndex = -1;
      elements.push(this.button);
      return elements;
    }

    /* -------------------------------------------- */

    /** @override */
    _refresh() {
      this.input.value = this._value;
    }

    /* -------------------------------------------- */

    /** @override */
    _toggleDisabled(disabled) {
      this.input.disabled = disabled;
      if ( this.button ) this.button.disabled = disabled;
    }

    /* -------------------------------------------- */

    /** @override */
    _activateListeners() {
      this.input.addEventListener("input", () => this._value = this.input.value);
      this.button?.addEventListener("click", this.#onClickButton.bind(this));
    }

    /* -------------------------------------------- */

    /**
     * Handle clicks on the button element to render the FilePicker UI.
     * @param {PointerEvent} event      The initiating click event
     */
    #onClickButton(event) {
      event.preventDefault();
      this.picker = new FilePicker({
        type: this.type,
        current: this.value,
        allowUpload: !this.noupload,
        callback: src => this.value = src
      });
      return this.picker.browse();
    }

    /* -------------------------------------------- */

    /**
     * Create a HTMLFilePickerElement using provided configuration data.
     * @param {FormInputConfig<string> & FilePickerInputConfig} config
     */
    static create(config) {
      const picker = document.createElement(this.tagName);
      picker.name = config.name;
      picker.setAttribute("value", config.value || "");
      picker.type = config.type;
      picker.noupload = config.noupload;
      foundry.applications.fields.setInputAttributes(picker, config);
      return picker;
    }
  }

  /**
   * A class designed to standardize the behavior for a hue selector UI component.
   * @extends {AbstractFormInputElement<number>}
   */
  class HTMLHueSelectorSlider extends AbstractFormInputElement {

    /** @override */
    static tagName = "hue-slider";

    /**
     * The color range associated with this element.
     * @type {HTMLInputElement|null}
     */
    #input;

    /* -------------------------------------------- */

    /** @override */
    _buildElements() {

      // Initialize existing value
      this._setValue(this.getAttribute("value"));

      // Build elements
      this.#input = this._primaryInput = document.createElement("input");
      this.#input.className = "color-range";
      this.#input.type = "range";
      this.#input.min = "0";
      this.#input.max = "360";
      this.#input.step = "1";
      this.#input.disabled = this.disabled;
      this.#input.value = this._value * 360;
      return [this.#input];
    }

    /* -------------------------------------------- */

    /**
     * Refresh the active state of the custom element.
     * @protected
     */
    _refresh() {
      this.#input.style.setProperty("--color-thumb", Color.fromHSL([this._value, 1, 0.5]).css);
    }

    /* -------------------------------------------- */

    /**
     * Activate event listeners which add dynamic behavior to the custom element.
     * @protected
     */
    _activateListeners() {
      this.#input.oninput = this.#onInputColorRange.bind(this);
    }

    /* -------------------------------------------- */

    /**
     * Update the thumb and the value.
     * @param {FormDataEvent} event
     */
    #onInputColorRange(event) {
      event.preventDefault();
      event.stopImmediatePropagation();
      this.value = this.#input.value / 360;
    }

    /* -------------------------------------------- */
    /*  Form Handling
    /* -------------------------------------------- */

    /** @override */
    _setValue(value) {
      value = Number(value);
      if ( !value.between(0, 1) ) throw new Error("The value of a hue-slider must be on the range [0,1]");
      this._value = value;
      this.setAttribute("value", String(value));
    }

    /* -------------------------------------------- */

    /** @override */
    _toggleDisabled(disabled) {
      this.#input.disabled = disabled;
    }
  }

  /**
   * An abstract base class designed to standardize the behavior for a multi-select UI component.
   * Multi-select components return an array of values as part of form submission.
   * Different implementations may provide different experiences around how inputs are presented to the user.
   * @extends {AbstractFormInputElement<Set<string>>}
   */
  class AbstractMultiSelectElement extends AbstractFormInputElement {
    constructor() {
      super();
      this._value = new Set();
      this._initialize();
    }

    /**
     * Predefined <option> and <optgroup> elements which were defined in the original HTML.
     * @type {(HTMLOptionElement|HTMLOptGroupElement)[]}
     * @protected
     */
    _options;

    /**
     * An object which maps option values to displayed labels.
     * @type {Record<string, string>}
     * @protected
     */
    _choices = {};

    /* -------------------------------------------- */

    /**
     * Preserve existing <option> and <optgroup> elements which are defined in the original HTML.
     * @protected
     */
    _initialize() {
      this._options = [...this.children];
      for ( const option of this.querySelectorAll("option") ) {
        if ( !option.value ) continue; // Skip predefined options which are already blank
        this._choices[option.value] = option.innerText;
        if ( option.selected ) {
          this._value.add(option.value);
          option.selected = false;
        }
      }
    }

    /* -------------------------------------------- */

    /**
     * Mark a choice as selected.
     * @param {string} value      The value to add to the chosen set
     */
    select(value) {
      const exists = this._value.has(value);
      if ( !exists ) {
        if ( !(value in this._choices) ) {
          throw new Error(`"${value}" is not an option allowed by this multi-select element`);
        }
        this._value.add(value);
        this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
        this._refresh();
      }
    }

    /* -------------------------------------------- */

    /**
     * Mark a choice as un-selected.
     * @param {string} value      The value to delete from the chosen set
     */
    unselect(value) {
      const exists = this._value.has(value);
      if ( exists ) {
        this._value.delete(value);
        this.dispatchEvent(new Event("change", { bubbles: true, cancelable: true }));
        this._refresh();
      }
    }

    /* -------------------------------------------- */
    /*  Form Handling                               */
    /* -------------------------------------------- */

    /** @override */
    _getValue() {
      return Array.from(this._value);
    }

    /** @override */
    _setValue(value) {
      if ( !Array.isArray(value) ) {
        throw new Error("The value assigned to a multi-select element must be an array.");
      }
      if ( value.some(v => !(v in this._choices)) ) {
        throw new Error("The values assigned to a multi-select element must all be valid options.");
      }
      this._value.clear();
      for ( const v of value ) this._value.add(v);
    }
  }

  /* -------------------------------------------- */

  /**
   * Provide a multi-select workflow using a select element as the input mechanism.
   *
   * @example Multi-Select HTML Markup
   * ```html
   * <multi-select name="select-many-things">
   *   <optgroup label="Basic Options">
   *     <option value="foo">Foo</option>
   *     <option value="bar">Bar</option>
   *     <option value="baz">Baz</option>
   *   </optgroup>
   *   <optgroup label="Advanced Options">
   *    <option value="fizz">Fizz</option>
   *     <option value="buzz">Buzz</option>
   *   </optgroup>
   * </multi-select>
   * ```
   */
  class HTMLMultiSelectElement extends AbstractMultiSelectElement {

    /** @override */
    static tagName = "multi-select";

    /**
     * A select element used to choose options.
     * @type {HTMLSelectElement}
     */
    #select;

    /**
     * A display element which lists the chosen options.
     * @type {HTMLDivElement}
     */
    #tags;

    /* -------------------------------------------- */

    /** @override */
    _buildElements() {

      // Create select element
      this.#select = this._primaryInput = document.createElement("select");
      this.#select.insertAdjacentHTML("afterbegin", '<option value=""></option>');
      this.#select.append(...this._options);
      this.#select.disabled = !this.editable;

      // Create a div element for display
      this.#tags = document.createElement("div");
      this.#tags.className = "tags input-element-tags";
      return [this.#tags, this.#select];
    }

    /* -------------------------------------------- */

    /** @override */
    _refresh() {

      // Update the displayed tags
      const tags = Array.from(this._value).map(id => {
        return HTMLStringTagsElement.renderTag(id, this._choices[id], this.editable);
      });
      this.#tags.replaceChildren(...tags);

      // Disable selected options
      for ( const option of this.#select.querySelectorAll("option") ) {
        option.disabled = this._value.has(option.value);
      }
    }

    /* -------------------------------------------- */

    /** @override */
    _activateListeners() {
      this.#select.addEventListener("change", this.#onChangeSelect.bind(this));
      this.#tags.addEventListener("click", this.#onClickTag.bind(this));
    }

    /* -------------------------------------------- */

    /**
     * Handle changes to the Select input, marking the selected option as a chosen value.
     * @param {Event} event         The change event on the select element
     */
    #onChangeSelect(event) {
      event.preventDefault();
      event.stopImmediatePropagation();
      const select = event.currentTarget;
      if ( !select.value ) return; // Ignore selection of the blank value
      this.select(select.value);
      select.value = "";
    }

    /* -------------------------------------------- */

    /**
     * Handle click events on a tagged value, removing it from the chosen set.
     * @param {PointerEvent} event    The originating click event on a chosen tag
     */
    #onClickTag(event) {
      event.preventDefault();
      if ( !event.target.classList.contains("remove") ) return;
      if ( !this.editable ) return;
      const tag = event.target.closest(".tag");
      this.unselect(tag.dataset.key);
    }

    /* -------------------------------------------- */

    /** @override */
    _toggleDisabled(disabled) {
      this.#select.toggleAttribute("disabled", disabled);
    }

    /* -------------------------------------------- */

    /**
     * Create a HTMLMultiSelectElement using provided configuration data.
     * @param {FormInputConfig<string[]> & Omit<SelectInputConfig, "blank">} config
     * @returns {HTMLMultiSelectElement}
     */
    static create(config) {
      return foundry.applications.fields.createMultiSelectInput(config);
    }
  }

  /* -------------------------------------------- */

  /**
   * Provide a multi-select workflow as a grid of input checkbox elements.
   *
   * @example Multi-Checkbox HTML Markup
   * ```html
   * <multi-checkbox name="check-many-boxes">
   *   <optgroup label="Basic Options">
   *     <option value="foo">Foo</option>
   *     <option value="bar">Bar</option>
   *     <option value="baz">Baz</option>
   *   </optgroup>
   *   <optgroup label="Advanced Options">
   *    <option value="fizz">Fizz</option>
   *     <option value="buzz">Buzz</option>
   *   </optgroup>
   * </multi-checkbox>
   * ```
   */
  class HTMLMultiCheckboxElement extends AbstractMultiSelectElement {

    /** @override */
    static tagName = "multi-checkbox";

    /**
     * The checkbox elements used to select inputs
     * @type {HTMLInputElement[]}
     */
    #checkboxes;

    /* -------------------------------------------- */

    /** @override */
    _buildElements() {
      this.#checkboxes = [];
      const children = [];
      for ( const option of this._options ) {
        if ( option instanceof HTMLOptGroupElement ) children.push(this.#buildGroup(option));
        else children.push(this.#buildOption(option));
      }
      return children;
    }

    /* -------------------------------------------- */

    /**
     * Translate an input <optgroup> element into a <fieldset> of checkboxes.
     * @param {HTMLOptGroupElement} optgroup    The originally configured optgroup
     * @returns {HTMLFieldSetElement}           The created fieldset grouping
     */
    #buildGroup(optgroup) {

      // Create fieldset group
      const group = document.createElement("fieldset");
      group.classList.add("checkbox-group");
      const legend = document.createElement("legend");
      legend.innerText = optgroup.label;
      group.append(legend);

      // Add child options
      for ( const option of optgroup.children ) {
        if ( option instanceof HTMLOptionElement ) {
          group.append(this.#buildOption(option));
        }
      }
      return group;
    }

    /* -------------------------------------------- */

    /**
     * Build an input <option> element into a <label class="checkbox"> element.
     * @param {HTMLOptionElement} option      The originally configured option
     * @returns {HTMLLabelElement}            The created labeled checkbox element
     */
    #buildOption(option) {
      const label = document.createElement("label");
      label.classList.add("checkbox");
      const checkbox = document.createElement("input");
      checkbox.type = "checkbox";
      checkbox.value = option.value;
      checkbox.checked = this._value.has(option.value);
      checkbox.disabled = this.disabled;
      label.append(checkbox, option.innerText);
      this.#checkboxes.push(checkbox);
      return label;
    }

    /* -------------------------------------------- */

    /** @override */
    _refresh() {
      for ( const checkbox of this.#checkboxes ) {
        checkbox.checked = this._value.has(checkbox.value);
      }
    }

    /* -------------------------------------------- */

    /** @override */
    _activateListeners() {
      for ( const checkbox of this.#checkboxes ) {
        checkbox.addEventListener("change", this.#onChangeCheckbox.bind(this));
      }
    }

    /* -------------------------------------------- */

    /**
     * Handle changes to a checkbox input, marking the selected option as a chosen value.
     * @param {Event} event         The change event on the checkbox input element
     */
    #onChangeCheckbox(event) {
      event.preventDefault();
      event.stopImmediatePropagation();
      const checkbox = event.currentTarget;
      if ( checkbox.checked ) this.select(checkbox.value);
      else this.unselect(checkbox.value);
    }

    /* -------------------------------------------- */

    /** @override */
    _toggleDisabled(disabled) {
      for ( const checkbox of this.#checkboxes ) {
        checkbox.disabled = disabled;
      }
    }
  }

  /**
   * @typedef {import("../forms/fields.mjs").FormInputConfig} FormInputConfig
   */

  /**
   * A custom HTMLElement used to select a color using a linked pair of input fields.
   * @extends {AbstractFormInputElement<string>}
   */
  class HTMLColorPickerElement extends AbstractFormInputElement {
    constructor() {
      super();
      this._setValue(this.getAttribute("value")); // Initialize existing color value
    }

    /** @override */
    static tagName = "color-picker";

    /* -------------------------------------------- */

    /**
     * The button element to add a new document.
     * @type {HTMLInputElement}
     */
    #colorSelector;

    /**
     * The input element to define a Document UUID.
     * @type {HTMLInputElement}
     */
    #colorString;

    /* -------------------------------------------- */

    /** @override */
    _buildElements() {

      // Create string input element
      this.#colorString = this._primaryInput = document.createElement("input");
      this.#colorString.type = "text";
      this.#colorString.placeholder = this.getAttribute("placeholder") || "";
      this._applyInputAttributes(this.#colorString);

      // Create color selector element
      this.#colorSelector = document.createElement("input");
      this.#colorSelector.type = "color";
      this._applyInputAttributes(this.#colorSelector);
      return [this.#colorString, this.#colorSelector];
    }

    /* -------------------------------------------- */

    /** @override */
    _refresh() {
      if ( !this.#colorString ) return; // Not yet connected
      this.#colorString.value = this._value;
      this.#colorSelector.value = this._value || this.#colorString.placeholder || "#000000";
    }

    /* -------------------------------------------- */

    /** @override */
    _activateListeners() {
      const onChange = this.#onChangeInput.bind(this);
      this.#colorString.addEventListener("change", onChange);
      this.#colorSelector.addEventListener("change", onChange);
    }

    /* -------------------------------------------- */

    /**
     * Handle changes to one of the inputs of the color picker element.
     * @param {InputEvent} event     The originating input change event
     */
    #onChangeInput(event) {
      event.stopPropagation();
      this.value = event.currentTarget.value;
    }

    /* -------------------------------------------- */

    /** @override */
    _toggleDisabled(disabled) {
      this.#colorString.toggleAttribute("disabled", disabled);
      this.#colorSelector.toggleAttribute("disabled", disabled);
    }

    /* -------------------------------------------- */

    /**
     * Create a HTMLColorPickerElement using provided configuration data.
     * @param {FormInputConfig} config
     * @returns {HTMLColorPickerElement}
     */
    static create(config) {
      const picker = document.createElement(HTMLColorPickerElement.tagName);
      picker.name = config.name;
      picker.setAttribute("value", config.value ?? "");
      foundry.applications.fields.setInputAttributes(picker, config);
      return picker;
    }
  }

  /**
   * @typedef {import("../forms/fields.mjs").FormInputConfig} FormInputConfig
   */

  /**
   * @typedef {Object} RangePickerInputConfig
   * @property {number} min
   * @property {number} max
   * @property {number} [step]
   */

  /**
   * A custom HTML element responsible selecting a value on a range slider with a linked number input field.
   * @extends {AbstractFormInputElement<number>}
   */
  class HTMLRangePickerElement extends AbstractFormInputElement {
    constructor() {
      super();
      this.#min = Number(this.getAttribute("min")) ?? 0;
      this.#max = Number(this.getAttribute("max")) ?? 1;
      this.#step = Number(this.getAttribute("step")) || undefined;
      this._setValue(Number(this.getAttribute("value"))); // Initialize existing value
    }

    /** @override */
    static tagName = "range-picker";

    /**
     * The range input.
     * @type {HTMLInputElement}
     */
    #rangeInput;

    /**
     * The number input.
     * @type {HTMLInputElement}
     */
    #numberInput;

    /**
     * The minimum allowed value for the range.
     * @type {number}
     */
    #min;

    /**
     * The maximum allowed value for the range.
     * @type {number}
     */
    #max;

    /**
     * A required step size for the range.
     * @type {number}
     */
    #step;

    /* -------------------------------------------- */

    /**
     * The value of the input element.
     * @type {number}
     */
    get valueAsNumber() {
      return this._getValue();
    }

    /* -------------------------------------------- */

    /** @override */
    _buildElements() {

      // Create range input element
      const r = this.#rangeInput = document.createElement("input");
      r.type = "range";
      r.min = String(this.#min);
      r.max = String(this.#max);
      r.step = String(this.#step ?? 0.1);
      this._applyInputAttributes(r);

      // Create the number input element
      const n = this.#numberInput = this._primaryInput = document.createElement("input");
      n.type = "number";
      n.min = String(this.#min);
      n.max = String(this.#max);
      n.step = this.#step ?? "any";
      this._applyInputAttributes(n);
      return [this.#rangeInput, this.#numberInput];
    }

    /* -------------------------------------------- */

    /** @override */
    _setValue(value) {
      value = Math.clamp(value, this.#min, this.#max);
      if ( this.#step ) value = value.toNearest(this.#step);
      this._value = value;
    }

    /* -------------------------------------------- */

    /** @override */
    _refresh() {
      if ( !this.#rangeInput ) return; // Not yet connected
      this.#rangeInput.valueAsNumber = this.#numberInput.valueAsNumber = this._value;
    }

    /* -------------------------------------------- */

    /** @override */
    _activateListeners() {
      const onChange = this.#onChangeInput.bind(this);
      this.#rangeInput.addEventListener("input", this.#onDragSlider.bind(this));
      this.#rangeInput.addEventListener("change", onChange);
      this.#numberInput.addEventListener("change", onChange);
    }

    /* -------------------------------------------- */

    /**
     * Update display of the number input as the range slider is actively changed.
     * @param {InputEvent} event     The originating input event
     */
    #onDragSlider(event) {
      event.preventDefault();
      this.#numberInput.valueAsNumber = this.#rangeInput.valueAsNumber;
    }

    /* -------------------------------------------- */

    /**
     * Handle changes to one of the inputs of the range picker element.
     * @param {InputEvent} event     The originating input change event
     */
    #onChangeInput(event) {
      event.stopPropagation();
      this.value = event.currentTarget.valueAsNumber;
    }

    /* -------------------------------------------- */

    /** @override */
    _toggleDisabled(disabled) {
      this.#rangeInput.toggleAttribute("disabled", disabled);
      this.#numberInput.toggleAttribute("disabled", disabled);
    }

    /* -------------------------------------------- */

    /**
     * Create a HTMLRangePickerElement using provided configuration data.
     * @param {FormInputConfig & RangePickerInputConfig} config
     * @returns {HTMLRangePickerElement}
     */
    static create(config) {
      const picker = document.createElement(HTMLRangePickerElement.tagName);
      picker.name = config.name;
      for ( const attr of ["value", "min", "max", "step"] ) {
        if ( attr in config ) picker.setAttribute(attr, config[attr]);
      }
      foundry.applications.fields.setInputAttributes(picker, config);
      return picker;
    }
  }

  /**
   * @typedef {import("../forms/fields.mjs").FormInputConfig} FormInputConfig
   */

  /**
   * @typedef {Object} ProseMirrorInputConfig
   * @property {boolean} toggled            Is this editor toggled (true) or always active (false)
   * @property {string} [enriched]          If the editor is toggled, provide the enrichedHTML which is displayed while
   *                                        the editor is not active.
   * @property {boolean} collaborate        Does this editor instance support collaborative editing?
   * @property {boolean} compact            Should the editor be presented in compact mode?
   * @property {string} documentUUID        A Document UUID. Required for collaborative editing
   */

  /**
   * A custom HTML element responsible displaying a ProseMirror rich text editor.
   * @extends {AbstractFormInputElement<string>}
   */
  class HTMLProseMirrorElement extends AbstractFormInputElement {
    constructor() {
      super();

      // Initialize raw content
      this._setValue(this.getAttribute("value") || "");
      this.removeAttribute("value");

      // Initialize enriched content
      this.#toggled = this.hasAttribute("toggled");
      this.#enriched = this.innerHTML;
    }

    /** @override */
    static tagName = "prose-mirror";

    /**
     * Is the editor in active edit mode?
     * @type {boolean}
     */
    #active = false;

    /**
     * The ProseMirror editor instance.
     * @type {ProseMirrorEditor}
     */
    #editor;

    /**
     * Current editor contents
     * @type {HTMLDivElement}
     */
    #content;

    /**
     * Does this editor function via a toggle button? Or is it always active?
     * @type {boolean}
     */
    #toggled;

    /**
     * Enriched content which is optionally used if the editor is toggled.
     * @type {string}
     */
    #enriched;

    /**
     * An optional edit button which activates edit mode for the editor
     * @type {HTMLButtonElement|null}
     */
    #button = null;

    /* -------------------------------------------- */

    /**
     * Actions to take when the custom element is removed from the document.
     */
    disconnectedCallback() {
      this.#editor?.destroy();
    }

    /* -------------------------------------------- */

    /** @override */
    _buildElements() {
      this.classList.add("editor", "prosemirror", "inactive");
      const elements = [];
      this.#content = document.createElement("div");
      this.#content.className = "editor-content";
      elements.push(this.#content);
      if ( this.#toggled ) {
        this.#button = document.createElement("button");
        this.#button.type = "button";
        this.#button.className = "icon toggle";
        this.#button.innerHTML = `<i class="fa-solid fa-edit"></i>`;
        elements.push(this.#button);
      }
      return elements;
    }

    /* -------------------------------------------- */

    /** @override */
    _refresh() {
      if ( this.#active ) return; // It is not safe to replace the content while the editor is active
      if ( this.#toggled ) this.#content.innerHTML = this.#enriched ?? this._value;
      else this.#content.innerHTML = this._value;
    }

    /* -------------------------------------------- */

    /** @override */
    _activateListeners() {
      if ( this.#toggled ) this.#button.addEventListener("click", this.#onClickButton.bind(this));
      else this.#activateEditor();
    }

    /* -------------------------------------------- */

    /** @override */
    _getValue() {
      if ( this.#active ) return ProseMirror.dom.serializeString(this.#editor.view.state.doc.content);
      return this._value;
    }

    /* -------------------------------------------- */

    /**
     * Activate the ProseMirror editor.
     * @returns {Promise<void>}
     */
    async #activateEditor() {

      // If the editor was toggled, replace with raw editable content
      if ( this.#toggled ) this.#content.innerHTML = this._value;

      // Create the TextEditor instance
      const document = await fromUuid(this.dataset.documentUuid ?? this.dataset.documentUUID);
      this.#editor = await TextEditor.create({
        engine: "prosemirror",
        plugins: this._configurePlugins(),
        fieldName: this.name,
        collaborate: this.hasAttribute("collaborate"),
        target: this.#content,
        document
      }, this._getValue());

      // Toggle active state
      this.#active = true;
      if ( this.#button ) this.#button.disabled = true;
      this.classList.add("active");
      this.classList.remove("inactive");
    }

    /* -------------------------------------------- */

    /**
     * Configure ProseMirror editor plugins.
     * @returns {Record<string, ProseMirror.Plugin>}
     * @protected
     */
    _configurePlugins() {
      return {
        menu: ProseMirror.ProseMirrorMenu.build(ProseMirror.defaultSchema, {
          compact: this.hasAttribute("compact"),
          destroyOnSave: this.#toggled,
          onSave: this.#save.bind(this)
        }),
        keyMaps: ProseMirror.ProseMirrorKeyMaps.build(ProseMirror.defaultSchema, {
          onSave: this.#save.bind(this)
        })
      };
    }

    /* -------------------------------------------- */

    /**
     * Handle clicking the editor activation button.
     * @param {PointerEvent} event  The triggering event.
     */
    #onClickButton(event) {
      event.preventDefault();
      this.#activateEditor();
    }

    /* -------------------------------------------- */

    /**
     * Handle saving the editor content.
     * Store new parsed HTML into the _value attribute of the element.
     * If the editor is toggled, also deactivate editing mode.
     */
    #save() {
      const value = ProseMirror.dom.serializeString(this.#editor.view.state.doc.content);
      if ( value !== this._value ) {
        this._setValue(value);
        this.dispatchEvent(new Event("change", {bubbles: true, cancelable: true}));
      }

      // Deactivate a toggled editor
      if ( this.#toggled ) {
        this.#button.disabled = this.disabled;
        this.#active = false;
        this.#editor.destroy();
        this.classList.remove("active");
        this.classList.add("inactive");
        this.replaceChildren(this.#button, this.#content);
        this._refresh();
        this.dispatchEvent(new Event("close", {bubbles: true, cancelable: true}));
      }
    }

    /* -------------------------------------------- */

    /** @override */
    _toggleDisabled(disabled) {
      if ( this.#toggled ) this.#button.disabled = disabled;
    }

    /* -------------------------------------------- */

    /**
     * Create a HTMLProseMirrorElement using provided configuration data.
     * @param {FormInputConfig & ProseMirrorInputConfig} config
     * @returns {HTMLProseMirrorElement}
     */
    static create(config) {
      const editor = document.createElement(HTMLProseMirrorElement.tagName);
      editor.name = config.name;

      // Configure editor properties
      editor.toggleAttribute("collaborate", config.collaborate ?? false);
      editor.toggleAttribute("compact", config.compact ?? false);
      editor.toggleAttribute("toggled", config.toggled ?? false);
      if ( "documentUUID" in config ) Object.assign(editor.dataset, {
        documentUuid: config.documentUUID,
        documentUUID: config.documentUUID
      });
      if ( Number.isNumeric(config.height) ) editor.style.height = `${config.height}px`;

      // Un-enriched content gets temporarily assigned to the value property of the element
      editor.setAttribute("value", config.value);

      // Enriched content gets temporarily assigned as the innerHTML of the element
      if ( config.toggled && config.enriched ) editor.innerHTML = config.enriched;
      return editor;
    }
  }

  /**
   * Custom HTMLElement implementations for use in template rendering.
   * @module elements
   */


  // Define custom elements
  window.customElements.define(HTMLColorPickerElement.tagName, HTMLColorPickerElement);
  window.customElements.define(HTMLDocumentTagsElement.tagName, HTMLDocumentTagsElement);
  window.customElements.define(HTMLFilePickerElement.tagName, HTMLFilePickerElement);
  window.customElements.define(HTMLHueSelectorSlider.tagName, HTMLHueSelectorSlider);
  window.customElements.define(HTMLMultiSelectElement.tagName, HTMLMultiSelectElement);
  window.customElements.define(HTMLMultiCheckboxElement.tagName, HTMLMultiCheckboxElement);
  window.customElements.define(HTMLRangePickerElement.tagName, HTMLRangePickerElement);
  window.customElements.define(HTMLStringTagsElement.tagName, HTMLStringTagsElement);
  window.customElements.define(HTMLProseMirrorElement.tagName, HTMLProseMirrorElement);

  var _module$9 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    AbstractFormInputElement: AbstractFormInputElement,
    AbstractMultiSelectElement: AbstractMultiSelectElement,
    HTMLColorPickerElement: HTMLColorPickerElement,
    HTMLDocumentTagsElement: HTMLDocumentTagsElement,
    HTMLFilePickerElement: HTMLFilePickerElement,
    HTMLHueSelectorSlider: HTMLHueSelectorSlider,
    HTMLMultiCheckboxElement: HTMLMultiCheckboxElement,
    HTMLMultiSelectElement: HTMLMultiSelectElement,
    HTMLProseMirrorElement: HTMLProseMirrorElement,
    HTMLRangePickerElement: HTMLRangePickerElement,
    HTMLStringTagsElement: HTMLStringTagsElement
  });

  /**
   * @callback CustomFormGroup
   * @param {DataField} field
   * @param {FormGroupConfig} groupConfig
   * @param {FormInputConfig} inputConfig
   * @returns {HTMLDivElement}
   */

  /**
   * @callback CustomFormInput
   * @param {DataField} field
   * @param {FormInputConfig} config
   * @returns {HTMLElement|HTMLCollection}
   */

  /**
   * @typedef {Object} FormGroupConfig
   * @property {string} label                       A text label to apply to the form group
   * @property {string} [units]                     An optional units string which is appended to the label
   * @property {HTMLElement|HTMLCollection} input   An HTML element or collection of elements which provide the inputs
   *                                                for the group
   * @property {string} [hint]                      Hint text displayed as part of the form group
   * @property {string} [rootId]                    Some parent CSS id within which field names are unique. If provided,
   *                                                this root ID is used to automatically assign "id" attributes to input
   *                                                elements and "for" attributes to corresponding labels
   * @property {string[]} [classes]                 An array of CSS classes applied to the form group element
   * @property {boolean} [stacked=false]            Is the "stacked" class applied to the form group
   * @property {boolean} [localize=false]           Should labels or other elements within this form group be
   *                                                automatically localized?
   * @property {CustomFormGroup} [widget]           A custom form group widget function which replaces the default
   *                                                group HTML generation
   */

  /**
   * @template FormInputValue
   * @typedef {Object} FormInputConfig
   * @property {string} name                        The name of the form element
   * @property {FormInputValue} [value]             The current value of the form element
   * @property {boolean} [required=false]           Is the field required?
   * @property {boolean} [disabled=false]           Is the field disabled?
   * @property {boolean} [readonly=false]           Is the field readonly?
   * @property {boolean} [autofocus=false]          Is the field autofocused?
   * @property {boolean} [localize=false]           Localize values of this field?
   * @property {Record<string,string>} [dataset]    Additional dataset attributes to assign to the input
   * @property {string} [placeholder]               A placeholder value, if supported by the element type
   * @property {CustomFormInput} [input]
   */

  /**
   * Create a standardized form field group.
   * @param {FormGroupConfig} config
   * @returns {HTMLDivElement}
   */
  function createFormGroup(config) {
    let {classes, hint, label, input, rootId, stacked, localize, units} = config;
    classes ||= [];
    if ( stacked ) classes.unshift("stacked");
    classes.unshift("form-group");

    // Assign identifiers to each input
    input = input instanceof HTMLCollection ? input : [input];
    let labelFor;
    if ( rootId ) {
      for ( const [i, el] of input.entries() ) {
        const id = [rootId, el.name, input.length > 1 ? i : ""].filterJoin("-");
        labelFor ||= id;
        el.setAttribute("id", id);
      }
    }

    // Create the group element
    const group = document.createElement("div");
    group.className = classes.join(" ");

    // Label element
    const lbl = document.createElement("label");
    lbl.innerText = localize ? game.i18n.localize(label) : label;
    if ( labelFor ) lbl.setAttribute("for", labelFor);
    if ( units ) lbl.insertAdjacentHTML("beforeend", ` <span class="units">(${game.i18n.localize(units)})</span>`);
    group.prepend(lbl);

    // Form fields and inputs
    const fields = document.createElement("div");
    fields.className = "form-fields";
    fields.append(...input);
    group.append(fields);

    // Hint element
    if ( hint ) {
      const h = document.createElement("p");
      h.className = "hint";
      h.innerText = localize ? game.i18n.localize(hint) : hint;
      group.append(h);
    }
    return group;
  }

  /* ---------------------------------------- */

  /**
   * Create an `<input type="checkbox">` element for a BooleanField.
   * @param {FormInputConfig<boolean>} config
   * @returns {HTMLInputElement}
   */
  function createCheckboxInput(config) {
    const input = document.createElement("input");
    input.type = "checkbox";
    input.name = config.name;
    if ( config.value ) input.setAttribute("checked", "");
    setInputAttributes(input, config);
    return input;
  }

  /* ---------------------------------------- */

  /**
   * @typedef {Object} EditorInputConfig
   * @property {string} [engine="prosemirror"]
   * @property {number} [height]
   * @property {boolean} [editable=true]
   * @property {boolean} [button=false]
   * @property {boolean} [collaborate=false]
   */

  /**
   * Create a `<div class="editor">` element for a StringField.
   * @param {FormInputConfig<string> & EditorInputConfig} config
   * @returns {HTMLDivElement}
   */
  function createEditorInput(config) {
    const {engine="prosemirror", editable=true, button=false, collaborate=false, height} = config;
    const editor = document.createElement("div");
    editor.className = "editor";
    if ( height !== undefined ) editor.style.height = `${height}px`;

    // Dataset attributes
    let dataset = { engine, collaborate };
    if ( editable ) dataset.edit = config.name;
    dataset = Object.entries(dataset).map(([k, v]) => `data-${k}="${v}"`).join(" ");

    // Editor HTML
    let editorHTML = "";
    if ( button && editable ) editorHTML += '<a class="editor-edit"><i class="fa-solid fa-edit"></i></a>';
    editorHTML += `<div class="editor-content" ${dataset}>${config.value ?? ""}</div>`;
    editor.innerHTML = editorHTML;
    return editor;
  }

  /* ---------------------------------------- */

  /**
   * Create a `<multi-select>` element for a StringField.
   * @param {FormInputConfig<string[]> & Omit<SelectInputConfig, "blank">} config
   * @returns {HTMLSelectElement}
   */
  function createMultiSelectInput(config) {
    const tagName = config.type === "checkboxes" ? "multi-checkbox" : "multi-select";
    const select = document.createElement(tagName);
    select.name = config.name;
    setInputAttributes(select, config);
    const groups = prepareSelectOptionGroups(config);
    for ( const g of groups ) {
      let parent = select;
      if ( g.group ) parent = _appendOptgroup(g.group, select);
      for ( const o of g.options ) _appendOption(o, parent);
    }
    return select;
  }

  /* ---------------------------------------- */

  /**
   * @typedef {Object} NumberInputConfig
   * @property {number} min
   * @property {number} max
   * @property {number|"any"} step
   * @property {"range"|"number"} [type]
   */

  /**
   * Create an `<input type="number">` element for a NumberField.
   * @param {FormInputConfig<number> & NumberInputConfig} config
   * @returns {HTMLInputElement}
   */
  function createNumberInput(config) {
    const input = document.createElement("input");
    input.type = "number";
    if ( config.name ) input.name = config.name;

    // Assign value
    let step = typeof config.step === "number" ? config.step : "any";
    let value = config.value;
    if ( Number.isNumeric(value) ) {
      if ( typeof config.step === "number" ) value = value.toNearest(config.step);
      input.setAttribute("value", String(value));
    }
    else input.setAttribute("value", "");

    // Min, max, and step size
    if ( typeof config.min === "number" ) input.setAttribute("min", String(config.min));
    if ( typeof config.max === "number" ) input.setAttribute("max", String(config.max));
    input.setAttribute("step", String(step));
    setInputAttributes(input, config);
    return input;
  }

  /* ---------------------------------------- */

  /**
   * @typedef {Object} FormSelectOption
   * @property {string} [value]
   * @property {string} [label]
   * @property {string} [group]
   * @property {boolean} [disabled]
   * @property {boolean} [selected]
   * @property {boolean} [rule]
   */

  /**
   * @typedef {Object} SelectInputConfig
   * @property {FormSelectOption[]} options
   * @property {string[]} [groups]        An option to control the order and display of optgroup elements. The order of
   *                                      strings defines the displayed order of optgroup elements.
   *                                      A blank string may be used to define the position of ungrouped options.
   *                                      If not defined, the order of groups corresponds to the order of options.
   * @property {string} [blank]
   * @property {string} [valueAttr]       An alternative value key of the object passed to the options array
   * @property {string} [labelAttr]       An alternative label key of the object passed to the options array
   * @property {boolean} [localize=false] Localize value labels
   * @property {boolean} [sort=false]     Sort options alphabetically by label within groups
   * @property {"single"|"multi"|"checkboxes"} [type] Customize the type of select that is created
   */

  /**
   * Create a `<select>` element for a StringField.
   * @param {FormInputConfig<string> & SelectInputConfig} config
   * @returns {HTMLSelectElement}
   */
  function createSelectInput(config) {
    const select = document.createElement("select");
    select.name = config.name;
    setInputAttributes(select, config);
    const groups = prepareSelectOptionGroups(config);
    for ( const g of groups ) {
      let parent = select;
      if ( g.group ) parent = _appendOptgroup(g.group, select);
      for ( const o of g.options ) _appendOption(o, parent);
    }
    return select;
  }

  /* ---------------------------------------- */

  /**
   * @typedef {Object} TextAreaInputConfig
   * @property {number} rows
   */

  /**
   * Create a `<textarea>` element for a StringField.
   * @param {FormInputConfig<string> & TextAreaInputConfig} config
   * @returns {HTMLTextAreaElement}
   */
  function createTextareaInput(config) {
    const textarea = document.createElement("textarea");
    textarea.name = config.name;
    textarea.textContent = config.value ?? "";
    if ( config.rows ) textarea.setAttribute("rows", String(config.rows));
    setInputAttributes(textarea, config);
    return textarea;
  }

  /* ---------------------------------------- */

  /**
   * Create an `<input type="text">` element for a StringField.
   * @param {FormInputConfig<string>} config
   * @returns {HTMLInputElement}
   */
  function createTextInput(config) {
    const input = document.createElement("input");
    input.type = "text";
    input.name = config.name;
    input.setAttribute("value", config.value ?? "");
    setInputAttributes(input, config);
    return input;
  }

  /* ---------------------------------------- */
  /*  Helper Methods                          */
  /* ---------------------------------------- */

  /**
   * Structure a provided array of select options into a standardized format for rendering optgroup and option elements.
   * @param {FormInputConfig & SelectInputConfig} config
   * @returns {{group: string, options: FormSelectOption[]}[]}
   *
   * @example
   * const options = [
   *   {value: "bar", label: "Bar", selected: true, group: "Good Options"},
   *   {value: "foo", label: "Foo", disabled: true, group: "Bad Options"},
   *   {value: "baz", label: "Baz", group: "Good Options"}
   * ];
   * const groups = ["Good Options", "Bad Options", "Unused Options"];
   * const optgroups = foundry.applications.fields.prepareSelectOptionGroups({options, groups, blank: true, sort: true});
   */
  function prepareSelectOptionGroups(config) {

    // Coerce values to string array
    let values = [];
    if ( (config.value === undefined) || (config.value === null) ) values = [];
    else if ( typeof config.value === "object" ) {
      for ( const v of config.value ) values.push(String(v));
    }
    else values = [String(config.value)];
    const isSelected = value => values.includes(value);

    // Organize options into groups
    let hasBlank = false;
    const groups = {};
    for ( const option of (config.options || []) ) {
      let {group, value, label, disabled, rule} = option;

      // Value
      if ( config.valueAttr ) value = option[config.valueAttr];
      if ( value !== undefined ) {
        value = String(value);
        if ( value === "" ) hasBlank = true;
      }

      // Label
      if ( config.labelAttr ) label = option[config.labelAttr];
      label ??= value;
      if ( label !== undefined ) {
        if ( typeof label !== "string" ) label = label.toString();
        if ( config.localize ) label = game.i18n.localize(label);
      }

      const selected = option.selected || isSelected(value);
      disabled = !!disabled;

      // Add to group
      group ||= "";
      groups[group] ||= [];
      groups[group].push({type: "option", value, label, selected, disabled, rule});
    }

    // Add groups into an explicitly desired order
    const result = [];
    if ( config.groups instanceof Array ) {
      for ( let group of config.groups ) {
        const options = groups[group] ?? [];
        delete groups[group];
        if ( config.localize ) group = game.i18n.localize(group);
        result.push({group, options});
      }
    }

    // Add remaining groups
    for ( let [groupName, options] of Object.entries(groups) ) {
      if ( groupName && config.localize ) groupName = game.i18n.localize(groupName);
      result.push({group: groupName, options});
    }

    // Sort options
    if ( config.sort ) {
      for ( const group of result ) group.options.sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang));
    }

    // A blank option always comes first
    if ( (typeof config.blank === "string") && !hasBlank ) result.unshift({group: "", options: [{
      value: "",
      label: config.localize ? game.i18n.localize(config.blank) : config.blank,
      selected: isSelected("")
    }]});
    return result;
  }

  /* ---------------------------------------- */

  /**
   * Create and append an option element to a parent select or optgroup.
   * @param {FormSelectOption} option
   * @param {HTMLSelectElement|HTMLOptGroupElement} parent
   * @internal
   */
  function _appendOption(option, parent) {
    const { value, label, selected, disabled, rule } = option;
    if ( (value !== undefined) && (label !== undefined) ) {
      const o = document.createElement("option");
      o.value = value;
      o.innerText = label;
      if ( selected ) o.toggleAttribute("selected", true);
      if ( disabled ) o.toggleAttribute("disabled", true);
      parent.appendChild(o);
    }
    if ( rule ) parent.insertAdjacentHTML("beforeend", "<hr>");
  }

  /* ---------------------------------------- */

  /**
   * Create and append an optgroup element to a parent select.
   * @param {string} label
   * @param {HTMLSelectElement} parent
   * @returns {HTMLOptGroupElement}
   * @internal
   */
  function _appendOptgroup(label, parent) {
    const g = document.createElement("optgroup");
    g.label = label;
    parent.appendChild(g);
    return g;
  }

  /* ---------------------------------------- */

  /**
   * Apply standard attributes to all input elements.
   * @param {HTMLElement} input           The element being configured
   * @param {FormInputConfig<*>} config   Configuration for the element
   */
  function setInputAttributes(input, config) {
    input.toggleAttribute("required", config.required === true);
    input.toggleAttribute("disabled", config.disabled === true);
    input.toggleAttribute("readonly", config.readonly === true);
    input.toggleAttribute("autofocus", config.autofocus === true);
    if ( config.placeholder ) input.setAttribute("placeholder", config.placeholder);
    if ( "dataset" in config ) {
      for ( const [k, v] of Object.entries(config.dataset) ) {
        input.dataset[k] = v;
      }
    }
  }

  var fields = /*#__PURE__*/Object.freeze({
    __proto__: null,
    createCheckboxInput: createCheckboxInput,
    createEditorInput: createEditorInput,
    createFormGroup: createFormGroup,
    createMultiSelectInput: createMultiSelectInput,
    createNumberInput: createNumberInput,
    createSelectInput: createSelectInput,
    createTextInput: createTextInput,
    createTextareaInput: createTextareaInput,
    prepareSelectOptionGroups: prepareSelectOptionGroups,
    setInputAttributes: setInputAttributes
  });

  /**
   * An application for configuring compendium art priorities.
   * @extends ApplicationV2
   * @mixes HandlebarsApplication
   * @alias CompendiumArtConfig
   */
  class CompendiumArtConfig extends HandlebarsApplicationMixin(ApplicationV2) {
    /** @override */
    static DEFAULT_OPTIONS = {
      id: "compendium-art-config",
      tag: "form",
      window: {
        contentClasses: ["standard-form"],
        icon: "fas fa-palette",
        title: "COMPENDIUM.ART.SETTING.Title"
      },
      position: {
        width: 600,
        height: "auto"
      },
      form: {
        closeOnSubmit: true,
        handler: CompendiumArtConfig.#onSubmit
      },
      actions: {
        priority: CompendiumArtConfig.#onAdjustPriority
      }
    };

    /** @override */
    static PARTS = {
      priorities: {
        id: "priorities",
        template: "templates/apps/compendium-art-config.hbs"
      },
      footer: {
        template: "templates/generic/form-footer.hbs"
      }
    };

    /* -------------------------------------------- */
    /*  Rendering                                   */
    /* -------------------------------------------- */

    /** @override */
    async _prepareContext(_options={}) {
      return {
        config: game.compendiumArt.getPackages(),
        buttons: [{ type: "submit", icon: "fas fa-save", label: "SETUP.SaveConfiguration" }]
      };
    }

    /* -------------------------------------------- */
    /*  Event Handlers                              */
    /* -------------------------------------------- */

    /**
     * Adjust the priority of a package.
     * @this {ApplicationV2}
     * @param {MouseEvent} _event         The click event.
     * @param {HTMLButtonElement} target  The button that was clicked.
     */
    static async #onAdjustPriority(_event, target) {
      const row = target.closest("[data-package-id]");
      const { packageId } = row.dataset;
      const configs = [];
      for ( const element of this.element.elements ) {
        const [id, key] = element.name.split(".");
        if ( key === "priority" ) configs.push({ packageId: id, priority: Number(element.value) });
      }
      const idx = configs.findIndex(config => config.packageId === packageId);
      if ( idx < 0 ) return;
      const sortBefore = "increase" in target.dataset;
      if ( sortBefore && (idx === 0) ) return;
      if ( !sortBefore && (idx >= configs.length - 1) ) return;
      const config = configs[idx];
      const sortTarget = configs[sortBefore ? idx - 1 : idx + 1];
      configs.splice(idx, 1);
      const updates = SortingHelpers.performIntegerSort(config, {
        sortBefore, target: sortTarget, siblings: configs, sortKey: "priority"
      });
      updates.forEach(({ target, update }) => {
        this.element.elements[`${target.packageId}.priority`].value = update.priority;
      });
      if ( sortBefore ) row.previousElementSibling.insertAdjacentElement("beforebegin", row);
      else row.nextElementSibling.insertAdjacentElement("afterend", row);
    }

    /* -------------------------------------------- */

    /**
     * Save the compendium art configuration.
     * @this {ApplicationV2}
     * @param {SubmitEvent} _event         The form submission event.
     * @param {HTMLFormElement} _form      The form element that was submitted.
     * @param {FormDataExtended} formData  Processed data for the submitted form.
     */
    static async #onSubmit(_event, _form, formData) {
      await game.settings.set("core", game.compendiumArt.SETTING, foundry.utils.expandObject(formData.object));
      return SettingsConfig.reloadConfirm({ world: true });
    }
  }

  /**
   * An application for configuring the permissions which are available to each User role.
   * @extends ApplicationV2
   * @mixes HandlebarsApplication
   * @alias PermissionConfig
   */
  class PermissionConfig extends HandlebarsApplicationMixin(ApplicationV2) {

    /** @inheritDoc */
    static DEFAULT_OPTIONS = {
      id: "permissions-config",
      tag: "form",
      window: {
        contentClasses: ["standard-form"],
        icon: "fa-solid fa-shield-keyhole",
        title: "PERMISSION.Title",
      },
      position: {
        width: 660,
        height: "auto"
      },
      form: {
        closeOnSubmit: true,
        handler: PermissionConfig.#onSubmit
      },
      actions: {
        reset: PermissionConfig.#onReset
      }
    };

    /** @override */
    static PARTS = {
      permissions: {
        id: "permissions",
        template: "templates/apps/permission-config.hbs",
        scrollable: [".permissions-list"]
      },
      footer: {
        template: "templates/generic/form-footer.hbs"
      }
    };


    /* -------------------------------------------- */
    /*  Rendering                                   */
    /* -------------------------------------------- */

    /** @override */
    async _prepareContext(_options={}) {
      const current = await game.settings.get("core", "permissions");
      return {
        roles: Object.keys(CONST.USER_ROLES).reduce((obj, r) => {
          if ( r === "NONE" ) return obj;
          obj[r] = `USER.Role${r.titleCase()}`;
          return obj;
        }, {}),
        permissions: this.#preparePermissions(current),
        buttons: [
          {type: "reset", action: "reset", icon: "fa-solid fa-sync", label: "PERMISSION.Reset"},
          {type: "submit", icon: "fa-solid fa-save", label: "PERMISSION.Submit"}
        ]
      };
    }

    /* -------------------------------------------- */

    /**
     * Prepare the permissions object used to render the configuration template
     * @param {object} current      The current permission configuration
     * @returns {object[]}          Permission data for sheet rendering
     */
    #preparePermissions(current) {
      const r = CONST.USER_ROLES;
      const rgm = r.GAMEMASTER;

      // Get permissions
      const perms = Object.entries(CONST.USER_PERMISSIONS).reduce((arr, e) => {
        const perm = foundry.utils.deepClone(e[1]);
        perm.id = e[0];
        perm.label = game.i18n.localize(perm.label);
        perm.hint = game.i18n.localize(perm.hint);
        arr.push(perm);
        return arr;
      }, []);
      perms.sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang));

      // Configure permission roles
      for ( let p of perms ) {
        const roles = current[p.id] || Array.fromRange(rgm + 1).slice(p.defaultRole);
        p.roles = Object.values(r).reduce((arr, role) => {
          if ( role === r.NONE ) return arr;
          arr.push({
            name: `${p.id}.${role}`,
            value: roles.includes(role),
            readonly: (role === rgm) && (!p.disableGM) ? "readonly" : ""
          });
          return arr;
        }, []);
      }
      return perms;
    }

    /* -------------------------------------------- */
    /*  Event Listeners and Handlers                */
    /* -------------------------------------------- */

    /**
     * Handle submission
     * @this {DocumentSheetV2}                      The handler is called with the application as its bound scope
     * @param {SubmitEvent} event                   The originating form submission event
     * @param {HTMLFormElement} form                The form element that was submitted
     * @param {FormDataExtended} formData           Processed data for the submitted form
     * @returns {Promise<void>}
     */
    static async #onSubmit(event, form, formData) {
      const permissions = foundry.utils.expandObject(formData.object);
      for ( let [k, v] of Object.entries(permissions) ) {
        if ( !(k in CONST.USER_PERMISSIONS ) ) {
          delete permissions[k];
          continue;
        }
        permissions[k] = Object.entries(v).reduce((arr, r) => {
          if ( r[1] === true ) arr.push(parseInt(r[0]));
          return arr;
        }, []);
      }
      await game.settings.set("core", "permissions", permissions);
      ui.notifications.info("SETTINGS.PermissionUpdate", {localize: true});
    }

    /* -------------------------------------------- */

    /**
     * Handle click actions to reset all permissions back to their initial state.
     * @this {PermissionConfig}
     * @param {PointerEvent} event
     * @returns {Promise<void>}
     */
    static async #onReset(event) {
      event.preventDefault();
      const defaults = Object.entries(CONST.USER_PERMISSIONS).reduce((obj, [id, perm]) => {
        obj[id] = Array.fromRange(CONST.USER_ROLES.GAMEMASTER + 1).slice(perm.defaultRole);
        return obj;
      }, {});
      await game.settings.set("core", "permissions", defaults);
      ui.notifications.info("SETTINGS.PermissionReset", {localize: true});
      await this.render();
    }
  }

  var _module$8 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    CompendiumArtConfig: CompendiumArtConfig,
    PermissionConfig: PermissionConfig
  });

  /**
   * A base class for providing Actor Sheet behavior using ApplicationV2.
   */
  class ActorSheetV2 extends DocumentSheetV2 {

    /** @inheritDoc */
    static DEFAULT_OPTIONS = {
      position: {
        width: 600
      },
      window: {
        controls: [
          {
            action: "configurePrototypeToken",
            icon: "fa-solid fa-user-circle",
            label: "TOKEN.TitlePrototype",
            ownership: "OWNER"
          },
          {
            action: "showPortraitArtwork",
            icon: "fa-solid fa-image",
            label: "SIDEBAR.CharArt",
            ownership: "OWNER"
          },
          {
            action: "showTokenArtwork",
            icon: "fa-solid fa-image",
            label: "SIDEBAR.TokenArt",
            ownership: "OWNER"
          }
        ]
      },
      actions: {
        configurePrototypeToken: ActorSheetV2.#onConfigurePrototypeToken,
        showPortraitArtwork: ActorSheetV2.#onShowPortraitArtwork,
        showTokenArtwork: ActorSheetV2.#onShowTokenArtwork,
      }
    };

    /**
     * The Actor document managed by this sheet.
     * @type {ClientDocument}
     */
    get actor() {
      return this.document;
    }

    /* -------------------------------------------- */

    /**
     * If this sheet manages the ActorDelta of an unlinked Token, reference that Token document.
     * @type {TokenDocument|null}
     */
    get token() {
      return this.document.token || null;
    }

    /* -------------------------------------------- */

    /** @override */
    _getHeaderControls() {
      const controls = this.options.window.controls;

      // Portrait image
      const img = this.actor.img;
      if ( img === CONST.DEFAULT_TOKEN ) controls.findSplice(c => c.action === "showPortraitArtwork");

      // Token image
      const pt = this.actor.prototypeToken;
      const tex = pt.texture.src;
      if ( pt.randomImg || [null, undefined, CONST.DEFAULT_TOKEN].includes(tex) ) {
        controls.findSplice(c => c.action === "showTokenArtwork");
      }
      return controls;
    }

    /* -------------------------------------------- */

    async _renderHTML(context, options) {
      return `<p>TESTING</p>`;
    }

    _replaceHTML(result, content, options) {
      content.insertAdjacentHTML("beforeend", result);
    }

    /* -------------------------------------------- */
    /*  Event Listeners and Handlers                */
    /* -------------------------------------------- */

    /**
     * Handle header control button clicks to render the Prototype Token configuration sheet.
     * @this {ActorSheetV2}
     * @param {PointerEvent} event
     */
    static #onConfigurePrototypeToken(event) {
      event.preventDefault();
      const renderOptions = {
        left: Math.max(this.position.left - 560 - 10, 10),
        top: this.position.top
      };
      new CONFIG.Token.prototypeSheetClass(this.actor.prototypeToken, renderOptions).render(true);
    }

    /* -------------------------------------------- */

    /**
     * Handle header control button clicks to display actor portrait artwork.
     * @this {ActorSheetV2}
     * @param {PointerEvent} event
     */
    static #onShowPortraitArtwork(event) {
      const {img, name, uuid} = this.actor;
      new ImagePopout(img, {title: name, uuid: uuid}).render(true);
    }

    /* -------------------------------------------- */

    /**
     * Handle header control button clicks to display actor portrait artwork.
     * @this {ActorSheetV2}
     * @param {PointerEvent} event
     */
    static #onShowTokenArtwork(event) {
      const {prototypeToken, name, uuid} = this.actor;
      new ImagePopout(prototypeToken.texture.src, {title: name, uuid: uuid}).render(true);
    }
  }

  /**
   * The AmbientSound configuration application.
   * @extends DocumentSheetV2
   * @mixes HandlebarsApplication
   * @alias AmbientSoundConfig
   */
  class AmbientSoundConfig extends HandlebarsApplicationMixin(DocumentSheetV2) {

    /** @inheritDoc */
    static DEFAULT_OPTIONS = {
      classes: ["ambient-sound-config"],
      window: {
        contentClasses: ["standard-form"]
      },
      position: {
        width: 560,
        height: "auto"
      },
      form: {
        handler: this.#onSubmit,
        closeOnSubmit: true
      }
    };

    /** @override */
    static PARTS = {
      body: {
        template: "templates/scene/ambient-sound-config.hbs"
      },
      footer: {
        template: "templates/generic/form-footer.hbs"
      }
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    get title() {
      if ( !this.document.id ) return game.i18n.localize("AMBIENT_SOUND.ACTIONS.CREATE");
      return super.title;
    }

    /* -------------------------------------------- */

    /** @override */
    async _prepareContext(_options) {
      return {
        sound: this.document,
        source: this.document.toObject(),
        fields: this.document.schema.fields,
        gridUnits: this.document.parent.grid.units || game.i18n.localize("GridUnits"),
        soundEffects: CONFIG.soundEffects,
        buttons: [{
          type: "submit",
          icon: "fa-solid fa-save",
          label: game.i18n.localize(this.document.id ? "AMBIENT_SOUND.ACTIONS.UPDATE" : "AMBIENT_SOUND.ACTIONS.CREATE")
        }]
      }
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _onRender(context, options) {
      this.#toggleDisabledFields();
      return super._onRender(context, options);
    }

    /* -------------------------------------------- */

    /** @override */
    _onClose(_options) {
      if ( !this.document.id ) canvas.sounds.clearPreviewContainer();
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _onChangeForm(formConfig, event) {
      this.#toggleDisabledFields();
      return super._onChangeForm(formConfig, event);
    }

    /* -------------------------------------------- */

    /**
     * Special logic to toggle the disabled state of form fields depending on the values of other fields.
     */
    #toggleDisabledFields() {
      const form = this.element;
      form["effects.base.intensity"].disabled = !form["effects.base.type"].value;
      form["effects.muffled.type"].disabled = form.walls.checked;
      form["effects.muffled.intensity"].disabled = form.walls.checked || !form["effects.muffled.type"].value;
    }

    /* -------------------------------------------- */

    /**
     * Process form submission for the sheet.
     * @param {SubmitEvent} event                   The originating form submission event
     * @param {HTMLFormElement} form                The form element that was submitted
     * @param {FormDataExtended} formData           Processed data for the submitted form
     * @this {AmbientSoundConfig}
     * @returns {Promise<void>}
     */
    static async #onSubmit(event, form, formData) {
      const submitData = this._prepareSubmitData(event, form, formData);
      if ( this.document.id ) await this.document.update(submitData);
      else await this.document.constructor.create(submitData, {parent: canvas.scene});
    }
  }

  /**
   * The AmbientLight configuration application.
   * @extends DocumentSheetV2
   * @mixes HandlebarsApplication
   * @alias AmbientLightConfig
   */
  class AmbientLightConfig extends HandlebarsApplicationMixin(DocumentSheetV2) {

    /** @inheritDoc */
    static DEFAULT_OPTIONS = {
      classes: ["ambient-light-config"],
      window: {
        contentClasses: ["standard-form"]
      },
      position: {
        width: 560,
        height: "auto"
      },
      form: {
        handler: this.#onSubmit,
        closeOnSubmit: true
      },
      actions:{
        reset: this.#onReset
      }
    };

    /** @override */
    static PARTS = {
      tabs: {
        template: "templates/generic/tab-navigation.hbs"
      },
      basic: {
        template: "templates/scene/parts/light-basic.hbs"
      },
      animation: {
        template: "templates/scene/parts/light-animation.hbs"
      },
      advanced: {
        template: "templates/scene/parts/light-advanced.hbs"
      },
      footer: {
        template: "templates/generic/form-footer.hbs"
      }
    }

    /**
     * Maintain a copy of the original to show a real-time preview of changes.
     * @type {AmbientLightDocument}
     */
    preview;

    /** @override */
    tabGroups = {
      sheet: "basic"
    }

    /* -------------------------------------------- */
    /*  Application Rendering                       */
    /* -------------------------------------------- */

    /** @inheritDoc */
    async _preRender(context, options) {
      await super._preRender(context, options);
      if ( this.preview?.rendered ) {
        await this.preview.object.draw();
        this.document.object.initializeLightSource({deleted: true});
        this.preview.object.layer.preview.addChild(this.preview.object);
        this._previewChanges();
      }
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _onRender(context, options) {
      super._onRender(context, options);
      this.#toggleReset();
    }

    /* -------------------------------------------- */

    /** @override */
    _onClose(options) {
      super._onClose(options);
      if ( this.preview ) this._resetPreview();
      if ( this.document.rendered ) this.document.object.initializeLightSource();
    }

    /* -------------------------------------------- */

    /** @override */
    async _prepareContext(options) {

      // Create the preview on first render
      if ( options.isFirstRender && this.document.object ) {
        const clone = this.document.object.clone();
        this.preview = clone.document;
      }

      // Prepare context
      const document = this.preview ?? this.document;
      const isDarkness = document.config.negative;
      return {
        light: document,
        source: document.toObject(),
        fields: document.schema.fields,
        colorationTechniques: AdaptiveLightingShader.SHADER_TECHNIQUES,
        gridUnits: document.parent.grid.units || game.i18n.localize("GridUnits"),
        isDarkness,
        lightAnimations: isDarkness ? CONFIG.Canvas.darknessAnimations : CONFIG.Canvas.lightAnimations,
        tabs: this.#getTabs(),
        buttons: [
          {
            type: "reset",
            action: "reset",
            icon: "fa-solid fa-undo",
            label: "AMBIENT_LIGHT.ACTIONS.RESET"
          },
          {
            type: "submit",
            icon: "fa-solid fa-save",
            label: this.document.id ? "AMBIENT_LIGHT.ACTIONS.UPDATE" : "AMBIENT_LIGHT.ACTIONS.CREATE"
          }
        ]
      }
    }

    /* -------------------------------------------- */

    /**
     * Prepare an array of form header tabs.
     * @returns {Record<string, Partial<ApplicationTab>>}
     */
    #getTabs() {
      const tabs = {
        basic: {id: "basic", group: "sheet", icon: "fa-solid fa-lightbulb", label: "AMBIENT_LIGHT.SECTIONS.BASIC"},
        animation: {id: "animation", group: "sheet", icon: "fa-solid fa-play", label: "AMBIENT_LIGHT.SECTIONS.ANIMATION"},
        advanced: {id: "advanced", group: "sheet", icon: "fa-solid fa-cogs", label: "AMBIENT_LIGHT.SECTIONS.ADVANCED"}
      };
      for ( const v of Object.values(tabs) ) {
        v.active = this.tabGroups[v.group] === v.id;
        v.cssClass = v.active ? "active" : "";
      }
      return tabs;
    }

    /* -------------------------------------------- */

    /**
     * Toggle visibility of the reset button which is only visible on the advanced tab.
     */
    #toggleReset() {
      const reset = this.element.querySelector("button[data-action=reset]");
      reset.classList.toggle("hidden", this.tabGroups.sheet !== "advanced");
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    changeTab(...args) {
      super.changeTab(...args);
      this.#toggleReset();
    }

    /* -------------------------------------------- */
    /*  Real-Time Preview                           */
    /* -------------------------------------------- */

    /** @inheritDoc */
    _onChangeForm(formConfig, event) {
      super._onChangeForm(formConfig, event);
      const formData = new FormDataExtended(this.element);
      this._previewChanges(formData.object);

      // Special handling for darkness state change
      if ( event.target.name === "config.negative") this.render({parts: ["animation", "advanced"]});
    }

    /* -------------------------------------------- */

    /**
     * Preview changes to the AmbientLight document as if they were true document updates.
     * @param {object} [change]  A change to preview.
     * @protected
     */
    _previewChanges(change) {
      if ( !this.preview ) return;
      if ( change ) this.preview.updateSource(change);
      if ( this.preview?.rendered ) {
        this.preview.object.renderFlags.set({refresh: true});
        this.preview.object.initializeLightSource();
      }
    }

    /* -------------------------------------------- */

    /**
     * Restore the true data for the AmbientLight document when the form is submitted or closed.
     * @protected
     */
    _resetPreview() {
      if ( !this.preview ) return;
      if ( this.preview.rendered ) {
        this.preview.object.destroy({children: true});
      }
      this.preview = null;
      if ( this.document.rendered ) {
        const object = this.document.object;
        object.renderable = true;
        object.initializeLightSource();
        object.renderFlags.set({refresh: true});
      }
    }

    /* -------------------------------------------- */
    /*  Event Listeners and Handlers                */
    /* -------------------------------------------- */

    /**
     * Process form submission for the sheet.
     * @param {SubmitEvent} event                   The originating form submission event
     * @param {HTMLFormElement} form                The form element that was submitted
     * @param {FormDataExtended} formData           Processed data for the submitted form
     * @this {AmbientLightConfig}
     * @returns {Promise<void>}
     */
    static async #onSubmit(event, form, formData) {
      const submitData = this._prepareSubmitData(event, form, formData);
      if ( this.document.id ) await this.document.update(submitData);
      else await this.document.constructor.create(submitData, {parent: canvas.scene});
    }

    /* -------------------------------------------- */

    /**
     * Process reset button click
     * @param {PointerEvent} event                  The originating button click
     * @this {AmbientLightConfig}
     * @returns {Promise<void>}
     */
    static async #onReset(event) {
      event.preventDefault();
      const defaults = AmbientLightDocument.cleanData();
      const keys = ["vision", "config"];
      const configKeys = ["coloration", "contrast", "attenuation", "luminosity", "saturation", "shadows"];
      for ( const k in defaults ) {
        if ( !keys.includes(k) ) delete defaults[k];
      }
      for ( const k in defaults.config ) {
        if ( !configKeys.includes(k) ) delete defaults.config[k];
      }
      this._previewChanges(defaults);
      await this.render();
    }
  }

  /**
   * A base class for providing Item Sheet behavior using ApplicationV2.
   */
  class ItemSheetV2 extends DocumentSheetV2 {

    /** @inheritDoc */
    static DEFAULT_OPTIONS = {
      position: {
        width: 480
      }
    };

    /**
     * The Item document managed by this sheet.
     * @type {ClientDocument}
     */
    get item() {
      return this.document;
    }

    /**
     * The Actor instance which owns this Item, if any.
     * @type {Actor|null}
     */
    get actor() {
      return this.document.actor;
    }
  }

  /**
   * @typedef {import("../_types.mjs").FormNode} FormNode
   * @typedef {import("../_types.mjs").FormFooterButton} FormFooterButton
   */

  /**
   * The Scene Region configuration application.
   * @extends DocumentSheetV2
   * @mixes HandlebarsApplication
   * @alias RegionBehaviorConfig
   */
  class RegionBehaviorConfig extends HandlebarsApplicationMixin(DocumentSheetV2) {
    constructor(options) {
      super(options);
      this.options.window.icon = CONFIG.RegionBehavior.typeIcons[this.document.type];
    }

    /** @inheritDoc */
    static DEFAULT_OPTIONS = {
      classes: ["region-behavior-config"],
      window: {
        contentClasses: ["standard-form"],
        icon: undefined // Defined in constructor
      },
      position: {
        width: 480,
        height: "auto"
      },
      form: {
        closeOnSubmit: true
      }
    };

    /** @override */
    static PARTS = {
      form: {
        template: "templates/generic/form-fields.hbs"
      },
      footer: {
        template: "templates/generic/form-footer.hbs"
      }
    }

    /* -------------------------------------------- */
    /*  Context Preparation                         */
    /* -------------------------------------------- */

    /** @override */
    async _prepareContext(_options) {
      const doc = this.document;
      return {
        region: doc,
        source: doc._source,
        fields: this._getFields(),
        buttons: this._getButtons()
      }
    }

    /* -------------------------------------------- */

    /**
     * Prepare form field structure for rendering.
     * @returns {FormNode[]}
     */
    _getFields() {
      const doc = this.document;
      const source = doc._source;
      const fields = doc.schema.fields;
      const {events, ...systemFields} = CONFIG.RegionBehavior.dataModels[doc.type]?.schema.fields;
      const fieldsets = [];

      // Identity
      fieldsets.push({
        fieldset: true,
        legend: "BEHAVIOR.SECTIONS.identity",
        fields: [
          {field: fields.name, value: source.name}
        ]
      });

      // Status
      fieldsets.push({
        fieldset: true,
        legend: "BEHAVIOR.SECTIONS.status",
        fields: [
          {field: fields.disabled, value: source.disabled}
        ]
      });

      // Subscribed events
      if ( events ) {
        fieldsets.push({
          fieldset: true,
          legend: "BEHAVIOR.TYPES.base.SECTIONS.events",
          fields: [
            {field: events, value: source.system.events}
          ]
        });
      }

      // Other system fields
      const sf = {fieldset: true, legend: CONFIG.RegionBehavior.typeLabels[doc.type], fields: []};
      this.#addSystemFields(sf, systemFields, source);
      if ( sf.fields.length ) fieldsets.push(sf);
      return fieldsets;
    }

    /* -------------------------------------------- */

    /**
     * Recursively add system model fields to the fieldset.
     */
    #addSystemFields(fieldset, schema, source, _path="system") {
      for ( const field of Object.values(schema) ) {
        const path = `${_path}.${field.name}`;
        if ( field instanceof foundry.data.fields.SchemaField ) {
          this.#addSystemFields(fieldset, field.fields, source, path);
        }
        else if ( field.constructor.hasFormSupport ) {
          fieldset.fields.push({field, value: foundry.utils.getProperty(source, path)});
        }
      }
    }

    /* -------------------------------------------- */

    /**
     * Get footer buttons for this behavior config sheet.
     * @returns {FormFooterButton[]}
     * @protected
     */
    _getButtons() {
      return [
        {type: "submit", icon: "fa-solid fa-save", label: "BEHAVIOR.ACTIONS.update"}
      ]
    }
  }

  /**
   * @typedef {import("../_types.mjs").ApplicationTab} ApplicationTab
   * @typedef {import("../_types.mjs").FormFooterButton} FormFooterButton
   */

  /**
   * The Scene Region configuration application.
   * @extends DocumentSheetV2
   * @mixes HandlebarsApplication
   * @alias RegionConfig
   */
  class RegionConfig extends HandlebarsApplicationMixin(DocumentSheetV2) {

    /** @inheritDoc */
    static DEFAULT_OPTIONS = {
      classes: ["region-config"],
      window: {
        contentClasses: ["standard-form"],
        icon: "fa-regular fa-game-board"
      },
      position: {
        width: 480,
        height: "auto"
      },
      form: {
        closeOnSubmit: true
      },
      viewPermission: DOCUMENT_OWNERSHIP_LEVELS.OWNER,
      actions: {
        shapeCreateFromWalls: RegionConfig.#onShapeCreateFromWalls,
        shapeToggleHole: RegionConfig.#onShapeToggleHole,
        shapeMoveUp: RegionConfig.#onShapeMoveUp,
        shapeMoveDown: RegionConfig.#onShapeMoveDown,
        shapeRemove: RegionConfig.#onShapeRemove,
        behaviorCreate: RegionConfig.#onBehaviorCreate,
        behaviorDelete: RegionConfig.#onBehaviorDelete,
        behaviorEdit: RegionConfig.#onBehaviorEdit,
        behaviorToggle: RegionConfig.#onBehaviorToggle
      }
    };

    /** @override */
    static PARTS = {
      tabs: {
        template: "templates/generic/tab-navigation.hbs"
      },
      identity: {
        template: "templates/scene/parts/region-identity.hbs"
      },
      shapes: {
        template: "templates/scene/parts/region-shapes.hbs",
        scrollable: [".scrollable"]
      },
      behaviors: {
        template: "templates/scene/parts/region-behaviors.hbs",
        scrollable: [".scrollable"]
      },
      footer: {
        template: "templates/generic/form-footer.hbs"
      }
    }

    /** @override */
    tabGroups = {
      sheet: "identity"
    }

    /* -------------------------------------------- */
    /*  Context Preparation                         */
    /* -------------------------------------------- */

    /** @override */
    async _prepareContext(_options) {
      const doc = this.document;
      return {
        region: doc,
        source: doc.toObject(),
        fields: doc.schema.fields,
        tabs: this.#getTabs(),
      }
    }

    /* -------------------------------------------- */

    /** @override */
    async _preparePartContext(partId, context) {
      const doc = this.document;
      switch ( partId ) {
        case "footer":
          context.buttons = this.#getFooterButtons();
          break;
        case "behaviors":
          context.tab = context.tabs.behaviors;
          context.behaviors = doc.behaviors.map(b => ({
            id: b.id,
            name: b.name,
            typeLabel: game.i18n.localize(CONFIG.RegionBehavior.typeLabels[b.type]),
            typeIcon: CONFIG.RegionBehavior.typeIcons[b.type] || "fa-regular fa-notdef",
            disabled: b.disabled
          })).sort((a, b) => (a.disabled - b.disabled) || a.name.localeCompare(b.name, game.i18n.lang));
          break;
        case "identity":
          context.tab = context.tabs.identity;
          break;
        case "shapes":
          context.tab = context.tabs.shapes;
          break;
      }
      return context;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _onRender(context, options) {
      super._onRender(context, options);
      this.element.querySelectorAll(".region-shape").forEach(e => {
        e.addEventListener("mouseover", this.#onShapeHoverIn.bind(this));
        e.addEventListener("mouseout", this.#onShapeHoverOut.bind(this));
      });
      this.document.object?.renderFlags.set({refreshState: true});
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _onClose(options) {
      super._onClose(options);
      this.document.object?.renderFlags.set({refreshState: true});
    }

    /* -------------------------------------------- */

    /**
     * Prepare an array of form header tabs.
     * @returns {Record<string, Partial<ApplicationTab>>}
     */
    #getTabs() {
      const tabs = {
        identity: {id: "identity", group: "sheet", icon: "fa-solid fa-tag", label: "REGION.SECTIONS.identity"},
        shapes: {id: "shapes", group: "sheet", icon: "fa-solid fa-shapes", label: "REGION.SECTIONS.shapes"},
        behaviors: {id: "behaviors", group: "sheet", icon: "fa-solid fa-child-reaching", label: "REGION.SECTIONS.behaviors"}
      };
      for ( const v of Object.values(tabs) ) {
        v.active = this.tabGroups[v.group] === v.id;
        v.cssClass = v.active ? "active" : "";
      }
      return tabs;
    }

    /* -------------------------------------------- */

    /**
     * Prepare an array of form footer buttons.
     * @returns {Partial<FormFooterButton>[]}
     */
    #getFooterButtons() {
      return [
        {type: "submit", icon: "fa-solid fa-save", label: "REGION.ACTIONS.update"}
      ]
    }

    /* -------------------------------------------- */
    /*  Event Listeners and Handlers                */
    /* -------------------------------------------- */

    /**
     * Handle mouse-hover events on a shape.
     */
    #onShapeHoverIn(event) {
      event.preventDefault();
      if ( !this.document.parent.isView ) return;
      const index = this.#getControlShapeIndex(event);
      canvas.regions._highlightShape(this.document.shapes[index]);
    }

    /* -------------------------------------------- */

    /**
     * Handle mouse-unhover events for shape.
     */
    #onShapeHoverOut(event) {
      event.preventDefault();
      if ( !this.document.parent.isView ) return;
      canvas.regions._highlightShape(null);
    }

    /* -------------------------------------------- */

    /**
     * Handle button clicks to move the shape up.
     * @param {PointerEvent} event
     * @this {RegionConfig}
     */
    static async #onShapeMoveUp(event) {
      if ( this.document.shapes.length <= 1 ) return;
      const index = this.#getControlShapeIndex(event);
      if ( index === 0 ) return;
      const shapes = [...this.document.shapes];
      [shapes[index - 1], shapes[index]] = [shapes[index], shapes[index - 1]];
      await this.document.update({shapes});
    }

    /* -------------------------------------------- */

    /**
     * Handle button clicks to move the shape down.
     * @param {PointerEvent} event
     * @this {RegionConfig}
     */
    static async #onShapeMoveDown(event) {
      if ( this.document.shapes.length <= 1 ) return;
      const index = this.#getControlShapeIndex(event);
      if ( index === this.document.shapes.length - 1 ) return;
      const shapes = [...this.document.shapes];
      [shapes[index], shapes[index + 1]] = [shapes[index + 1], shapes[index]];
      await this.document.update({shapes});
    }

    /* -------------------------------------------- */

    /**
     * Handle button clicks to create shapes from the controlled walls.
     * @param {PointerEvent} event
     * @this {RegionConfig}
     */
    static async #onShapeCreateFromWalls(event) {
      event.preventDefault(); // Don't open context menu
      event.stopPropagation(); // Don't trigger other events
      if ( !canvas.ready || (event.detail > 1) ) return; // Ignore repeated clicks

      // If no walls are controlled, inform the user they need to control walls
      if ( !canvas.walls.controlled.length ) {
        if ( canvas.walls.active ) {
          ui.notifications.error("REGION.NOTIFICATIONS.NoControlledWalls", {localize: true});
        }
        else {
          canvas.walls.activate({tool: "select"});
          ui.notifications.info("REGION.NOTIFICATIONS.ControlWalls", {localize: true});
        }
        return;
      }

      // Create the shape
      const polygons = canvas.walls.identifyInteriorArea(canvas.walls.controlled);
      if ( polygons.length === 0 ) {
        ui.notifications.error("REGION.NOTIFICATIONS.EmptyEnclosedArea", {localize: true});
        return;
      }
      const shapes = polygons.map(p => new foundry.data.PolygonShapeData({points: p.points}));

      // Merge the new shape with form submission data
      const form = this.element;
      const formData = new FormDataExtended(form);
      const submitData = this._prepareSubmitData(event, form, formData);
      submitData.shapes = [...this.document._source.shapes, ...shapes];

      // Update the region
      await this.document.update(submitData);
    }

    /* -------------------------------------------- */

    /**
     * Handle button clicks to toggle the hold field of a shape.
     * @param {PointerEvent} event
     * @this {RegionConfig}
     */
    static async #onShapeToggleHole(event) {
      const index = this.#getControlShapeIndex(event);
      const shapes = this.document.shapes.map(s => s.toObject());
      shapes[index].hole = !shapes[index].hole;
      await this.document.update({shapes});
    }

    /* -------------------------------------------- */

    /**
     * Handle button clicks to remove a shape.
     * @param {PointerEvent} event
     * @this {RegionConfig}
     */
    static async #onShapeRemove(event) {
      const index = this.#getControlShapeIndex(event);
      let shapes = this.document.shapes;
      return foundry.applications.api.DialogV2.confirm({
        window: {
          title: game.i18n.localize("REGION.ACTIONS.shapeRemove")
        },
        content: `<p>${game.i18n.localize("AreYouSure")}</p>`,
        rejectClose: false,
        yes: {
          callback: () => {
            // Test that there haven't been any changes to the shapes since the dialog the button was clicked
            if ( this.document.shapes !== shapes ) return false;
            shapes = [...shapes];
            shapes.splice(index, 1);
            this.document.update({shapes});
            return true;
          }
        }
      });
    }

    /* -------------------------------------------- */

    /**
     * Get the shape index from a control button click.
     * @param {PointerEvent} event    The button-click event
     * @returns {number}              The shape index
     */
    #getControlShapeIndex(event) {
      const button = event.target;
      const li = button.closest(".region-shape");
      return Number(li.dataset.shapeIndex);
    }

    /* -------------------------------------------- */


    /**
     * Handle button clicks to create a new behavior.
     * @this {RegionConfig}
     */
    static async #onBehaviorCreate(_event) {
      await RegionBehavior.implementation.createDialog({}, {parent: this.document});
    }

    /* -------------------------------------------- */

    /**
     * Handle button clicks to delete a behavior.
     * @param {PointerEvent} event
     * @this {RegionConfig}
     */
    static async #onBehaviorDelete(event) {
      const behavior = this.#getControlBehavior(event);
      await behavior.deleteDialog();
    }

    /* -------------------------------------------- */

    /**
     * Handle button clicks to edit a behavior.
     * @param {PointerEvent} event
     * @this {RegionConfig}
     */
    static async #onBehaviorEdit(event) {
      const target = event.target;
      if ( target.closest(".region-element-name") && (event.detail !== 2) ) return; // Double-click on name
      const behavior = this.#getControlBehavior(event);
      await behavior.sheet.render(true);
    }

    /* -------------------------------------------- */

    /**
     * Handle button clicks to toggle a behavior.
     * @param {PointerEvent} event
     * @this {RegionConfig}
     */
    static async #onBehaviorToggle(event) {
      const behavior = this.#getControlBehavior(event);
      await behavior.update({disabled: !behavior.disabled});
    }

    /* -------------------------------------------- */

    /**
     * Get the RegionBehavior document from a control button click.
     * @param {PointerEvent} event    The button-click event
     * @returns {RegionBehavior}      The region behavior document
     */
    #getControlBehavior(event) {
      const button = event.target;
      const li = button.closest(".region-behavior");
      return this.document.behaviors.get(li.dataset.behaviorId);
    }
  }

  /**
   * The User configuration application.
   * @extends DocumentSheetV2
   * @mixes HandlebarsApplication
   * @alias UserConfig
   */
  class UserConfig extends HandlebarsApplicationMixin(DocumentSheetV2) {

    /** @inheritDoc */
    static DEFAULT_OPTIONS = {
      classes: ["user-config"],
      position: {
        width: 480,
        height: "auto"
      },
      actions: {
        releaseCharacter: UserConfig.#onReleaseCharacter
      },
      form: {
        closeOnSubmit: true
      }
    };

    /** @override */
    static PARTS = {
      form: {
        id: "form",
        template: "templates/sheets/user-config.hbs"
      }
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    get title() {
      return `${game.i18n.localize("PLAYERS.ConfigTitle")}: ${this.document.name}`;
    }

    /* -------------------------------------------- */

    /** @override */
    async _prepareContext(_options) {
      return {
        user: this.document,
        source: this.document.toObject(),
        fields: this.document.schema.fields,
        characterWidget: this.#characterChoiceWidget.bind(this)
      }
    }

    /* -------------------------------------------- */

    /**
     * Render the Character field as a choice between observed Actors.
     * @returns {HTMLDivElement}
     */
    #characterChoiceWidget(field, _groupConfig, inputConfig) {

      // Create the form field
      const fg = document.createElement("div");
      fg.className = "form-group stacked character";
      const ff = fg.appendChild(document.createElement("div"));
      ff.className = "form-fields";
      fg.insertAdjacentHTML("beforeend", `<p class="hint">${field.hint}</p>`);

      // Actor select
      const others = game.users.reduce((s, u) => {
        if ( u.character && !u.isSelf ) s.add(u.character.id);
        return s;
      }, new Set());

      const options = [];
      const ownerGroup = game.i18n.localize("OWNERSHIP.OWNER");
      const observerGroup = game.i18n.localize("OWNERSHIP.OBSERVER");
      for ( const actor of game.actors ) {
        if ( !actor.testUserPermission(this.document, "OBSERVER") ) continue;
        const a = {value: actor.id, label: actor.name, disabled: others.has(actor.id)};
        options.push({group: actor.isOwner ? ownerGroup : observerGroup, ...a});
      }

      const input = foundry.applications.fields.createSelectInput({...inputConfig,
        name: field.fieldPath,
        options,
        blank: "",
        sort: true
      });
      ff.appendChild(input);

      // Player character
      const c = this.document.character;
      if ( c ) {
        ff.insertAdjacentHTML("afterbegin", `<img class="avatar" src="${c.img}" alt="${c.name}">`);
        const release = `<button type="button" class="icon fa-solid fa-ban" data-action="releaseCharacter" 
                               data-tooltip="USER.SHEET.BUTTONS.RELEASE"></button>`;
        ff.insertAdjacentHTML("beforeend", release);
      }
      return fg;
    }

    /* -------------------------------------------- */

    /**
     * Handle button clicks to release the currently selected character.
     * @param {PointerEvent} event
     */
    static #onReleaseCharacter(event) {
      event.preventDefault();
      const button = event.target;
      const fields = button.parentElement;
      fields.querySelector("select[name=character]").value = "";
      fields.querySelector("img.avatar").remove();
      button.remove();
      this.setPosition({height: "auto"});
    }
  }

  var _module$7 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    ActorSheetV2: ActorSheetV2,
    AmbientLightConfig: AmbientLightConfig,
    AmbientSoundConfig: AmbientSoundConfig,
    ItemSheetV2: ItemSheetV2,
    RegionBehaviorConfig: RegionBehaviorConfig,
    RegionConfig: RegionConfig,
    UserConfig: UserConfig
  });

  /**
   * Scene Region Legend.
   * @extends ApplicationV2
   * @mixes HandlebarsApplication
   * @alias RegionLegend
   */
  class RegionLegend extends HandlebarsApplicationMixin(ApplicationV2) {

    /** @inheritDoc */
    static DEFAULT_OPTIONS = {
      id: "region-legend",
      tag: "aside",
      position: {
        width: 320,
        height: "auto"
      },
      window: {
        title: "REGION.LEGEND.title",
        icon: "fa-regular fa-game-board",
        minimizable: false
      },
      actions: {
        config: RegionLegend.#onConfig,
        control: RegionLegend.#onControl,
        create: RegionLegend.#onCreate,
        delete: RegionLegend.#onDelete,
        lock: RegionLegend.#onLock
      },
    };

    /** @override */
    static PARTS = {
      list: {
        id: "list",
        template: "templates/scene/region-legend.hbs",
        scrollable: ["ol.region-list"]
      }
    }

    /* -------------------------------------------- */

    /**
     * The currently filtered Regions.
     * @type {{bottom: number, top: number}}
     */
    #visibleRegions = new Set();

    /* -------------------------------------------- */

    /**
     * The currently viewed elevation range.
     * @type {{bottom: number, top: number}}
     */
    elevation = {bottom: -Infinity, top: Infinity};

    /* -------------------------------------------- */

    /** @type {SearchFilter} */
    #searchFilter = new SearchFilter({
      inputSelector: 'input[name="search"]',
      contentSelector: ".region-list",
      callback: this.#onSearchFilter.bind(this)
    });

    /* -------------------------------------------- */

    /**
     * Record a reference to the currently highlighted Region.
     * @type {Region|null}
     */
    #hoveredRegion = null;

    /* -------------------------------------------- */

    /** @override */
    _configureRenderOptions(options) {
      super._configureRenderOptions(options);
      if ( options.isFirstRender ) {
        options.position.left ??= ui.nav?.element[0].getBoundingClientRect().left;
        options.position.top ??= ui.controls?.element[0].getBoundingClientRect().top;
      }
    }

    /* -------------------------------------------- */

    /** @override */
    _canRender(options) {
      const rc = options.renderContext;
      if ( rc && !["createregions", "updateregions", "deleteregions"].includes(rc) ) return false;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    async _renderFrame(options) {
      const frame = await super._renderFrame(options);
      this.window.close.remove(); // Prevent closing
      return frame;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    async close(options={}) {
      if ( !options.closeKey ) return super.close(options);
      return this;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _onFirstRender(context, options) {
      super._onFirstRender(context, options);
      canvas.scene.apps[this.id] = this;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _onRender(context, options) {
      super._onRender(context, options);
      this.#searchFilter.bind(this.element);
      for ( const li of this.element.querySelectorAll(".region") ) {
        li.addEventListener("mouseover", this.#onRegionHoverIn.bind(this));
        li.addEventListener("mouseout", this.#onRegionHoverOut.bind(this));
      }
      this.element.querySelector(`input[name="elevationBottom"]`)
        .addEventListener("change", this.#onElevationBottomChange.bind(this));
      this.element.querySelector(`input[name="elevationTop"]`)
        .addEventListener("change", this.#onElevationTopChange.bind(this));
      this.#updateVisibleRegions();
    }

    /* -------------------------------------------- */

    /** @override */
    _onClose(options) {
      super._onClose(options);
      this.#visibleRegions.clear();
      this.elevation.bottom = -Infinity;
      this.elevation.top = Infinity;
      delete canvas.scene.apps[this.id];
    }

    /* -------------------------------------------- */

    /** @override */
    async _prepareContext(_options) {
      const regions = canvas.scene.regions.map(r => this.#prepareRegion(r));
      regions.sort((a, b) => a.name.localeCompare(b.name, game.i18n.lang));
      return {
        regions,
        elevation: {
          bottom: Number.isFinite(this.elevation.bottom) ? this.elevation.bottom : "",
          top: Number.isFinite(this.elevation.top) ? this.elevation.top : "",
        }
      };
    }

    /*-------------------------------------------- */

    /**
     * Prepare each Region for rendering in the legend.
     * @param {Region} region
     * @returns {object}
     */
    #prepareRegion(region) {
      const hasElv = (region.elevation.bottom !== null) || (region.elevation.top !== null);
      return {
        id: region.id,
        name: region.name,
        color: region.color.css,
        elevation: region.elevation,
        elevationLabel: hasElv ? `[${region.elevation.bottom ?? "&infin;"}, ${region.elevation.top ?? "&infin;"}]` : "",
        empty: !region.shapes.length,
        locked: region.locked,
        controlled: region.object?.controlled,
        hover: region.object?.hover,
        buttons: [
          {
            action: "config",
            icon: "fa-cogs",
            tooltip: game.i18n.localize("REGION.LEGEND.config"),
            disabled: ""
          },
          {
            action: "lock",
            icon: region.locked ? "fa-lock" : "fa-unlock",
            tooltip: game.i18n.localize(region.locked ? "REGION.LEGEND.unlock" : "REGION.LEGEND.lock"),
            disabled: ""
          },
          {
            action: "delete",
            icon: "fa-trash",
            tooltip: game.i18n.localize("REGION.LEGEND.delete"),
            disabled: region.locked ? "disabled" : ""
          }
        ]
      }
    }

    /* -------------------------------------------- */

    /**
     * Update the region list and hide regions that are not visible.
     */
    #updateVisibleRegions() {
      this.#visibleRegions.clear();
      for ( const li of this.element.querySelectorAll(".region-list > .region") ) {
        const id = li.dataset.regionId;
        const region = canvas.scene.regions.get(id);
        const hidden = !((this.#searchFilter.rgx?.test(SearchFilter.cleanQuery(region.name)) !== false)
          && (Math.max(region.object.bottom, this.elevation.bottom) <= Math.min(region.object.top, this.elevation.top)));
        if ( !hidden ) this.#visibleRegions.add(region);
        li.classList.toggle("hidden", hidden);
      }
      this.setPosition({height: "auto"});
      for ( const region of canvas.regions.placeables ) region.renderFlags.set({refreshState: true});
    }

    /* -------------------------------------------- */

    /**
     * Filter regions.
     * @param {KeyboardEvent} event   The key-up event from keyboard input
     * @param {string} query          The raw string input to the search field
     * @param {RegExp} rgx            The regular expression to test against
     * @param {HTMLElement} html      The HTML element which should be filtered
     */
    #onSearchFilter(event, query, rgx, html) {
      if ( !this.rendered ) return;
      this.#updateVisibleRegions();
    }

    /* -------------------------------------------- */

    /**
     * Handle change events of the elevation range (bottom) input.
     * @param {KeyboardEvent} event
     */
    #onElevationBottomChange(event) {
      this.elevation.bottom = Number(event.currentTarget.value || -Infinity);
      this.#updateVisibleRegions();
    }

    /* -------------------------------------------- */

    /**
     * Handle change events of the elevation range (top) input.
     * @param {KeyboardEvent} event
     */
    #onElevationTopChange(event) {
      this.elevation.top = Number(event.currentTarget.value || Infinity);
      this.#updateVisibleRegions();
    }

    /* -------------------------------------------- */

    /**
     * Is this Region visible in this RegionLegend?
     * @param {Region} region    The region
     * @returns {boolean}
     * @internal
     */
    _isRegionVisible(region) {
      if ( !this.rendered ) return true;
      return this.#visibleRegions.has(region.document);
    }

    /* -------------------------------------------- */

    /**
     * Handle mouse-in events on a region in the legend.
     * @param {PointerEvent} event
     */
    #onRegionHoverIn(event) {
      event.preventDefault();
      if ( !canvas.ready ) return;
      const li = event.currentTarget.closest(".region");
      const region = canvas.regions.get(li.dataset.regionId);
      region._onHoverIn(event, {hoverOutOthers: true, updateLegend: false});
      this.#hoveredRegion = region;
      li.classList.add("hovered");
    }

    /* -------------------------------------------- */

    /**
     * Handle mouse-out events for a region in the legend.
     * @param {PointerEvent} event
     */
    #onRegionHoverOut(event) {
      event.preventDefault();
      const li = event.currentTarget.closest(".region");
      this.#hoveredRegion?._onHoverOut(event, {updateLegend: false});
      this.#hoveredRegion = null;
      li.classList.remove("hovered");
    }

    /* -------------------------------------------- */

    /**
     * Highlight a hovered region in the legend.
     * @param {Region} region    The Region
     * @param {boolean} hover    Whether they are being hovered in or out.
     * @internal
     */
    _hoverRegion(region, hover) {
      if ( !this.rendered ) return;
      const li = this.element.querySelector(`.region[data-region-id="${region.id}"]`);
      if ( !li ) return;
      if ( hover ) li.classList.add("hovered");
      else li.classList.remove("hovered");
    }

    /* -------------------------------------------- */

    /**
     * Handle clicks to configure a Region.
     * @param {PointerEvent} event
     */
    static #onConfig(event) {
      const regionId = event.target.closest(".region").dataset.regionId;
      const region = canvas.scene.regions.get(regionId);
      region.sheet.render({force: true});
    }

    /* -------------------------------------------- */

    /**
     * Handle clicks to assume control over a Region.
     * @param {PointerEvent} event
     */
    static #onControl(event) {
      const regionId = event.target.closest(".region").dataset.regionId;
      const region = canvas.scene.regions.get(regionId);

      // Double-click = toggle sheet
      if ( event.detail === 2 ) {
        region.object.control({releaseOthers: true});
        region.sheet.render({force: true});
      }

      // Single-click = toggle control
      else if ( event.detail === 1 ) {
        if ( region.object.controlled ) region.object.release();
        else region.object.control({releaseOthers: true});
      }
    }

    /* -------------------------------------------- */

    /**
     * Handle button clicks to create a new Region.
     * @param {PointerEvent} event
     */
    static async #onCreate(event) {
      await canvas.scene.createEmbeddedDocuments("Region", [{
        name: RegionDocument.implementation.defaultName({parent: canvas.scene})
      }]);
    }

    /* -------------------------------------------- */

    /**
     * Handle clicks to delete a Region.
     * @param {PointerEvent} event
     */
    static async #onDelete(event) {
      const regionId = event.target.closest(".region").dataset.regionId;
      const region = canvas.scene.regions.get(regionId);
      await region.deleteDialog();
    }

    /* -------------------------------------------- */

    /**
     * Handle clicks to toggle the locked state of a Region.
     * @param {PointerEvent} event
     */
    static async #onLock(event) {
      const regionId = event.target.closest(".region").dataset.regionId;
      const region = canvas.scene.regions.get(regionId);
      await region.update({locked: !region.locked});
    }
  }

  var _module$6 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    RegionLegend: RegionLegend
  });

  /** @module applications */


  /**
   * A registry of currently rendered ApplicationV2 instances.
   * @type {Map<number, ApplicationV2>}
   */
  const instances = new Map();

  /**
   * Parse an HTML string, returning a processed HTMLElement or HTMLCollection.
   * A single HTMLElement is returned if the provided string contains only a single top-level element.
   * An HTMLCollection is returned if the provided string contains multiple top-level elements.
   * @param {string} htmlString
   * @returns {HTMLCollection|HTMLElement}
   */
  function parseHTML(htmlString) {
    const div = document.createElement("div");
    div.innerHTML = htmlString;
    const children = div.children;
    return children.length > 1 ? children : children[0];
  }

  var applications = /*#__PURE__*/Object.freeze({
    __proto__: null,
    api: _module$b,
    apps: _module$8,
    dice: _module$a,
    elements: _module$9,
    fields: fields,
    instances: instances,
    parseHTML: parseHTML,
    sheets: _module$7,
    types: _types$3,
    ui: _module$6
  });

  /**
   * @typedef {Object} AudioBufferCacheEntry
   * @property {string} src
   * @property {AudioBuffer} buffer
   * @property {number} size
   * @property {boolean} [locked]
   * @property {AudioBufferCacheEntry} [next]
   * @property {AudioBufferCacheEntry} [previous]
   */

  /**
   * @typedef {Object} SoundCreationOptions
   * @property {string} src                    The source URL for the audio file
   * @property {AudioContext} [context]        A specific AudioContext to attach the sound to
   * @property {boolean} [singleton=true]      Reuse an existing Sound for this source?
   * @property {boolean} [preload=false]       Begin loading the audio immediately?
   * @property {boolean} [autoplay=false]      Begin playing the audio as soon as it is ready?
   * @property {SoundPlaybackOptions} [autoplayOptions={}]  Options passed to the play method if autoplay is true
   */

  /**
   * @typedef {Object} SoundPlaybackOptions
   * @property {number} [delay=0]               A delay in seconds by which to delay playback
   * @property {number} [duration]              A limited duration in seconds for which to play
   * @property {number} [fade=0]                A duration in milliseconds over which to fade in playback
   * @property {boolean} [loop=false]           Should sound playback loop?
   * @property {number} [loopStart=0]           Seconds of the AudioBuffer when looped playback should start.
   *                                            Only works for AudioBufferSourceNode.
   * @property {number} [loopEnd]               Seconds of the Audio buffer when looped playback should restart.
   *                                            Only works for AudioBufferSourceNode.
   * @property {number} [offset=0]              An offset in seconds at which to start playback
   * @property {Function|null} [onended]        A callback function attached to the source node
   * @property {number} [volume=1.0]            The volume at which to play the sound
   */

  /**
   * @callback SoundScheduleCallback
   * @param {Sound} sound                       The Sound instance being scheduled
   * @returns {any}                             A return value of the callback is returned as the resolved value of the
   *                                            Sound#schedule promise
   */

  var _types$2 = /*#__PURE__*/Object.freeze({
    __proto__: null
  });

  /** @typedef {import("./_types.mjs").AudioBufferCacheEntry} AudioBufferCacheEntry

   /**
   * A specialized cache used for audio buffers.
   * This is an LRU cache which expires buffers from the cache once the maximum cache size is exceeded.
   * @extends {Map<string, AudioBufferCacheEntry>}
   */
  class AudioBufferCache extends Map {
    /**
     * Construct an AudioBufferCache providing a maximum disk size beyond which entries are expired.
     * @param {number} [cacheSize]    The maximum cache size in bytes. 1GB by default.
     */
    constructor(cacheSize=Math.pow(1024, 3)) {
      super();
      this.#maxSize = cacheSize;
    }

    /**
     * The maximum cache size in bytes.
     * @type {number}
     */
    #maxSize;

    /**
     * The current memory utilization in bytes.
     * @type {number}
     */
    #memorySize = 0;

    /**
     * The head of the doubly-linked list.
     * @type {AudioBufferCacheEntry}
     */
    #head;

    /**
     * The tail of the doubly-linked list
     * @type {AudioBufferCacheEntry}
     */
    #tail;

    /**
     * A string representation of the current cache utilization.
     * @type {{current: number, max: number, pct: number, currentString: string, maxString: string, pctString: string}}
     */
    get usage() {
      return {
        current: this.#memorySize,
        max: this.#maxSize,
        pct: this.#memorySize / this.#maxSize,
        currentString: foundry.utils.formatFileSize(this.#memorySize),
        maxString: foundry.utils.formatFileSize(this.#maxSize),
        pctString: `${(this.#memorySize * 100 / this.#maxSize).toFixed(2)}%`
      };
    }

    /* -------------------------------------------- */
    /*  Cache Methods                               */
    /* -------------------------------------------- */

    /**
     * Retrieve an AudioBuffer from the cache.
     * @param {string} src      The audio buffer source path
     * @returns {AudioBuffer}   The cached audio buffer, or undefined
     */
    getBuffer(src) {
      const node = super.get(src);
      let buffer;
      if ( node ) {
        buffer = node.buffer;
        if ( this.#head !== node ) this.#shift(node);
      }
      return buffer;
    }

    /* -------------------------------------------- */

    /**
     * Insert an AudioBuffer into the buffers cache.
     * @param {string} src          The audio buffer source path
     * @param {AudioBuffer} buffer  The audio buffer to insert
     * @returns {AudioBufferCache}
     */
    setBuffer(src, buffer) {
      if ( !(buffer instanceof AudioBuffer) ) {
        throw new Error("The AudioBufferCache is only used to store AudioBuffer instances");
      }
      let node = super.get(src);
      if ( node ) this.#remove(node);
      node = {src, buffer, size: buffer.length * buffer.numberOfChannels * 4, next: this.#head};
      super.set(src, node);
      this.#insert(node);
      game.audio.debug(`Cached audio buffer "${src}" | ${this}`);
      this.#expire();
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Delete an entry from the cache.
     * @param {string} src          The audio buffer source path
     * @returns {boolean}           Was the buffer deleted from the cache?
     */
    delete(src) {
      const node = super.get(src);
      if ( node ) this.#remove(node);
      return super.delete(src);
    }

    /* -------------------------------------------- */

    /**
     * Lock a buffer, preventing it from being expired even if it is least-recently-used.
     * @param {string} src              The audio buffer source path
     * @param {boolean} [locked=true]   Lock the buffer, preventing its expiration?
     */
    lock(src, locked=true) {
      const node = super.get(src);
      if ( !node ) return;
      node.locked = locked;
    }

    /* -------------------------------------------- */

    /**
     * Insert a new node into the cache, updating the linked list and cache size.
     * @param {AudioBufferCacheEntry} node    The node to insert
     */
    #insert(node) {
      if ( this.#head ) {
        this.#head.previous = node;
        this.#head = node;
      }
      else this.#head = this.#tail = node;
      this.#memorySize += node.size;
    }

    /* -------------------------------------------- */

    /**
     * Remove a node from the cache, updating the linked list and cache size.
     * @param {AudioBufferCacheEntry} node    The node to remove
     */
    #remove(node) {
      if ( node.previous ) node.previous.next = node.next;
      else this.#head = node.next;
      if ( node.next ) node.next.previous = node.previous;
      else this.#tail = node.previous;
      this.#memorySize -= node.size;
    }

    /* -------------------------------------------- */

    /**
     * Shift an accessed node to the head of the linked list.
     * @param {AudioBufferCacheEntry} node    The node to shift
     */
    #shift(node) {
      node.previous = undefined;
      node.next = this.#head;
      this.#head.previous = node;
      this.#head = node;
    }

    /* -------------------------------------------- */

    /**
     * Recursively expire entries from the cache in least-recently used order.
     * Skip expiration of any entries which are locked.
     * @param {AudioBufferCacheEntry} [node]  A node from which to start expiring. Otherwise, starts from the tail.
     */
    #expire(node) {
      if ( this.#memorySize < this.#maxSize ) return;
      node ||= this.#tail;
      if ( !node.locked ) {
        this.#remove(node);
        game.audio.debug(`Expired audio buffer ${node.src} | ${this}`);
      }
      if ( node.previous ) this.#expire(node.previous);
    }

    /* -------------------------------------------- */

    /** @override */
    toString() {
      const {currentString, maxString, pctString} = this.usage;
      return `AudioBufferCache: ${currentString} / ${maxString} (${pctString})`;
    }
  }

  /**
   * @typedef {Object} AudioTimeoutOptions
   * @property {AudioContext} [context]
   * @property {function(): any} [callback]
   */

  /**
   * A special error class used for cancellation.
   */
  class AudioTimeoutCancellation extends Error {}

  /**
   * A framework for scheduled audio events with more precise and synchronized timing than using window.setTimeout.
   * This approach creates an empty audio buffer of the desired duration played using the shared game audio context.
   * The onended event of the AudioBufferSourceNode provides a very precise way to synchronize audio events.
   * For audio timing, this is preferable because it avoids numerous issues with window.setTimeout.
   *
   * @example Using a callback function
   * ```js
   * function playForDuration(sound, duration) {
   *   sound.play();
   *   const wait = new AudioTimeout(duration, {callback: () => sound.stop()})
   * }
   * ```
   *
   * @example Using an awaited Promise
   * ```js
   * async function playForDuration(sound, duration) {
   *   sound.play();
   *   const timeout = new AudioTimeout(delay);
   *   await timeout.complete;
   *   sound.stop();
   * }
   * ```
   *
   * @example Using the wait helper
   * ```js
   * async function playForDuration(sound, duration) {
   *   sound.play();
   *   await AudioTimeout.wait(duration);
   *   sound.stop();
   * }
   * ```
   */
  class AudioTimeout {
    /**
     * Create an AudioTimeout by providing a delay and callback.
     * @param {number} delayMS                    A desired delay timing in milliseconds
     * @param {AudioTimeoutOptions} [options]     Additional options which modify timeout behavior
     */
    constructor(delayMS, {callback, context}={}) {
      if ( !(typeof delayMS === "number") ) throw new Error("Numeric timeout duration must be provided");
      this.#callback = callback;
      this.complete = new Promise((resolve, reject) => {
        this.#resolve = resolve;
        this.#reject = reject;

        // Immediately evaluated
        if ( delayMS <= 0 ) return this.end();

        // Create and play a blank AudioBuffer of the desired delay duration
        context ||= game.audio.music;
        const seconds = delayMS / 1000;
        const sampleRate = context.sampleRate;
        const buffer = new AudioBuffer({length: seconds * sampleRate, sampleRate});
        this.#sourceNode = new AudioBufferSourceNode(context, {buffer});
        this.#sourceNode.onended = this.end.bind(this);
        this.#sourceNode.start();
      })

      // The promise may get cancelled
      .catch(err => {
        if ( err instanceof AudioTimeoutCancellation ) return;
        throw err;
      });
    }

    /**
     * Is the timeout complete?
     * This can be used to await the completion of the AudioTimeout if necessary.
     * The Promise resolves to the returned value of the provided callback function.
     * @type {Promise<*>}
     */
    complete;

    /**
     * The resolution function for the wrapping Promise.
     * @type {Function}
     */
    #resolve;

    /**
     * The rejection function for the wrapping Promise.
     * @type {Function}
     */
    #reject;

    /**
     * A scheduled callback function
     * @type {Function}
     */
    #callback;

    /**
     * The source node used to maintain the timeout
     * @type {AudioBufferSourceNode}
     */
    #sourceNode;

    /* -------------------------------------------- */

    /**
     * Cancel an AudioTimeout by ending it early, rejecting its completion promise, and skipping any callback function.
     */
    cancel() {
      if ( !this.#reject ) return;
      const reject = this.#reject;
      this.#resolve = this.#reject = undefined;
      reject(new AudioTimeoutCancellation("AudioTimeout cancelled"));
      this.#sourceNode.onended = null;
      this.#sourceNode.stop();
    }

    /* -------------------------------------------- */

    /**
     * End the timeout, either on schedule or prematurely. Executing any callback function
     */
    end() {
      const resolve = this.#resolve;
      this.#resolve = this.#reject = undefined;
      resolve(this.#callback?.());
    }

    /* -------------------------------------------- */

    /**
     * Schedule a task according to some audio timeout.
     * @param {number} delayMS                  A desired delay timing in milliseconds
     * @param {AudioTimeoutOptions} [options]   Additional options which modify timeout behavior
     * @returns {Promise<void|any>}             A promise which resolves as a returned value of the callback or void
     */
    static async wait(delayMS, options) {
      const timeout = new this(delayMS, options);
      return timeout.complete;
    }
  }

  /**
   * @typedef {import("./_types.mjs").SoundPlaybackOptions} SoundPlaybackOptions
   * @typedef {import("./_types.mjs").SoundScheduleCallback} SoundScheduleCallback
   */

  /**
   * A container around an AudioNode which manages sound playback in Foundry Virtual Tabletop.
   * Each Sound is either an AudioBufferSourceNode (for short sources) or a MediaElementAudioSourceNode (for long ones).
   * This class provides an interface around both types which allows standardized control over playback.
   * @alias foundry.audio.Sound
   * @see {EventEmitterMixin}
   */
  class Sound extends EventEmitterMixin(Object) {
    /**
     * Construct a Sound by providing the source URL and other options.
     * @param {string} src                    The audio source path, either a relative path or a remote URL
     * @param {object} [options]              Additional options which configure the Sound
     * @param {AudioContext} [options.context]  A non-default audio context within which the sound should play
     * @param {boolean} [options.forceBuffer]   Force use of an AudioBufferSourceNode even if the audio duration is long
     */
    constructor(src, {context, forceBuffer=false}={}) {
      super();
      Object.defineProperties(this, {
        id: {value: Sound.#nodeId++, writable: false, enumerable: true, configurable: false},
        src: {value: src, writable: false, enumerable: true, configurable: false}
      });
      this.#context = context || game.audio.music;
      this.#forceBuffer = forceBuffer;
    }

    /**
     * The sequence of container loading states.
     * @enum {Readonly<number>}
     */
    static STATES = Object.freeze({
      FAILED: -1,
      NONE: 0,
      LOADING: 1,
      LOADED: 2,
      STARTING: 3,
      PLAYING: 4,
      PAUSED: 5,
      STOPPING: 6,
      STOPPED: 7
    });

    /**
     * The maximum duration, in seconds, for which an AudioBufferSourceNode will be used.
     * Otherwise, a MediaElementAudioSourceNode will be used.
     * @type {number}
     */
    static MAX_BUFFER_DURATION = 10 * 60;  // 10 Minutes

    /**
     * An incrementing counter used to assign each Sound a unique id.
     * @type {number}
     */
    static #nodeId = 0;

    /** @override */
    static emittedEvents = ["load", "play", "pause", "end", "stop"];

    /**
     * A unique integer identifier for this sound.
     * @type {number}
     */
    id;

    /**
     * The audio source path.
     * Either a relative path served by the running Foundry VTT game server or a remote URL.
     * @type {string}
     */
    src;

    /**
     * The audio context within which this Sound is played.
     * @type {AudioContext}
     */
    get context() {
      return this.#context;
    }
    #context;

    /**
     * When this Sound uses an AudioBuffer, this is an AudioBufferSourceNode.
     * @type {AudioBufferSourceNode}
     */
    #bufferNode;

    /**
     * When this Sound uses an HTML Audio stream, this is a MediaElementAudioSourceNode.
     * @type {MediaElementAudioSourceNode}
     */
    #mediaNode;

    /**
     * The AudioSourceNode used to control sound playback.
     * @type {AudioBufferSourceNode|MediaElementAudioSourceNode}
     */
    get sourceNode() {
      return this.#bufferNode || this.#mediaNode;
    }

    /**
     * The GainNode used to control volume for this sound.
     * @type {GainNode}
     */
    gainNode;

    /**
     * An AudioBuffer instance, if this Sound uses an AudioBufferSourceNode for playback.
     * @type {AudioBuffer|null}
     */
    buffer = null;

    /**
     * An HTMLAudioElement, if this Sound uses a MediaElementAudioSourceNode for playback.
     * @type {HTMLAudioElement|null}
     */
    element = null;

    /**
     * Playback configuration options specified at the time that Sound#play is called.
     * @type {SoundPlaybackOptions}
     */
    #playback = {
      delay: 0,
      duration: undefined,
      fade: 0,
      loop: false,
      loopStart: 0,
      loopEnd: undefined,
      offset: 0,
      onended: null,
      volume: 1.0
    };

    /**
     * Force usage of an AudioBufferSourceNode regardless of audio duration?
     * @type {boolean}
     */
    #forceBuffer = false;

    /**
     * The life-cycle state of the sound.
     * @see {Sound.STATES}
     * @type {number}
     * @protected
     */
    _state = Sound.STATES.NONE;

    /**
     * Has the audio file been loaded either fully or for streaming.
     * @type {boolean}
     */
    get loaded() {
      if ( this._state < Sound.STATES.LOADED ) return false;
      return !!(this.buffer || this.element);
    }

    /**
     * Did the audio file fail to load.
     * @type {boolean}
     */
    get failed() {
      return this._state === Sound.STATES.FAILED;
    }

    /**
     * Is this sound currently playing?
     * @type {boolean}
     */
    get playing() {
      return (this._state === Sound.STATES.STARTING) || (this._state === Sound.STATES.PLAYING);
    }

    /**
     * Does this Sound use an AudioBufferSourceNode?
     * Otherwise, the Sound uses a streamed MediaElementAudioSourceNode.
     * @type {boolean}
     */
    get isBuffer() {
      return !!this.buffer && (this.sourceNode instanceof AudioBufferSourceNode);
    }

    /**
     * A convenience reference to the GainNode gain audio parameter.
     * @type {AudioParam}
     */
    get gain() {
      return this.gainNode?.gain;
    }

    /**
     * The AudioNode destination which is the output target for the Sound.
     * @type {AudioNode}
     */
    destination;

    /**
     * Record the pipeline of nodes currently used by this Sound.
     * @type {AudioNode[]}
     */
    #pipeline = [];

    /**
     * A pipeline of AudioNode instances to be applied to Sound playback.
     * @type {AudioNode[]}
     */
    effects = [];

    /**
     * The currently playing volume of the sound.
     * Undefined until playback has started for the first time.
     * @type {number}
     */
    get volume() {
      return this.gain?.value;
    }

    set volume(value) {
      if ( !this.gainNode || !Number.isFinite(value) ) return;
      const ct = this.#context.currentTime;
      this.gain.cancelScheduledValues(ct);
      this.gain.value = value;
      this.gain.setValueAtTime(value, ct); // Immediately schedule the new value
    }

    /**
     * The time in seconds at which playback was started.
     * @type {number}
     */
    startTime;

    /**
     * The time in seconds at which playback was paused.
     * @type {number}
     */
    pausedTime;

    /**
     * The total duration of the audio source in seconds.
     * @type {number}
     */
    get duration() {
      if ( this._state < Sound.STATES.LOADED ) return undefined;
      if ( this.buffer ) {
        const {loop, loopStart, loopEnd} = this.#playback;
        if ( loop && Number.isFinite(loopStart) && Number.isFinite(loopEnd) ) return loopEnd - loopStart;
        return this.buffer.duration;
      }
      return this.element?.duration;
    }

    /**
     * The current playback time of the sound.
     * @type {number}
     */
    get currentTime() {
      if ( !this.playing ) return undefined;
      if ( this.pausedTime ) return this.pausedTime;
      let time = this.#context.currentTime - this.startTime;
      if ( Number.isFinite(this.duration) ) time %= this.duration;
      return time;
    }

    /**
     * Is the sound looping?
     * @type {boolean}
     */
    get loop() {
      return this.#playback.loop;
    }

    set loop(value) {
      const loop = this.#playback.loop = Boolean(value);
      if ( this.#bufferNode ) this.#bufferNode.loop = loop;
      else if ( this.element ) this.element.loop = loop;
    }

    /**
     * A set of scheduled events orchestrated using the Sound#schedule function.
     * @type {Set<AudioTimeout>}
     */
    #scheduledEvents = new Set();

    /**
     * An operation in progress on the sound which must be queued.
     * @type {Promise}
     */
    #operation;

    /**
     * A delay timeout before the sound starts or stops.
     * @type {AudioTimeout}
     */
    #delay;

    /**
     * An internal reference to some object which is managing this Sound instance.
     * @type {Object|null}
     * @internal
     */
    _manager = null;

    /* -------------------------------------------- */
    /*  Life-Cycle Methods                          */
    /* -------------------------------------------- */

    /**
     * Load the audio source and prepare it for playback, either using an AudioBuffer or a streamed HTMLAudioElement.
     * @param {object} [options={}]   Additional options which affect resource loading
     * @param {boolean} [options.autoplay=false]  Automatically begin playback of the sound once loaded
     * @param {SoundPlaybackOptions} [options.autoplayOptions]  Playback options passed to Sound#play, if autoplay
     * @returns {Promise<Sound>}      A Promise which resolves to the Sound once it is loaded
     */
    async load({autoplay=false, autoplayOptions={}}={}) {
      const {STATES} = Sound;

      // Await audio unlock
      if ( game.audio.locked ) {
        game.audio.debug(`Delaying load of sound "${this.src}" until after first user gesture`);
        await game.audio.unlock;
      }

      // Wait for another ongoing operation
      if ( this.#operation ) {
        await this.#operation;
        return this.load({autoplay, autoplayOptions});
      }

      // Queue loading
      if ( !this.loaded ) {
        this._state = STATES.LOADING;
        this.#context ||= game.audio.music;
        try {
          this.#operation = this._load();
          await this.#operation;
          this._state = STATES.LOADED;
          this.dispatchEvent(new Event("load"));
        } catch(err) {
          console.error(err);
          this._state = STATES.FAILED;
        }
        finally {
          this.#operation = undefined;
        }
      }

      // Autoplay after load
      if ( autoplay && !this.failed && !this.playing ) {
        // noinspection ES6MissingAwait
        this.play(autoplayOptions);
      }
      return this;
    }

    /* -------------------------------------------- */

    /**
     * An inner method which handles loading so that it can be de-duplicated under a single shared Promise resolution.
     * This method is factored out to allow for subclasses to override loading behavior.
     * @returns {Promise<void>}                       A Promise which resolves once the sound is loaded
     * @throws {Error}                                An error if loading failed for any reason
     * @protected
     */
    async _load() {

      // Attempt to load a cached AudioBuffer
      this.buffer = game.audio.buffers.getBuffer(this.src) || null;
      this.element = null;

      // Otherwise, load the audio as an HTML5 audio element to learn its playback duration
      if ( !this.buffer ) {
        const element = await this.#createAudioElement();
        const isShort = (element?.duration || Infinity) <= Sound.MAX_BUFFER_DURATION;

        // For short sounds create and cache the audio buffer and use an AudioBufferSourceNode
        if ( isShort || this.#forceBuffer ) {
          this.buffer = await this.#createAudioBuffer();
          game.audio.buffers.setBuffer(this.src, this.buffer);
          Sound.#unloadAudioElement(element);
        }
        else this.element = element;
      }
    }

    /* -------------------------------------------- */

    /**
     * Begin playback for the Sound.
     * This method is asynchronous because playback may not start until after an initially provided delay.
     * The Promise resolves *before* the fade-in of any configured volume transition.
     * @param {SoundPlaybackOptions} [options]  Options which configure the beginning of sound playback
     * @returns {Promise<Sound>}                A Promise which resolves once playback has started (excluding fade)
     */
    async play(options={}) {

      // Signal our intention to start immediately
      const {STATES} = Sound;
      if ( ![STATES.LOADED, STATES.PAUSED, STATES.STOPPED].includes(this._state) ) return this;
      this._state = STATES.STARTING;

      // Wait for another ongoing operation
      if ( this.#operation ) {
        await this.#operation;
        return this.play(options);
      }

      // Configure options
      if ( typeof options === "number" ) {
        options = {offset: options};
        if ( arguments[1] instanceof Function ) options.onended = arguments[1];
        foundry.utils.logCompatibilityWarning("Sound#play now takes an object of playback options instead of "
          + "positional arguments.", {since: 12, until: 14});
      }

      // Queue playback
      try {
        this.#operation = this.#queuePlay(options);
        await this.#operation;
        this._state = STATES.PLAYING;
      } finally {
        this.#operation = undefined;
      }
      return this;
    }

    /* -------------------------------------------- */

    /**
     * An inner method that is wrapped in an enqueued promise. See {@link Sound#play}.
     */
    async #queuePlay(options={}) {

      // Configure playback
      this.#configurePlayback(options);
      const {delay, fade, offset, volume} = this.#playback;

      // Create the audio pipeline including gainNode and sourceNode used for playback
      this._createNodes();
      this._connectPipeline();

      // Delay playback start
      if ( delay ) {
        await this.wait(delay * 1000);
        if ( this._state !== Sound.STATES.STARTING ) return; // We may no longer be starting if the delay was cancelled
      }

      // Begin playback
      this._play();

      // Record state change
      this.startTime = this.#context.currentTime - offset;
      this.pausedTime = undefined;

      // Set initial volume
      this.volume = fade ? 0 : volume;
      if ( fade ) this.fade(volume, {duration: fade});
      this.#onStart();
    }

    /* -------------------------------------------- */

    /**
     * Begin playback for the configured pipeline and playback options.
     * This method is factored out so that subclass implementations of Sound can implement alternative behavior.
     * @protected
     */
    _play() {
      const {loop, loopStart, loopEnd, offset, duration} = this.#playback;
      if ( this.buffer ) {
        this.#bufferNode.loop = loop;
        if ( loop && Number.isFinite(loopStart) && Number.isFinite(loopEnd) ) {
          this.#bufferNode.loopStart = loopStart;
          this.#bufferNode.loopEnd = loopEnd;
        }
        this.#bufferNode.onended = this.#onEnd.bind(this);
        this.#bufferNode.start(0, offset, duration);
      }
      else if ( this.element ) {
        this.element.loop = loop;
        this.element.currentTime = offset;
        this.element.onended = this.#onEnd.bind(this);
        this.element.play();
      }
      game.audio.debug(`Beginning playback of Sound "${this.src}"`);
    }

    /* -------------------------------------------- */

    /**
     * Pause playback of the Sound.
     * For AudioBufferSourceNode this stops playback after recording the current time.
     * Calling Sound#play will resume playback from the pausedTime unless some other offset is passed.
     * For a MediaElementAudioSourceNode this simply calls the HTMLAudioElement#pause method directly.
     */
    pause() {
      const {STATES} = Sound;
      if ( this._state !== STATES.PLAYING ) {
        throw new Error("You may only call Sound#pause for a Sound which is PLAYING");
      }
      this._pause();
      this.pausedTime = this.currentTime;
      this._state = STATES.PAUSED;
      this.#onPause();
    }

    /* -------------------------------------------- */

    /**
     * Pause playback of the Sound.
     * This method is factored out so that subclass implementations of Sound can implement alternative behavior.
     * @protected
     */
    _pause() {
      if ( this.isBuffer ) {
        this.#bufferNode.onended = undefined;
        this.#bufferNode.stop(0);
      }
      else this.element.pause();
      game.audio.debug(`Pausing playback of Sound "${this.src}"`);
    }

    /* -------------------------------------------- */

    /**
     * Stop playback for the Sound.
     * This method is asynchronous because playback may not stop until after an initially provided delay.
     * The Promise resolves *after* the fade-out of any configured volume transition.
     * @param {SoundPlaybackOptions} [options]  Options which configure the stopping of sound playback
     * @returns {Promise<Sound>}                A Promise which resolves once playback is fully stopped (including fade)
     */
    async stop(options={}) {

      // Signal our intention to stop immediately
      if ( !this.playing ) return this;
      this._state = Sound.STATES.STOPPING;
      this.#delay?.cancel();

      // Wait for another operation to conclude
      if ( this.#operation ) {
        await this.#operation;
        return this.stop(options);
      }

      // Queue stop
      try {
        this.#operation = this.#queueStop(options);
        await this.#operation;
        this._state = Sound.STATES.STOPPED;
      } finally {
        this.#operation = undefined;
      }
      return this;
    }

    /* -------------------------------------------- */

    /**
     * An inner method that is wrapped in an enqueued promise. See {@link Sound#stop}.
     */
    async #queueStop(options) {

      // Immediately disconnect the onended callback
      if ( this.#bufferNode ) this.#bufferNode.onended = undefined;
      if ( this.#mediaNode ) this.element.onended = undefined;

      // Configure playback settings
      this.#configurePlayback(options);
      const {delay, fade, volume} = this.#playback;

      // Fade out
      if ( fade ) await this.fade(volume, {duration: fade});
      else this.volume = volume;

      // Stop playback
      if ( delay ) {
        await this.wait(delay * 1000);
        if ( this._state !== Sound.STATES.STOPPING ) return; // We may no longer be stopping if the delay was cancelled
      }
      this._stop();

      // Disconnect the audio pipeline
      this._disconnectPipeline();

      // Record state changes
      this.#bufferNode = this.#mediaNode = undefined;
      this.startTime = this.pausedTime = undefined;
      this.#onStop();
    }

    /* -------------------------------------------- */

    /**
     * Stop playback of the Sound.
     * This method is factored out so that subclass implementations of Sound can implement alternative behavior.
     * @protected
     */
    _stop() {
      this.gain.cancelScheduledValues(this.context.currentTime);
      if ( this.buffer && this.sourceNode && (this._state === Sound.STATES.PLAYING) ) this.#bufferNode.stop(0);
      else if ( this.element ) {
        Sound.#unloadAudioElement(this.element);
        this.element = null;
      }
      game.audio.debug(`Stopping playback of Sound "${this.src}"`);
    }

    /* -------------------------------------------- */

    /**
     * Fade the volume for this sound between its current level and a desired target volume.
     * @param {number} volume                     The desired target volume level between 0 and 1
     * @param {object} [options={}]               Additional options that configure the fade operation
     * @param {number} [options.duration=1000]      The duration of the fade effect in milliseconds
     * @param {number} [options.from]               A volume level to start from, the current volume by default
     * @param {string} [options.type=linear]        The type of fade easing, "linear" or "exponential"
     * @returns {Promise<void>}                   A Promise that resolves after the requested fade duration
     */
    async fade(volume, {duration=1000, from, type="linear"}={}) {
      if ( !this.gain ) return;
      const ramp = this.gain[`${type}RampToValueAtTime`];
      if ( !ramp ) throw new Error(`Invalid fade type ${type} requested`);

      // Cancel any other ongoing transitions
      const startTime = this.#context.currentTime;
      this.gain.cancelScheduledValues(startTime);

      // Immediately schedule the starting volume
      from ??= this.gain.value;
      this.gain.setValueAtTime(from, startTime);

      // Ramp to target volume
      ramp.call(this.gain, volume, startTime + (duration / 1000));

      // Wait for the transition
      if ( volume !== from ) await this.wait(duration);
    }

    /* -------------------------------------------- */

    /**
     * Wait a certain scheduled duration within this sound's own AudioContext.
     * @param {number} duration                   The duration to wait in milliseconds
     * @returns {Promise<void>}                   A promise which resolves after the waited duration
     */
    async wait(duration) {
      this.#delay = new AudioTimeout(duration, {context: this.#context});
      await this.#delay.complete;
      this.#delay = undefined;
    }

    /* -------------------------------------------- */

    /**
     * Schedule a function to occur at the next occurrence of a specific playbackTime for this Sound.
     * @param {SoundScheduleCallback} fn  A function that will be called with this Sound as its single argument
     * @param {number} playbackTime       The desired playback time at which the function should be called
     * @returns {Promise<any>}            A Promise which resolves to the returned value of the provided function once
     *                                    it has been evaluated.
     *
     * @example Schedule audio playback changes
     * ```js
     * sound.schedule(() => console.log("Do something exactly 30 seconds into the track"), 30);
     * sound.schedule(() => console.log("Do something next time the track loops back to the beginning"), 0);
     * sound.schedule(() => console.log("Do something 5 seconds before the end of the track"), sound.duration - 5);
     * ```
     */
    async schedule(fn, playbackTime) {

      // Determine the amount of time until the next occurrence of playbackTime
      const {currentTime, duration} = this;
      playbackTime = Math.clamp(playbackTime, 0, duration);
      if ( this.#playback.loop && Number.isFinite(duration) ) {
        while ( playbackTime < currentTime ) playbackTime += duration;
      }
      const deltaMS = Math.max(0, (playbackTime - currentTime) * 1000);

      // Wait for an AudioTimeout completion then invoke the scheduled function
      const timeout = new AudioTimeout(deltaMS, {context: this.#context});
      this.#scheduledEvents.add(timeout);
      try {
        await timeout.complete;
        return fn(this);
      }
      catch {}
      finally {
        this.#scheduledEvents.delete(timeout);
      }
    }

    /* -------------------------------------------- */

    /**
     * Update the array of effects applied to a Sound instance.
     * Optionally a new array of effects can be assigned. If no effects are passed, the current effects are re-applied.
     * @param {AudioNode[]} [effects]     An array of AudioNode effects to apply
     */
    applyEffects(effects) {
      if ( Array.isArray(effects) ) this.effects = effects;
      this._disconnectPipeline();
      this._connectPipeline();
      game.audio.debug(`Applied effects to Sound "${this.src}": ${this.effects.map(e => e.constructor.name)}`);
    }

    /* -------------------------------------------- */
    /*  Playback Events                             */
    /* -------------------------------------------- */

    /**
     * Additional workflows when playback of the Sound begins.
     */
    #onStart() {
      game.audio.playing.set(this.id, this); // Track playing sounds
      this.dispatchEvent(new Event("play"));
    }

    /* -------------------------------------------- */

    /**
     * Additional workflows when playback of the Sound is paused.
     */
    #onPause() {
      this.#cancelScheduledEvents();
      this.dispatchEvent(new Event("pause"));
    }

    /* -------------------------------------------- */

    /**
     * Additional workflows when playback of the Sound concludes.
     * This is called by the AudioNode#onended callback.
     */
    async #onEnd() {
      await this.stop();
      this.#playback.onended?.(this);
      this.dispatchEvent(new Event("end"));
    }

    /* -------------------------------------------- */

    /**
     * Additional workflows when playback of the Sound is stopped, either manually or by concluding its playback.
     */
    #onStop() {
      game.audio.playing.delete(this.id);
      this.#cancelScheduledEvents();
      this.dispatchEvent(new Event("stop"));
    }

    /* -------------------------------------------- */
    /*  Helper Methods                              */
    /* -------------------------------------------- */

    /**
     * Create an HTML5 Audio element which has loaded the metadata for the provided source.
     * @returns {Promise<HTMLAudioElement>}     A created HTML Audio element
     * @throws {Error}                          An error if audio element creation failed
     */
    async #createAudioElement() {
      game.audio.debug(`Loading audio element "${this.src}"`);
      return new Promise((resolve, reject) => {
        const element = new Audio();
        element.autoplay = false;
        element.crossOrigin = "anonymous";
        element.preload = "metadata";
        element.onloadedmetadata = () => resolve(element);
        element.onerror = () => reject(`Failed to load audio element "${this.src}"`);
        element.src = this.src;
      });
    }

    /* -------------------------------------------- */

    /**
     * Ensure to safely unload a media stream
     * @param {HTMLAudioElement} element      The audio element to unload
     */
    static #unloadAudioElement(element) {
      element.onended = undefined;
      element.pause();
      element.src = "";
      element.remove();
    }

    /* -------------------------------------------- */

    /**
     * Load an audio file and decode it to create an AudioBuffer.
     * @returns {Promise<AudioBuffer>}        A created AudioBuffer
     * @throws {Error}                        An error if buffer creation failed
     */
    async #createAudioBuffer() {
      game.audio.debug(`Loading audio buffer "${this.src}"`);
      try {
        const response = await foundry.utils.fetchWithTimeout(this.src);
        const arrayBuffer = await response.arrayBuffer();
        return this.#context.decodeAudioData(arrayBuffer);
      } catch(err) {
        err.message = `Failed to load audio buffer "${this.src}"`;
        throw err;
      }
    }

    /* -------------------------------------------- */

    /**
     * Create any AudioNode instances required for playback of this Sound.
     * @protected
     */
    _createNodes() {
      this.gainNode ||= this.#context.createGain();
      this.destination ||= (this.#context.gainNode ?? this.#context.destination); // Prefer a context gain if present
      const {buffer, element: mediaElement} = this;
      if ( buffer ) this.#bufferNode = new AudioBufferSourceNode(this.#context, {buffer});
      else if ( mediaElement ) this.#mediaNode = new MediaElementAudioSourceNode(this.#context, {mediaElement});
    }

    /* -------------------------------------------- */

    /**
     * Create the audio pipeline used to play this Sound.
     * The GainNode is reused each time to link volume changes across multiple playbacks.
     * The AudioSourceNode is re-created every time that Sound#play is called.
     * @protected
     */
    _connectPipeline() {
      if ( !this.sourceNode ) return;
      this.#pipeline.length = 0;

      // Start with the sourceNode
      let node = this.sourceNode;
      this.#pipeline.push(node);

      // Connect effect nodes
      for ( const effect of this.effects ) {
        node.connect(effect);
        effect.onConnectFrom?.(node);  // Special behavior to inform the effect node it has been connected
        node = effect;
        this.#pipeline.push(effect);
      }

      // End with the gainNode
      node.connect(this.gainNode);
      this.#pipeline.push(this.gainNode);
      this.gainNode.connect(this.destination);
    }

    /* -------------------------------------------- */

    /**
     * Disconnect the audio pipeline once playback is stopped.
     * Walk backwards along the Sound##pipeline from the Sound#destination, disconnecting each node.
     * @protected
     */
    _disconnectPipeline() {
      for ( let i=this.#pipeline.length-1; i>=0; i-- ) {
        const node = this.#pipeline[i];
        node.disconnect();
      }
    }

    /* -------------------------------------------- */

    /**
     * Configure playback parameters for the Sound.
     * @param {SoundPlaybackOptions}    Provided playback options
     */
    #configurePlayback({delay, duration, fade, loop, loopStart, loopEnd, offset, onended, volume}={}) {

      // Some playback options only update if they are explicitly passed
      this.#playback.loop = loop ?? this.#playback.loop;
      this.#playback.loopStart = loopStart ?? this.#playback.loopStart;
      this.#playback.loopEnd = loopEnd ?? this.#playback.loopEnd;
      this.#playback.volume = volume ?? this.#playback.volume;
      this.#playback.onended = onended !== undefined ? onended : this.#playback.onended;

      // Determine playback offset and duration timing
      const loopTime = (this.#playback.loopEnd ?? Infinity) - this.#playback.loopStart;

      // Starting offset
      offset ??= this.#playback.loopStart;
      if ( Number.isFinite(this.pausedTime) ) offset += this.pausedTime;

      // Loop forever
      if ( this.#playback.loop ) duration ??= undefined;

      // Play once
      else if ( Number.isFinite(loopTime) ) {
        offset = Math.clamp(offset, this.#playback.loopStart, this.#playback.loopEnd);
        duration ??= loopTime;
        duration = Math.min(duration, loopTime);
      }

      // Some playback options reset unless they are explicitly passed
      this.#playback.delay = delay ?? 0;
      this.#playback.offset = offset;
      this.#playback.duration = duration;
      this.#playback.fade = fade ?? 0;
    }

    /* -------------------------------------------- */

    /**
     * Cancel any scheduled events which have not yet occurred.
     */
    #cancelScheduledEvents() {
      for ( const timeout of this.#scheduledEvents ) timeout.cancel();
      this.#scheduledEvents.clear();
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    static get LOAD_STATES() {
      foundry.utils.logCompatibilityWarning("AudioContainer.LOAD_STATES is deprecated in favor of Sound.STATES",
        {since: 12, until: 14});
      return this.STATES;
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    get loadState() {
      foundry.utils.logCompatibilityWarning("AudioContainer#loadState is deprecated in favor of Sound#_state",
        {since: 12, until: 14});
      return this._state;
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    get container() {
      foundry.utils.logCompatibilityWarning("Sound#container is deprecated without replacement because the Sound and "
        + "AudioContainer classes are now merged", {since: 12, until: 14});
      return this;
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    get node() {
      foundry.utils.logCompatibilityWarning("Sound#node is renamed Sound#sourceNode", {since: 12, until: 14});
      return this.sourceNode;
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    on(eventName, fn, {once=false}={}) {
      foundry.utils.logCompatibilityWarning("Sound#on is deprecated in favor of Sound#addEventListener",
        {since: 12, until: 14});
      return this.addEventListener(eventName, fn, {once});
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    off(eventName, fn) {
      foundry.utils.logCompatibilityWarning("Sound#off is deprecated in favor of Sound#removeEventListener",
        {since: 12, until: 14});
      return this.removeEventListener(eventName, fn);
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    emit(eventName) {
      foundry.utils.logCompatibilityWarning("Sound#emit is deprecated in favor of Sound#dispatchEvent",
        {since: 12, until: 14});
      const event = new Event(eventName, {cancelable: true});
      return this.dispatchEvent(event);
    }
  }

  /**
   * @typedef {import("./_types.mjs").SoundCreationOptions} SoundCreationOptions
   */

  /**
   * A helper class to provide common functionality for working with the Web Audio API.
   * https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API
   * A singleton instance of this class is available as game#audio.
   * @see Game#audio
   * @alias game.audio
   */
  class AudioHelper {
    constructor() {
      if ( game.audio instanceof this.constructor ) {
        throw new Error("You may not re-initialize the singleton AudioHelper. Use game.audio instead.");
      }
      this.unlock = this.awaitFirstGesture();
    }

    /**
     * The Native interval for the AudioHelper to analyse audio levels from streams
     * Any interval passed to startLevelReports() would need to be a multiple of this value.
     * @type {number}
     */
    static levelAnalyserNativeInterval = 50;

    /**
     * The cache size threshold after which audio buffers will be expired from the cache to make more room.
     * 1 gigabyte, by default.
     */
    static THRESHOLD_CACHE_SIZE_BYTES = Math.pow(1024, 3);

    /**
     * Audio Context singleton used for analysing audio levels of each stream
     * Only created if necessary to listen to audio streams.
     * @type {AudioContext}
     */
    static #analyzerContext;

    /**
     * The set of singleton Sound instances which are shared across multiple uses of the same sound path.
     * @type {Map<string,WeakRef<Sound>>}
     */
    sounds = new Map();

    /**
     * Get a map of the Sound objects which are currently playing.
     * @type {Map<number,Sound>}
     */
    playing = new Map();

    /**
     * A user gesture must be registered before audio can be played.
     * This Array contains the Sound instances which are requested for playback prior to a gesture.
     * Once a gesture is observed, we begin playing all elements of this Array.
     * @type {Function[]}
     * @see Sound
     */
    pending = [];

    /**
     * A Promise which resolves once the game audio API is unlocked and ready to use.
     * @type {Promise<void>}
     */
    unlock;

    /**
     * A flag for whether video playback is currently locked by awaiting a user gesture
     * @type {boolean}
     */
    locked = true;

    /**
     * A singleton audio context used for playback of music.
     * @type {AudioContext}
     */
    music;

    /**
     * A singleton audio context used for playback of environmental audio.
     * @type {AudioContext}
     */
    environment;

    /**
     * A singleton audio context used for playback of interface sounds and effects.
     * @type {AudioContext}
     */
    interface;

    /**
     * For backwards compatibility, AudioHelper#context refers to the context used for music playback.
     * @type {AudioContext}
     */
    get context() {
      return this.music;
    }

    /**
     * Interval ID as returned by setInterval for analysing the volume of streams
     * When set to 0, means no timer is set.
     * @type {number}
     */
    #analyserInterval;

    /**
     * A singleton cache used for audio buffers.
     * @type {AudioBufferCache}
     */
    buffers = new AudioBufferCache(AudioHelper.THRESHOLD_CACHE_SIZE_BYTES);

    /**
     * Map of all streams that we listen to for determining the decibel levels.
     * Used for analyzing audio levels of each stream.
     * @type {Record<string, {stream: MediaStream, analyser: AnalyserNode, interval: number, callback: Function}>}
     */
    #analyserStreams = {};

    /**
     * Fast Fourier Transform Array.
     * Used for analysing the decibel level of streams. The array is allocated only once
     * then filled by the analyser repeatedly. We only generate it when we need to listen to
     * a stream's level, so we initialize it to null.
     * @type {Float32Array}
     */
    #fftArray = null;

    /* -------------------------------------------- */

    /**
     * Create a Sound instance for a given audio source URL
     * @param {SoundCreationOptions} options        Sound creation options
     * @returns {Sound}
     */
    create({src, context, singleton=true, preload=false, autoplay=false, autoplayOptions={}}) {
      let sound;

      // Share singleton sounds across multiple use cases
      if ( singleton ) {
        const ref = this.sounds.get(src);
        sound = ref?.deref();
        if ( !sound ) {
          sound = new Sound(src, {context});
          this.sounds.set(src, new WeakRef(sound));
        }
      }

      // Create an independent sound instance
      else sound = new Sound(src, {context});

      // Preload or autoplay
      if ( preload && !sound.loaded ) sound.load({autoplay, autoplayOptions});
      else if ( autoplay ) sound.play(autoplayOptions);
      return sound;
    }

    /* -------------------------------------------- */

    /**
     * Test whether a source file has a supported audio extension type
     * @param {string} src      A requested audio source path
     * @returns {boolean}       Does the filename end with a valid audio extension?
     */
    static hasAudioExtension(src) {
      let rgx = new RegExp(`(\\.${Object.keys(CONST.AUDIO_FILE_EXTENSIONS).join("|\\.")})(\\?.*)?`, "i");
      return rgx.test(src);
    }

    /* -------------------------------------------- */

    /**
     * Given an input file path, determine a default name for the sound based on the filename
     * @param {string} src      An input file path
     * @returns {string}        A default sound name for the path
     */
    static getDefaultSoundName(src) {
      const parts = src.split("/").pop().split(".");
      parts.pop();
      let name = decodeURIComponent(parts.join("."));
      return name.replace(/[-_.]/g, " ").titleCase();
    }

    /* -------------------------------------------- */

    /**
     * Play a single Sound by providing its source.
     * @param {string} src            The file path to the audio source being played
     * @param {object} [options]      Additional options which configure playback
     * @param {AudioContext} [options.context]  A specific AudioContext within which to play
     * @returns {Promise<Sound>}      The created Sound which is now playing
     */
    async play(src, {context, ...options}={}) {
      const sound = new Sound(src, {context});
      await sound.load();
      sound.play(options);
      return sound;
    }

    /* -------------------------------------------- */

    /**
     * Register an event listener to await the first mousemove gesture and begin playback once observed.
     * @returns {Promise<void>}       The unlocked audio context
     */
    async awaitFirstGesture() {
      if ( !this.locked ) return;
      await new Promise(resolve => {
        for ( let eventName of ["contextmenu", "auxclick", "pointerdown", "pointerup", "keydown"] ) {
          document.addEventListener(eventName, event => this._onFirstGesture(event, resolve), {once: true});
        }
      });
    }

    /* -------------------------------------------- */

    /**
     * Request that other connected clients begin preloading a certain sound path.
     * @param {string} src          The source file path requested for preload
     * @returns {Promise<Sound>}    A Promise which resolves once the preload is complete
     */
    preload(src) {
      if ( !src || !AudioHelper.hasAudioExtension(src) ) {
        throw new Error(`Invalid audio source path ${src} provided for preload request`);
      }
      game.socket.emit("preloadAudio", src);
      return this.constructor.preloadSound(src);
    }

    /* -------------------------------------------- */
    /*  Settings and Volume Controls                */
    /* -------------------------------------------- */

    /**
     * Register client-level settings for global volume controls.
     */
    static registerSettings() {

      // Playlist Volume
      game.settings.register("core", "globalPlaylistVolume", {
        name: "Global Playlist Volume",
        hint: "Define a global playlist volume modifier",
        scope: "client",
        config: false,
        type: new foundry.data.fields.AlphaField({required: true, initial: 0.5}),
        onChange: AudioHelper.#onChangeMusicVolume
      });

      // Ambient Volume
      game.settings.register("core", "globalAmbientVolume", {
        name: "Global Ambient Volume",
        hint: "Define a global ambient volume modifier",
        scope: "client",
        config: false,
        type: new foundry.data.fields.AlphaField({required: true, initial: 0.5}),
        onChange: AudioHelper.#onChangeEnvironmentVolume
      });

      // Interface Volume
      game.settings.register("core", "globalInterfaceVolume", {
        name: "Global Interface Volume",
        hint: "Define a global interface volume modifier",
        scope: "client",
        config: false,
        type: new foundry.data.fields.AlphaField({required: true, initial: 0.5}),
        onChange: AudioHelper.#onChangeInterfaceVolume
      });
    }

    /* -------------------------------------------- */

    /**
     * Handle changes to the global music volume slider.
     * @param {number} volume
     */
    static #onChangeMusicVolume(volume) {
      volume = Math.clamp(volume, 0, 1);
      const ctx = game.audio.music;
      if ( !ctx ) return;
      ctx.gainNode.gain.setValueAtTime(volume, ctx.currentTime);
      ui.playlists?.render();
      Hooks.callAll("globalPlaylistVolumeChanged", volume);
    }

    /* -------------------------------------------- */

    /**
     * Handle changes to the global environment volume slider.
     * @param {number} volume
     */
    static #onChangeEnvironmentVolume(volume) {
      volume = Math.clamp(volume, 0, 1);
      const ctx = game.audio.environment;
      if ( !ctx ) return;
      ctx.gainNode.gain.setValueAtTime(volume, ctx.currentTime);
      if ( canvas.ready ) {
        for ( const mesh of canvas.primary.videoMeshes ) {
          mesh.sourceElement.volume = mesh.object instanceof Tile ? mesh.object.volume : volume;
        }
      }
      ui.playlists?.render();
      Hooks.callAll("globalAmbientVolumeChanged", volume);
    }

    /* -------------------------------------------- */

    /**
     * Handle changes to the global interface volume slider.
     * @param {number} volume
     */
    static #onChangeInterfaceVolume(volume) {
      volume = Math.clamp(volume, 0, 1);
      const ctx = game.audio.interface;
      if ( !ctx ) return;
      ctx.gainNode.gain.setValueAtTime(volume, ctx.currentTime);
      ui.playlists?.render();
      Hooks.callAll("globalInterfaceVolumeChanged", volume);
    }

    /* -------------------------------------------- */
    /*  Socket Listeners and Handlers               */
    /* -------------------------------------------- */

    /**
     * Open socket listeners which transact ChatMessage data
     * @param socket
     */
    static _activateSocketListeners(socket) {
      socket.on("playAudio", audioData => this.play(audioData, false));
      socket.on("playAudioPosition", args => canvas.sounds.playAtPosition(...args));
      socket.on("preloadAudio", src => this.preloadSound(src));
    }

    /* -------------------------------------------- */

    /**
     * Play a one-off sound effect which is not part of a Playlist
     *
     * @param {Object} data           An object configuring the audio data to play
     * @param {string} data.src       The audio source file path, either a public URL or a local path relative to the public directory
     * @param {string} [data.channel] An audio channel in CONST.AUDIO_CHANNELS where the sound should play
     * @param {number} data.volume    The volume level at which to play the audio, between 0 and 1.
     * @param {boolean} data.autoplay Begin playback of the audio effect immediately once it is loaded.
     * @param {boolean} data.loop     Loop the audio effect and continue playing it until it is manually stopped.
     * @param {object|boolean} socketOptions  Options which only apply when emitting playback over websocket.
     *                                As a boolean, emits (true) or does not emit (false) playback to all other clients
     *                                As an object, can configure which recipients should receive the event.
     * @param {string[]} [socketOptions.recipients] An array of user IDs to push audio playback to. All users by default.
     *
     * @returns {Sound}               A Sound instance which controls audio playback.
     *
     * @example Play the sound of a locked door for all players
     * ```js
     * AudioHelper.play({src: "sounds/lock.wav", volume: 0.8, loop: false}, true);
     * ```
     */
    static play(data, socketOptions) {
      const audioData = foundry.utils.mergeObject({
        src: null,
        volume: 1.0,
        loop: false,
        channel: "interface"
      }, data, {insertKeys: true});

      // Push the sound to other clients
      const push = socketOptions && (socketOptions !== false);
      if ( push ) {
        socketOptions = foundry.utils.getType(socketOptions) === "Object" ? socketOptions : {};
        if ( "recipients" in socketOptions && !Array.isArray(socketOptions.recipients)) {
          throw new Error("Socket recipients must be an array of User IDs");
        }
        game.socket.emit("playAudio", audioData, socketOptions);
      }

      // Backwards compatibility, if autoplay was passed as false take no further action
      if ( audioData.autoplay === false ) return;

      // Play the sound locally
      return game.audio.play(audioData.src, {
        volume: audioData.volume ?? 1.0,
        loop: audioData.loop,
        context: game.audio[audioData.channel]
      });
    }

    /* -------------------------------------------- */

    /**
     * Begin loading the sound for a provided source URL adding its
     * @param {string} src            The audio source path to preload
     * @returns {Promise<Sound>}      The created and loaded Sound ready for playback
     */
    static async preloadSound(src) {
      const sound = game.audio.create({src: src, preload: false, singleton: true});
      await sound.load();
      return sound;
    }

    /* -------------------------------------------- */

    /**
     * Returns the volume value based on a range input volume control's position.
     * This is using an exponential approximation of the logarithmic nature of audio level perception
     * @param {number|string} value   Value between [0, 1] of the range input
     * @param {number} [order=1.5]    The exponent of the curve
     * @returns {number}
     */
    static inputToVolume(value, order=1.5) {
      if ( typeof value === "string" ) value = parseFloat(value);
      return Math.pow(value, order);
    }

    /* -------------------------------------------- */

    /**
     * Counterpart to inputToVolume()
     * Returns the input range value based on a volume
     * @param {number} volume         Value between [0, 1] of the volume level
     * @param {number} [order=1.5]    The exponent of the curve
     * @returns {number}
     */
    static volumeToInput(volume, order=1.5) {
      return Math.pow(volume, 1 / order);
    }

    /* -------------------------------------------- */
    /*  Audio Stream Analysis                       */
    /* -------------------------------------------- */

    /**
     * Returns a singleton AudioContext if one can be created.
     * An audio context may not be available due to limited resources or browser compatibility
     * in which case null will be returned
     *
     * @returns {AudioContext}  A singleton AudioContext or null if one is not available
     */
    getAnalyzerContext() {
      if ( !AudioHelper.#analyzerContext ) AudioHelper.#analyzerContext = new AudioContext();
      return AudioHelper.#analyzerContext;
    }

    /* -------------------------------------------- */

    /**
     * Registers a stream for periodic reports of audio levels.
     * Once added, the callback will be called with the maximum decibel level of
     * the audio tracks in that stream since the last time the event was fired.
     * The interval needs to be a multiple of AudioHelper.levelAnalyserNativeInterval which defaults at 50ms
     *
     * @param {string} id             An id to assign to this report. Can be used to stop reports
     * @param {MediaStream} stream    The MediaStream instance to report activity on.
     * @param {Function} callback     The callback function to call with the decibel level. `callback(dbLevel)`
     * @param {number} [interval]     The interval at which to produce reports.
     * @param {number} [smoothing]    The smoothingTimeConstant to set on the audio analyser.
     * @returns {boolean}             Returns whether listening to the stream was successful
     */
    startLevelReports(id, stream, callback, interval=50, smoothing=0.1) {
      if ( !stream || !id ) return false;
      let audioContext = this.getAnalyzerContext();
      if (audioContext === null) return false;

      // Clean up any existing report with the same ID
      this.stopLevelReports(id);

      // Make sure this stream has audio tracks, otherwise we can't connect the analyser to it
      if (stream.getAudioTracks().length === 0) return false;

      // Create the analyser
      let analyser = audioContext.createAnalyser();
      analyser.fftSize = 512;
      analyser.smoothingTimeConstant = smoothing;

      // Connect the analyser to the MediaStreamSource
      audioContext.createMediaStreamSource(stream).connect(analyser);
      this.#analyserStreams[id] = {stream, analyser, interval, callback, _lastEmit: 0};

      // Ensure the analyser timer is started as we have at least one valid stream to listen to
      this.#ensureAnalyserTimer();
      return true;
    }

    /* -------------------------------------------- */

    /**
     * Stop sending audio level reports
     * This stops listening to a stream and stops sending reports.
     * If we aren't listening to any more streams, cancel the global analyser timer.
     * @param {string} id      The id of the reports that passed to startLevelReports.
     */
    stopLevelReports(id) {
      delete this.#analyserStreams[id];
      if ( foundry.utils.isEmpty(this.#analyserStreams) ) this.#cancelAnalyserTimer();
    }

    /* -------------------------------------------- */

    /**
     * Ensures the global analyser timer is started
     *
     * We create only one timer that runs every 50ms and only create it if needed, this is meant to optimize things
     * and avoid having multiple timers running if we want to analyse multiple streams at the same time.
     * I don't know if it actually helps much with performance but it's expected that limiting the number of timers
     * running at the same time is good practice and with JS itself, there's a potential for a timer congestion
     * phenomenon if too many are created.
     */
    #ensureAnalyserTimer() {
      if ( !this.#analyserInterval ) {
        this.#analyserInterval = setInterval(this.#emitVolumes.bind(this), AudioHelper.levelAnalyserNativeInterval);
      }
    }

    /* -------------------------------------------- */

    /**
     * Cancel the global analyser timer
     * If the timer is running and has become unnecessary, stops it.
     */
    #cancelAnalyserTimer() {
      if ( this.#analyserInterval ) {
        clearInterval(this.#analyserInterval);
        this.#analyserInterval = undefined;
      }
    }

    /* -------------------------------------------- */

    /**
     * Capture audio level for all speakers and emit a webrtcVolumes custom event with all the volume levels
     * detected since the last emit.
     * The event's detail is in the form of {userId: decibelLevel}
     */
    #emitVolumes() {
      for ( const stream of Object.values(this.#analyserStreams) ) {
        if ( ++stream._lastEmit < (stream.interval / AudioHelper.levelAnalyserNativeInterval) ) continue;

        // Create the Fast Fourier Transform Array only once. Assume all analysers use the same fftSize
        if ( this.#fftArray === null ) this.#fftArray = new Float32Array(stream.analyser.frequencyBinCount);

        // Fill the array
        stream.analyser.getFloatFrequencyData(this.#fftArray);
        const maxDecibel = Math.max(...this.#fftArray);
        stream.callback(maxDecibel, this.#fftArray);
        stream._lastEmit = 0;
      }
    }

    /* -------------------------------------------- */
    /*  Event Handlers                              */
    /* -------------------------------------------- */

    /**
     * Handle the first observed user gesture
     * @param {Event} event         The mouse-move event which enables playback
     * @param {Function} resolve    The Promise resolution function
     * @private
     */
    _onFirstGesture(event, resolve) {
      if ( !this.locked ) return resolve();

      // Create audio contexts
      this.music = AudioHelper.#createContext("globalPlaylistVolume");
      this.environment = AudioHelper.#createContext("globalAmbientVolume");
      this.interface = AudioHelper.#createContext("globalInterfaceVolume");

      // Unlock and evaluate pending playbacks
      this.locked = false;
      if ( this.pending.length ) {
        console.log(`${vtt} | Activating pending audio playback with user gesture.`);
        this.pending.forEach(fn => fn());
        this.pending = [];
      }
      return resolve();
    }

    /* -------------------------------------------- */

    /**
     * Create an AudioContext with an attached GainNode for master volume control.
     * @returns {AudioContext}
     */
    static #createContext(volumeSetting) {
      const ctx = new AudioContext();
      ctx.gainNode = ctx.createGain();
      ctx.gainNode.connect(ctx.destination);
      const volume = game.settings.get("core", volumeSetting);
      ctx.gainNode.gain.setValueAtTime(volume, ctx.currentTime);
      return ctx;
    }

    /* -------------------------------------------- */

    /**
     * Log a debugging message if the audio debugging flag is enabled.
     * @param {string} message      The message to log
     */
    debug(message) {
      if ( CONFIG.debug.audio ) console.debug(`${vtt} | ${message}`);
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    getCache(src) {
      foundry.utils.logCompatibilityWarning("AudioHelper#getCache is deprecated in favor of AudioHelper#buffers#get");
      return this.buffers.getBuffer(src, {since: 12, until: 14});
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    updateCache(src, playing=false) {
      foundry.utils.logCompatibilityWarning("AudioHelper#updateCache is deprecated without replacement");
    }

    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    setCache(src, buffer) {
      foundry.utils.logCompatibilityWarning("AudioHelper#setCache is deprecated in favor of AudioHelper#buffers#set");
      this.buffers.setBuffer(src, buffer);
    }
  }

  /**
   * A sound effect which applies a biquad filter.
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode}
   * @alias foundry.audio.BiquadFilterEffect
   */
  class BiquadFilterEffect extends BiquadFilterNode {
    /**
     * A ConvolverEffect is constructed by passing the following parameters.
     * @param {AudioContext} context      The audio context required by the BiquadFilterNode
     * @param {object} [options]          Additional options which modify the BiquadFilterEffect behavior
     * @param {BiquadFilterType} [options.type=lowpass]  The filter type to apply
     * @param {number} [options.intensity=5]   The initial intensity of the effect
     */
    constructor(context, {type="lowpass", intensity=5, ...options}={}) {
      if ( !BiquadFilterEffect.#ALLOWED_TYPES.includes(type) ) {
        throw new Error(`Invalid BiquadFilterEffect type "${type}" provided`);
      }
      super(context, options);
      this.#type = this.type = type;
      this.#intensity = intensity;
      this.update();
    }

    /**
     * The allowed filter types supported by this effect class.
     */
    static #ALLOWED_TYPES = ["lowpass", "highpass", "bandpass", "lowshelf", "highshelf", "peaking", "notch"];

    /**
     * The original configured type of the effect.
     * @type {BiquadFilterType}
     */
    #type;

    /* -------------------------------------------- */

    /**
     * Adjust the intensity of the effect on a scale of 0 to 10.
     * @type {number}
     */
    get intensity() {
      return this.#intensity;
    }

    set intensity(intensity) {
      this.update({intensity});
    }

    #intensity;

    /* -------------------------------------------- */

    /**
     * Update the state of the effect node given the active flag and numeric intensity.
     * @param {object} options            Options which are updated
     * @param {number} [options.intensity]  A new effect intensity
     * @param {BiquadFilterType} [options.type] A new filter type
     */
    update({intensity, type} = {}) {
      if ( Number.isFinite(intensity) ) this.#intensity = Math.clamp(intensity, 1, 10);
      if ( BiquadFilterEffect.#ALLOWED_TYPES.includes(type) ) this.#type = type;
      this.type = this.#type;
      switch ( this.#type ) {
        case "lowpass":
          this.frequency.value = 1100 - (100 * this.#intensity); // More intensity cuts at a lower frequency
          break;
        case "highpass":
          this.frequency.value = 100 * this.#intensity; // More intensity cuts at higher frequency
          break;
        default:
          throw new Error(`BiquadFilterEffect type "${this.#type}" not yet configured`);
      }
    }
  }

  /**
   * A sound effect which applies a convolver filter.
   * The convolver effect splits the input sound into two separate paths:
   * 1. A "dry" node which is the original sound
   * 2. A "wet" node which contains the result of the convolution
   * This effect mixes between the dry and wet channels based on the intensity of the reverb effect.
   * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ConvolverNode}
   * @alias foundry.audio.ConvolverFilterEffect
   */
  class ConvolverEffect extends ConvolverNode {
    /**
     * A ConvolverEffect is constructed by passing the following parameters.
     * @param {AudioContext} context      The audio context required by the ConvolverNode
     * @param {object} [options]          Additional options which modify the ConvolverEffect behavior
     * @param {string} [options.impulseResponsePath]  The file path to the impulse response buffer to use
     * @param {number} [options.intensity]            The initial intensity of the effect
     */
    constructor(context, {impulseResponsePath="sounds/impulse-responses/ir-full.wav", intensity=5, ...options}={}) {
      super(context, options);
      this.#impulseResponsePath = impulseResponsePath;
      this.#intensity = intensity;
      this.#dryGain = context.createGain();
      this.#wetGain = context.createGain();
      this.update();
    }

    /**
     * The identifier of the impulse response buffer currently used.
     * The default impulse response function was generated using https://aldel.com/reverbgen/.
     * @type {string}
     */
    #impulseResponsePath;

    /**
     * A GainNode which mixes base, non-convolved, audio playback into the final result.
     * @type {GainNode}
     */
    #dryGain;

    /**
     * A GainNode which mixes convolved audio playback into the final result.
     * @type {GainNode}
     */
    #wetGain;

    /**
     * Flag whether the impulse response buffer has been loaded to prevent duplicate load requests.
     * @type {boolean}
     */
    #loaded = false;

    /* -------------------------------------------- */

    /**
     * Adjust the intensity of the effect on a scale of 0 to 10.
     * @type {number}
     */
    get intensity() {
      return this.#intensity;
    }

    set intensity(value) {
      this.update({intensity: value});
    }

    #intensity;

    /* -------------------------------------------- */

    /**
     * Update the state of the effect node given the active flag and numeric intensity.
     * @param {object} options            Options which are updated
     * @param {number} [options.intensity]  A new effect intensity
     */
    update({intensity} = {}) {
      if ( Number.isFinite(intensity) ) this.#intensity = Math.clamp(intensity, 1, 10);

      // Load an impulse response buffer
      if ( !this.#loaded ) {
        const irSound = new Sound(this.#impulseResponsePath, {context: this.context});
        this.#loaded = true;
        irSound.load().then(s => this.buffer = s.buffer);
      }

      // Set mix of wet and dry gain based on reverb intensity
      this.#wetGain.gain.value = 0.2 + Math.sqrt(this.#intensity / 10); // [0.2, 1.2]
      this.#dryGain.gain.value = Math.sqrt((11 - this.#intensity) / 10);
    }

    /* -------------------------------------------- */

    /** @override */
    disconnect(...args) {
      this.#wetGain.disconnect();
      this.#dryGain.disconnect();
      return super.disconnect(...args);
    }

    /* -------------------------------------------- */

    /** @override */
    connect(destinationNode, ...args) {
      super.connect(this.#wetGain, ...args);
      this.#dryGain.connect(destinationNode);
      this.#wetGain.connect(destinationNode);
      return destinationNode;
    }

    /* -------------------------------------------- */

    /**
     * Additional side effects performed when some other AudioNode connects to this one.
     * This behavior is not supported by the base WebAudioAPI but is needed here for more complex effects.
     * @param {AudioNode} sourceNode      An upstream source node that is connecting to this one
     */
    onConnectFrom(sourceNode) {
      sourceNode.connect(this.#dryGain);
    }
  }

  var audio = /*#__PURE__*/Object.freeze({
    __proto__: null,
    AudioBufferCache: AudioBufferCache,
    AudioHelper: AudioHelper,
    AudioTimeout: AudioTimeout,
    BiquadFilterEffect: BiquadFilterEffect,
    ConvolverEffect: ConvolverEffect,
    Sound: Sound,
    types: _types$2
  });

  /**
   * A framework for imbuing special scripted behaviors into a single specific Scene.
   * Managed scenes are registered in CONFIG.Canvas.managedScenes.
   *
   * The SceneManager instance is called at various points in the Scene rendering life-cycle.
   *
   * This also provides a framework for registering additional hook events which are required only for the life-cycle of
   * the managed Scene.
   *
   * @example Registering a custom SceneManager
   * ```js
   * // Define a custom SceneManager subclass
   * class MyCustomSceneManager extends SceneManager {
   *   async _onInit() {
   *     console.log(`Initializing managed Scene "${this.scene.name}"`);
   *   }
   *
   *   async _onDraw() {
   *     console.log(`Drawing managed Scene "${this.scene.name}"`);
   *   }
   *
   *   async _onReady() {
   *     console.log(`Readying managed Scene "${this.scene.name}"`);
   *   }
   *
   *   async _onTearDown() {
   *     console.log(`Deconstructing managed Scene "${this.scene.name}"`);
   *   }
   *
   *   _registerHooks() {
   *     this.registerHook("updateToken", this.#onUpdateToken.bind(this));
   *   }
   *
   *   #onUpdateToken(document, updateData, options, userId) {
   *     console.log("Updating a token within the managed Scene");
   *   }
   * }
   *
   * // Register MyCustomSceneManager to be used for a specific Scene
   * CONFIG.Canvas.sceneManagers = {
   *   [sceneId]: MyCustomSceneManager
   * }
   * ```
   */
  class SceneManager {
    /**
     * The SceneManager is constructed by passing a reference to the active Scene document.
     * @param {Scene} scene
     */
    constructor(scene) {
      this.#scene = scene;
    }

    /**
     * The managed Scene.
     * @type {Scene}
     */
    get scene() {
      return this.#scene;
    }
    #scene;

    /* -------------------------------------------- */
    /*  Scene Life-Cycle Methods                    */
    /* -------------------------------------------- */

    /**
     * Additional behaviors to perform when the Canvas is first initialized for the Scene.
     * @returns {Promise<void>}
     * @internal
     */
    async _onInit() {}

    /**
     * Additional behaviors to perform after core groups and layers are drawn to the canvas.
     * @returns {Promise<void>}
     * @internal
     */
    async _onDraw() {}

    /**
     * Additional behaviors to perform after the Canvas is fully initialized for the Scene.
     * @returns {Promise<void>}
     * @internal
     */
    async _onReady() {}

    /**
     * Additional behaviors to perform when the Scene is deactivated.
     * @returns {Promise<void>}
     * @internal
     */
    async _onTearDown() {}

    /* -------------------------------------------- */
    /*  Scene Hooks                                 */
    /* -------------------------------------------- */

    /**
     * Registered hook functions used within this specific Scene that are automatically deactivated.
     * @type {Record<string, number>}
     */
    #hooks = {};

    /**
     * Register additional hook functions are only used while this Scene is active and is automatically deactivated.
     * Hooks should be registered in this function by calling this._registerHook(hookName, handler)
     * @internal
     */
    _registerHooks() {}

    /**
     * Register additional hook functions are only used while this Scene is active and is automatically deactivated.
     * @param {string} hookName
     * @param {Function} handler
     */
    registerHook(hookName, handler) {
      this.#hooks[hookName] = Hooks.on(hookName, handler);
    }

    /**
     * Deactivate Hook functions that were added specifically for this Scene.
     * @internal
     */
    _deactivateHooks() {
      for ( const [hookName, hookId] of Object.entries(this.#hooks) ) Hooks.off(hookName, hookId);
    }
  }

  /**
   * The edge detection filter for {@link foundry.canvas.SMAAFilter}.
   */
  class SMAAEdgeDetectionFilter extends PIXI.Filter {
    /**
     * @param {SMAAFilterConfig} config
     */
    constructor(config) {
      super(VERTEX_SOURCE$1, generateFragmentSource$1(config));
    }
  }

  /* -------------------------------------------- */

  /**
   * The vertex shader source of {@link SMAAEdgeDetectionFilter}.
   * @type {string}
   */
  const VERTEX_SOURCE$1 = `\
#define mad(a, b, c) (a * b + c)

attribute vec2 aVertexPosition;

uniform mat3 projectionMatrix;
uniform vec4 inputSize;
uniform vec4 inputPixel;
uniform vec4 outputFrame;

#define resolution (inputPixel.xy)
#define SMAA_RT_METRICS (inputPixel.zwxy)

varying vec2 vTexCoord0;
varying vec4 vOffset[3];

void main() {
    vTexCoord0 = aVertexPosition * (outputFrame.zw * inputSize.zw);

    vOffset[0] = mad(SMAA_RT_METRICS.xyxy, vec4(-1.0, 0.0, 0.0, -1.0), vTexCoord0.xyxy);
    vOffset[1] = mad(SMAA_RT_METRICS.xyxy, vec4( 1.0, 0.0, 0.0,  1.0), vTexCoord0.xyxy);
    vOffset[2] = mad(SMAA_RT_METRICS.xyxy, vec4(-2.0, 0.0, 0.0, -2.0), vTexCoord0.xyxy);

    vec3 position = vec3(aVertexPosition * max(outputFrame.zw, vec2(0.0)) + outputFrame.xy, 1.0);
    gl_Position = vec4((projectionMatrix * position).xy, 0.0, 1.0);
}
`;

  /* -------------------------------------------- */

  /**
   * The fragment shader source of {@link SMAAEdgeDetectionFilter}.
   * @param {SMAAFilterConfig} config
   * @returns {string}
   */
  function generateFragmentSource$1(config) {
    return `\
precision highp float;

/**
 * Color Edge Detection
 *
 * IMPORTANT NOTICE: color edge detection requires gamma-corrected colors, and
 * thus 'colorTex' should be a non-sRGB texture.
 */

#define SMAA_THRESHOLD ${config.threshold.toFixed(8)}
#define SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR ${config.localContrastAdaptionFactor.toFixed(8)}

uniform sampler2D uSampler; // colorTex

#define colorTex uSampler

varying vec2 vTexCoord0;
varying vec4 vOffset[3];

void main() {
    // Calculate the threshold:
    vec2 threshold = vec2(SMAA_THRESHOLD);

    // Calculate color deltas:
    vec4 delta;
    vec3 c = texture2D(colorTex, vTexCoord0).rgb;

    vec3 cLeft = texture2D(colorTex, vOffset[0].xy).rgb;
    vec3 t = abs(c - cLeft);
    delta.x = max(max(t.r, t.g), t.b);

    vec3 cTop  = texture2D(colorTex, vOffset[0].zw).rgb;
    t = abs(c - cTop);
    delta.y = max(max(t.r, t.g), t.b);

    // We do the usual threshold:
    vec2 edges = step(threshold, delta.xy);

    // Then discard if there is no edge:
    if (dot(edges, vec2(1.0, 1.0)) == 0.0)
        discard;

    // Calculate right and bottom deltas:
    vec3 cRight = texture2D(colorTex, vOffset[1].xy).rgb;
    t = abs(c - cRight);
    delta.z = max(max(t.r, t.g), t.b);

    vec3 cBottom  = texture2D(colorTex, vOffset[1].zw).rgb;
    t = abs(c - cBottom);
    delta.w = max(max(t.r, t.g), t.b);

    // Calculate the maximum delta in the direct neighborhood:
    vec2 maxDelta = max(delta.xy, delta.zw);

    // Calculate left-left and top-top deltas:
    vec3 cLeftLeft  = texture2D(colorTex, vOffset[2].xy).rgb;
    t = abs(c - cLeftLeft);
    delta.z = max(max(t.r, t.g), t.b);

    vec3 cTopTop = texture2D(colorTex, vOffset[2].zw).rgb;
    t = abs(c - cTopTop);
    delta.w = max(max(t.r, t.g), t.b);

    // Calculate the final maximum delta:
    maxDelta = max(maxDelta.xy, delta.zw);
    float finalDelta = max(maxDelta.x, maxDelta.y);

    // Local contrast adaptation:
    edges.xy *= step(finalDelta, SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR * delta.xy);

    gl_FragColor = vec4(edges, 0.0, 1.0);
}
`;
  }

  /**
   * The blending weight calculation filter for {@link foundry.canvas.SMAAFilter}.
   */
  class SMAABWeightCalculationFilter extends PIXI.Filter {
    /**
     * @param {SMAAFilterConfig} config
     */
    constructor(config) {
      super(generateVertexSource(config), generateFragmentSource(config), {areaTex, searchTex});
    }
  }

  /* -------------------------------------------- */

  /**
   * The fragment shader source of {@link SMAABWeightCalculationFilter}.
   * @param {SMAAFilterConfig} config
   * @returns {string}
   */
  function generateVertexSource(config) {
    return `\
#define mad(a, b, c) (a * b + c)

#define SMAA_MAX_SEARCH_STEPS ${config.maxSearchSteps}

attribute vec2 aVertexPosition;

uniform mat3 projectionMatrix;
uniform vec4 inputSize;
uniform vec4 inputPixel;
uniform vec4 outputFrame;

#define resolution (inputPixel.xy)
#define SMAA_RT_METRICS (inputPixel.zwxy)

varying vec2 vTexCoord0;
varying vec2 vPixCoord;
varying vec4 vOffset[3];

void main() {
    vTexCoord0 = aVertexPosition * (outputFrame.zw * inputSize.zw);

    vPixCoord = vTexCoord0 * SMAA_RT_METRICS.zw;

    // We will use these offsets for the searches later on (see @PSEUDO_GATHER4):
    vOffset[0] = mad(SMAA_RT_METRICS.xyxy, vec4(-0.25, -0.125,  1.25, -0.125), vTexCoord0.xyxy);
    vOffset[1] = mad(SMAA_RT_METRICS.xyxy, vec4(-0.125, -0.25, -0.125,  1.25), vTexCoord0.xyxy);

    // And these for the searches, they indicate the ends of the loops:
    vOffset[2] = mad(
      SMAA_RT_METRICS.xxyy,
      vec4(-2.0, 2.0, -2.0, 2.0) * float(SMAA_MAX_SEARCH_STEPS),
      vec4(vOffset[0].xz, vOffset[1].yw)
    );

    vec3 position = vec3(aVertexPosition * max(outputFrame.zw, vec2(0.0)) + outputFrame.xy, 1.0);
    gl_Position = vec4((projectionMatrix * position).xy, 0.0, 1.0);
}
`;
  }

  /* -------------------------------------------- */

  /**
   * The fragment shader source of {@link SMAABWeightCalculationFilter}.
   * @param {SMAAFilterConfig} config
   * @returns {string}
   */
  function generateFragmentSource(config) {
    return `\
precision highp float;
precision highp int;

#define SMAA_THRESHOLD ${config.threshold.toFixed(8)}
#define SMAA_MAX_SEARCH_STEPS ${config.maxSearchSteps}
#define SMAA_MAX_SEARCH_STEPS_DIAG ${config.maxSearchStepsDiag}
#define SMAA_CORNER_ROUNDING ${config.cornerRounding}
${config.disableDiagDetection ? "#define SMAA_DISABLE_DIAG_DETECTION" : ""}
${config.disableCornerDetection ? "#define SMAA_DISABLE_CORNER_DETECTION" : ""}

// Non-Configurable Defines
#define SMAA_AREATEX_MAX_DISTANCE 16
#define SMAA_AREATEX_MAX_DISTANCE_DIAG 20
#define SMAA_AREATEX_PIXEL_SIZE (1.0 / vec2(160.0, 560.0))
#define SMAA_AREATEX_SUBTEX_SIZE (1.0 / 7.0)
#define SMAA_SEARCHTEX_SIZE vec2(66.0, 33.0)
#define SMAA_SEARCHTEX_PACKED_SIZE vec2(64.0, 16.0)
#define SMAA_CORNER_ROUNDING_NORM (float(SMAA_CORNER_ROUNDING) / 100.0)

// Texture Access Defines
#ifndef SMAA_AREATEX_SELECT
#define SMAA_AREATEX_SELECT(sample) sample.rg
#endif

#ifndef SMAA_SEARCHTEX_SELECT
#define SMAA_SEARCHTEX_SELECT(sample) sample.r
#endif

uniform sampler2D uSampler; // edgesTex
uniform sampler2D areaTex;
uniform sampler2D searchTex;
uniform vec4 inputPixel;

#define edgesTex uSampler
#define resolution (inputPixel.xy)
#define SMAA_RT_METRICS (inputPixel.zwxy)

varying vec2 vTexCoord0;
varying vec4 vOffset[3];
varying vec2 vPixCoord;

#define mad(a, b, c) (a * b + c)
#define saturate(a) clamp(a, 0.0, 1.0)
#define round(v) floor(v + 0.5)
#define SMAASampleLevelZeroOffset(tex, coord, offset) texture2D(tex, coord + offset * SMAA_RT_METRICS.xy)

/**
 * Conditional move:
 */
void SMAAMovc(bvec2 cond, inout vec2 variable, vec2 value) {
  if (cond.x) variable.x = value.x;
  if (cond.y) variable.y = value.y;
}

void SMAAMovc(bvec4 cond, inout vec4 variable, vec4 value) {
  SMAAMovc(cond.xy, variable.xy, value.xy);
  SMAAMovc(cond.zw, variable.zw, value.zw);
}

/**
 * Allows to decode two binary values from a bilinear-filtered access.
 */
vec2 SMAADecodeDiagBilinearAccess(vec2 e) {
  // Bilinear access for fetching 'e' have a 0.25 offset, and we are
  // interested in the R and G edges:
  //
  // +---G---+-------+
  // |   x o R   x   |
  // +-------+-------+
  //
  // Then, if one of these edge is enabled:
  //   Red:   (0.75 * X + 0.25 * 1) => 0.25 or 1.0
  //   Green: (0.75 * 1 + 0.25 * X) => 0.75 or 1.0
  //
  // This function will unpack the values (mad + mul + round):
  // wolframalpha.com: round(x * abs(5 * x - 5 * 0.75)) plot 0 to 1
  e.r = e.r * abs(5.0 * e.r - 5.0 * 0.75);
  return round(e);
}

vec4 SMAADecodeDiagBilinearAccess(vec4 e) {
  e.rb = e.rb * abs(5.0 * e.rb - 5.0 * 0.75);
  return round(e);
}

/**
 * These functions allows to perform diagonal pattern searches.
 */
vec2 SMAASearchDiag1(sampler2D edgesTex, vec2 texcoord, vec2 dir, out vec2 e) {
  vec4 coord = vec4(texcoord, -1.0, 1.0);
  vec3 t = vec3(SMAA_RT_METRICS.xy, 1.0);

  for (int i = 0; i < SMAA_MAX_SEARCH_STEPS; i++) {
    if (!(coord.z < float(SMAA_MAX_SEARCH_STEPS_DIAG - 1) && coord.w > 0.9)) break;
    coord.xyz = mad(t, vec3(dir, 1.0), coord.xyz);
    e = texture2D(edgesTex, coord.xy).rg; // LinearSampler
    coord.w = dot(e, vec2(0.5, 0.5));
  }
  return coord.zw;
}

vec2 SMAASearchDiag2(sampler2D edgesTex, vec2 texcoord, vec2 dir, out vec2 e) {
  vec4 coord = vec4(texcoord, -1.0, 1.0);
  coord.x += 0.25 * SMAA_RT_METRICS.x; // See @SearchDiag2Optimization
  vec3 t = vec3(SMAA_RT_METRICS.xy, 1.0);

  for (int i = 0; i < SMAA_MAX_SEARCH_STEPS; i++) {
    if (!(coord.z < float(SMAA_MAX_SEARCH_STEPS_DIAG - 1) && coord.w > 0.9)) break;
    coord.xyz = mad(t, vec3(dir, 1.0), coord.xyz);

    // @SearchDiag2Optimization
    // Fetch both edges at once using bilinear filtering:
    e = texture2D(edgesTex, coord.xy).rg; // LinearSampler
    e = SMAADecodeDiagBilinearAccess(e);

    // Non-optimized version:
    // e.g = texture2D(edgesTex, coord.xy).g; // LinearSampler
    // e.r = SMAASampleLevelZeroOffset(edgesTex, coord.xy, vec2(1, 0)).r;

    coord.w = dot(e, vec2(0.5, 0.5));
  }
  return coord.zw;
}

/**
 * Similar to SMAAArea, this calculates the area corresponding to a certain
 * diagonal distance and crossing edges 'e'.
 */
vec2 SMAAAreaDiag(sampler2D areaTex, vec2 dist, vec2 e, float offset) {
  vec2 texcoord = mad(vec2(SMAA_AREATEX_MAX_DISTANCE_DIAG, SMAA_AREATEX_MAX_DISTANCE_DIAG), e, dist);

  // We do a scale and bias for mapping to texel space:
  texcoord = mad(SMAA_AREATEX_PIXEL_SIZE, texcoord, 0.5 * SMAA_AREATEX_PIXEL_SIZE);

  // Diagonal areas are on the second half of the texture:
  texcoord.x += 0.5;

  // Move to proper place, according to the subpixel offset:
  texcoord.y += SMAA_AREATEX_SUBTEX_SIZE * offset;

  // Do it!
  return SMAA_AREATEX_SELECT(texture2D(areaTex, texcoord)); // LinearSampler
}

/**
 * This searches for diagonal patterns and returns the corresponding weights.
 */
vec2 SMAACalculateDiagWeights(sampler2D edgesTex, sampler2D areaTex, vec2 texcoord, vec2 e, vec4 subsampleIndices) {
  vec2 weights = vec2(0.0, 0.0);

  // Search for the line ends:
  vec4 d;
  vec2 end;
  if (e.r > 0.0) {
      d.xz = SMAASearchDiag1(edgesTex, texcoord, vec2(-1.0,  1.0), end);
      d.x += float(end.y > 0.9);
  } else
      d.xz = vec2(0.0, 0.0);
  d.yw = SMAASearchDiag1(edgesTex, texcoord, vec2(1.0, -1.0), end);

  if (d.x + d.y > 2.0) { // d.x + d.y + 1 > 3
    // Fetch the crossing edges:
    vec4 coords = mad(vec4(-d.x + 0.25, d.x, d.y, -d.y - 0.25), SMAA_RT_METRICS.xyxy, texcoord.xyxy);
    vec4 c;
    c.xy = SMAASampleLevelZeroOffset(edgesTex, coords.xy, vec2(-1,  0)).rg;
    c.zw = SMAASampleLevelZeroOffset(edgesTex, coords.zw, vec2( 1,  0)).rg;
    c.yxwz = SMAADecodeDiagBilinearAccess(c.xyzw);

    // Non-optimized version:
    // vec4 coords = mad(vec4(-d.x, d.x, d.y, -d.y), SMAA_RT_METRICS.xyxy, texcoord.xyxy);
    // vec4 c;
    // c.x = SMAASampleLevelZeroOffset(edgesTex, coords.xy, vec2(-1,  0)).g;
    // c.y = SMAASampleLevelZeroOffset(edgesTex, coords.xy, vec2( 0,  0)).r;
    // c.z = SMAASampleLevelZeroOffset(edgesTex, coords.zw, vec2( 1,  0)).g;
    // c.w = SMAASampleLevelZeroOffset(edgesTex, coords.zw, vec2( 1, -1)).r;

    // Merge crossing edges at each side into a single value:
    vec2 cc = mad(vec2(2.0, 2.0), c.xz, c.yw);

    // Remove the crossing edge if we didn't found the end of the line:
    SMAAMovc(bvec2(step(0.9, d.zw)), cc, vec2(0.0, 0.0));

    // Fetch the areas for this line:
    weights += SMAAAreaDiag(areaTex, d.xy, cc, subsampleIndices.z);
  }

  // Search for the line ends:
  d.xz = SMAASearchDiag2(edgesTex, texcoord, vec2(-1.0, -1.0), end);
  if (SMAASampleLevelZeroOffset(edgesTex, texcoord, vec2(1, 0)).r > 0.0) {
    d.yw = SMAASearchDiag2(edgesTex, texcoord, vec2(1.0, 1.0), end);
    d.y += float(end.y > 0.9);
  } else {
    d.yw = vec2(0.0, 0.0);
  }

  if (d.x + d.y > 2.0) { // d.x + d.y + 1 > 3
    // Fetch the crossing edges:
    vec4 coords = mad(vec4(-d.x, -d.x, d.y, d.y), SMAA_RT_METRICS.xyxy, texcoord.xyxy);
    vec4 c;
    c.x  = SMAASampleLevelZeroOffset(edgesTex, coords.xy, vec2(-1,  0)).g;
    c.y  = SMAASampleLevelZeroOffset(edgesTex, coords.xy, vec2( 0, -1)).r;
    c.zw = SMAASampleLevelZeroOffset(edgesTex, coords.zw, vec2( 1,  0)).gr;
    vec2 cc = mad(vec2(2.0, 2.0), c.xz, c.yw);

    // Remove the crossing edge if we didn't found the end of the line:
    SMAAMovc(bvec2(step(0.9, d.zw)), cc, vec2(0.0, 0.0));

    // Fetch the areas for this line:
    weights += SMAAAreaDiag(areaTex, d.xy, cc, subsampleIndices.w).gr;
  }

  return weights;
}

/**
 * This allows to determine how much length should we add in the last step
 * of the searches. It takes the bilinearly interpolated edge (see
 * @PSEUDO_GATHER4), and adds 0, 1 or 2, depending on which edges and
 * crossing edges are active.
 */
float SMAASearchLength(sampler2D searchTex, vec2 e, float offset) {
  // The texture is flipped vertically, with left and right cases taking half
  // of the space horizontally:
  vec2 scale = SMAA_SEARCHTEX_SIZE * vec2(0.5, -1.0);
  vec2 bias = SMAA_SEARCHTEX_SIZE * vec2(offset, 1.0);

  // Scale and bias to access texel centers:
  scale += vec2(-1.0,  1.0);
  bias  += vec2( 0.5, -0.5);

  // Convert from pixel coordinates to texcoords:
  // (We use SMAA_SEARCHTEX_PACKED_SIZE because the texture is cropped)
  scale *= 1.0 / SMAA_SEARCHTEX_PACKED_SIZE;
  bias *= 1.0 / SMAA_SEARCHTEX_PACKED_SIZE;

  // Lookup the search texture:
  return SMAA_SEARCHTEX_SELECT(texture2D(searchTex, mad(scale, e, bias))); // LinearSampler
}

/**
 * Horizontal/vertical search functions for the 2nd pass.
 */
float SMAASearchXLeft(sampler2D edgesTex, sampler2D searchTex, vec2 texcoord, float end) {
  /**
    * @PSEUDO_GATHER4
    * This texcoord has been offset by (-0.25, -0.125) in the vertex shader to
    * sample between edge, thus fetching four edges in a row.
    * Sampling with different offsets in each direction allows to disambiguate
    * which edges are active from the four fetched ones.
    */
  vec2 e = vec2(0.0, 1.0);
  for (int i = 0; i < SMAA_MAX_SEARCH_STEPS; i++) {
    if (!(texcoord.x > end && e.g > 0.8281 && e.r == 0.0)) break;
    e = texture2D(edgesTex, texcoord).rg; // LinearSampler
    texcoord = mad(-vec2(2.0, 0.0), SMAA_RT_METRICS.xy, texcoord);
  }

  float offset = mad(-(255.0 / 127.0), SMAASearchLength(searchTex, e, 0.0), 3.25);
  return mad(SMAA_RT_METRICS.x, offset, texcoord.x);

  // Non-optimized version:
  // We correct the previous (-0.25, -0.125) offset we applied:
  // texcoord.x += 0.25 * SMAA_RT_METRICS.x;

  // The searches are bias by 1, so adjust the coords accordingly:
  // texcoord.x += SMAA_RT_METRICS.x;

  // Disambiguate the length added by the last step:
  // texcoord.x += 2.0 * SMAA_RT_METRICS.x; // Undo last step
  // texcoord.x -= SMAA_RT_METRICS.x * (255.0 / 127.0) * SMAASearchLength(searchTex, e, 0.0);
  // return mad(SMAA_RT_METRICS.x, offset, texcoord.x);
}

float SMAASearchXRight(sampler2D edgesTex, sampler2D searchTex, vec2 texcoord, float end) {
  vec2 e = vec2(0.0, 1.0);
  for (int i = 0; i < SMAA_MAX_SEARCH_STEPS; i++) { if (!(texcoord.x < end && e.g > 0.8281 && e.r == 0.0)) break;
    e = texture2D(edgesTex, texcoord).rg; // LinearSampler
    texcoord = mad(vec2(2.0, 0.0), SMAA_RT_METRICS.xy, texcoord);
  }
  float offset = mad(-(255.0 / 127.0), SMAASearchLength(searchTex, e, 0.5), 3.25);
  return mad(-SMAA_RT_METRICS.x, offset, texcoord.x);
}

float SMAASearchYUp(sampler2D edgesTex, sampler2D searchTex, vec2 texcoord, float end) {
  vec2 e = vec2(1.0, 0.0);
  for (int i = 0; i < SMAA_MAX_SEARCH_STEPS; i++) { if (!(texcoord.y > end && e.r > 0.8281 && e.g == 0.0)) break;
    e = texture2D(edgesTex, texcoord).rg; // LinearSampler
    texcoord = mad(-vec2(0.0, 2.0), SMAA_RT_METRICS.xy, texcoord);
  }
  float offset = mad(-(255.0 / 127.0), SMAASearchLength(searchTex, e.gr, 0.0), 3.25);
  return mad(SMAA_RT_METRICS.y, offset, texcoord.y);
}

float SMAASearchYDown(sampler2D edgesTex, sampler2D searchTex, vec2 texcoord, float end) {
  vec2 e = vec2(1.0, 0.0);
  for (int i = 0; i < SMAA_MAX_SEARCH_STEPS; i++) { if (!(texcoord.y < end && e.r > 0.8281 && e.g == 0.0)) break;
    e = texture2D(edgesTex, texcoord).rg; // LinearSampler
    texcoord = mad(vec2(0.0, 2.0), SMAA_RT_METRICS.xy, texcoord);
  }
  float offset = mad(-(255.0 / 127.0), SMAASearchLength(searchTex, e.gr, 0.5), 3.25);
  return mad(-SMAA_RT_METRICS.y, offset, texcoord.y);
}

/**
 * Ok, we have the distance and both crossing edges. So, what are the areas
 * at each side of current edge?
 */
vec2 SMAAArea(sampler2D areaTex, vec2 dist, float e1, float e2, float offset) {
  // Rounding prevents precision errors of bilinear filtering:
  vec2 texcoord = mad(vec2(SMAA_AREATEX_MAX_DISTANCE, SMAA_AREATEX_MAX_DISTANCE), round(4.0 * vec2(e1, e2)), dist);

  // We do a scale and bias for mapping to texel space:
  texcoord = mad(SMAA_AREATEX_PIXEL_SIZE, texcoord, 0.5 * SMAA_AREATEX_PIXEL_SIZE);

  // Move to proper place, according to the subpixel offset:
  texcoord.y = mad(SMAA_AREATEX_SUBTEX_SIZE, offset, texcoord.y);

  // Do it!
  return SMAA_AREATEX_SELECT(texture2D(areaTex, texcoord)); // LinearSampler
}

// Corner Detection Functions
void SMAADetectHorizontalCornerPattern(sampler2D edgesTex, inout vec2 weights, vec4 texcoord, vec2 d) {
  #if !defined(SMAA_DISABLE_CORNER_DETECTION)
  vec2 leftRight = step(d.xy, d.yx);
  vec2 rounding = (1.0 - SMAA_CORNER_ROUNDING_NORM) * leftRight;

  rounding /= leftRight.x + leftRight.y; // Reduce blending for pixels in the center of a line.

  vec2 factor = vec2(1.0, 1.0);
  factor.x -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, vec2(0,  1)).r;
  factor.x -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, vec2(1,  1)).r;
  factor.y -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, vec2(0, -2)).r;
  factor.y -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, vec2(1, -2)).r;

  weights *= saturate(factor);
  #endif
}

void SMAADetectVerticalCornerPattern(sampler2D edgesTex, inout vec2 weights, vec4 texcoord, vec2 d) {
  #if !defined(SMAA_DISABLE_CORNER_DETECTION)
  vec2 leftRight = step(d.xy, d.yx);
  vec2 rounding = (1.0 - SMAA_CORNER_ROUNDING_NORM) * leftRight;

  rounding /= leftRight.x + leftRight.y;

  vec2 factor = vec2(1.0, 1.0);
  factor.x -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, vec2( 1, 0)).g;
  factor.x -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, vec2( 1, 1)).g;
  factor.y -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, vec2(-2, 0)).g;
  factor.y -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, vec2(-2, 1)).g;

  weights *= saturate(factor);
  #endif
}

void main() {
  vec4 subsampleIndices = vec4(0.0); // Just pass zero for SMAA 1x, see @SUBSAMPLE_INDICES.
  // subsampleIndices = vec4(1.0, 1.0, 1.0, 0.0);
  vec4 weights = vec4(0.0, 0.0, 0.0, 0.0);
  vec2 e = texture2D(edgesTex, vTexCoord0).rg;

  if (e.g > 0.0) { // Edge at north

    #if !defined(SMAA_DISABLE_DIAG_DETECTION)
    // Diagonals have both north and west edges, so searching for them in
    // one of the boundaries is enough.
    weights.rg = SMAACalculateDiagWeights(edgesTex, areaTex, vTexCoord0, e, subsampleIndices);

    // We give priority to diagonals, so if we find a diagonal we skip
    // horizontal/vertical processing.
    if (weights.r == -weights.g) { // weights.r + weights.g == 0.0
    #endif

    vec2 d;

    // Find the distance to the left:
    vec3 coords;
    coords.x = SMAASearchXLeft(edgesTex, searchTex, vOffset[0].xy, vOffset[2].x);
    coords.y = vOffset[1].y; // vOffset[1].y = vTexCoord0.y - 0.25 * SMAA_RT_METRICS.y (@CROSSING_OFFSET)
    d.x = coords.x;

    // Now fetch the left crossing edges, two at a time using bilinear
    // filtering. Sampling at -0.25 (see @CROSSING_OFFSET) enables to
    // discern what value each edge has:
    float e1 = texture2D(edgesTex, coords.xy).r; // LinearSampler

    // Find the distance to the right:
    coords.z = SMAASearchXRight(edgesTex, searchTex, vOffset[0].zw, vOffset[2].y);
    d.y = coords.z;

    // We want the distances to be in pixel units (doing this here allow to
    // better interleave arithmetic and memory accesses):
    d = abs(round(mad(SMAA_RT_METRICS.zz, d, -vPixCoord.xx)));

    // SMAAArea below needs a sqrt, as the areas texture is compressed
    // quadratically:
    vec2 sqrt_d = sqrt(d);

    // Fetch the right crossing edges:
    float e2 = SMAASampleLevelZeroOffset(edgesTex, coords.zy, vec2(1, 0)).r;

    // Ok, we know how this pattern looks like, now it is time for getting
    // the actual area:
    weights.rg = SMAAArea(areaTex, sqrt_d, e1, e2, subsampleIndices.y);

    // Fix corners:
    coords.y = vTexCoord0.y;
    SMAADetectHorizontalCornerPattern(edgesTex, weights.rg, coords.xyzy, d);

    #if !defined(SMAA_DISABLE_DIAG_DETECTION)
    } else
    e.r = 0.0; // Skip vertical processing.
    #endif
  }

  if (e.r > 0.0) { // Edge at west
    vec2 d;

    // Find the distance to the top:
    vec3 coords;
    coords.y = SMAASearchYUp(edgesTex, searchTex, vOffset[1].xy, vOffset[2].z);
    coords.x = vOffset[0].x; // vOffset[1].x = vTexCoord0.x - 0.25 * SMAA_RT_METRICS.x;
    d.x = coords.y;

    // Fetch the top crossing edges:
    float e1 = texture2D(edgesTex, coords.xy).g; // LinearSampler

    // Find the distance to the bottom:
    coords.z = SMAASearchYDown(edgesTex, searchTex, vOffset[1].zw, vOffset[2].w);
    d.y = coords.z;

    // We want the distances to be in pixel units:
    d = abs(round(mad(SMAA_RT_METRICS.ww, d, -vPixCoord.yy)));

    // SMAAArea below needs a sqrt, as the areas texture is compressed
    // quadratically:
    vec2 sqrt_d = sqrt(d);

    // Fetch the bottom crossing edges:
    float e2 = SMAASampleLevelZeroOffset(edgesTex, coords.xz, vec2(0, 1)).g;

    // Get the area for this direction:
    weights.ba = SMAAArea(areaTex, sqrt_d, e1, e2, subsampleIndices.x);

    // Fix corners:
    coords.x = vTexCoord0.x;
    SMAADetectVerticalCornerPattern(edgesTex, weights.ba, coords.xyxz, d);
  }

  gl_FragColor = weights;
}
`;
  }

  /* -------------------------------------------- */

  /**
   * The area texture of {@link SMAABWeightCalculationFilter}.
   * @type {PIXI.Texture}
   */
  const areaTex = new PIXI.Texture(new PIXI.BaseTexture(
    "data:image/webp;base64, ",
    {
      mipmap: PIXI.MIPMAP_MODES.OFF,
      anisotropicLevel: 0,
      wrapMode: PIXI.WRAP_MODES.CLAMP,
      scaleMode: PIXI.SCALE_MODES.LINEAR,
      format: PIXI.FORMATS.RG,
      type: PIXI.TYPES.UNSIGNED_BYTE
    }
  ));

  /* -------------------------------------------- */

  /**
   * The search texture of {@link SMAABWeightCalculationFilter}.
   * @type {PIXI.Texture}
   */
  const searchTex = new PIXI.Texture(new PIXI.BaseTexture(
    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAQCAYAAACm53kpAAAAAXNSR0IArs4c6QAAAIBJREFUWEftl1ELgCAMhD/749Ift1pNEBlRvfRw82XisQ3mbqelwQawANU2wOq2uY0wh0IzxpljRpidWc6edww6+9/l/YIV+QIcF392gOrKDkgKqFNAfgZU7wCToCeSFUnVPEDfymD3/0UG5QuQFEgKXA8h2RkgT4EsQD6EtD9DO7PRXAFvjEKRAAAAAElFTkSuQmCC",
    {
      mipmap: PIXI.MIPMAP_MODES.OFF,
      anisotropicLevel: 0,
      wrapMode: PIXI.WRAP_MODES.CLAMP,
      scaleMode: PIXI.SCALE_MODES.LINEAR,
      format: PIXI.FORMATS.RED,
      type: PIXI.TYPES.UNSIGNED_BYTE
    }
  ));

  /**
   * The neighborhood blending filter for {@link foundry.canvas.SMAAFilter}.
   */
  class SMAANeighborhoodBlendingFilter extends PIXI.Filter {
    constructor() {
      super(VERTEX_SOURCE, FRAGMENT_SOURCE);
    }
  }

  /* -------------------------------------------- */

  /**
   * The vertex shader source of {@link SMAANeighborhoodBlendingFilter}.
   * @type {string}
   */
  const VERTEX_SOURCE = `\
#define mad(a, b, c) (a * b + c)

attribute vec2 aVertexPosition;

uniform mat3 projectionMatrix;
uniform vec4 inputSize;
uniform vec4 inputPixel;
uniform vec4 outputFrame;

#define resolution (inputPixel.xy)
#define SMAA_RT_METRICS (inputPixel.zwxy)

varying vec2 vTexCoord0;
varying vec4 vOffset;

void main() {
    vTexCoord0 = aVertexPosition * (outputFrame.zw * inputSize.zw);
    vOffset = mad(SMAA_RT_METRICS.xyxy, vec4(1.0, 0.0, 0.0,  1.0), vTexCoord0.xyxy);

    vec3 position = vec3(aVertexPosition * max(outputFrame.zw, vec2(0.0)) + outputFrame.xy, 1.0);
    gl_Position = vec4((projectionMatrix * position).xy, 0.0, 1.0);
}
`;

  /* -------------------------------------------- */

  /**
   * The fragment shader source of {@link SMAANeighborhoodBlendingFilter}.
   * @type {string}
   */
  const FRAGMENT_SOURCE = `\
precision highp float;

#define mad(a, b, c) (a * b + c)

uniform sampler2D blendTex;
uniform sampler2D uSampler; // colorTex
uniform vec4 inputPixel;

#define colorTex uSampler
#define resolution (inputPixel.xy)
#define SMAA_RT_METRICS (inputPixel.zwxy)

varying vec2 vTexCoord0;
varying vec4 vOffset;

/**
 * Conditional move:
 */
void SMAAMovc(bvec2 cond, inout vec2 variable, vec2 value) {
  if (cond.x) variable.x = value.x;
  if (cond.y) variable.y = value.y;
}

void SMAAMovc(bvec4 cond, inout vec4 variable, vec4 value) {
  SMAAMovc(cond.xy, variable.xy, value.xy);
  SMAAMovc(cond.zw, variable.zw, value.zw);
}

void main() {
  vec4 color;

  // Fetch the blending weights for current pixel:
  vec4 a;
  a.x = texture2D(blendTex, vOffset.xy).a; // Right
  a.y = texture2D(blendTex, vOffset.zw).g; // Top
  a.wz = texture2D(blendTex, vTexCoord0).xz; // Bottom / Left

  // Is there any blending weight with a value greater than 0.0?
  if (dot(a, vec4(1.0, 1.0, 1.0, 1.0)) <= 1e-5) {
    color = texture2D(colorTex, vTexCoord0); // LinearSampler
  } else {
    bool h = max(a.x, a.z) > max(a.y, a.w); // max(horizontal) > max(vertical)

    // Calculate the blending offsets:
    vec4 blendingOffset = vec4(0.0, a.y, 0.0, a.w);
    vec2 blendingWeight = a.yw;
    SMAAMovc(bvec4(h, h, h, h), blendingOffset, vec4(a.x, 0.0, a.z, 0.0));
    SMAAMovc(bvec2(h, h), blendingWeight, a.xz);
    blendingWeight /= dot(blendingWeight, vec2(1.0, 1.0));

    // Calculate the texture coordinates:
    vec4 blendingCoord = mad(blendingOffset, vec4(SMAA_RT_METRICS.xy, -SMAA_RT_METRICS.xy), vTexCoord0.xyxy);

    // We exploit bilinear filtering to mix current pixel with the chosen
    // neighbor:
    color = blendingWeight.x * texture2D(colorTex, blendingCoord.xy); // LinearSampler
    color += blendingWeight.y * texture2D(colorTex, blendingCoord.zw); // LinearSampler
  }

  gl_FragColor = color;
}
`;

  /**
   * @typedef {object} SMAAFilterConfig
   * @property {number} threshold                    Specifies the threshold or sensitivity to edges. Lowering this value you will be able to detect more edges at the expense of performance. Range: [0, 0.5]. 0.1 is a reasonable value, and allows to catch most visible edges. 0.05 is a rather overkill value, that allows to catch 'em all.
   * @property {number} localContrastAdaptionFactor  If there is an neighbor edge that has SMAA_LOCAL_CONTRAST_FACTOR times bigger contrast than current edge, current edge will be discarded.
   *                                                 This allows to eliminate spurious crossing edges, and is based on the fact that, if there is too much contrast in a direction, that will hide perceptually contrast in the other neighbors.
   * @property {number} maxSearchSteps               Specifies the maximum steps performed in the horizontal/vertical pattern searches, at each side of the pixel. In number of pixels, it's actually the double. So the maximum line length perfectly handled by, for example 16, is 64 (by perfectly, we meant that longer lines won't look as good, but still antialiased. Range: [0, 112].
   * @property {number} maxSearchStepsDiag           Specifies the maximum steps performed in the diagonal pattern searches, at each side of the pixel. In this case we jump one pixel at time, instead of two. Range: [0, 20].
   * @property {number} cornerRounding               Specifies how much sharp corners will be rounded. Range: [0, 100].
   * @property {boolean} disableDiagDetection        Is diagonal detection disabled?
   * @property {boolean} disableCornerDetection      Is corner detection disabled?
   */

  class SMAAFilter extends PIXI.Filter {
    /**
     * @param {Partial<SMAAFilterConfig>} [config]    The config (defaults: {@link SMAAFilter.PRESETS.DEFAULT})
     */
    constructor({threshold=0.1, localContrastAdaptionFactor=2.0, maxSearchSteps=16, maxSearchStepsDiag=8, cornerRounding=25, disableDiagDetection=false, disableCornerDetection=false}={}) {
      super();
      const config = {threshold, localContrastAdaptionFactor, maxSearchSteps, maxSearchStepsDiag, cornerRounding, disableDiagDetection, disableCornerDetection};
      this.#edgesFilter = new SMAAEdgeDetectionFilter(config);
      this.#weightsFilter = new SMAABWeightCalculationFilter(config);
      this.#blendFilter = new SMAANeighborhoodBlendingFilter();
    }

    /* -------------------------------------------- */

    /**
     * The presets.
     * @enum {SMAAFilterConfig}
     */
    static get PRESETS() {
      return SMAAFilter.#PRESETS;
    }

    static #PRESETS = {
      LOW: {
        threshold: 0.15,
        localContrastAdaptionFactor: 2.0,
        maxSearchSteps: 4,
        maxSearchStepsDiag: 0,
        cornerRounding: 0,
        disableDiagDetection: true,
        disableCornerDetection: true
      },
      MEDIUM: {
        threshold: 0.1,
        localContrastAdaptionFactor: 2.0,
        maxSearchSteps: 8,
        maxSearchStepsDiag: 0,
        cornerRounding: 0,
        disableDiagDetection: true,
        disableCornerDetection: true
      },
      HIGH: {
        threshold: 0.1,
        localContrastAdaptionFactor: 2.0,
        maxSearchSteps: 16,
        maxSearchStepsDiag: 8,
        cornerRounding: 25,
        disableDiagDetection: false,
        disableCornerDetection: false
      },
      ULTRA: {
        threshold: 0.05,
        localContrastAdaptionFactor: 2.0,
        maxSearchSteps: 32,
        maxSearchStepsDiag: 16,
        cornerRounding: 25,
        disableDiagDetection: false,
        disableCornerDetection: false
      }
    };

    /* -------------------------------------------- */

    /**
     * The edge detection filter.
     * @type {SMAAEdgeDetectionFilter}
     */
    #edgesFilter;

    /* -------------------------------------------- */

    /**
     * The blending weight calculation filter.
     * @type {SMAABlendingWeightCalculationFilter}
     */
    #weightsFilter;

    /* -------------------------------------------- */

    /**
     * The neighborhood blending filter.
     * @type {SMAANeighborhoodBlendingFilter}
     */
    #blendFilter;

    /* -------------------------------------------- */

    /** @override */
    apply(filterManager, input, output, clearMode, currentState) {
      const edgesTex = filterManager.getFilterTexture();
      const blendTex = filterManager.getFilterTexture();
      this.#edgesFilter.apply(filterManager, input, edgesTex, PIXI.CLEAR_MODES.CLEAR, currentState);
      this.#weightsFilter.apply(filterManager, edgesTex, blendTex, PIXI.CLEAR_MODES.CLEAR, currentState);
      this.#blendFilter.uniforms.blendTex = blendTex;
      this.#blendFilter.apply(filterManager, input, output, clearMode, currentState);
      filterManager.returnFilterTexture(edgesTex);
      filterManager.returnFilterTexture(blendTex);
    }
  }

  /**
   * @typedef {import("../../../common/types.mjs").Point} Point
   */

  /**
   * @typedef {"wall"|"darkness"|"innerBounds"|"outerBounds"} EdgeTypes
   */

  /**
   * A data structure used to represent potential edges used by the ClockwiseSweepPolygon.
   * Edges are not polygon-specific, meaning they can be reused across many polygon instances.
   */
  class Edge {
    /**
     * Construct an Edge by providing the following information.
     * @param {Point} a                     The first endpoint of the edge
     * @param {Point} b                     The second endpoint of the edge
     * @param {object} [options]            Additional options which describe the edge
     * @param {string} [options.id]               A string used to uniquely identify this edge
     * @param {PlaceableObject} [options.object]  A PlaceableObject that is responsible for this edge, if any
     * @param {EdgeTypes} [options.type]          The type of edge
     * @param {WALL_SENSE_TYPES} [options.light]  How this edge restricts light
     * @param {WALL_SENSE_TYPES} [options.move]   How this edge restricts movement
     * @param {WALL_SENSE_TYPES} [options.sight]  How this edge restricts sight
     * @param {WALL_SENSE_TYPES} [options.sound]  How this edge restricts sound
     * @param {WALL_DIRECTIONS} [options.direction=0] A direction of effect for the edge
     * @param {WallThresholdData} [options.threshold] Configuration of threshold data for this edge
     */
    constructor(a, b, {id, object, direction, type, light, move, sight, sound, threshold}={}) {
      this.a = new PIXI.Point(a.x, a.y);
      this.b = new PIXI.Point(b.x, b.y);
      this.id = id ?? object?.id ?? undefined;
      this.object = object;
      this.type = type || "wall";
      this.direction = direction ?? CONST.WALL_DIRECTIONS.BOTH;
      this.light = light ?? CONST.WALL_SENSE_TYPES.NONE;
      this.move = move ?? CONST.WALL_SENSE_TYPES.NONE;
      this.sight = sight ?? CONST.WALL_SENSE_TYPES.NONE;
      this.sound = sound ?? CONST.WALL_SENSE_TYPES.NONE;
      this.threshold = threshold;

      // Record the edge orientation arranged from top-left to bottom-right
      const isSE = b.x === a.x ? b.y > a.y : b.x > a.x;
      if ( isSE ) {
        this.nw = a;
        this.se = b;
      }
      else {
        this.nw = b;
        this.se = a;
      }
      this.bounds = new PIXI.Rectangle(this.nw.x, this.nw.y, this.se.x - this.nw.x, this.se.y - this.nw.y);
    }

    /* -------------------------------------------- */

    /**
     * The first endpoint of the edge.
     * @type {PIXI.Point}
     */
    a;

    /**
     * The second endpoint of the edge.
     * @type {PIXI.Point}
     */
    b;

    /**
     * The endpoint of the edge which is oriented towards the top-left.
     */
    nw;

    /**
     * The endpoint of the edge which is oriented towards the bottom-right.
     */
    se;

    /**
     * The rectangular bounds of the edge. Used by the quadtree.
     * @type {PIXI.Rectangle}
     */
    bounds;

    /**
     * The direction of effect for the edge.
     * @type {WALL_DIRECTIONS}
     */
    direction;

    /**
     * A string used to uniquely identify this edge.
     * @type {string}
     */
    id;

    /**
     * How this edge restricts light.
     * @type {WALL_SENSE_TYPES}
     */
    light;

    /**
     * How this edge restricts movement.
     * @type {WALL_SENSE_TYPES}
     */
    move;

    /**
     * How this edge restricts sight.
     * @type {WALL_SENSE_TYPES}
     */
    sight;

    /**
     * How this edge restricts sound.
     * @type {WALL_SENSE_TYPES}
     */
    sound;

    /**
     * Specialized threshold data for this edge.
     * @type {WallThresholdData}
     */
    threshold;

    /**
     * Record other edges which this one intersects with.
     * @type {{edge: Edge, intersection: LineIntersection}[]}
     */
    intersections = [];

    /**
     * A PolygonVertex instance.
     * Used as part of ClockwiseSweepPolygon computation.
     * @type {PolygonVertex}
     */
    vertexA;

    /**
     * A PolygonVertex instance.
     * Used as part of ClockwiseSweepPolygon computation.
     * @type {PolygonVertex}
     */
    vertexB;

    /* -------------------------------------------- */

    /**
     * Is this edge limited for a particular type?
     * @returns {boolean}
     */
    isLimited(type) {
      return this[type] === CONST.WALL_SENSE_TYPES.LIMITED;
    }

    /* -------------------------------------------- */

    /**
     * Create a copy of the Edge which can be safely mutated.
     * @returns {Edge}
     */
    clone() {
      const clone = new this.constructor(this.a, this.b, this);
      clone.intersections = [...this.intersections];
      clone.vertexA = this.vertexA;
      clone.vertexB = this.vertexB;
      return clone;
    }

    /* -------------------------------------------- */

    /**
     * Get an intersection point between this Edge and another.
     * @param {Edge} other
     * @returns {LineIntersection|void}
     */
    getIntersection(other) {
      if ( this === other ) return;
      const {a: a0, b: b0} = this;
      const {a: a1, b: b1} = other;

      // Ignore edges which share an endpoint
      if ( a0.equals(a1) || a0.equals(b1) || b0.equals(a1) || b0.equals(b1) ) return;

      // Initial fast CCW test for intersection
      if ( !foundry.utils.lineSegmentIntersects(a0, b0, a1, b1) ) return;

      // Slower computation of intersection point
      const i = foundry.utils.lineLineIntersection(a0, b0, a1, b1, {t1: true});
      if ( !i ) return;  // Eliminates co-linear lines, theoretically should not be necessary but just in case
      return i;
    }

    /* -------------------------------------------- */

    /**
     * Test whether to apply a proximity threshold to this edge.
     * If the proximity threshold is met, this edge excluded from perception calculations.
     * @param {string} sourceType     Sense type for the source
     * @param {Point} sourceOrigin    The origin or position of the source on the canvas
     * @param {number} [externalRadius=0] The external radius of the source
     * @returns {boolean}             True if the edge has a threshold greater than 0 for the source type,
     *                                and the source type is within that distance.
     */
    applyThreshold(sourceType, sourceOrigin, externalRadius=0) {
      const d = this.threshold?.[sourceType];
      const t = this[sourceType];
      if ( !d || (t < CONST.WALL_SENSE_TYPES.PROXIMITY) ) return false; // Threshold behavior does not apply
      const proximity = t === CONST.WALL_SENSE_TYPES.PROXIMITY;
      const pt = foundry.utils.closestPointToSegment(sourceOrigin, this.a, this.b);
      const sourceDistance = Math.hypot(pt.x - sourceOrigin.x, pt.y - sourceOrigin.y);
      return proximity ? Math.max(sourceDistance - externalRadius, 0) < d : (sourceDistance + externalRadius) > d;
    }

    /* -------------------------------------------- */

    /**
     * Determine the orientation of this Edge with respect to a reference point.
     * @param {Point} point       Some reference point, relative to which orientation is determined
     * @returns {number}          An orientation in CONST.WALL_DIRECTIONS which indicates whether the Point is left,
     *                            right, or collinear (both) with the Edge
     */
    orientPoint(point) {
      const orientation = foundry.utils.orient2dFast(this.a, this.b, point);
      if ( orientation === 0 ) return CONST.WALL_DIRECTIONS.BOTH;
      return orientation < 0 ? CONST.WALL_DIRECTIONS.LEFT : CONST.WALL_DIRECTIONS.RIGHT;
    }

    /* -------------------------------------------- */
    /*  Intersection Management                     */
    /* -------------------------------------------- */

    /**
     * Identify intersections between a provided iterable of edges.
     * @param {Iterable<Edge>} edges    An iterable of edges
     */
    static identifyEdgeIntersections(edges) {

      // Sort edges by their north-west x value, breaking ties with the south-east x value
      const sorted = [];
      for ( const edge of edges ) {
        edge.intersections.length = 0; // Clear prior intersections
        sorted.push(edge);
      }
      sorted.sort((e1, e2) => (e1.nw.x - e2.nw.x) || (e1.se.x - e2.se.x));

      // Iterate over all known edges, identifying intersections
      const ln = sorted.length;
      for ( let i=0; i<ln; i++ ) {
        const e1 = sorted[i];
        for ( let j=i+1; j<ln; j++ ) {
          const e2 = sorted[j];
          if ( e2.nw.x > e1.se.x ) break; // Segment e2 is entirely right of segment e1
          e1.recordIntersections(e2);
        }
      }
    }

    /* -------------------------------------------- */

    /**
     * Record the intersections between two edges.
     * @param {Edge} other          Another edge to test and record
     */
    recordIntersections(other) {
      if ( other === this ) return;
      const i = this.getIntersection(other);
      if ( !i ) return;
      this.intersections.push({edge: other, intersection: i});
      other.intersections.push({edge: this, intersection: {x: i.x, y: i.y, t0: i.t1, t1: i.t0}});
    }

    /* -------------------------------------------- */

    /**
     * Remove intersections of this edge with all other edges.
     */
    removeIntersections() {
      for ( const {edge: other} of this.intersections ) {
        other.intersections.findSplice(e => e.edge === this);
      }
      this.intersections.length = 0;
    }
  }

  /**
   * A special class of Map which defines all the edges used to restrict perception in a Scene.
   * @extends {Map<string, Edge>}
   */
  class CanvasEdges extends Map {

    /**
     * Edge instances which represent the outer boundaries of the game canvas.
     * @type {Edge[]}
     */
    #outerBounds = [];

    /**
     * Edge instances which represent the inner boundaries of the scene rectangle.
     * @type {Edge[]}
     */
    #innerBounds = [];

    /* -------------------------------------------- */

    /**
     * Initialize all active edges for the Scene. This workflow occurs once only when the Canvas is first initialized.
     * Edges are created from the following sources:
     * 1. Wall documents
     * 2. Canvas boundaries (inner and outer bounds)
     * 3. Darkness sources
     * 4. Programmatically defined in the "initializeEdges" hook
     */
    initialize() {
      this.clear();

      // Wall Documents
      for ( /** @type {Wall} */ const wall of canvas.walls.placeables ) wall.initializeEdge();

      // Canvas Boundaries
      this.#defineBoundaries();

      // Darkness Sources
      for ( const source of canvas.effects.darknessSources ) {
        for ( const edge of source.edges ) this.set(edge.id, edge);
      }

      // Programmatic Edges
      Hooks.callAll("initializeEdges");
    }

    /* -------------------------------------------- */

    /**
     * Incrementally refresh Edges by computing intersections between all registered edges.
     */
    refresh() {
      Edge.identifyEdgeIntersections(canvas.edges.values());
    }

    /* -------------------------------------------- */

    /**
     * Define Edge instances for outer and inner canvas bounds rectangles.
     */
    #defineBoundaries() {
      const d = canvas.dimensions;
      const define = (type, r) => {
        const top = new Edge({x: r.x, y: r.y}, {x: r.right, y: r.y}, {id: `${type}Top`, type});
        const right = new Edge({x: r.right, y: r.y}, {x: r.right, y: r.bottom}, {id: `${type}Right`, type});
        const bottom = new Edge({x: r.right, y: r.bottom}, {x: r.x, y: r.bottom}, {id: `${type}Bottom`, type});
        const left = new Edge({x: r.x, y: r.bottom}, {x: r.x, y: r.y}, {id: `${type}Left`, type});
        return [top, right, bottom, left];
      };

      // Outer canvas bounds
      this.#outerBounds = define("outerBounds", d.rect);
      for ( const b of this.#outerBounds ) this.set(b.id, b);

      // Inner canvas bounds (if there is padding)
      if ( d.rect.x === d.sceneRect.x ) this.#innerBounds = this.#outerBounds;
      else {
        this.#innerBounds = define("innerBounds", d.sceneRect);
        for ( const b of this.#innerBounds ) this.set(b.id, b);
      }
    }
  }

  /**
   * A specialized object that contains the result of a collision in the context of the ClockwiseSweepPolygon.
   * This class is not designed or intended for use outside of that context.
   * @alias CollisionResult
   */
  class CollisionResult {
    constructor({target, collisions=[], cwEdges, ccwEdges, isBehind, isLimited, wasLimited}={}) {
      this.target = target;
      this.collisions = collisions;
      this.cwEdges = cwEdges || new Set();
      this.ccwEdges = ccwEdges || new Set();
      this.isBehind = isBehind;
      this.isLimited = isLimited;
      this.wasLimited = wasLimited;
    }

    /**
     * The vertex that was the target of this result
     * @type {PolygonVertex}
     */
    target;

    /**
     * The array of collision points which apply to this result
     * @type {PolygonVertex[]}
     */
    collisions;

    /**
     * The set of edges connected to the target vertex that continue clockwise
     * @type {EdgeSet}
     */
    cwEdges;

    /**
     * The set of edges connected to the target vertex that continue counter-clockwise
     * @type {EdgeSet}
     */
    ccwEdges;

    /**
     * Is the target vertex for this result behind some closer active edge?
     * @type {boolean}
     */
    isBehind;

    /**
     * Does the target vertex for this result impose a limited collision?
     * @type {boolean}
     */
    isLimited;

    /**
     * Has the set of collisions for this result encountered a limited edge?
     * @type {boolean}
     */
    wasLimited;

    /**
     * Is this result limited in the clockwise direction?
     * @type {boolean}
     */
    limitedCW = false;

    /**
     * Is this result limited in the counter-clockwise direction?
     * @type {boolean}
     */
    limitedCCW = false;

    /**
     * Is this result blocking in the clockwise direction?
     * @type {boolean}
     */
    blockedCW = false;

    /**
     * Is this result blocking in the counter-clockwise direction?
     * @type {boolean}
     */
    blockedCCW = false;

    /**
     * Previously blocking in the clockwise direction?
     * @type {boolean}
     */
    blockedCWPrev = false;

    /**
     * Previously blocking in the counter-clockwise direction?
     * @type {boolean}
     */
    blockedCCWPrev = false;
  }

  /**
   * A specialized point data structure used to represent vertices in the context of the ClockwiseSweepPolygon.
   * This class is not designed or intended for use outside of that context.
   * @alias PolygonVertex
   */
  class PolygonVertex {
    constructor(x, y, {distance, index}={}) {
      this.x = Math.round(x);
      this.y = Math.round(y);
      this.key = PolygonVertex.getKey(this.x, this.y);
      this._distance = distance;
      this._d2 = undefined;
      this._index = index;
    }

    /**
     * The effective maximum texture size that Foundry VTT "ever" has to worry about.
     * @type {number}
     */
    static #MAX_TEXTURE_SIZE = Math.pow(2, 16);

    /**
     * Determine the sort key to use for this vertex, arranging points from north-west to south-east.
     * @param {number} x    The x-coordinate
     * @param {number} y    The y-coordinate
     * @returns {number}    The key used to identify the vertex
     */
    static getKey(x, y) {
      return (this.#MAX_TEXTURE_SIZE * x) + y;
    }

    /**
     * The set of edges which connect to this vertex.
     * This set is initially empty and populated later after vertices are de-duplicated.
     * @type {EdgeSet}
     */
    edges = new Set();

    /**
     * The subset of edges which continue clockwise from this vertex.
     * @type {EdgeSet}
     */
    cwEdges = new Set();

    /**
     * The subset of edges which continue counter-clockwise from this vertex.
     * @type {EdgeSet}
     */
    ccwEdges = new Set();

    /**
     * The set of vertices collinear to this vertex
     * @type {Set<PolygonVertex>}
     */
    collinearVertices = new Set();

    /**
     * Is this vertex an endpoint of one or more edges?
     * @type {boolean}
     */
    isEndpoint;

    /**
     * Does this vertex have a single counterclockwise limiting edge?
     * @type {boolean}
     */
    isLimitingCCW;

    /**
     * Does this vertex have a single clockwise limiting edge?
     * @type {boolean}
     */
    isLimitingCW;

    /**
     * Does this vertex have non-limited edges or 2+ limited edges counterclockwise?
     * @type {boolean}
     */
    isBlockingCCW;

    /**
     * Does this vertex have non-limited edges or 2+ limited edges clockwise?
     * @type {boolean}
     */
    isBlockingCW;

    /**
     * Does this vertex result from an internal collision?
     * @type {boolean}
     */
    isInternal = false;

    /**
     * The maximum restriction imposed by this vertex.
     * @type {number}
     */
    restriction = 0;

    /**
     * Record whether this PolygonVertex has been visited in the sweep
     * @type {boolean}
     * @internal
     */
    _visited = false;

    /* -------------------------------------------- */

    /**
     * Is this vertex limited in type?
     * @returns {boolean}
     */
    get isLimited() {
      return this.restriction === CONST.WALL_SENSE_TYPES.LIMITED;
    }

    /* -------------------------------------------- */

    /**
     * Associate an edge with this vertex.
     * @param {Edge} edge             The edge being attached
     * @param {number} orientation    The orientation of the edge with respect to the origin
     * @param {string} type           The restriction type of polygon being created
     */
    attachEdge(edge, orientation, type) {
      this.edges.add(edge);
      this.restriction = Math.max(this.restriction ?? 0, edge[type]);
      if ( orientation <= 0 ) this.cwEdges.add(edge);
      if ( orientation >= 0 ) this.ccwEdges.add(edge);
      this.#updateFlags(type);
    }

    /* -------------------------------------------- */

    /**
     * Update flags for whether this vertex is limiting or blocking in certain direction.
     * @param {string} type
     */
    #updateFlags(type) {
      const classify = edges => {
        const s = edges.size;
        if ( s === 0 ) return {isLimiting: false, isBlocking: false};
        if ( s > 1 ) return {isLimiting: false, isBlocking: true};
        else {
          const isLimiting = edges.first().isLimited(type);
          return {isLimiting, isBlocking: !isLimiting};
        }
      };

      // Flag endpoint
      this.isEndpoint = this.edges.some(edge => {
        return (edge.vertexA || edge.a).equals(this) || (edge.vertexB || edge.b).equals(this);
      });

      // Flag CCW edges
      const ccwFlags = classify(this.ccwEdges);
      this.isLimitingCCW = ccwFlags.isLimiting;
      this.isBlockingCCW = ccwFlags.isBlocking;

      // Flag CW edges
      const cwFlags = classify(this.cwEdges);
      this.isLimitingCW = cwFlags.isLimiting;
      this.isBlockingCW = cwFlags.isBlocking;
    }

    /* -------------------------------------------- */

    /**
     * Is this vertex the same point as some other vertex?
     * @param {PolygonVertex} other   Some other vertex
     * @returns {boolean}             Are they the same point?
     */
    equals(other) {
      return this.key === other.key;
    }

    /* -------------------------------------------- */

    /**
     * Construct a PolygonVertex instance from some other Point structure.
     * @param {Point} point           The point
     * @param {object} [options]      Additional options that apply to this vertex
     * @returns {PolygonVertex}       The constructed vertex
     */
    static fromPoint(point, options) {
      return new this(point.x, point.y, options);
    }
  }

  var _module$5 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    CanvasEdges: CanvasEdges,
    CollisionResult: CollisionResult,
    Edge: Edge,
    PolygonVertex: PolygonVertex
  });

  /**
   * The geometry of a {@link Region}.
   * - Vertex Attribute: `aVertexPosition` (`vec2`)
   * - Draw Mode: `PIXI.DRAW_MODES.TRIANGLES`
   */
  class RegionGeometry extends PIXI.Geometry {

    /**
     * Create a RegionGeometry.
     * @param {Region} region    The Region to create the RegionGeometry from.
     * @internal
     */
    constructor(region) {
      super();
      this.#region = region;
      this.addAttribute("aVertexPosition", new PIXI.Buffer(new Float32Array(), true, false), 2);
      this.addIndex(new PIXI.Buffer(new Uint16Array(), true, true));
    }

    /* -------------------------------------------- */

    /**
     * The Region this geometry belongs to.
     * @type {Region}
     */
    get region() {
      return this.#region;
    }

    #region;

    /* -------------------------------------------- */

    /**
     * Do the buffers need to be updated?
     * @type {boolean}
     */
    #invalidBuffers = true;

    /* -------------------------------------------- */

    /**
     * Update the buffers.
     * @internal
     */
    _clearBuffers() {
      this.buffers[0].update(new Float32Array());
      this.indexBuffer.update(new Uint16Array());
      this.#invalidBuffers = true;
    }

    /* -------------------------------------------- */

    /**
     * Update the buffers.
     * @internal
     */
    _updateBuffers() {
      if ( !this.#invalidBuffers ) return;
      const triangulation = this.region.triangulation;
      this.buffers[0].update(triangulation.vertices);
      this.indexBuffer.update(triangulation.indices);
      this.#invalidBuffers = false;
    }
  }

  /**
   * A mesh of a {@link Region}.
   * @extends {PIXI.Container}
   */
  class RegionMesh extends PIXI.Container {

    /**
     * Create a RegionMesh.
     * @param {Region} region                       The Region to create the RegionMesh from.
     * @param {AbstractBaseShader} [shaderClass]    The shader class to use.
     */
    constructor(region, shaderClass=RegionShader) {
      super();
      this.#region = region;
      this.region.geometry.refCount++;
      if ( !AbstractBaseShader.isPrototypeOf(shaderClass) ) {
        throw new Error("RegionMesh shader class must inherit from AbstractBaseShader.");
      }
      this.#shader = shaderClass.create();
    }

    /* ---------------------------------------- */

    /**
     * Shared point instance.
     * @type {PIXI.Point}
     */
    static #SHARED_POINT = new PIXI.Point();

    /* ---------------------------------------- */

    /**
     * The Region of this RegionMesh.
     * @type {RegionMesh}
     */
    get region() {
      return this.#region;
    }

    #region;

    /* ---------------------------------------- */

    /**
     * The shader bound to this RegionMesh.
     * @type {AbstractBaseShader}
     */
    get shader() {
      return this.#shader;
    }

    #shader;

    /* ---------------------------------------- */

    /**
     * The blend mode assigned to this RegionMesh.
     * @type {PIXI.BLEND_MODES}
     */
    get blendMode() {
      return this.#state.blendMode;
    }

    set blendMode(value) {
      if ( this.#state.blendMode === value ) return;
      this.#state.blendMode = value;
      this._tintAlphaDirty = true;
    }

    #state = PIXI.State.for2d();

    /* ---------------------------------------- */

    /**
     * The tint applied to the mesh. This is a hex value.
     *
     * A value of 0xFFFFFF will remove any tint effect.
     * @type {number}
     * @defaultValue 0xFFFFFF
     */
    get tint() {
      return this._tintColor.value;
    }

    set tint(tint) {
      const currentTint = this._tintColor.value;
      this._tintColor.setValue(tint);
      if ( currentTint === this._tintColor.value ) return;
      this._tintAlphaDirty = true;
    }

    /* ---------------------------------------- */

    /**
     * The tint applied to the mesh. This is a hex value. A value of 0xFFFFFF will remove any tint effect.
     * @type {PIXI.Color}
     * @protected
     */
    _tintColor = new PIXI.Color(0xFFFFFF);

    /* ---------------------------------------- */

    /**
     * Cached tint value for the shader uniforms.
     * @type {[red: number, green: number, blue: number, alpha: number]}
     * @protected
     * @internal
     */
    _cachedTint = [1, 1, 1, 1];

    /* ---------------------------------------- */

    /**
     * Used to track a tint or alpha change to execute a recomputation of _cachedTint.
     * @type {boolean}
     * @protected
     */
    _tintAlphaDirty = true;

    /* ---------------------------------------- */

    /**
     * Initialize shader based on the shader class type.
     * @param {type AbstractBaseShader} shaderClass  The shader class, which must inherit from {@link AbstractBaseShader}.
     */
    setShaderClass(shaderClass) {
      if ( !AbstractBaseShader.isPrototypeOf(shaderClass) ) {
        throw new Error("RegionMesh shader class must inherit from AbstractBaseShader.");
      }
      if ( this.#shader.constructor === shaderClass ) return;

      // Create shader program
      this.#shader = shaderClass.create();
    }

    /* ---------------------------------------- */

    /** @override */
    updateTransform() {
      super.updateTransform();

      // We set tintAlphaDirty to true if the worldAlpha has changed
      // It is needed to recompute the _cachedTint vec4 which is a combination of tint and alpha
      if ( this.#worldAlpha !== this.worldAlpha ) {
        this.#worldAlpha = this.worldAlpha;
        this._tintAlphaDirty = true;
      }
    }

    #worldAlpha;

    /* ---------------------------------------- */

    /** @override */
    _render(renderer) {
      if ( this._tintAlphaDirty ) {
        const premultiply = PIXI.utils.premultiplyBlendMode[1][this.blendMode] === this.blendMode;
        PIXI.Color.shared.setValue(this._tintColor)
          .premultiply(this.worldAlpha, premultiply)
          .toArray(this._cachedTint);
        this._tintAlphaDirty = false;
      }
      this.#shader._preRender(this, renderer);
      this.#shader.uniforms.translationMatrix = this.transform.worldTransform.toArray(true);

      // Flush batch renderer
      renderer.batch.flush();

      // Set state
      renderer.state.set(this.#state);

      // Bind shader and geometry
      renderer.shader.bind(this.#shader);
      const geometry = this.region.geometry;
      geometry._updateBuffers();
      renderer.geometry.bind(geometry, this.#shader);

      // Draw the geometry
      renderer.geometry.draw(PIXI.DRAW_MODES.TRIANGLES);
    }

    /* ---------------------------------------- */

    /** @override */
    _calculateBounds() {
      const {left, top, right, bottom} = this.region.bounds;
      this._bounds.addFrame(this.transform, left, top, right, bottom);
    }

    /* ---------------------------------------- */

    /**
     * Tests if a point is inside this RegionMesh.
     * @param {PIXI.IPointData} point
     * @returns {boolean}
     */
    containsPoint(point) {
      return this.region.polygonTree.testPoint(this.worldTransform.applyInverse(point, RegionMesh.#SHARED_POINT));
    }

    /* ---------------------------------------- */

    /** @override */
    destroy(options) {
      super.destroy(options);
      const geometry = this.region.geometry;
      geometry.refCount--;
      if ( geometry.refCount === 0 ) geometry.dispose();
      this.#shader = null;
      this.#state = null;
    }
  }

  /**
   * The node of a {@link RegionPolygonTree}.
   */
  class RegionPolygonTreeNode {

    /**
     * Create a RegionPolygonTreeNode.
     * @param {RegionPolygonTreeNode|null} parent    The parent node.
     * @internal
     */
    constructor(parent) {
      this.#parent = parent;
      this.#children = [];
      this.#depth = parent ? parent.depth + 1 : 0;
      this.#isHole = this.#depth % 2 === 0;
      if ( parent ) parent.#children.push(this);
      else {
        this.#polygon = null;
        this.#clipperPath = null;
      }
    }

    /* -------------------------------------------- */

    /**
     * Create a node from the Clipper path and add it to the children of the parent.
     * @param {ClipperLib.IntPoint[]} clipperPath              The clipper path of this node.
     * @param {RegionPolygonTreeNode|null} parent    The parent node or `null` if root.
     * @internal
     */
    static _fromClipperPath(clipperPath, parent) {
      const node = new RegionPolygonTreeNode(parent);
      if ( parent ) node.#clipperPath = clipperPath;
      return node;
    }

    /* -------------------------------------------- */

    /**
     * The parent of this node or `null` if this is the root node.
     * @type {RegionPolygonTreeNode|null}
     */
    get parent() {
      return this.#parent;
    }

    #parent;

    /* -------------------------------------------- */

    /**
     * The children of this node.
     * @type {ReadonlyArray<RegionPolygonTreeNode>}
     */
    get children() {
      return this.#children;
    }

    #children;

    /* -------------------------------------------- */

    /**
     * The depth of this node.
     * The depth of the root node is 0.
     * @type {number}
     */
    get depth() {
      return this.#depth;
    }

    #depth;

    /* -------------------------------------------- */

    /**
     * Is this a hole?
     * The root node is a hole.
     * @type {boolean}
     */
    get isHole() {
      return this.#isHole;
    }

    #isHole;

    /* -------------------------------------------- */

    /**
     * The Clipper path of this node.
     * It is empty in case of the root node.
     * @type {ReadonlyArray<ClipperLib.IntPoint>|null}
     */
    get clipperPath() {
      return this.#clipperPath;
    }

    #clipperPath;

    /* -------------------------------------------- */

    /**
     * The polygon of this node.
     * It is `null` in case of the root node.
     * @type {PIXI.Polygon|null}
     */
    get polygon() {
      let polygon = this.#polygon;
      if ( polygon === undefined ) polygon = this.#polygon = this.#createPolygon();
      return polygon;
    }

    #polygon;

    /* -------------------------------------------- */

    /**
     * The points of the polygon ([x0, y0, x1, y1, ...]).
     * They are `null` in case of the root node.
     * @type {ReadonlyArray<number>|null}
     */
    get points() {
      const polygon = this.polygon;
      if ( !polygon ) return null;
      return polygon.points;
    }

    /* -------------------------------------------- */

    /**
     * The bounds of the polygon.
     * They are `null` in case of the root node.
     * @type {PIXI.Rectangle|null}
     */
    get bounds() {
      let bounds = this.#bounds;
      if ( bounds === undefined ) bounds = this.#bounds = this.polygon?.getBounds() ?? null;
      return bounds;
    }

    #bounds;

    /* -------------------------------------------- */

    /**
     * Iterate over recursively over the children in depth-first order.
     * @yields {RegionPolygonTreeNode}
     */
    *[Symbol.iterator]() {
      for ( const child of this.children ) {
        yield child;
        yield *child;
      }
    }

    /* -------------------------------------------- */

    /**
     * Test whether given point is contained within this node.
     * @param {Point} point    The point.
     * @returns {boolean}
     */
    testPoint(point) {
      return this.#testPoint(point) === 2;
    }

    /* -------------------------------------------- */

    /**
     * Test point containment.
     * @param {Point} point    The point.
     * @returns {0|1|2}        - 0: not contained within the polygon of this node.
     *                         - 1: contained within the polygon of this node but also contained
     *                              inside the polygon of a sub-node that is a hole.
     *                         - 2: contained within the polygon of this node and not contained
     *                              inside any polygon of a sub-node that is a hole.
     */
    #testPoint(point) {
      const {x, y} = point;
      if ( this.parent ) {
        if ( !this.bounds.contains(x, y) || !this.polygon.contains(x, y) ) return 0;
      }
      const children = this.children;
      for ( let i = 0, n = children.length; i < n; i++ ) {
        const result = children[i].#testPoint(point);
        if ( result !== 0 ) return result;
      }
      return this.isHole ? 1 : 2;
    }

    /* -------------------------------------------- */

    /**
     * Test circle containment/intersection with this node.
     * @param {Point} center     The center point of the circle.
     * @param {number} radius    The radius of the circle.
     * @returns {-1|0|1}          - -1: the circle is in the exterior and does not intersect the boundary.
     *                            - 0: the circle is intersects the boundary.
     *                            - 1: the circle is in the interior and does not intersect the boundary.
     */
    testCircle(center, radius) {
      switch ( this.#testCircle(center, radius) ) {
        case 2: return 1;
        case 3: return 0;
        default: return -1;
      }
    }

    /* -------------------------------------------- */

    /**
     * Test circle containment/intersection with this node.
     * @param {Point} center     The center point of the circle.
     * @param {number} radius    The radius of the circle.
     * @returns {0|1|2|3}         - 0: does not intersect the boundary or interior of this node.
     *                            - 1: contained within the polygon of this node but also contained
     *                                 inside the polygon of a sub-node that is a hole.
     *                            - 2: contained within the polygon of this node and not contained
     *                                 inside any polygon of a sub-node that is a hole.
     *                            - 3: intersects the boundary of this node or any sub-node.
     */
    #testCircle(center, radius) {
      if ( this.parent ) {
        const {x, y} = center;

        // Test whether the circle intersects the bounds of this node
        const {left, right, top, bottom} = this.bounds;
        if ( (x < left - radius) || (x > right + radius) || (y < top - radius) || (y > bottom + radius) ) return 0;

        // Test whether the circle intersects any edge of the polygon of this node
        const intersects = foundry.utils.pathCircleIntersects(this.points, true, center, radius);
        if ( intersects ) return 3;

        // Test whether the circle is completely outside of the polygon
        const inside = this.polygon.contains(x, y);
        if ( !inside ) return 0;
      }

      // Test the children of this node now that we know that the circle is
      // completely inside of the polygon of this node
      const children = this.children;
      for ( let i = 0, n = children.length; i < n; i++ ) {
        const result = children[i].#testCircle(center, radius);
        if ( result !== 0 ) return result;
      }
      return this.isHole ? 1 : 2;
    }

    /* -------------------------------------------- */

    /**
     * Create the polygon of this node.
     * @returns {PIXI.Polygon|null}
     */
    #createPolygon() {
      if ( !this.parent ) return null;
      const scalingFactor = Region.CLIPPER_SCALING_FACTOR;
      const polygon = PIXI.Polygon.fromClipperPoints(this.clipperPath, {scalingFactor});
      polygon._isPositive = !this.isHole;
      return polygon;
    }
  }

  /* -------------------------------------------- */

  /**
   * The polygon tree of a {@link Region}.
   */
  class RegionPolygonTree extends RegionPolygonTreeNode {

    /**
     * Create a RegionPolygonTree.
     * @internal
     */
    constructor() {
      super(null);
    }

    /* -------------------------------------------- */

    /**
     * Create the tree from a Clipper polygon tree.
     * @param {ClipperLib.PolyTree} clipperPolyTree
     * @internal
     */
    static _fromClipperPolyTree(clipperPolyTree) {
      const visit = (clipperPolyNode, parent) => {
        const clipperPath = clipperPolyNode.Contour();
        const node = RegionPolygonTreeNode._fromClipperPath(clipperPath, parent);
        clipperPolyNode.Childs().forEach(child => visit(child, node));
        return node;
      };
      const tree = new RegionPolygonTree();
      clipperPolyTree.Childs().forEach(child => visit(child, tree));
      return tree;
    }
  }

  /**
   * The data model for a behavior that receives Region events.
   * @extends TypeDataModel
   * @memberof data.behaviors
   * @abstract
   *
   * @property {Set<string>} events    The Region events that are handled by the behavior.
   */
  class RegionBehaviorType extends TypeDataModel {

    /**
     * Create the events field.
     * @param {object} options      Options which configure how the events field is declared
     * @param {string[]} [options.events]     The event names to restrict to.
     * @param {string[]} [options.initial]    The initial set of events that should be default for the field
     * @returns {fields.SetField}
     * @protected
     */
    static _createEventsField({events, initial}={}) {
      const setFieldOptions = {
        label: "BEHAVIOR.TYPES.base.FIELDS.events.label",
        hint: "BEHAVIOR.TYPES.base.FIELDS.events.hint"
      };
      if ( initial ) setFieldOptions.initial = initial;
      return new SetField(new StringField({
        required: true,
        choices: Object.values(CONST.REGION_EVENTS).reduce((obj, e) => {
          if ( events && !events.includes(e) ) return obj;
          obj[e] = `REGION.EVENTS.${e}.label`;
          return obj;
        }, {})
      }), setFieldOptions);
    }

    /* ---------------------------------------- */

    /**
     * @callback EventBehaviorStaticHandler  Run in the context of a {@link RegionBehaviorType}.
     * @param {RegionEvent} event
     * @returns {Promise<void>}
     */

    /**
     * A RegionBehaviorType may register to always receive certain events by providing a record of handler functions.
     * These handlers are called with the behavior instance as its bound scope.
     * @type {Record<string, EventBehaviorStaticHandler>}
     */
    static events = {};

    /* ---------------------------------------- */

    /**
     * The events that are handled by the behavior.
     * @type {Set<string>}
     */
    events = this.events ?? new Set();

    /* ---------------------------------------- */

    /**
     * A convenience reference to the RegionBehavior which contains this behavior sub-type.
     * @type {RegionBehavior|null}
     */
    get behavior() {
      return this.parent;
    }

    /* ---------------------------------------- */

    /**
     * A convenience reference to the RegionDocument which contains this behavior sub-type.
     * @type {RegionDocument|null}
     */
    get region() {
      return this.behavior?.region ?? null;
    }

    /* ---------------------------------------- */

    /**
     * A convenience reference to the Scene which contains this behavior sub-type.
     * @type {Scene|null}
     */
    get scene() {
      return this.behavior?.scene ?? null;
    }

    /* ---------------------------------------- */

    /**
     * Handle the Region event.
     * @param {RegionEvent} event    The Region event
     * @returns {Promise<void>}
     * @protected
     * @internal
     */
    async _handleRegionEvent(event) {}
  }

  /**
   * The data model for a behavior that allows to suppress weather effects within the Region
   */
  class AdjustDarknessLevelRegionBehaviorType extends RegionBehaviorType {

    /** @override */
    static LOCALIZATION_PREFIXES = ["BEHAVIOR.TYPES.adjustDarknessLevel", "BEHAVIOR.TYPES.base"];

    /* ---------------------------------------- */

    /**
     * Darkness level behavior modes.
     * @enum {number}
     */
    static get MODES() {
      return AdjustDarknessLevelRegionBehaviorType.#MODES;
    }

    static #MODES = Object.freeze({
      /**
       * Override the darkness level with the modifier.
       */
      OVERRIDE: 0,

      /**
       * Brighten the darkness level: `darknessLevel * (1 - modifier)`
       */
      BRIGHTEN: 1,

      /**
       * Darken the darkness level: `1 - (1 - darknessLevel) * (1 - modifier)`.
       */
      DARKEN: 2
    });

    /* ---------------------------------------- */

    /** @override */
    static defineSchema() {
      return {
        mode: new NumberField({required: true, blank: false, choices: Object.fromEntries(Object.entries(this.MODES)
            .map(([key, value]) => [value, `BEHAVIOR.TYPES.adjustDarknessLevel.MODES.${key}.label`])),
          initial: this.MODES.OVERRIDE, validationError: "must be a value in AdjustDarknessLevelRegionBehaviorType.MODES"}),
        modifier: new AlphaField({initial: 0, step: 0.01})
      };
    }

    /* ---------------------------------------- */

    /**
     * Called when the status of the weather behavior is changed.
     * @param {RegionEvent} event
     * @this {AdjustDarknessLevelRegionBehaviorType}
     */
    static async #onBehaviorStatus(event) {

      // Create mesh
      if ( event.data.viewed === true ) {
        // Create darkness level mesh
        const dlMesh = new RegionMesh(this.region.object, AdjustDarknessLevelRegionShader);
        if ( canvas.performance.mode > CONST.CANVAS_PERFORMANCE_MODES.LOW ) {
          dlMesh._blurFilter = canvas.createBlurFilter(8, 2);
          dlMesh.filters = [dlMesh._blurFilter];
        }

        // Create illumination mesh
        const illMesh = new RegionMesh(this.region.object, IlluminationDarknessLevelRegionShader);

        // Common properties
        illMesh.name = dlMesh.name = this.behavior.uuid;
        illMesh.shader.mode = dlMesh.shader.mode = this.mode;
        illMesh.shader.modifier = dlMesh.shader.modifier = this.modifier;

        // Adding the mesh to their respective containers
        canvas.effects.illumination.darknessLevelMeshes.addChild(dlMesh);
        canvas.visibility.vision.light.global.meshes.addChild(illMesh);

        // Invalidate darkness level container and refresh vision if global light is enabled
        canvas.effects.illumination.invalidateDarknessLevelContainer(true);
        canvas.perception.update({refreshLighting: true, refreshVision: canvas.environment.globalLightSource.active});
      }

      // Destroy mesh
      else if ( event.data.viewed === false ) {
        const dlMesh = canvas.effects.illumination.darknessLevelMeshes.getChildByName(this.behavior.uuid);
        if ( dlMesh._blurFilter ) canvas.blurFilters.delete(dlMesh._blurFilter);
        dlMesh.destroy();
        const ilMesh = canvas.visibility.vision.light.global.meshes.getChildByName(this.behavior.uuid);
        ilMesh.destroy();
        canvas.effects.illumination.invalidateDarknessLevelContainer(true);
        canvas.perception.update({refreshLighting: true, refreshVision: canvas.environment.globalLightSource.active});
      }
    }

    /* ---------------------------------------- */

    /**
     * Called when the boundary of an event has changed.
     * @param {RegionEvent} event
     * @this {AdjustDarknessLevelRegionBehaviorType}
     */
    static async #onRegionBoundary(event) {
      if ( !this.behavior.viewed ) return;
      canvas.effects.illumination.invalidateDarknessLevelContainer(true);
      canvas.perception.update({refreshLighting: true, refreshVision: canvas.environment.globalLightSource.active});
    }

    /* ---------------------------------------- */

    /** @override */
    static events = {
      [REGION_EVENTS.BEHAVIOR_STATUS]: this.#onBehaviorStatus,
      [REGION_EVENTS.REGION_BOUNDARY]: this.#onRegionBoundary
    };

    /* ---------------------------------------- */

    /** @inheritDoc */
    _onUpdate(changed, options, userId) {
      super._onUpdate(changed, options, userId);
      if ( !("system" in changed) || !this.behavior.viewed ) return;
      const dlMesh = canvas.effects.illumination.darknessLevelMeshes.getChildByName(this.behavior.uuid);
      dlMesh.shader.mode = this.mode;
      dlMesh.shader.modifier = this.modifier;
      const ilMesh = canvas.visibility.vision.light.global.meshes.getChildByName(this.behavior.uuid);
      ilMesh.shader.mode = this.mode;
      ilMesh.shader.modifier = this.modifier;
      canvas.effects.illumination.invalidateDarknessLevelContainer(true);
      canvas.perception.update({refreshLighting: true, refreshVision: canvas.environment.globalLightSource.active});
    }
  }

  /**
   * The data model for a behavior that displays scrolling text above a token when one of the subscribed events occurs.
   *
   * @property {boolean} once           Disable the behavior after it triggers once
   * @property {string} text            The text to display
   * @property {string} color           Optional color setting for the text
   * @property {number} visibility      Which users the scrolling text will display for
                                        (see {@link DisplayScrollingTextRegionBehaviorType.VISIBILITY_MODES})
   */
  class DisplayScrollingTextRegionBehaviorType extends RegionBehaviorType {

    /** @override */
    static LOCALIZATION_PREFIXES = ["BEHAVIOR.TYPES.displayScrollingText", "BEHAVIOR.TYPES.base"];

    /* ---------------------------------------- */

    /**
     * Text visibility behavior modes.
     * @enum {number}
     */
    static get VISIBILITY_MODES() {
      return DisplayScrollingTextRegionBehaviorType.#VISIBILITY_MODES;
    }

    static #VISIBILITY_MODES = Object.freeze({
      /**
       * Display only for gamemaster users
       */
      GAMEMASTER: 0,

      /**
       * Display only for users with observer permissions on the triggering token (and for the GM)
       */
      OBSERVER: 1,

      /**
       * Display for all users
       */
      ANYONE: 2,
    });

    /* ---------------------------------------- */

    /** @override */
    static defineSchema() {
      return {
        events: this._createEventsField({events: [
          REGION_EVENTS.TOKEN_ENTER,
          REGION_EVENTS.TOKEN_EXIT,
          REGION_EVENTS.TOKEN_MOVE,
          REGION_EVENTS.TOKEN_MOVE_IN,
          REGION_EVENTS.TOKEN_MOVE_OUT,
          REGION_EVENTS.TOKEN_TURN_START,
          REGION_EVENTS.TOKEN_TURN_END,
          REGION_EVENTS.TOKEN_ROUND_START,
          REGION_EVENTS.TOKEN_ROUND_END
        ]}),
        text: new StringField({required: true}),
        color: new ColorField({required: true, nullable: false, initial: "#ffffff"}),
        visibility: new NumberField({
          required: true,
          choices: Object.entries(this.VISIBILITY_MODES).reduce((obj, [key, value]) => {
            obj[value] = `BEHAVIOR.TYPES.displayScrollingText.VISIBILITY_MODES.${key}.label`;
            return obj;
          }, {}),
          initial: this.VISIBILITY_MODES.ANYONE,
          validationError: "must be a value in DisplayScrollingTextRegionBehaviorType.VISIBILITY_MODES"}),
        once: new BooleanField()
      };
    }

    /* ---------------------------------------- */

    /**
     * Display the scrolling text to the current User?
     * @param {RegionEvent} event    The Region event.
     * @returns {boolean}            Display the scrolling text to the current User?
     */
    #canView(event) {
      if ( !this.parent.scene.isView ) return false;
      if ( game.user.isGM ) return true;
      if ( event.data.token.isSecret ) return false;

      const token = event.data.token.object;
      if ( !token || !token.visible ) return false;

      const M = DisplayScrollingTextRegionBehaviorType.VISIBILITY_MODES;
      if ( this.visibility === M.ANYONE ) return true;
      if ( this.visibility === M.OBSERVER ) return event.data.token.testUserPermission(game.user, "OBSERVER");
      return false;
    }

    /* ---------------------------------------- */

    /** @override */
    async _handleRegionEvent(event) {
      if ( this.once && game.users.activeGM?.isSelf ) {
        // noinspection ES6MissingAwait
        this.parent.update({disabled: true});
      }

      if ( !this.text ) return;
      const canView = this.#canView(event);
      if ( !canView ) return;

      const token = event.data.token.object;
      const animation = CanvasAnimation.getAnimation(token.animationName);
      if ( animation ) await animation.promise;
      await canvas.interface.createScrollingText(
        token.center,
        this.text,
        {
          distance: 2 * token.h,
          fontSize: 28,
          fill: this.color,
          stroke: 0x000000,
          strokeThickness: 4
        }
      );
    }
  }

  /**
   * The data model for a behavior that executes a Macro.
   *
   * @property {string} uuid           The Macro UUID.
   */
  class ExecuteMacroRegionBehaviorType extends RegionBehaviorType {

    /** @override */
    static LOCALIZATION_PREFIXES = ["BEHAVIOR.TYPES.executeMacro", "BEHAVIOR.TYPES.base"];

    /* ---------------------------------------- */

    /** @override */
    static defineSchema() {
      return {
        events: this._createEventsField(),
        uuid: new DocumentUUIDField({type: "Macro"}),
        everyone: new BooleanField()
      };
    }

    /* ---------------------------------------- */

    /** @override */
    async _handleRegionEvent(event) {
      if ( !this.uuid ) return;
      const macro = await fromUuid(this.uuid);
      if ( !(macro instanceof Macro) ) {
        console.error(`${this.uuid} does not exist`);
        return;
      }
      if ( !this.#shouldExecute(macro, event.user) ) return;
      const {scene, region, behavior} = this;
      const token = event.data.token;
      const speaker = token
        ? {scene: token.parent?.id ?? null, actor: token.actor?.id ?? null, token: token.id, alias: token.name}
        : {scene: scene.id, actor: null, token: null, alias: region.name};
      await macro.execute({speaker, actor: token?.actor, token: token?.object, scene, region, behavior, event});
    }

    /* ---------------------------------------- */

    /**
     * Should the client execute the macro?
     * @param {Macro} macro    The macro.
     * @param {User} user      The user that triggered the event.
     * @returns {boolean}      Should the client execute the macro?
     */
    #shouldExecute(macro, user) {
      if ( this.everyone ) return true;
      if ( macro.canUserExecute(user) ) return user.isSelf;
      const eligibleUsers = game.users.filter(u => u.active && macro.canUserExecute(u));
      if ( eligibleUsers.length === 0 ) return false;
      eligibleUsers.sort((a, b) => (b.role - a.role) || a.id.compare(b.id));
      const designatedUser = eligibleUsers[0];
      return designatedUser.isSelf;
    }
  }

  /**
   * The data model for a behavior that executes a script.
   *
   * @property {string} source    The source code of the script.
   */
  class ExecuteScriptRegionBehaviorType extends RegionBehaviorType {

    /** @override */
    static LOCALIZATION_PREFIXES = ["BEHAVIOR.TYPES.executeScript", "BEHAVIOR.TYPES.base"];

    /* ---------------------------------------- */

    /** @override */
    static defineSchema() {
      return {
        events: this._createEventsField(),
        source: new JavaScriptField({async: true, gmOnly: true})
      };
    }

    /* ---------------------------------------- */

    /** @override */
    async _handleRegionEvent(event) {
      try {
        // eslint-disable-next-line no-new-func
        const fn = new AsyncFunction("scene", "region", "behavior", "event", `{${this.source}\n}`);
        await fn.call(globalThis, this.scene, this.region, this.behavior, event);
      } catch(err) {
        console.error(err);
      }
    }
  }

  /**
   * The data model for a behavior that pauses the game when a player-controlled Token enters the Region.
   *
   * @property {boolean} once    Disable the behavior once a player-controlled Token enters the region?
   */
  class PauseGameRegionBehaviorType extends RegionBehaviorType {

    /** @override */
    static LOCALIZATION_PREFIXES = ["BEHAVIOR.TYPES.pauseGame", "BEHAVIOR.TYPES.base"];

    /* ---------------------------------------- */

    /** @override */
    static defineSchema() {
      return {
        once: new BooleanField()
      };
    }

    /* ---------------------------------------- */

    /**
     * Pause the game if a player-controlled Token moves into the Region.
     * @param {RegionEvent} event
     * @this {PauseGameRegionBehaviorType}
     */
    static async #onTokenMoveIn(event) {
      if ( event.data.forced || event.user.isGM || !game.users.activeGM?.isSelf ) return;
      game.togglePause(true, true);
      if ( this.once ) {
        // noinspection ES6MissingAwait
        this.parent.update({disabled: true});
      }
    }

    /* ---------------------------------------- */

    /**
     * Stop movement after a player-controlled Token enters the Region.
     * @param {RegionEvent} event
     * @this {PauseGameRegionBehaviorType}
     */
    static async #onTokenPreMove(event) {
      if ( event.user.isGM ) return;
      for ( const segment of event.data.segments ) {
        if ( segment.type === Region.MOVEMENT_SEGMENT_TYPES.ENTER ) {
          event.data.destination = segment.to;
          break;
        }
      }
    }

    /* ---------------------------------------- */

    /** @override */
    static events = {
      [REGION_EVENTS.TOKEN_MOVE_IN]: this.#onTokenMoveIn,
      [REGION_EVENTS.TOKEN_PRE_MOVE]: this.#onTokenPreMove
    };
  }

  /**
   * The data model for a behavior that allows to suppress weather effects within the Region
   */
  class SuppressWeatherRegionBehaviorType extends RegionBehaviorType {

    /** @override */
    static LOCALIZATION_PREFIXES = ["BEHAVIOR.TYPES.suppressWeather", "BEHAVIOR.TYPES.base"];

    /* ---------------------------------------- */

    /** @override */
    static defineSchema() {
      return {};
    }

    /* ---------------------------------------- */

    /**
     * Called when the status of the weather behavior is changed.
     * @param {RegionEvent} event
     * @this {SuppressWeatherRegionBehaviorType}
     */
    static async #onBehaviorStatus(event) {

      // Create mesh
      if ( event.data.viewed === true ) {
        const mesh = new RegionMesh(this.region.object);
        mesh.name = this.behavior.uuid;
        mesh.blendMode = PIXI.BLEND_MODES.ERASE;
        canvas.weather.suppression.addChild(mesh);
      }

      // Destroy mesh
      else if ( event.data.viewed === false ) {
        const mesh = canvas.weather.suppression.getChildByName(this.behavior.uuid);
        mesh.destroy();
      }
    }

    /* ---------------------------------------- */

    /** @override */
    static events = {
      [REGION_EVENTS.BEHAVIOR_STATUS]: this.#onBehaviorStatus
    };
  }

  /**
   * The data model for a behavior that teleports Token that enter the Region to a preset destination Region.
   *
   * @property {RegionDocument} destination    The destination Region the Token is teleported to.
   */
  class TeleportTokenRegionBehaviorType extends RegionBehaviorType {

    /** @override */
    static LOCALIZATION_PREFIXES = ["BEHAVIOR.TYPES.teleportToken", "BEHAVIOR.TYPES.base"];

    /* ---------------------------------------- */

    /** @override */
    static defineSchema() {
      return {
        destination: new DocumentUUIDField({type: "Region"}),
        choice: new BooleanField()
      };
    }

    /* ---------------------------------------- */

    /**
     * Teleport the Token if it moves into the Region.
     * @param {RegionEvent} event
     * @this {TeleportTokenRegionBehaviorType}
     */
    static async #onTokenMoveIn(event) {
      if ( !this.destination || event.data.forced ) return;
      const destination = fromUuidSync(this.destination);
      if ( !(destination instanceof RegionDocument) ) {
        console.error(`${this.destination} does not exist`);
        return;
      }
      const token = event.data.token;
      const user = event.user;
      if ( !TeleportTokenRegionBehaviorType.#shouldTeleport(token, destination, user) ) return false;
      if ( token.object ) {
        const animation = CanvasAnimation.getAnimation(token.object.animationName);
        if ( animation ) await animation.promise;
      }
      if ( this.choice ) {
        let confirmed;
        if ( user.isSelf ) confirmed = await TeleportTokenRegionBehaviorType.#confirmDialog(token, destination);
        else {
          confirmed = await new Promise(resolve => {
            game.socket.emit("confirmTeleportToken", {
              behaviorUuid: this.parent.uuid,
              tokenUuid: token.uuid,
              userId: user.id
            }, resolve);
          });
        }
        if ( !confirmed ) return;
      }
      await TeleportTokenRegionBehaviorType.#teleportToken(token, destination, user);
    }

    /* ---------------------------------------- */

    /**
     * Stop movement after a Token enters the Region.
     * @param {RegionEvent} event
     * @this {TeleportTokenRegionBehaviorType}
     */
    static async #onTokenPreMove(event) {
      if ( !this.destination ) return;
      for ( const segment of event.data.segments ) {
        if ( segment.type === Region.MOVEMENT_SEGMENT_TYPES.ENTER ) {
          event.data.destination = segment.to;
          break;
        }
      }
    }

    /* ---------------------------------------- */

    /** @override */
    static events = {
      [REGION_EVENTS.TOKEN_MOVE_IN]: this.#onTokenMoveIn,
      [REGION_EVENTS.TOKEN_PRE_MOVE]: this.#onTokenPreMove
    };

    /* ---------------------------------------- */

    /**
     * Should the current user teleport the token?
     * @param {TokenDocument} token           The token that is teleported.
     * @param {RegionDocument} destination    The destination region.
     * @param {User} user                     The user that moved the token.
     * @returns {boolean}                     Should the current user teleport the token?
     */
    static #shouldTeleport(token, destination, user) {
      const userCanTeleport = (token.parent === destination.parent) || (user.can("TOKEN_CREATE") && user.can("TOKEN_DELETE"));
      if ( userCanTeleport ) return user.isSelf;
      const eligibleGMs = game.users.filter(u => u.active && u.isGM && u.can("TOKEN_CREATE") && u.can("TOKEN_DELETE"));
      if ( eligibleGMs.length === 0 ) return false;
      eligibleGMs.sort((a, b) => (b.role - a.role) || a.id.compare(b.id));
      const designatedGM = eligibleGMs[0];
      return designatedGM.isSelf;
    }

    /* ---------------------------------------- */

    /**
     * Teleport the Token to the destination Region, which is in Scene that is not viewed.
     * @param {TokenDocument} originToken           The token that is teleported.
     * @param {RegionDocument} destinationRegion    The destination region.
     * @param {User} user                           The user that moved the token.
     */
    static async #teleportToken(originToken, destinationRegion, user) {
      const destinationScene = destinationRegion.parent;
      const destinationRegionObject = destinationRegion.object ?? new CONFIG.Region.objectClass(destinationRegion);
      const originScene = originToken.parent;
      let destinationToken;
      if ( originScene === destinationScene ) destinationToken = originToken;
      else {
        const originTokenData = originToken.toObject();
        delete originTokenData._id;
        destinationToken = TokenDocument.implementation.fromSource(originTokenData, {parent: destinationScene});
      }
      const destinationTokenObject = destinationToken.object ?? new CONFIG.Token.objectClass(destinationToken);

      // Reset destination token so that it isn't in an animated state
      if ( destinationTokenObject.animationContexts.size !== 0 ) destinationToken.reset();

      // Get the destination position
      let destination;
      try {
        destination = TeleportTokenRegionBehaviorType.#getDestination(destinationRegionObject, destinationTokenObject);
      } finally {
        if ( !destinationRegion.object ) destinationRegionObject.destroy({children: true});
        if ( !destinationToken.id || !destinationToken.object ) destinationTokenObject.destroy({children: true});
      }

      // If the origin and destination scene are the same
      if ( originToken === destinationToken ) {
        await originToken.update(destination, {teleport: true, forced: true});
        return;
      }

      // Otherwise teleport the token to the different scene
      destinationToken.updateSource(destination);

      // Create the new token
      const destinationTokenData = destinationToken.toObject();
      if ( destinationScene.tokens.has(originToken.id) ) delete destinationTokenData._id;
      else destinationTokenData._id = originToken.id;
      destinationToken = await TokenDocument.implementation.create(destinationToken,
        {parent: destinationScene, keepId: true});

      // Update all combatants of the token
      for ( const combat of game.combats ) {
        const toUpdate = [];
        for ( const combatant of combat.combatants ) {
          if ( (combatant.sceneId === originScene.id) && (combatant.tokenId === originToken.id) ) {
            toUpdate.push({_id: combatant.id, sceneId: destinationScene.id, tokenId: destinationToken.id});
          }
        }
        if ( toUpdate.length ) await combat.updateEmbeddedDocuments("Combatant", toUpdate);
      }

      // Delete the old token
      await originToken.delete();

      // View destination scene / Pull the user to the destination scene only if the user is currently viewing the origin scene
      if ( user.isSelf ) {
        if ( originScene.isView ) await destinationScene.view();
      } else {
        if ( originScene.id === user.viewedScene ) await game.socket.emit("pullToScene", destinationScene.id, user.id);
      }
    }

    /* ---------------------------------------- */

    /**
     * Get a destination for the Token within the Region that places the token and its center point inside it.
     * @param {Region} region                                  The region that is the destination of the teleportation.
     * @param {Token} token                                    The token that is teleported.
     * @returns {{x: number, y: number, elevation: number}}    The destination.
     */
    static #getDestination(region, token) {
      const scene = region.document.parent;
      const grid = scene.grid;

      // Not all regions are valid teleportation destinations
      if ( region.polygons.length === 0 ) throw new Error(`${region.document.uuid} is empty`);

      // Clamp the elevation of the token the elevation range of the destination region
      const elevation = Math.clamp(token.document.elevation, region.bottom, region.top);

      // Now we look for a random position within the destination region for the token
      let position;
      const pivot = token.getCenterPoint({x: 0, y: 0});

      // Find a random snapped position in square/hexagonal grids that place the token within the destination region
      if ( !grid.isGridless ) {

        // Identify token positions that place the token and its center point within the region
        const positions = [];
        const [i0, j0, i1, j1] = grid.getOffsetRange(new PIXI.Rectangle(
          0, 0, scene.dimensions.width, scene.dimensions.height).fit(region.bounds).pad(1));
        for ( let i = i0; i < i1; i++ ) {
          for ( let j = j0; j < j1; j++ ) {

            // Drop the token with its center point on the grid space center and snap the token position
            const center = grid.getCenterPoint({i, j});

            // The grid space center must be inside the region to be a valid drop target
            if ( !region.polygonTree.testPoint(center) ) continue;

            const position = token.getSnappedPosition({x: center.x - pivot.x, y: center.y - pivot.y});
            position.x = Math.round(position.x);
            position.y = Math.round(position.y);
            position.elevation = elevation;

            // The center point of the token must be inside the region
            if ( !region.polygonTree.testPoint(token.getCenterPoint(position)) ) continue;

            // The token itself must be inside the region
            if ( !token.testInsideRegion(region, position) ) continue;

            positions.push(position);
          }
        }

        // Pick a random position
        if ( positions.length !== 0 ) position = positions[Math.floor(positions.length * Math.random())];
      }

      // If we found a snapped position, we're done. Otherwise, search for an unsnapped position.
      if ( position ) return position;

      // Calculate the areas of each triangle of the triangulation
      const {vertices, indices} = region.triangulation;
      const areas = [];
      let totalArea = 0;
      for ( let k = 0; k < indices.length; k += 3 ) {
        const i0 = indices[k] * 2;
        const i1 = indices[k + 1] * 2;
        const i2 = indices[k + 2] * 2;
        const x0 = vertices[i0];
        const y0 = vertices[i0 + 1];
        const x1 = vertices[i1];
        const y1 = vertices[i1 + 1];
        const x2 = vertices[i2];
        const y2 = vertices[i2 + 1];
        const area = Math.abs(((x1 - x0) * (y2 - y0)) - ((x2 - x0) * (y1 - y0))) / 2;
        totalArea += area;
        areas.push(area);
      }

      // Try to find a position that places the token inside the region
      for ( let n = 0; n < 10; n++ ) {
        position = undefined;

        // Choose a triangle randomly weighted by area
        let j;
        let a = totalArea * Math.random();
        for ( j = 0; j < areas.length - 1; j++ ) {
          a -= areas[j];
          if ( a < 0 ) break;
        }
        const k = 3 * j;
        const i0 = indices[k] * 2;
        const i1 = indices[k + 1] * 2;
        const i2 = indices[k + 2] * 2;
        const x0 = vertices[i0];
        const y0 = vertices[i0 + 1];
        const x1 = vertices[i1];
        const y1 = vertices[i1 + 1];
        const x2 = vertices[i2];
        const y2 = vertices[i2 + 1];

        // Select a random point within the triangle
        const r1 = Math.sqrt(Math.random());
        const r2 = Math.random();
        const s = r1 * (1 - r2);
        const t = r1 * r2;
        const x = Math.round(x0 + ((x1 - x0) * s) + ((x2 - x0) * t) - pivot.x);
        const y = Math.round(y0 + ((y1 - y0) * s) + ((y2 - y0) * t) - pivot.y);
        position = {x, y, elevation};

        // The center point of the token must be inside the region
        if ( !region.polygonTree.testPoint(token.getCenterPoint(position)) ) continue;

        // The token itself must be inside the region
        if ( !token.testInsideRegion(region, position) ) continue;
      }

      // If we still didn't find a position that places the token within the destination region,
      // the region is not a valid destination for teleporation or we didn't have luck finding one in 10 tries.
      if ( !position ) throw new Error(`${region.document.uuid} cannot accomodate ${token.document.uuid}`);

      return position;
    }

    /* -------------------------------------------- */

    /**
     * Activate the Socket event listeners.
     * @param {Socket} socket    The active game socket
     * @internal
     */
    static _activateSocketListeners(socket) {
      socket.on("confirmTeleportToken", this.#onSocketEvent.bind(this));
    }

    /* -------------------------------------------- */

    /**
     * Handle the socket event that handles teleporation confirmation.
     * @param {object} data                    The socket data.
     * @param {string} data.tokenUuid          The UUID of the Token that is teleported.
     * @param {string} data.destinationUuid    The UUID of the Region that is the destination of the teleportation.
     * @param {Function} ack                   The acknowledgement function to return the result of the confirmation to the server.
     */
    static async #onSocketEvent({behaviorUuid, tokenUuid}, ack) {
      let confirmed = false;
      try {
        const behavior = await fromUuid(behaviorUuid);
        if ( !behavior || (behavior.type !== "teleportToken") || !behavior.system.destination ) return;
        const destination = await fromUuid(behavior.system.destination);
        if ( !destination ) return;
        const token = await fromUuid(tokenUuid);
        if ( !token ) return;
        confirmed = await TeleportTokenRegionBehaviorType.#confirmDialog(token, destination);
      } finally {
        ack(confirmed);
      }
    }

    /* -------------------------------------------- */

    /**
     * Display a dialog to confirm the teleportation?
     * @param {TokenDocument} token           The token that is teleported.
     * @param {RegionDocument} destination    The destination region.
     * @returns {Promise<boolean>}            The result of the dialog.
     */
    static async #confirmDialog(token, destination) {
      return DialogV2.confirm({
        window: {title: game.i18n.localize(CONFIG.RegionBehavior.typeLabels.teleportToken)},
        content: `<p>${game.i18n.format(game.user.isGM ? "BEHAVIOR.TYPES.teleportToken.ConfirmGM"
        : "BEHAVIOR.TYPES.teleportToken.Confirm", {token: token.name, region: destination.name,
          scene: destination.parent.name})}</p>`,
        rejectClose: false
      });
    }
  }

  /**
   * The data model for a behavior that toggles Region Behaviors when one of the subscribed events occurs.
   *
   * @property {Set<string>} enable     The Region Behavior UUIDs that are enabled.
   * @property {Set<string>} disable    The Region Behavior UUIDs that are disabled.
   */
  class ToggleBehaviorRegionBehaviorType extends RegionBehaviorType {

    /** @override */
    static LOCALIZATION_PREFIXES = ["BEHAVIOR.TYPES.toggleBehavior", "BEHAVIOR.TYPES.base"];

    /* ---------------------------------------- */

    /** @override */
    static defineSchema() {
      return {
        events: this._createEventsField({events: [
          REGION_EVENTS.TOKEN_ENTER,
          REGION_EVENTS.TOKEN_EXIT,
          REGION_EVENTS.TOKEN_MOVE,
          REGION_EVENTS.TOKEN_MOVE_IN,
          REGION_EVENTS.TOKEN_MOVE_OUT,
          REGION_EVENTS.TOKEN_TURN_START,
          REGION_EVENTS.TOKEN_TURN_END,
          REGION_EVENTS.TOKEN_ROUND_START,
          REGION_EVENTS.TOKEN_ROUND_END
        ]}),
        enable: new SetField(new DocumentUUIDField({type: "RegionBehavior"})),
        disable: new SetField(new DocumentUUIDField({type: "RegionBehavior"}))
      };
    }

    /* -------------------------------------------- */

    /** @override */
    static validateJoint(data) {
      if ( new Set(data.enable).intersection(new Set(data.disable)).size !== 0 ) {
        throw new Error("A RegionBehavior cannot be both enabled and disabled");
      }
    }

    /* ---------------------------------------- */

    /** @override */
    async _handleRegionEvent(event) {
      if ( !game.users.activeGM?.isSelf ) return;
      const toggle = async (uuid, disabled) => {
        const behavior = await fromUuid(uuid);
        if ( !(behavior instanceof RegionBehavior) ) {
          console.error(`${uuid} does not exist`);
          return;
        }
        await behavior.update({disabled});
      };
      await Promise.allSettled(this.disable.map(uuid => toggle(uuid, true)));
      await Promise.allSettled(this.enable.map(uuid => toggle(uuid, false)));
    }
  }

  /** @module foundry.data.regionBehaviors */

  var _module$4 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    AdjustDarknessLevelRegionBehaviorType: AdjustDarknessLevelRegionBehaviorType,
    DisplayScrollingTextRegionBehaviorType: DisplayScrollingTextRegionBehaviorType,
    ExecuteMacroRegionBehaviorType: ExecuteMacroRegionBehaviorType,
    ExecuteScriptRegionBehaviorType: ExecuteScriptRegionBehaviorType,
    PauseGameRegionBehaviorType: PauseGameRegionBehaviorType,
    RegionBehaviorType: RegionBehaviorType,
    SuppressWeatherRegionBehaviorType: SuppressWeatherRegionBehaviorType,
    TeleportTokenRegionBehaviorType: TeleportTokenRegionBehaviorType,
    ToggleBehaviorRegionBehaviorType: ToggleBehaviorRegionBehaviorType
  });

  /**
   * @typedef {import("../../common/abstract/_types.mjs").DatabaseAction} DatabaseAction
   * @typedef {import("../../common/abstract/_types.mjs").DatabaseOperation} DatabaseOperation
   * @typedef {import("../../common/abstract/_types.mjs").DatabaseGetOperation} DatabaseGetOperation
   * @typedef {import("../../common/abstract/_types.mjs").DatabaseCreateOperation} DatabaseCreateOperation
   * @typedef {import("../../common/abstract/_types.mjs").DatabaseUpdateOperation} DatabaseUpdateOperation
   * @typedef {import("../../common/abstract/_types.mjs").DatabaseDeleteOperation} DatabaseDeleteOperation
   * @typedef {import("../../common/abstract/_types.mjs").DocumentSocketRequest} DocumentSocketRequest
   */

  /**
   * The client-side database backend implementation which handles Document modification operations.
   * @alias foundry.data.ClientDatabaseBackend
   */
  class ClientDatabaseBackend extends DatabaseBackend {

    /* -------------------------------------------- */
    /*  Get Operations                              */
    /* -------------------------------------------- */

    /**
     * @override
     * @ignore
     */
    async _getDocuments(documentClass, operation, user) {
      const request = ClientDatabaseBackend.#buildRequest(documentClass, "get", operation);
      const response = await ClientDatabaseBackend.#dispatchRequest(request);
      if ( operation.index ) return response.result;
      return response.result.map(data => documentClass.fromSource(data, {pack: operation.pack}));
    }

    /* -------------------------------------------- */
    /*  Create Operations                           */
    /* -------------------------------------------- */

    /**
     * @override
     * @ignore
     */
    async _createDocuments(documentClass, operation, user) {
      user ||= game.user;
      await ClientDatabaseBackend.#preCreateDocumentArray(documentClass, operation, user);
      if ( !operation.data.length ) return [];
      /** @deprecated since v12 */
      // Legacy support for temporary creation option
      if ( "temporary" in operation ) {
        foundry.utils.logCompatibilityWarning("It is no longer supported to create temporary documents using the " +
          "Document.createDocuments API. Use the new Document() constructor instead.", {since: 12, until: 14});
        if ( operation.temporary ) return operation.data;
      }
      const request = ClientDatabaseBackend.#buildRequest(documentClass, "create", operation);
      const response = await ClientDatabaseBackend.#dispatchRequest(request);
      return this.#handleCreateDocuments(response);
    }

    /* -------------------------------------------- */

    /**
     * Perform a standardized pre-creation workflow for all Document types.
     * This workflow mutates the operation data array.
     * @param {typeof ClientDocument} documentClass
     * @param {DatabaseCreateOperation} operation
     * @param {User} user
     */
    static async #preCreateDocumentArray(documentClass, operation, user) {
      const {data, noHook, pack, parent, ...options} = operation;
      const type = documentClass.documentName;
      const toCreate = [];
      const documents = [];
      for ( let d of data ) {

        // Clean input data
        d = ( d instanceof foundry.abstract.DataModel ) ? d.toObject() : foundry.utils.expandObject(d);
        d = documentClass.migrateData(d);
        const createData = foundry.utils.deepClone(d); // Copy for later passing original input data to preCreate

        // Create pending document
        let doc;
        try {
          doc = new documentClass(createData, {parent, pack});
        } catch(err) {
          Hooks.onError("ClientDatabaseBackend##preCreateDocumentArray", err, {id: d._id, log: "error", notify: "error"});
          continue;
        }

        // Call per-document workflows
        let documentAllowed = await doc._preCreate(d, options, user) ?? true;
        documentAllowed &&= (noHook || Hooks.call(`preCreate${type}`, doc, d, options, user.id));
        if ( documentAllowed === false ) {
          console.debug(`${vtt} | ${type} creation prevented by _preCreate`);
          continue;
        }
        documents.push(doc);
        toCreate.push(d);
      }
      operation.data = toCreate;
      if ( !documents.length ) return;

      // Call final pre-operation workflow
      Object.assign(operation, options); // Hooks may have changed options
      const operationAllowed = await documentClass._preCreateOperation(documents, operation, user);
      if ( operationAllowed === false ) {
        console.debug(`${vtt} | ${type} creation operation prevented by _preCreateOperation`);
        operation.data = [];
      }
      else operation.data = documents;
    }

    /* -------------------------------------------- */

    /**
     * Handle a SocketResponse from the server when one or multiple documents were created.
     * @param {foundry.abstract.DocumentSocketResponse} response  A document modification socket response
     * @returns {Promise<ClientDocument[]>}  An Array of created Document instances
     */
    async #handleCreateDocuments(response) {
      const {type, operation, result, userId} = response;
      const documentClass = getDocumentClass(type);
      const parent = /** @type {ClientDocument|null} */ operation.parent = await this._getParent(operation);
      const collection = ClientDatabaseBackend.#getCollection(documentClass, operation);
      const user = game.users.get(userId);
      const {pack, parentUuid, syntheticActorUpdate, ...options} = operation;
      operation.data = response.result; // Record created data objects back to the operation

      // Initial descendant document events
      const preArgs = [result, options, userId];
      parent?._dispatchDescendantDocumentEvents("preCreate", collection.name, preArgs);

      // Create documents and prepare post-creation callback functions
      const callbacks = result.map(data => {
        const doc = collection.createDocument(data, {parent, pack});
        collection.set(doc.id, doc, options);
        return () => {
          doc._onCreate(data, options, userId);
          Hooks.callAll(`create${type}`, doc, options, userId);
          return doc;
        }
      });
      parent?.reset();
      let documents = callbacks.map(fn => fn());

      // Call post-operation workflows
      const postArgs = [documents, result, options, userId];
      parent?._dispatchDescendantDocumentEvents("onCreate", collection.name, postArgs);
      await documentClass._onCreateOperation(documents, operation, user);
      collection._onModifyContents("create", documents, result, operation, user);

      // Log and return result
      if ( CONFIG.debug.documents ) this._logOperation("Created", type, documents, {level: "info", parent, pack});
      if ( syntheticActorUpdate ) documents = ClientDatabaseBackend.#adjustActorDeltaResponse(documents);
      return documents;
    }

    /* -------------------------------------------- */
    /*  Update Operations                           */
    /* -------------------------------------------- */

    /**
     * @override
     * @ignore
     */
    async _updateDocuments(documentClass, operation, user) {
      user ||= game.user;
      await ClientDatabaseBackend.#preUpdateDocumentArray(documentClass, operation, user);
      if ( !operation.updates.length ) return [];
      const request = ClientDatabaseBackend.#buildRequest(documentClass, "update", operation);
      const response = await ClientDatabaseBackend.#dispatchRequest(request);
      return this.#handleUpdateDocuments(response);
    }

    /* -------------------------------------------- */

    /**
     * Perform a standardized pre-update workflow for all Document types.
     * This workflow mutates the operation updates array.
     * @param {typeof ClientDocument} documentClass
     * @param {DatabaseUpdateOperation} operation
     * @param {User} user
     */
    static async #preUpdateDocumentArray(documentClass, operation, user) {
      const collection = ClientDatabaseBackend.#getCollection(documentClass, operation);
      const type = documentClass.documentName;
      const {updates, restoreDelta, noHook, pack, parent, ...options} = operation;

      // Ensure all Documents which are update targets have been loaded
      await ClientDatabaseBackend.#loadCompendiumDocuments(collection, updates);

      // Iterate over requested changes
      const toUpdate = [];
      const documents = [];
      for ( let update of updates ) {
        if ( !update._id ) throw new Error("You must provide an _id for every object in the update data Array.");

        // Retrieve the target document and the request changes
        let changes;
        if ( update instanceof foundry.abstract.DataModel ) changes = update.toObject();
        else changes = foundry.utils.expandObject(update);
        const doc = collection.get(update._id, {strict: true, invalid: true});

        // Migrate provided changes, including document sub-type
        const addType = ("type" in doc) && !("type" in changes);
        if ( addType ) changes.type = doc.type;
        changes = documentClass.migrateData(changes);

        // Perform pre-update operations
        let documentAllowed = await doc._preUpdate(changes, options, user) ?? true;
        documentAllowed &&= (noHook || Hooks.call(`preUpdate${type}`, doc, changes, options, user.id));
        if ( documentAllowed === false ) {
          console.debug(`${vtt} | ${type} update prevented during pre-update`);
          continue;
        }

        // Attempt updating the document to validate the changes
        let diff = {};
        try {
          diff = doc.updateSource(changes, {dryRun: true, fallback: false, restoreDelta});
        } catch(err) {
          ui.notifications.error(err.message.split("] ").pop());
          Hooks.onError("ClientDatabaseBackend##preUpdateDocumentArray", err, {id: doc.id, log: "error"});
          continue;
        }

        // Retain only the differences against the current source
        if ( options.diff ) {
          if ( foundry.utils.isEmpty(diff) ) continue;
          diff._id = doc.id;
          changes = documentClass.shimData(diff); // Re-apply shims for backwards compatibility in _preUpdate hooks
        }
        else if ( addType ) delete changes.type;
        documents.push(doc);
        toUpdate.push(changes);
      }
      operation.updates = toUpdate;
      if ( !toUpdate.length ) return;

      // Call final pre-operation workflow
      Object.assign(operation, options); // Hooks may have changed options
      const operationAllowed = await documentClass._preUpdateOperation(documents, operation, user);
      if ( operationAllowed === false ) {
        console.debug(`${vtt} | ${type} creation operation prevented by _preUpdateOperation`);
        operation.updates = [];
      }
    }

    /* -------------------------------------------- */

    /**
     * Handle a SocketResponse from the server when one or multiple documents were updated.
     * @param {foundry.abstract.DocumentSocketResponse} response  A document modification socket response
     * @returns {Promise<ClientDocument[]>}  An Array of updated Document instances
     */
    async #handleUpdateDocuments(response) {
      const {type, operation, result, userId} = response;
      const documentClass = getDocumentClass(type);
      const parent = /** @type {ClientDocument|null} */ operation.parent = await this._getParent(operation);
      const collection = ClientDatabaseBackend.#getCollection(documentClass, operation);
      const user = game.users.get(userId);
      const {pack, parentUuid, syntheticActorUpdate, ...options} = operation;
      operation.updates = response.result; // Record update data objects back to the operation

      // Ensure all Documents which are update targets have been loaded.
      await ClientDatabaseBackend.#loadCompendiumDocuments(collection, operation.updates);

      // Pre-operation actions
      const preArgs = [result, options, userId];
      parent?._dispatchDescendantDocumentEvents("preUpdate", collection.name, preArgs);

      // Perform updates and create a callback function for each document
      const callbacks = [];
      const changes = [];
      for ( let change of result ) {
        const doc = collection.get(change._id, {strict: false});
        if ( !doc ) continue;
        doc.updateSource(change, options);
        collection.set(doc.id, doc, options);
        callbacks.push(() => {
          change = documentClass.shimData(change);
          doc._onUpdate(change, options, userId);
          Hooks.callAll(`update${type}`, doc, change, options, userId);
          changes.push(change);
          return doc;
        });
      }
      parent?.reset();
      let documents = callbacks.map(fn => fn());
      operation.updates = changes;

      // Post-operation actions
      const postArgs = [documents, changes, options, userId];
      parent?._dispatchDescendantDocumentEvents("onUpdate", collection.name, postArgs);
      await documentClass._onUpdateOperation(documents, operation, user);
      collection._onModifyContents("update", documents, changes, operation, user);

      // Log and return result
      if ( CONFIG.debug.documents ) this._logOperation("Updated", type, documents, {level: "debug", parent, pack});
      if ( syntheticActorUpdate ) documents = ClientDatabaseBackend.#adjustActorDeltaResponse(documents);
      return documents;
    }

    /* -------------------------------------------- */
    /*  Delete Operations                           */
    /* -------------------------------------------- */

    /**
     * @override
     * @ignore
     */
    async _deleteDocuments(documentClass, operation, user) {
      user ||= game.user;
      await ClientDatabaseBackend.#preDeleteDocumentArray(documentClass, operation, user);
      if ( !operation.ids.length ) return operation.ids;
      const request = ClientDatabaseBackend.#buildRequest(documentClass, "delete", operation);
      const response = await ClientDatabaseBackend.#dispatchRequest(request);
      return this.#handleDeleteDocuments(response);
    }

    /* -------------------------------------------- */

    /**
     * Perform a standardized pre-delete workflow for all Document types.
     * This workflow mutates the operation ids array.
     * @param {typeof ClientDocument} documentClass
     * @param {DatabaseDeleteOperation} operation
     * @param {User} user
     */
    static async #preDeleteDocumentArray(documentClass, operation, user) {
      let {ids, deleteAll, noHook, pack, parent, ...options} = operation;
      const collection = ClientDatabaseBackend.#getCollection(documentClass, operation);
      const type = documentClass.documentName;

      // Ensure all Documents which are deletion targets have been loaded
      if ( deleteAll ) ids = Array.from(collection.index?.keys() ?? collection.keys());
      await ClientDatabaseBackend.#loadCompendiumDocuments(collection, ids);

      // Iterate over ids requested for deletion
      const toDelete = [];
      const documents = [];
      for ( const id of ids ) {
        const doc = collection.get(id, {strict: true, invalid: true});
        let documentAllowed = await doc._preDelete(options, user) ?? true;
        documentAllowed &&= (noHook || Hooks.call(`preDelete${type}`, doc, options, user.id));
        if ( documentAllowed === false ) {
          console.debug(`${vtt} | ${type} deletion prevented during pre-delete`);
          continue;
        }
        toDelete.push(id);
        documents.push(doc);
      }
      operation.ids = toDelete;
      if ( !toDelete.length ) return;

      // Call final pre-operation workflow
      Object.assign(operation, options); // Hooks may have changed options
      const operationAllowed = await documentClass._preDeleteOperation(documents, operation, user);
      if ( operationAllowed === false ) {
        console.debug(`${vtt} | ${type} creation operation prevented by _preDeleteOperation`);
        operation.ids = [];
      }
    }

    /* -------------------------------------------- */

    /**
     * Handle a SocketResponse from the server where Documents are deleted.
     * @param {foundry.abstract.DocumentSocketResponse} response  A document modification socket response
     * @returns {Promise<ClientDocument[]>}  An Array of deleted Document instances
     */
    async #handleDeleteDocuments(response) {
      const {type, operation, result, userId} = response;
      const documentClass = getDocumentClass(type);
      const parent = /** @type {ClientDocument|null} */ operation.parent = await this._getParent(operation);
      const collection = ClientDatabaseBackend.#getCollection(documentClass, operation);
      const user = game.users.get(userId);
      const {deleteAll, pack, parentUuid, syntheticActorUpdate, ...options} = operation;
      operation.ids = response.result; // Record deleted document ids back to the operation

      await ClientDatabaseBackend.#loadCompendiumDocuments(collection, operation.ids);

      // Pre-operation actions
      const preArgs = [result, options, userId];
      parent?._dispatchDescendantDocumentEvents("preDelete", collection.name, preArgs);

      // Perform deletions and create a callback function for each document
      const callbacks = [];
      const ids = [];
      for ( const id of result ) {
        const doc = collection.get(id, {strict: false});
        if ( !doc ) continue;
        collection.delete(id);
        callbacks.push(() => {
          doc._onDelete(options, userId);
          Hooks.callAll(`delete${type}`, doc, options, userId);
          ids.push(id);
          return doc;
        });
      }
      parent?.reset();
      let documents = callbacks.map(fn => fn());
      operation.ids = ids;

      // Post-operation actions
      const postArgs = [documents, ids, options, userId];
      parent?._dispatchDescendantDocumentEvents("onDelete", collection.name, postArgs);
      await documentClass._onDeleteOperation(documents, operation, user);
      collection._onModifyContents("delete", documents, ids, operation, user);

      // Log and return result
      if ( CONFIG.debug.documents ) this._logOperation("Deleted", type, documents, {level: "info", parent, pack});
      if ( syntheticActorUpdate ) documents = ClientDatabaseBackend.#adjustActorDeltaResponse(documents);
      return documents;
    }

    /* -------------------------------------------- */
    /*  Socket Workflows                            */
    /* -------------------------------------------- */

    /**
     * Activate the Socket event listeners used to receive responses from events which modify database documents
     * @param {Socket} socket                           The active game socket
     * @internal
     * @ignore
     */
    activateSocketListeners(socket) {
      socket.on("modifyDocument", this.#onModifyDocument.bind(this));
    }

    /* -------------------------------------------- */

    /**
     * Handle a socket response broadcast back from the server.
     * @param {foundry.abstract.DocumentSocketResponse} response  A document modification socket response
     */
    #onModifyDocument(response) {
      switch ( response.action ) {
        case "create":
          this.#handleCreateDocuments(response);
          break;
        case "update":
          this.#handleUpdateDocuments(response);
          break;
        case "delete":
          this.#handleDeleteDocuments(response);
          break;
        default:
          throw new Error(`Invalid Document modification action ${response.action} provided`);
      }
    }

    /* -------------------------------------------- */
    /*  Helper Methods                              */
    /* -------------------------------------------- */

    /** @inheritdoc */
    getFlagScopes() {
      if ( this.#flagScopes ) return this.#flagScopes;
      const scopes = ["core", "world", game.system.id];
      for ( const module of game.modules ) {
        if ( module.active ) scopes.push(module.id);
      }
      return this.#flagScopes = scopes;
    }

    /**
     * A cached array of valid flag scopes which can be read and written.
     * @type {string[]}
     */
    #flagScopes;

    /* -------------------------------------------- */

    /** @inheritdoc */
    getCompendiumScopes() {
      return Array.from(game.packs.keys());
    }

    /* -------------------------------------------- */

    /** @override */
    _log(level, message) {
      globalThis.logger[level](`${vtt} | ${message}`);
    }

    /* -------------------------------------------- */

      /**
       * Obtain the document collection for a given Document class and database operation.
       * @param {typeof foundry.abstract.Document} documentClass   The Document class being operated upon
       * @param {object} operation                The database operation being performed
       * @param {ClientDocument|null} operation.parent  A parent Document, if applicable
       * @param {string|null} operation.pack        A compendium pack identifier, if applicable
       * @returns {DocumentCollection|CompendiumCollection}  The relevant collection instance for this request
       */
    static #getCollection(documentClass, {parent, pack}) {
      const documentName = documentClass.documentName;
        if ( parent ) return parent.getEmbeddedCollection(documentName);
        if ( pack ) {
          const collection = game.packs.get(pack);
          return documentName === "Folder" ? collection.folders : collection;
        }
        return game.collections.get(documentName);
    }

    /* -------------------------------------------- */

    /**
     * Structure a database operation as a web socket request.
     * @param {typeof foundry.abstract.Document} documentClass
     * @param {DatabaseAction} action
     * @param {DatabaseOperation} operation
     * @returns {DocumentSocketRequest}
     */
    static #buildRequest(documentClass, action, operation) {
      const request = {type: documentClass.documentName, action, operation};
      if ( operation.parent ) { // Don't send full parent data
        operation.parentUuid = operation.parent.uuid;
        ClientDatabaseBackend.#adjustActorDeltaRequest(documentClass, request);
        delete operation.parent;
      }
      return request;
    }

    /* -------------------------------------------- */

    /**
     * Dispatch a document modification socket request to the server.
     * @param {DocumentSocketRequest} request
     * @returns {foundry.abstract.DocumentSocketResponse}
     */
    static async #dispatchRequest(request) {
      const responseData = await SocketInterface.dispatch("modifyDocument", request);
      return new foundry.abstract.DocumentSocketResponse(responseData);
    }

    /* -------------------------------------------- */

    /**
     * Ensure the given list of documents is loaded into the compendium collection so that they can be retrieved by
     * subsequent operations.
     * @param {Collection} collection        The candidate collection.
     * @param {object[]|string[]} documents  An array of update deltas, or IDs, depending on the operation.
     */
    static async #loadCompendiumDocuments(collection, documents) {
      // Ensure all Documents which are update targets have been loaded
      if ( collection instanceof CompendiumCollection ) {
        const ids = documents.reduce((arr, doc) => {
          const id = doc._id ?? doc;
          if ( id && !collection.has(id) ) arr.push(id);
          return arr;
        }, []);
        await collection.getDocuments({_id__in: ids});
      }
    }

    /* -------------------------------------------- */
    /*  Token and ActorDelta Special Case           */
    /* -------------------------------------------- */

    /**
     * Augment a database operation with alterations needed to support ActorDelta and TokenDocuments.
     * @param {typeof foundry.abstract.Document} documentClass    The document class being operated upon
     * @param {DocumentSocketRequest} request                     The document modification socket request
     */
    static #adjustActorDeltaRequest(documentClass, request) {
      const operation = request.operation;
      const parent = operation.parent;

      // Translate updates to a token actor to the token's ActorDelta instead.
      if ( foundry.utils.isSubclass(documentClass, Actor) && (parent instanceof TokenDocument) ) {
        request.type = "ActorDelta";
        if ( "updates" in operation ) operation.updates[0]._id = parent.delta.id;
        operation.syntheticActorUpdate = true;
      }

      // Translate operations on a token actor's embedded children to the token's ActorDelta instead.
      const token = ClientDatabaseBackend.#getTokenAncestor(parent);
      if ( token && !(parent instanceof TokenDocument) ) {
        const {embedded} = foundry.utils.parseUuid(parent.uuid);
        operation.parentUuid = [token.delta.uuid, embedded.slice(4).join(".")].filterJoin(".");
      }
    }

    /* -------------------------------------------- */

    /**
     * Retrieve a Document's Token ancestor, if it exists.
     * @param {Document|null} parent              The parent Document
     * @returns {TokenDocument|null}              The Token ancestor, or null
     */
    static #getTokenAncestor(parent) {
      if ( !parent ) return null;
      if ( parent instanceof TokenDocument ) return parent;
      return ClientDatabaseBackend.#getTokenAncestor(parent.parent);
    }

    /* -------------------------------------------- */

    /**
     * Build a CRUD response.
     * @param {ActorDelta[]} documents              An array of ActorDelta documents modified by a database workflow
     * @returns {foundry.abstract.Document[]}       The modified ActorDelta documents mapped to their synthetic Actor
     */
    static #adjustActorDeltaResponse(documents) {
      return documents.map(delta => delta.syntheticActor);
    }
  }

  /** @module foundry.data */

  var data = /*#__PURE__*/Object.freeze({
    __proto__: null,
    BaseShapeData: BaseShapeData,
    CircleShapeData: CircleShapeData,
    ClientDatabaseBackend: ClientDatabaseBackend,
    EllipseShapeData: EllipseShapeData,
    LightData: LightData,
    PolygonShapeData: PolygonShapeData,
    PrototypeToken: PrototypeToken,
    RectangleShapeData: RectangleShapeData,
    ShapeData: ShapeData,
    TextureData: TextureData,
    TombstoneData: TombstoneData,
    fields: fields$1,
    regionBehaviors: _module$4,
    validation: validationFailure,
    validators: validators
  });

  /**
   * A shape of a {@link Region}.
   * @template {data.BaseShapeData} T
   * @abstract
   */
  class RegionShape {

    /**
     * Create a RegionShape.
     * @param {T} data    The shape data.
     * @internal
     */
    constructor(data) {
      this.#data = data;
    }

    /* -------------------------------------------- */

    /**
     * Create the RegionShape from the shape data.
     * @template {data.BaseShapeData} T
     * @param {T} data    The shape data.
     * @returns {RegionShape<T>}
     */
    static create(data) {
      switch ( data.type ) {
        case "circle": return new RegionCircle(data);
        case "ellipse": return new RegionEllipse(data);
        case "polygon": return new RegionPolygon(data);
        case "rectangle": return new RegionRectangle(data);
        default: throw new Error("Invalid shape type");
      }
    }

    /* -------------------------------------------- */

    /**
     * The data of this shape.
     * It is owned by the shape and must not be modified.
     * @type {T}
     */
    get data() {
      return this.#data;
    }

    #data;

    /* -------------------------------------------- */

    /**
     * Is this a hole?
     * @type {boolean}
     */
    get isHole() {
      return this.data.hole;
    }

    /* -------------------------------------------- */

    /**
     * The Clipper paths of this shape.
     * The winding numbers are 1 or 0.
     * @type {ReadonlyArray<ReadonlyArray<ClipperLib.IntPoint>>}
     */
    get clipperPaths() {
      return this.#clipperPaths ??= ClipperLib.Clipper.PolyTreeToPaths(this.clipperPolyTree);
    }

    #clipperPaths;

    /* -------------------------------------------- */

    /**
     * The Clipper polygon tree of this shape.
     * @type {ClipperLib.PolyTree}
     */
    get clipperPolyTree() {
      let clipperPolyTree = this.#clipperPolyTree;
      if ( !clipperPolyTree ) {
        clipperPolyTree = this._createClipperPolyTree();
        if ( Array.isArray(clipperPolyTree) ) {
          const clipperPolyNode = new ClipperLib.PolyNode();
          clipperPolyNode.m_polygon = clipperPolyTree;
          clipperPolyTree = new ClipperLib.PolyTree();
          clipperPolyTree.AddChild(clipperPolyNode);
          clipperPolyTree.m_AllPolys.push(clipperPolyNode);
        }
        this.#clipperPolyTree = clipperPolyTree;
      }
      return clipperPolyTree;
    }

    #clipperPolyTree;

    /* -------------------------------------------- */

    /**
     * Create the Clipper polygon tree of this shape.
     * This function may return a single positively-orientated and non-selfintersecting Clipper path instead of a tree,
     * which is automatically converted to a Clipper polygon tree.
     * This function is called only once. It is not called if the shape is empty.
     * @returns {ClipperLib.PolyTree|ClipperLib.IntPoint[]}
     * @protected
     * @abstract
     */
    _createClipperPolyTree() {
      throw new Error("A subclass of the RegionShape must implement the _createClipperPolyTree method.");
    }

    /* -------------------------------------------- */

    /**
     * Draw shape into the graphics.
     * @param {PIXI.Graphics} graphics    The graphics to draw the shape into.
     * @protected
     * @internal
     */
    _drawShape(graphics) {
      throw new Error("A subclass of the RegionShape must implement the _drawShape method.");
    }
  }

  /* -------------------------------------------- */

  /**
   * A circle of a {@link Region}.
   * @extends {RegionShape<data.CircleShapeData>}
   *
   * @param {data.CircleShapeData} data    The shape data.
   */
  class RegionCircle extends RegionShape {
    constructor(data) {
      if ( !(data instanceof CircleShapeData) ) throw new Error("Invalid shape data");
      super(data);
    }

    /* -------------------------------------------- */

    /**
     * The vertex density epsilon used to create a polygon approximation of the circle.
     * @type {number}
     */
    static #VERTEX_DENSITY_EPSILON = 1;

    /* -------------------------------------------- */

    /** @override */
    _createClipperPolyTree() {
      const scalingFactor = Region.CLIPPER_SCALING_FACTOR;
      const data = this.data;
      const x = data.x * scalingFactor;
      const y = data.y * scalingFactor;
      const radius = data.radius * scalingFactor;
      const epsilon = RegionCircle.#VERTEX_DENSITY_EPSILON * scalingFactor;
      const density = PIXI.Circle.approximateVertexDensity(radius, epsilon);
      const path = new Array(density);
      for ( let i = 0; i < density; i++ ) {
        const angle = 2 * Math.PI * (i / density);
        path[i] = new ClipperLib.IntPoint(
          Math.round(x + (Math.cos(angle) * radius)),
          Math.round(y + (Math.sin(angle) * radius))
        );
      }
      return path;
    }

    /* -------------------------------------------- */

    /** @override */
    _drawShape(graphics) {
      const {x, y, radius} = this.data;
      graphics.drawCircle(x, y, radius);
    }
  }

  /* -------------------------------------------- */

  /**
   * An ellipse of a {@link Region}.
   * @extends {RegionShape<data.EllipseShapeData>}
   *
   * @param {data.EllipseShapeData} data    The shape data.
   */
  class RegionEllipse extends RegionShape {
    constructor(data) {
      if ( !(data instanceof EllipseShapeData) ) throw new Error("Invalid shape data");
      super(data);
    }

    /* -------------------------------------------- */

    /**
     * The vertex density epsilon used to create a polygon approximation of the circle.
     * @type {number}
     */
    static #VERTEX_DENSITY_EPSILON = 1;

    /* -------------------------------------------- */

    /** @override */
    _createClipperPolyTree() {
      const scalingFactor = Region.CLIPPER_SCALING_FACTOR;
      const data = this.data;
      const x = data.x * scalingFactor;
      const y = data.y * scalingFactor;
      const radiusX = data.radiusX * scalingFactor;
      const radiusY = data.radiusY * scalingFactor;
      const epsilon = RegionEllipse.#VERTEX_DENSITY_EPSILON * scalingFactor;
      const density = PIXI.Circle.approximateVertexDensity((radiusX + radiusY) / 2, epsilon);
      const rotation = Math.toRadians(data.rotation);
      const cos = Math.cos(rotation);
      const sin = Math.sin(rotation);
      const path = new Array(density);
      for ( let i = 0; i < density; i++ ) {
        const angle = 2 * Math.PI * (i / density);
        const dx = Math.cos(angle) * radiusX;
        const dy = Math.sin(angle) * radiusY;
        path[i] = new ClipperLib.IntPoint(
          Math.round(x + ((cos * dx) - (sin * dy))),
          Math.round(y + ((sin * dx) + (cos * dy)))
        );
      }
      return path;
    }

    /* -------------------------------------------- */

    /** @override */
    _drawShape(graphics) {
      const {x, y, radiusX, radiusY, rotation} = this.data;
      if ( rotation === 0 ) {
        graphics.drawEllipse(x, y, radiusX, radiusY);
      } else {
        graphics.setMatrix(new PIXI.Matrix()
          .translate(-x, -x)
          .rotate(Math.toRadians(rotation))
          .translate(x, y));
        graphics.drawEllipse(x, y, radiusX, radiusY);
        graphics.setMatrix(null);
      }
    }
  }

  /* -------------------------------------------- */

  /**
   * A polygon of a {@link Region}.
   * @extends {RegionShape<data.PolygonShapeData>}
   *
   * @param {data.PolygonShapeData} data    The shape data.
   */
  class RegionPolygon extends RegionShape {
    constructor(data) {
      if ( !(data instanceof PolygonShapeData) ) throw new Error("Invalid shape data");
      super(data);
    }

    /* -------------------------------------------- */

    /** @override */
    _createClipperPolyTree() {
      const scalingFactor = Region.CLIPPER_SCALING_FACTOR;
      const points = this.data.points;
      const path = new Array(points.length / 2);
      for ( let i = 0, j = 0; i < path.length; i++ ) {
        path[i] = new ClipperLib.IntPoint(
          Math.round(points[j++] * scalingFactor),
          Math.round(points[j++] * scalingFactor)
        );
      }
      if ( !ClipperLib.Clipper.Orientation(path) ) path.reverse();
      return path;
    }

    /* -------------------------------------------- */

    /** @override */
    _drawShape(graphics) {
      graphics.drawPolygon(this.data.points);
    }
  }

  /* -------------------------------------------- */

  /**
   * A rectangle of a {@link Region}.
   * @extends {RegionShape<data.RectangleShapeData>}
   *
   * @param {data.RectangleShapeData} data    The shape data.
   */
  class RegionRectangle extends RegionShape {
    constructor(data) {
      if ( !(data instanceof RectangleShapeData) ) throw new Error("Invalid shape data");
      super(data);
    }

    /* -------------------------------------------- */

    /** @override */
    _createClipperPolyTree() {
      let p0;
      let p1;
      let p2;
      let p3;
      const scalingFactor = Region.CLIPPER_SCALING_FACTOR;
      const {x, y, width, height, rotation} = this.data;
      let x0 = x * scalingFactor;
      let y0 = y * scalingFactor;
      let x1 = (x + width) * scalingFactor;
      let y1 = (y + height) * scalingFactor;

      // The basic non-rotated case
      if ( rotation === 0 ) {
        x0 = Math.round(x0);
        y0 = Math.round(y0);
        x1 = Math.round(x1);
        y1 = Math.round(y1);
        p0 = new ClipperLib.IntPoint(x0, y0);
        p1 = new ClipperLib.IntPoint(x1, y0);
        p2 = new ClipperLib.IntPoint(x1, y1);
        p3 = new ClipperLib.IntPoint(x0, y1);
      }

      // The more complex rotated case
      else {
        const tx = (x0 + x1) / 2;
        const ty = (y0 + y1) / 2;
        x0 -= tx;
        y0 -= ty;
        x1 -= tx;
        y1 -= ty;
        const angle = Math.toRadians(rotation);
        const cos = Math.cos(angle);
        const sin = Math.sin(angle);
        const x00 = Math.round((cos * x0) - (sin * y0) + tx);
        const y00 = Math.round((sin * x0) + (cos * y0) + ty);
        const x10 = Math.round((cos * x1) - (sin * y0) + tx);
        const y10 = Math.round((sin * x1) + (cos * y0) + ty);
        const x11 = Math.round((cos * x1) - (sin * y1) + tx);
        const y11 = Math.round((sin * x1) + (cos * y1) + ty);
        const x01 = Math.round((cos * x0) - (sin * y1) + tx);
        const y01 = Math.round((sin * x0) + (cos * y1) + ty);
        p0 = new ClipperLib.IntPoint(x00, y00);
        p1 = new ClipperLib.IntPoint(x10, y10);
        p2 = new ClipperLib.IntPoint(x11, y11);
        p3 = new ClipperLib.IntPoint(x01, y01);
      }
      return [p0, p1, p2, p3];
    }

    /* -------------------------------------------- */

    /** @override */
    _drawShape(graphics) {
      const {x, y, width, height, rotation} = this.data;
      if ( rotation === 0 ) {
        graphics.drawRect(x, y, width, height);
      } else {
        const centerX = x + (width / 2);
        const centerY = y + (height / 2);
        graphics.setMatrix(new PIXI.Matrix()
          .translate(-centerX, -centerY)
          .rotate(Math.toRadians(rotation))
          .translate(centerX, centerY));
        graphics.drawRect(x, y, width, height);
        graphics.setMatrix(null);
      }
    }
  }

  var _module$3 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    RegionGeometry: RegionGeometry,
    RegionMesh: RegionMesh,
    RegionPolygonTree: RegionPolygonTree,
    RegionShape: RegionShape
  });

  /**
   * @typedef {Object} BasseEffectSourceOptions
   * @property {PlaceableObject} [options.object] An optional PlaceableObject which is responsible for this source
   * @property {string} [options.sourceId]        A unique ID for this source. This will be set automatically if an
   *                                              object is provided, otherwise is required.
   */

  /**
   * @typedef {Object} BaseEffectSourceData
   * @property {number} x                   The x-coordinate of the source location
   * @property {number} y                   The y-coordinate of the source location
   * @property {number} elevation           The elevation of the point source
   * @property {boolean} disabled           Whether or not the source is disabled
   */

  /**
   * TODO - Re-document after ESM refactor.
   * An abstract base class which defines a framework for effect sources which originate radially from a specific point.
   * This abstraction is used by the LightSource, VisionSource, SoundSource, and MovementSource subclasses.
   *
   * @example A standard PointSource lifecycle:
   * ```js
   * const source = new PointSource({object}); // Create the point source
   * source.initialize(data);                  // Configure the point source with new data
   * source.refresh();                         // Refresh the point source
   * source.destroy();                         // Destroy the point source
   * ```
   *
   * @template {BaseEffectSourceData} SourceData
   * @template {PIXI.Polygon} SourceShape
   * @abstract
   */
  class BaseEffectSource {
    /**
     * An effect source is constructed by providing configuration options.
     * @param {BasseEffectSourceOptions} [options]  Options which modify the base effect source instance
     */
    constructor(options={}) {
      if ( options instanceof PlaceableObject ) {
        const warning = "The constructor PointSource(PlaceableObject) is deprecated. "
          + "Use new PointSource({ object }) instead.";
        foundry.utils.logCompatibilityWarning(warning, {since: 11, until: 13});
        this.object = options;
        this.sourceId = this.object.sourceId;
      }
      else {
        this.object = options.object ?? null;
        this.sourceId = options.sourceId;
      }
    }

    /**
     * The type of source represented by this data structure.
     * Each subclass must implement this attribute.
     * @type {string}
     */
    static sourceType;

    /**
     * The target collection into the effects canvas group.
     * @type {string}
     * @abstract
     */
    static effectsCollection;

    /**
     * Effect source default data.
     * @type {SourceData}
     */
    static defaultData = {
      x: 0,
      y: 0,
      elevation: 0,
      disabled: false
    }

    /* -------------------------------------------- */
    /*  Source Data                                 */
    /* -------------------------------------------- */

    /**
     * Some other object which is responsible for this source.
     * @type {object|null}
     */
    object;

    /**
     * The source id linked to this effect source.
     * @type {Readonly<string>}
     */
    sourceId;

    /**
     * The data of this source.
     * @type {SourceData}
     */
    data = foundry.utils.deepClone(this.constructor.defaultData);

    /**
     * The geometric shape of the effect source which is generated later.
     * @type {SourceShape}
     */
    shape;

    /**
     * A collection of boolean flags which control rendering and refresh behavior for the source.
     * @type {Record<string, boolean|number>}
     * @protected
     */
    _flags = {};

    /**
     * The x-coordinate of the point source origin.
     * @type {number}
     */
    get x() {
      return this.data.x;
    }

    /**
     * The y-coordinate of the point source origin.
     * @type {number}
     */
    get y() {
      return this.data.y;
    }

    /**
     * The elevation bound to this source.
     * @type {number}
     */
    get elevation() {
      return this.data.elevation;
    }

    /* -------------------------------------------- */
    /*  Source State                                */
    /* -------------------------------------------- */

    /**
     * The EffectsCanvasGroup collection linked to this effect source.
     * @type {Collection<string, BaseEffectSource>}
     */
    get effectsCollection() {
      return canvas.effects[this.constructor.effectsCollection];
    }

    /**
     * Returns the update ID associated with this source.
     * The update ID is increased whenever the shape of the source changes.
     * @type {number}
     */
    get updateId() {
      return this.#updateId;
    }

    #updateId = 0;

    /**
     * Is this source currently active?
     * A source is active if it is attached to an effect collection and is not disabled or suppressed.
     * @type {boolean}
     */
    get active() {
      return this.#attached && !this.data.disabled && !this.suppressed;
    }

    /**
     * Is this source attached to an effect collection?
     * @type {boolean}
     */
    get attached() {
      return this.#attached;
    }

    #attached = false;

    /* -------------------------------------------- */
    /*  Source Suppression Management               */
    /* -------------------------------------------- */

    /**
     * Is this source temporarily suppressed?
     * @type {boolean}
     */
    get suppressed() {
      return Object.values(this.suppression).includes(true);
    };

    /**
     * Records of suppression strings with a boolean value.
     * If any of this record is true, the source is suppressed.
     * @type {Record<string, boolean>}
     */
    suppression = {};

    /* -------------------------------------------- */
    /*  Source Initialization                       */
    /* -------------------------------------------- */

    /**
     * Initialize and configure the source using provided data.
     * @param {Partial<SourceData>} data      Provided data for configuration
     * @param {object} options                Additional options which modify source initialization
     * @param {object} [options.behaviors]      An object containing optional behaviors to apply.
     * @param {boolean} [options.reset=false]   Should source data be reset to default values before applying changes?
     * @returns {BaseEffectSource}            The initialized source
     */
    initialize(data={}, {reset=false}={}) {
      // Reset the source back to default data
      if ( reset ) data = Object.assign(foundry.utils.deepClone(this.constructor.defaultData), data);

      // Update data for the source
      let changes = {};
      if ( !foundry.utils.isEmpty(data) ) {
        const prior = foundry.utils.deepClone(this.data) || {};
        for ( const key in data ) {
          if ( !(key in this.data) ) continue;
          this.data[key] = data[key] ?? this.constructor.defaultData[key];
        }
        this._initialize(data);
        changes = foundry.utils.flattenObject(foundry.utils.diffObject(prior, this.data));
      }

      // Update shapes for the source
      try {
        this._createShapes();
        this.#updateId++;
      }
      catch (err) {
        console.error(err);
        this.remove();
      }

      // Configure attached and non disabled sources
      if ( this.#attached && !this.data.disabled ) this._configure(changes);
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Subclass specific data initialization steps.
     * @param {Partial<SourceData>} data    Provided data for configuration
     * @abstract
     */
    _initialize(data) {}

    /* -------------------------------------------- */

    /**
     * Create the polygon shape (or shapes) for this source using configured data.
     * @protected
     * @abstract
     */
    _createShapes() {}

    /* -------------------------------------------- */

    /**
     * Subclass specific configuration steps. Occurs after data initialization and shape computation.
     * Only called if the source is attached and not disabled.
     * @param {Partial<SourceData>} changes   Changes to the source data which were applied
     * @protected
     */
    _configure(changes) {}

    /* -------------------------------------------- */
    /*  Source Refresh                              */
    /* -------------------------------------------- */

    /**
     * Refresh the state and uniforms of the source.
     * Only active sources are refreshed.
     */
    refresh() {
      if ( !this.active ) return;
      this._refresh();
    }

    /* -------------------------------------------- */

    /**
     * Subclass-specific refresh steps.
     * @protected
     * @abstract
     */
    _refresh() {}

    /* -------------------------------------------- */
    /*  Source Destruction                          */
    /* -------------------------------------------- */

    /**
     * Steps that must be performed when the source is destroyed.
     */
    destroy() {
      this.remove();
      this._destroy();
    }

    /* -------------------------------------------- */

    /**
     * Subclass specific destruction steps.
     * @protected
     */
    _destroy() {}

    /* -------------------------------------------- */
    /*  Source Management                           */
    /* -------------------------------------------- */

    /**
     * Add this BaseEffectSource instance to the active collection.
     */
    add() {
      if ( !this.sourceId ) throw new Error("A BaseEffectSource cannot be added to the active collection unless it has"
        + " a sourceId assigned.");
      this.effectsCollection.set(this.sourceId, this);
      const wasConfigured = this.#attached && !this.data.disabled;
      this.#attached = true;
      if ( !wasConfigured && !this.data.disabled ) this._configure({});
    }

    /* -------------------------------------------- */

    /**
     * Remove this BaseEffectSource instance from the active collection.
     */
    remove() {
      if ( !this.effectsCollection.has(this.sourceId) ) return;
      this.effectsCollection.delete(this.sourceId);
      this.#attached = false;
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * @deprecated since v11
     * @ignore
     */
    get sourceType() {
      const msg = "BaseEffectSource#sourceType is deprecated. Use BaseEffectSource.sourceType instead.";
      foundry.utils.logCompatibilityWarning(msg, { since: 11, until: 13});
      return this.constructor.sourceType;
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    _createShape() {
      const msg = "BaseEffectSource#_createShape is deprecated in favor of BaseEffectSource#_createShapes.";
      foundry.utils.logCompatibilityWarning(msg, { since: 11, until: 13});
      return this._createShapes();
    }

    /**
     * @deprecated since v12
     * @ignore
     */
    get disabled() {
      foundry.utils.logCompatibilityWarning("BaseEffectSource#disabled is deprecated in favor of " +
        "BaseEffectSource#data#disabled or BaseEffectSource#active depending on your use case.", { since: 11, until: 13});
      return this.data.disabled;
    }
  }

  /**
   * @typedef {import("./base-effect-source.mjs").BaseEffectSourceData} BaseEffectSourceData
   */

  /**
   * @typedef {Object}                      RenderedEffectSourceData
   * @property {object} animation           An animation configuration for the source
   * @property {number|null} color          A color applied to the rendered effect
   * @property {number|null} seed           An integer seed to synchronize (or de-synchronize) animations
   * @property {boolean} preview            Is this source a temporary preview?
   */

  /**
   * @typedef {Object} RenderedEffectSourceAnimationConfig
   * @property {string} [label]                                   The human-readable (localized) label for the animation
   * @property {Function} [animation]                             The animation function that runs every frame
   * @property {AdaptiveIlluminationShader} [illuminationShader]  A custom illumination shader used by this animation
   * @property {AdaptiveColorationShader} [colorationShader]      A custom coloration shader used by this animation
   * @property {AdaptiveBackgroundShader} [backgroundShader]      A custom background shader used by this animation
   * @property {AdaptiveDarknessShader} [darknessShader]          A custom darkness shader used by this animation
   * @property {number} [seed]                                    The animation seed
   * @property {number} [time]                                    The animation time
   */

  /**
   * @typedef {Object} RenderedEffectLayerConfig
   * @property {AdaptiveLightingShader} defaultShader             The default shader used by this layer
   * @property {PIXI.BLEND_MODES} blendMode                       The blend mode used by this layer
   */

  /**
   * An abstract class which extends the base PointSource to provide common functionality for rendering.
   * This class is extended by both the LightSource and VisionSource subclasses.
   * @extends {BaseEffectSource<BaseEffectSourceData & RenderedEffectSourceData>}
   * @abstract
   */
  class RenderedEffectSource extends BaseEffectSource {

    /**
     * Keys of the data object which require shaders to be re-initialized.
     * @type {string[]}
     * @protected
     */
    static _initializeShaderKeys = ["animation.type"];

    /**
     * Keys of the data object which require uniforms to be refreshed.
     * @type {string[]}
     * @protected
     */
    static _refreshUniformsKeys = [];

    /**
     * Layers handled by this rendered source.
     * @type {Record<string, RenderedEffectLayerConfig>}
     * @protected
     */
    static get _layers() {
      return {
        background: {
          defaultShader: AdaptiveBackgroundShader,
          blendMode: "MAX_COLOR"
        },
        coloration: {
          defaultShader: AdaptiveColorationShader,
          blendMode: "SCREEN"
        },
        illumination: {
          defaultShader: AdaptiveIlluminationShader,
          blendMode: "MAX_COLOR"
        }
      };
    }

    /**
     * The offset in pixels applied to create soft edges.
     * @type {number}
     */
    static EDGE_OFFSET = -8;

    /** @inheritDoc */
    static defaultData = {
      ...super.defaultData,
      animation: {},
      seed: null,
      preview: false,
      color: null
    }

    /* -------------------------------------------- */
    /*  Rendered Source Attributes                  */
    /* -------------------------------------------- */

    /**
     * The animation configuration applied to this source
     * @type {RenderedEffectSourceAnimationConfig}
     */
    animation = {};

    /**
     * @typedef {Object} RenderedEffectSourceLayer
     * @property {boolean} active             Is this layer actively rendered?
     * @property {boolean} reset              Do uniforms need to be reset?
     * @property {boolean} suppressed         Is this layer temporarily suppressed?
     * @property {PointSourceMesh} mesh       The rendered mesh for this layer
     * @property {AdaptiveLightingShader} shader  The shader instance used for the layer
     */

    /**
     * Track the status of rendering layers
     * @type {{
     *  background: RenderedEffectSourceLayer,
     *  coloration: RenderedEffectSourceLayer,
     *  illumination: RenderedEffectSourceLayer
     * }}
     */
    layers = Object.entries(this.constructor._layers).reduce((obj, [layer, config]) => {
      obj[layer] = {active: true, reset: true, suppressed: false,
        mesh: undefined, shader: undefined, defaultShader: config.defaultShader,
        vmUniforms: undefined, blendMode: config.blendMode};
      return obj;
    }, {});

    /**
     * Array of update uniforms functions.
     * @type {Function[]}
     */
    #updateUniformsFunctions = (() => {
      const initializedFunctions = [];
      for ( const layer in this.layers ) {
        const fn = this[`_update${layer.titleCase()}Uniforms`];
        if ( fn ) initializedFunctions.push(fn);
      }
      return initializedFunctions;
    })();

    /**
     * The color of the source as an RGB vector.
     * @type {[number, number, number]|null}
     */
    colorRGB = null;

    /**
     * PIXI Geometry generated to draw meshes.
     * @type {PIXI.Geometry|null}
     * @protected
     */
    _geometry = null;

    /* -------------------------------------------- */
    /*  Source State                                */
    /* -------------------------------------------- */

    /**
     * Is the rendered source animated?
     * @type {boolean}
     */
    get isAnimated() {
      return this.active && this.data.animation?.type;
    }

    /**
     * Has the rendered source at least one active layer?
     * @type {boolean}
     */
    get hasActiveLayer() {
      return this.#hasActiveLayer;
    }

    #hasActiveLayer = false;

    /**
     * Is this RenderedEffectSource a temporary preview?
     * @returns {boolean}
     */
    get isPreview() {
      return !!this.data.preview;
    }

    /* -------------------------------------------- */
    /*  Rendered Source Properties                  */
    /* -------------------------------------------- */

    /**
     * A convenience accessor to the background layer mesh.
     * @type {PointSourceMesh}
     */
    get background() {
      return this.layers.background.mesh;
    }

    /**
     * A convenience accessor to the coloration layer mesh.
     * @type {PointSourceMesh}
     */
    get coloration() {
      return this.layers.coloration.mesh;
    }

    /**
     * A convenience accessor to the illumination layer mesh.
     * @type {PointSourceMesh}
     */
    get illumination() {
      return this.layers.illumination.mesh;
    }

    /* -------------------------------------------- */
    /*  Rendered Source Initialization              */
    /* -------------------------------------------- */

    /** @inheritDoc */
    _initialize(data) {
      super._initialize(data);
      const color = Color.from(this.data.color ?? null);
      this.data.color = color.valid ? color.valueOf() : null;
      const seed = this.data.seed ?? this.animation.seed ?? Math.floor(Math.random() * 100000);
      this.animation = this.data.animation = {seed, ...this.data.animation};

      // Initialize the color attributes
      const hasColor = this._flags.hasColor = (this.data.color !== null);
      if ( hasColor ) Color.applyRGB(color, this.colorRGB ??= [0, 0, 0]);
      else this.colorRGB = null;

      // We need to update the hasColor uniform attribute immediately
      for ( const layer of Object.values(this.layers) ) {
        if ( layer.shader ) layer.shader.uniforms.hasColor = hasColor;
      }
      this._initializeSoftEdges();
    }

    /* -------------------------------------------- */

    /**
     * Decide whether to render soft edges with a blur.
     * @protected
     */
    _initializeSoftEdges() {
      this._flags.renderSoftEdges = canvas.performance.lightSoftEdges && !this.isPreview;
    }

    /* -------------------------------------------- */

    /** @override */
    _configure(changes) {
      // To know if we need a first time initialization of the shaders
      const initializeShaders = !this._geometry;

      // Initialize meshes using the computed shape
      this.#initializeMeshes();

      // Initialize shaders
      if ( initializeShaders || this.constructor._initializeShaderKeys.some(k => k in changes) ) {
        this.#initializeShaders();
      }

      // Refresh uniforms
      else if ( this.constructor._refreshUniformsKeys.some(k => k in changes) ) {
        for ( const config of Object.values(this.layers) ) {
          config.reset = true;
        }
      }

      // Update the visible state the layers
      this.#updateVisibleLayers();
    }

    /* -------------------------------------------- */

    /**
     * Configure which shaders are used for each rendered layer.
     * @returns {{
     *  background: AdaptiveLightingShader,
     *  coloration: AdaptiveLightingShader,
     *  illumination: AdaptiveLightingShader
     * }}
     * @private
     */
    _configureShaders() {
      const a = this.animation;
      const shaders = {};
      for ( const layer in this.layers ) {
        shaders[layer] = a[`${layer.toLowerCase()}Shader`] || this.layers[layer].defaultShader;
      }
      return shaders;
    }

    /* -------------------------------------------- */

    /**
     * Specific configuration for a layer.
     * @param {object} layer
     * @param {string} layerId
     * @protected
     */
    _configureLayer(layer, layerId) {}

    /* -------------------------------------------- */

    /**
     * Initialize the shaders used for this source, swapping to a different shader if the animation has changed.
     */
    #initializeShaders() {
      const shaders = this._configureShaders();
      for ( const [layerId, layer] of Object.entries(this.layers) ) {
        layer.shader = RenderedEffectSource.#createShader(shaders[layerId], layer.mesh);
        this._configureLayer(layer, layerId);
      }
      this.#updateUniforms();
      Hooks.callAll(`initialize${this.constructor.name}Shaders`, this);
    }

    /* -------------------------------------------- */

    /**
     * Create a new shader using a provider shader class
     * @param {typeof AdaptiveLightingShader} cls   The shader class to create
     * @param {PointSourceMesh} container           The container which requires a new shader
     * @returns {AdaptiveLightingShader}            The shader instance used
     */
    static #createShader(cls, container) {
      const current = container.shader;
      if ( current?.constructor === cls ) return current;
      const shader = cls.create({
        primaryTexture: canvas.primary.renderTexture
      });
      shader.container = container;
      container.shader = shader;
      container.uniforms = shader.uniforms;
      if ( current ) current.destroy();
      return shader;
    }

    /* -------------------------------------------- */

    /**
     * Initialize the geometry and the meshes.
     */
    #initializeMeshes() {
      this._updateGeometry();
      if ( !this._flags.initializedMeshes ) this.#createMeshes();
    }

    /* -------------------------------------------- */

    /**
     * Create meshes for each layer of the RenderedEffectSource that is drawn to the canvas.
     */
    #createMeshes() {
      if ( !this._geometry ) return;
      const shaders = this._configureShaders();
      for ( const [l, layer] of Object.entries(this.layers) ) {
        layer.mesh = this.#createMesh(shaders[l]);
        layer.mesh.blendMode = PIXI.BLEND_MODES[layer.blendMode];
        layer.shader = layer.mesh.shader;
      }
      this._flags.initializedMeshes = true;
    }

    /* -------------------------------------------- */

    /**
     * Create a new Mesh for this source using a provided shader class
     * @param {typeof AdaptiveLightingShader} shaderCls   The shader class used for this mesh
     * @returns {PointSourceMesh}                         The created Mesh
     */
    #createMesh(shaderCls) {
      const state = new PIXI.State();
      const mesh = new PointSourceMesh(this._geometry, shaderCls.create(), state);
      mesh.drawMode = PIXI.DRAW_MODES.TRIANGLES;
      mesh.uniforms = mesh.shader.uniforms;
      mesh.cullable = true;
      return mesh;
    }

    /* -------------------------------------------- */

    /**
     * Create the geometry for the source shape that is used in shaders and compute its bounds for culling purpose.
     * Triangulate the form and create buffers.
     * @protected
     * @abstract
     */
    _updateGeometry() {}

    /* -------------------------------------------- */
    /*  Rendered Source Canvas Rendering            */
    /* -------------------------------------------- */

    /**
     * Render the containers used to represent this light source within the LightingLayer
     * @returns {{background: PIXI.Mesh, coloration: PIXI.Mesh, illumination: PIXI.Mesh}}
     */
    drawMeshes() {
      const meshes = {};
      for ( const layerId of Object.keys(this.layers) ) {
        meshes[layerId] = this._drawMesh(layerId);
      }
      return meshes;
    }

    /* -------------------------------------------- */

    /**
     * Create a Mesh for a certain rendered layer of this source.
     * @param {string} layerId            The layer key in layers to draw
     * @returns {PIXI.Mesh|null}          The drawn mesh for this layer, or null if no mesh is required
     * @protected
     */
    _drawMesh(layerId) {
      const layer = this.layers[layerId];
      const mesh = layer.mesh;

      if ( layer.reset ) {
        const fn = this[`_update${layerId.titleCase()}Uniforms`];
        fn.call(this);
      }
      if ( !layer.active ) {
        mesh.visible = false;
        return null;
      }

      // Update the mesh
      const {x, y} = this.data;
      mesh.position.set(x, y);
      mesh.visible = mesh.renderable = true;
      return layer.mesh;
    }

    /* -------------------------------------------- */
    /*  Rendered Source Refresh                     */
    /* -------------------------------------------- */

    /** @override */
    _refresh() {
      this.#updateUniforms();
      this.#updateVisibleLayers();
    }

    /* -------------------------------------------- */

    /**
     * Update uniforms for all rendered layers.
     */
    #updateUniforms() {
      for ( const updateUniformsFunction of this.#updateUniformsFunctions ) updateUniformsFunction.call(this);
    }

    /* -------------------------------------------- */

    /**
     * Update the visible state of the component channels of this RenderedEffectSource.
     * @returns {boolean}     Is there an active layer?
     */
    #updateVisibleLayers() {
      const active = this.active;
      let hasActiveLayer = false;
      for ( const layer of Object.values(this.layers) ) {
        layer.active = active && (layer.shader?.isRequired !== false);
        if ( layer.active ) hasActiveLayer = true;
      }
      this.#hasActiveLayer = hasActiveLayer;
    }

    /* -------------------------------------------- */

    /**
     * Update shader uniforms used by every rendered layer.
     * @param {AbstractBaseShader} shader
     * @protected
     */
    _updateCommonUniforms(shader) {}

    /* -------------------------------------------- */

    /**
     * Update shader uniforms used for the background layer.
     * @protected
     */
    _updateBackgroundUniforms() {
      const shader = this.layers.background.shader;
      if ( !shader ) return;
      this._updateCommonUniforms(shader);
    }

    /* -------------------------------------------- */

    /**
     * Update shader uniforms used for the coloration layer.
     * @protected
     */
    _updateColorationUniforms() {
      const shader = this.layers.coloration.shader;
      if ( !shader ) return;
      this._updateCommonUniforms(shader);
    }

    /* -------------------------------------------- */

    /**
     * Update shader uniforms used for the illumination layer.
     * @protected
     */
    _updateIlluminationUniforms() {
      const shader = this.layers.illumination.shader;
      if ( !shader ) return;
      this._updateCommonUniforms(shader);
    }

    /* -------------------------------------------- */
    /*  Rendered Source Destruction                 */
    /* -------------------------------------------- */

    /** @override */
    _destroy() {
      for ( const layer of Object.values(this.layers) ) layer.mesh?.destroy();
      this._geometry?.destroy();
    }

    /* -------------------------------------------- */
    /*  Animation Functions                         */
    /* -------------------------------------------- */

    /**
     * Animate the PointSource, if an animation is enabled and if it currently has rendered containers.
     * @param {number} dt         Delta time.
     */
    animate(dt) {
      if ( !this.isAnimated ) return;
      const {animation, ...options} = this.animation;
      return animation?.call(this, dt, options);
    }

    /* -------------------------------------------- */

    /**
     * Generic time-based animation used for Rendered Point Sources.
     * @param {number} dt           Delta time.
     * @param {object} [options]    Options which affect the time animation
     * @param {number} [options.speed=5]            The animation speed, from 0 to 10
     * @param {number} [options.intensity=5]        The animation intensity, from 1 to 10
     * @param {boolean} [options.reverse=false]     Reverse the animation direction
     */
    animateTime(dt, {speed=5, intensity=5, reverse=false}={}) {

      // Determine the animation timing
      let t = canvas.app.ticker.lastTime;
      if ( reverse ) t *= -1;
      this.animation.time = ( (speed * t) / 5000 ) + this.animation.seed;

      // Update uniforms
      for ( const layer of Object.values(this.layers) ) {
        const u = layer.mesh.uniforms;
        u.time = this.animation.time;
        u.intensity = intensity;
      }
    }
    /* -------------------------------------------- */
    /*  Static Helper Methods                       */
    /* -------------------------------------------- */

    /**
     * Get corrected level according to level and active vision mode data.
     * @param {VisionMode.LIGHTING_LEVELS} level
     * @returns {number} The corrected level.
     */
    static getCorrectedLevel(level) {
      // Retrieving the lighting mode and the corrected level, if any
      const lightingOptions = canvas.visibility.visionModeData?.activeLightingOptions;
      return (lightingOptions?.levels?.[level]) ?? level;
    }

    /* -------------------------------------------- */

    /**
     * Get corrected color according to level, dim color, bright color and background color.
     * @param {VisionMode.LIGHTING_LEVELS} level
     * @param {Color} colorDim
     * @param {Color} colorBright
     * @param {Color} [colorBackground]
     * @returns {Color}
     */
    static getCorrectedColor(level, colorDim, colorBright, colorBackground) {
      colorBackground ??= canvas.colors.background;

      // Returning the corrected color according to the lighting options
      const levels = VisionMode.LIGHTING_LEVELS;
      switch ( this.getCorrectedLevel(level) ) {
        case levels.HALFDARK:
        case levels.DIM: return colorDim;
        case levels.BRIGHT:
        case levels.DARKNESS: return colorBright;
        case levels.BRIGHTEST: return canvas.colors.ambientBrightest;
        case levels.UNLIT: return colorBackground;
        default: return colorDim;
      }
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * @deprecated since v11
     * @ignore
     */
    get preview() {
      const msg = "The RenderedEffectSource#preview is deprecated. Use RenderedEffectSource#isPreview instead.";
      foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
      return this.isPreview;
    }

    /**
     * @deprecated since v11
     * @ignore
     */
    set preview(preview) {
      const msg = "The RenderedEffectSource#preview is deprecated. "
        + "Set RenderedEffectSource#preview as part of RenderedEffectSource#initialize instead.";
      foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
      this.data.preview = preview;
    }
  }

  /**
   * @typedef {import("./base-effect-source.mjs").BaseEffectSourceData} BaseEffectSourceData
   * @typedef {import("./rendered-effect-source-source.mjs").RenderedEffectSourceData} RenderedEffectSourceData
   */

  /**
   * @typedef {Object}                      LightSourceData
   * @property {number} alpha               An opacity for the emitted light, if any
   * @property {number} bright              The allowed radius of bright vision or illumination
   * @property {number} coloration          The coloration technique applied in the shader
   * @property {number} contrast            The amount of contrast this light applies to the background texture
   * @property {number} dim                 The allowed radius of dim vision or illumination
   * @property {number} attenuation         Strength of the attenuation between bright, dim, and dark
   * @property {number} luminosity          The luminosity applied in the shader
   * @property {number} saturation          The amount of color saturation this light applies to the background texture
   * @property {number} shadows             The depth of shadows this light applies to the background texture
   * @property {boolean} vision             Whether or not this source provides a source of vision
   * @property {number} priority            Strength of this source to beat or not negative/positive sources
   */

  /**
   * A specialized subclass of BaseEffectSource which deals with the rendering of light or darkness.
   * @extends {RenderedEffectSource<BaseEffectSourceData & RenderedEffectSourceData & LightSourceData>}
   * @abstract
   */
  class BaseLightSource extends RenderedEffectSource {
    /** @override */
    static sourceType = "light";

    /** @override */
    static _initializeShaderKeys = ["animation.type", "walls"];

    /** @override */
    static _refreshUniformsKeys = ["dim", "bright", "attenuation", "alpha", "coloration", "color", "contrast",
      "saturation", "shadows", "luminosity"];

    /**
     * The corresponding lighting levels for dim light.
     * @type {number}
     * @protected
     */
    static _dimLightingLevel = LIGHTING_LEVELS.DIM;

    /**
     * The corresponding lighting levels for bright light.
     * @type {string}
     * @protected
     */
    static _brightLightingLevel = LIGHTING_LEVELS.BRIGHT;

    /**
     * The corresponding animation config.
     * @type {LightSourceAnimationConfig}
     * @protected
     */
    static get ANIMATIONS() {
      return CONFIG.Canvas.lightAnimations;
    }

    /** @inheritDoc */
    static defaultData = {
      ...super.defaultData,
      priority: 0,
      alpha: 0.5,
      bright: 0,
      coloration: 1,
      contrast: 0,
      dim: 0,
      attenuation: 0.5,
      luminosity: 0.5,
      saturation: 0,
      shadows: 0,
      vision: false
    }

    /* -------------------------------------------- */
    /*  Light Source Attributes                     */
    /* -------------------------------------------- */

    /**
     * A ratio of dim:bright as part of the source radius
     * @type {number}
     */
    ratio = 1;

    /* -------------------------------------------- */
    /*  Light Source Initialization                 */
    /* -------------------------------------------- */

    /** @override */
    _initialize(data) {
      super._initialize(data);
      const animationConfig = foundry.utils.deepClone(this.constructor.ANIMATIONS[this.data.animation.type] || {});
      this.animation = Object.assign(this.data.animation, animationConfig);
    }

    /* -------------------------------------------- */
    /*  Shader Management                           */
    /* -------------------------------------------- */

    /** @inheritDoc */
    _updateColorationUniforms() {
      super._updateColorationUniforms();
      const u = this.layers.coloration.shader?.uniforms;
      if ( !u ) return;

      // Adapting color intensity to the coloration technique
      switch ( this.data.coloration ) {
        case 0: // Legacy
                // Default 0.25 -> Legacy technique needs quite low intensity default to avoid washing background
          u.colorationAlpha = Math.pow(this.data.alpha, 2);
          break;
        case 4: // Color burn
        case 5: // Internal burn
        case 6: // External burn
        case 9: // Invert absorption
                // Default 0.5 -> These techniques are better at low color intensity
          u.colorationAlpha = this.data.alpha;
          break;
        default:
          // Default 1 -> The remaining techniques use adaptive lighting,
          // which produces interesting results in the [0, 2] range.
          u.colorationAlpha = this.data.alpha * 2;
      }

      u.useSampler = this.data.coloration > 0;  // Not needed for legacy coloration (technique id 0)

      // Flag uniforms as updated
      this.layers.coloration.reset = false;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _updateIlluminationUniforms() {
      super._updateIlluminationUniforms();
      const u = this.layers.illumination.shader?.uniforms;
      if ( !u ) return;
      u.useSampler = false;

      // Flag uniforms as updated
      const i = this.layers.illumination;
      i.reset = i.suppressed = false;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _updateBackgroundUniforms() {
      super._updateBackgroundUniforms();
      const u = this.layers.background.shader?.uniforms;
      if ( !u ) return;

      canvas.colors.background.applyRGB(u.colorBackground);
      u.backgroundAlpha = this.data.alpha;
      u.useSampler = true;

      // Flag uniforms as updated
      this.layers.background.reset = false;
    }

    /* -------------------------------------------- */

    /** @override */
    _updateCommonUniforms(shader) {
      const u = shader.uniforms;
      const c = canvas.colors;

      // Passing common environment values
      u.computeIllumination = true;
      u.darknessLevel = canvas.environment.darknessLevel;
      c.ambientBrightest.applyRGB(u.ambientBrightest);
      c.ambientDarkness.applyRGB(u.ambientDarkness);
      c.ambientDaylight.applyRGB(u.ambientDaylight);
      u.weights[0] = canvas.environment.weights.dark;
      u.weights[1] = canvas.environment.weights.halfdark;
      u.weights[2] = canvas.environment.weights.dim;
      u.weights[3] = canvas.environment.weights.bright;
      u.dimLevelCorrection = this.constructor.getCorrectedLevel(this.constructor._dimLightingLevel);
      u.brightLevelCorrection = this.constructor.getCorrectedLevel(this.constructor._brightLightingLevel);

      // Passing advanced color correction values
      u.luminosity = this.data.luminosity;
      u.exposure = this.data.luminosity * 2.0 - 1.0;
      u.contrast = (this.data.contrast < 0 ? this.data.contrast * 0.5 : this.data.contrast);
      u.saturation = this.data.saturation;
      u.shadows = this.data.shadows;
      u.hasColor = this._flags.hasColor;
      u.ratio = this.ratio;
      u.technique = this.data.coloration;
      // Graph: https://www.desmos.com/calculator/e7z0i7hrck
      // mapping [0,1] attenuation user value to [0,1] attenuation shader value
      if ( this.cachedAttenuation !== this.data.attenuation ) {
        this.computedAttenuation = (Math.cos(Math.PI * Math.pow(this.data.attenuation, 1.5)) - 1) / -2;
        this.cachedAttenuation = this.data.attenuation;
      }
      u.attenuation = this.computedAttenuation;
      u.elevation = this.data.elevation;
      u.color = this.colorRGB ?? shader.constructor.defaultUniforms.color;

      // Passing screenDimensions to use screen size render textures
      u.screenDimensions = canvas.screenDimensions;
      if ( !u.depthTexture ) u.depthTexture = canvas.masks.depth.renderTexture;
      if ( !u.primaryTexture ) u.primaryTexture = canvas.primary.renderTexture;
      if ( !u.darknessLevelTexture ) u.darknessLevelTexture = canvas.effects.illumination.renderTexture;
    }

    /* -------------------------------------------- */
    /*  Animation Functions                         */
    /* -------------------------------------------- */

    /**
     * An animation with flickering ratio and light intensity.
     * @param {number} dt                       Delta time
     * @param {object} [options={}]             Additional options which modify the flame animation
     * @param {number} [options.speed=5]        The animation speed, from 0 to 10
     * @param {number} [options.intensity=5]    The animation intensity, from 1 to 10
     * @param {boolean} [options.reverse=false] Reverse the animation direction
     */
    animateTorch(dt, {speed=5, intensity=5, reverse=false} = {}) {
      this.animateFlickering(dt, {speed, intensity, reverse, amplification: intensity / 5});
    }

    /* -------------------------------------------- */

    /**
     * An animation with flickering ratio and light intensity
     * @param {number} dt                                 Delta time
     * @param {object} [options={}]                       Additional options which modify the flame animation
     * @param {number} [options.speed=5]                  The animation speed, from 0 to 10
     * @param {number} [options.intensity=5]              The animation intensity, from 1 to 10
     * @param {number} [options.amplification=1]          Noise amplification (>1) or dampening (<1)
     * @param {boolean} [options.reverse=false]           Reverse the animation direction
     */
    animateFlickering(dt, {speed=5, intensity=5, reverse=false, amplification=1} = {}) {
      this.animateTime(dt, {speed, intensity, reverse});

      // Create the noise object for the first frame
      const amplitude = amplification * 0.45;
      if ( !this._noise ) this._noise = new SmoothNoise({amplitude: amplitude, scale: 3, maxReferences: 2048});

      // Update amplitude
      if ( this._noise.amplitude !== amplitude ) this._noise.amplitude = amplitude;

      // Create noise from animation time. Range [0.0, 0.45]
      let n = this._noise.generate(this.animation.time);

      // Update brightnessPulse and ratio with some noise in it
      const co = this.layers.coloration.shader;
      const il = this.layers.illumination.shader;
      co.uniforms.brightnessPulse = il.uniforms.brightnessPulse = 0.55 + n;    // Range [0.55, 1.0 <* amplification>]
      co.uniforms.ratio = il.uniforms.ratio = (this.ratio * 0.9) + (n * 0.222);// Range [ratio * 0.9, ratio * ~1.0 <* amplification>]
    }

    /* -------------------------------------------- */

    /**
     * A basic "pulse" animation which expands and contracts.
     * @param {number} dt                           Delta time
     * @param {object} [options={}]                 Additional options which modify the pulse animation
     * @param {number} [options.speed=5]              The animation speed, from 0 to 10
     * @param {number} [options.intensity=5]          The animation intensity, from 1 to 10
     * @param {boolean} [options.reverse=false]       Reverse the animation direction
     */
    animatePulse(dt, {speed=5, intensity=5, reverse=false}={}) {

      // Determine the animation timing
      let t = canvas.app.ticker.lastTime;
      if ( reverse ) t *= -1;
      this.animation.time = ((speed * t)/5000) + this.animation.seed;

      // Define parameters
      const i = (10 - intensity) * 0.1;
      const w = 0.5 * (Math.cos(this.animation.time * 2.5) + 1);
      const wave = (a, b, w) => ((a - b) * w) + b;

      // Pulse coloration
      const co = this.layers.coloration.shader;
      co.uniforms.intensity = intensity;
      co.uniforms.time = this.animation.time;
      co.uniforms.pulse = wave(1.2, i, w);

      // Pulse illumination
      const il = this.layers.illumination.shader;
      il.uniforms.intensity = intensity;
      il.uniforms.time = this.animation.time;
      il.uniforms.ratio = wave(this.ratio, this.ratio * i, w);
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get isDarkness() {
      const msg = "BaseLightSource#isDarkness is now obsolete. Use DarknessSource instead.";
      foundry.utils.logCompatibilityWarning(msg, { since: 12, until: 14});
      return false;
    }
  }

  /**
   * A specialized subclass of the BaseLightSource which is used to render global light source linked to the scene.
   */
  class GlobalLightSource extends BaseLightSource {

    /** @inheritDoc */
    static sourceType = "GlobalLight";

    /** @override */
    static effectsCollection = "lightSources";

    /** @inheritDoc */
    static defaultData = {
      ...super.defaultData,
      rotation: 0,
      angle: 360,
      attenuation: 0,
      priority: -Infinity,
      vision: false,
      walls: false,
      elevation: Infinity,
      darkness: {min: 0, max: 0}
    }

    /**
     * Name of this global light source.
     * @type {string}
     * @defaultValue GlobalLightSource.sourceType
     */
    name = this.constructor.sourceType;

    /**
     * A custom polygon placeholder.
     * @type {PIXI.Polygon|number[]|null}
     */
    customPolygon = null;

    /* -------------------------------------------- */
    /*  Global Light Source Initialization          */
    /* -------------------------------------------- */

    /** @override */
    _createShapes() {
      this.shape = this.customPolygon ?? canvas.dimensions.sceneRect.toPolygon();
    }

    /* -------------------------------------------- */

    /** @override */
    _initializeSoftEdges() {
      this._flags.renderSoftEdges = false;
    }

    /* -------------------------------------------- */

    /** @override */
    _updateGeometry() {
      const offset = this._flags.renderSoftEdges ? this.constructor.EDGE_OFFSET : 0;
      const pm = new PolygonMesher(this.shape, {offset});
      this._geometry = pm.triangulate(this._geometry);
      const bounds = this.shape instanceof PointSourcePolygon ? this.shape.bounds : this.shape.getBounds();
      if ( this._geometry.bounds ) this._geometry.bounds.copyFrom(bounds);
      else this._geometry.bounds = bounds;
    }


    /* -------------------------------------------- */

    /** @override */
    _updateCommonUniforms(shader) {
      super._updateCommonUniforms(shader);
      const {min, max} = this.data.darkness;
      const u = shader.uniforms;
      u.globalLight = true;
      u.globalLightThresholds[0] = min;
      u.globalLightThresholds[1] = max;
    }
  }

  /**
   * @typedef {import("./base-effect-source.mjs").BaseEffectSourceData} BaseEffectSourceData
   */

  /**
   * @typedef {Object} PointEffectSourceData
   * @property {number} radius              The radius of the source
   * @property {number} externalRadius      A secondary radius used for limited angles
   * @property {number} rotation            The angle of rotation for this point source
   * @property {number} angle               The angle of emission for this point source
   * @property {boolean} walls              Whether or not the source is constrained by walls
   */

  /**
   * TODO - documentation required about what a PointEffectSource is.
   * @param BaseSource
   * @returns {{new(): PointEffectSource, prototype: PointEffectSource}}
   * @mixin
   */
  function PointEffectSourceMixin(BaseSource) {
    /**
     * @extends {BaseEffectSource<BaseEffectSourceData & PointEffectSourceData, PointSourcePolygon>}
     * @abstract
     */
    return class PointEffectSource extends BaseSource {

      /** @inheritDoc */
      static defaultData = {
        ...super.defaultData,
        radius: 0,
        externalRadius: 0,
        rotation: 0,
        angle: 360,
        walls: true
      }

      /* -------------------------------------------- */

      /**
       * A convenience reference to the radius of the source.
       * @type {number}
       */
      get radius() {
        return this.data.radius ?? 0;
      }

      /* -------------------------------------------- */

      /** @inheritDoc */
      _initialize(data) {
        super._initialize(data);
        if ( this.data.radius > 0 ) this.data.radius = Math.max(this.data.radius, this.data.externalRadius);
      }

      /* -------------------------------------------- */
      /*  Point Source Geometry Methods               */
      /* -------------------------------------------- */

      /** @inheritDoc */
      _initializeSoftEdges() {
        super._initializeSoftEdges();
        const isCircle = (this.shape instanceof PointSourcePolygon) && this.shape.isCompleteCircle();
        this._flags.renderSoftEdges &&= !isCircle;
      }

      /* -------------------------------------------- */

      /**
       * Configure the parameters of the polygon that is generated for this source.
       * @returns {PointSourcePolygonConfig}
       * @protected
       */
      _getPolygonConfiguration() {
        return {
          type: this.data.walls ? this.constructor.sourceType : "universal",
          radius: (this.data.disabled || this.suppressed) ? 0 : this.radius,
          externalRadius: this.data.externalRadius,
          angle: this.data.angle,
          rotation: this.data.rotation,
          source: this
        };
      }

      /* -------------------------------------------- */

      /** @inheritDoc */
      _createShapes() {
        const origin = {x: this.data.x, y: this.data.y};
        const config = this._getPolygonConfiguration();
        const polygonClass = CONFIG.Canvas.polygonBackends[this.constructor.sourceType];
        this.shape = polygonClass.create(origin, config);
      }

      /* -------------------------------------------- */
      /*  Rendering methods                           */
      /* -------------------------------------------- */

      /** @override */
      _drawMesh(layerId) {
        const mesh = super._drawMesh(layerId);
        if ( mesh ) mesh.scale.set(this.radius);
        return mesh;
      }

      /** @override */
      _updateGeometry() {
        const {x, y} = this.data;
        const radius = this.radius;
        const offset = this._flags.renderSoftEdges ? this.constructor.EDGE_OFFSET : 0;
        const pm = new PolygonMesher(this.shape, {x, y, radius, normalize: true, offset});
        this._geometry = pm.triangulate(this._geometry);
        const bounds = new PIXI.Rectangle(0, 0, 0, 0);
        if ( radius > 0 ) {
          const b = this.shape instanceof PointSourcePolygon ? this.shape.bounds : this.shape.getBounds();
          bounds.x = (b.x - x) / radius;
          bounds.y = (b.y - y) / radius;
          bounds.width = b.width / radius;
          bounds.height = b.height / radius;
        }
        if ( this._geometry.bounds ) this._geometry.bounds.copyFrom(bounds);
        else this._geometry.bounds = bounds;
      }

      /* -------------------------------------------- */
      /*  Deprecations and Compatibility              */
      /* -------------------------------------------- */

      /**
       * @deprecated since v11
       * @ignore
       */
      set radius(radius) {
        const msg = "The setter PointEffectSource#radius is deprecated."
          + " The radius should not be set anywhere except in PointEffectSource#_initialize.";
        foundry.utils.logCompatibilityWarning(msg, { since: 11, until: 13});
        this.data.radius = radius;
      }

      /* -------------------------------------------- */

      /**
       * @deprecated since v11
       * @ignore
       */
      get los() {
        const msg = "PointEffectSource#los is deprecated in favor of PointEffectSource#shape.";
        foundry.utils.logCompatibilityWarning(msg, { since: 11, until: 13});
        return this.shape;
      }

      /* -------------------------------------------- */

      /**
       * @deprecated since v11
       * @ignore
       */
      set los(shape) {
        const msg = "PointEffectSource#los is deprecated in favor of PointEffectSource#shape.";
        foundry.utils.logCompatibilityWarning(msg, { since: 11, until: 13});
        this.shape = shape;
      }
    }
  }

  /**
   * A specialized subclass of the BaseLightSource which renders a source of darkness as a point-based effect.
   * @extends {BaseLightSource}
   * @mixes {PointEffectSource}
   */
  class PointDarknessSource extends PointEffectSourceMixin(BaseLightSource) {

    /** @override */
    static effectsCollection = "darknessSources";

    /** @override */
    static _dimLightingLevel = LIGHTING_LEVELS.HALFDARK;

    /** @override */
    static _brightLightingLevel = LIGHTING_LEVELS.DARKNESS;

    /** @override */
    static get ANIMATIONS() {
      return CONFIG.Canvas.darknessAnimations;
    }

    /** @override */
    static get _layers() {
      return {
        darkness: {
          defaultShader: AdaptiveDarknessShader,
          blendMode: "MAX_COLOR"
        }
      };
    }

    /**
     * The optional geometric shape is solely utilized for visual representation regarding darkness sources.
     * Used only when an additional radius is added for visuals.
     * @protected
     * @type {SourceShape}
     */
    _visualShape;

    /**
     * Padding applied on the darkness source shape for visual appearance only.
     * Note: for now, padding is increased radius. It might evolve in a future release.
     * @type {number}
     * @protected
     */
    _padding = (CONFIG.Canvas.darknessSourcePaddingMultiplier ?? 0) * canvas.grid.size;

    /**
     * The Edge instances added by this darkness source.
     * @type {Edge[]}
     */
    edges = [];

    /**
     * The normalized border distance.
     * @type {number}
     */
    #borderDistance = 0;

    /* -------------------------------------------- */
    /*  Darkness Source Properties                  */
    /* -------------------------------------------- */

    /**
     * A convenience accessor to the darkness layer mesh.
     * @type {PointSourceMesh}
     */
    get darkness() {
      return this.layers.darkness.mesh;
    }

    /* -------------------------------------------- */
    /*  Source Initialization and Management        */
    /* -------------------------------------------- */

    /** @inheritDoc */
    _initialize(data) {
      super._initialize(data);
      this.data.radius = this.data.bright = this.data.dim = Math.max(this.data.dim ?? 0, this.data.bright ?? 0);
      this.#borderDistance = this.radius / (this.radius + this._padding);
    }

    /* -------------------------------------------- */


    /** @inheritDoc */
    _createShapes() {
      this.#deleteEdges();
      const origin = {x: this.data.x, y: this.data.y};
      const config = this._getPolygonConfiguration();
      const polygonClass = CONFIG.Canvas.polygonBackends[this.constructor.sourceType];

      // Create shapes based on padding
      if ( this.radius < config.radius ) {
        this._visualShape = polygonClass.create(origin, config);
        this.shape = this.#createShapeFromVisualShape(this.radius);
      }
      else {
        this._visualShape = null;
        this.shape = polygonClass.create(origin, config);
      }
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _configure(changes) {
      super._configure(changes);
      this.#createEdges();
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _getPolygonConfiguration() {
      return Object.assign(super._getPolygonConfiguration(), {
        useThreshold: true,
        includeDarkness: false,
        radius: (this.data.disabled || this.suppressed) ? 0 : this.radius + this._padding,
      });
    }

    /* -------------------------------------------- */

    _drawMesh(layerId) {
      const mesh = super._drawMesh(layerId);
      if ( mesh ) mesh.scale.set(this.radius + this._padding);
      return mesh;
    }

    /* -------------------------------------------- */

    /** @override */
    _updateGeometry() {
      const {x, y} = this.data;
      const radius = this.radius + this._padding;
      const offset = this._flags.renderSoftEdges ? this.constructor.EDGE_OFFSET : 0;
      const shape = this._visualShape ?? this.shape;
      const pm = new PolygonMesher(shape, {x, y, radius, normalize: true, offset});
      this._geometry = pm.triangulate(this._geometry);
      const bounds = new PIXI.Rectangle(0, 0, 0, 0);
      if ( radius > 0 ) {
        const b = shape instanceof PointSourcePolygon ? shape.bounds : shape.getBounds();
        bounds.x = (b.x - x) / radius;
        bounds.y = (b.y - y) / radius;
        bounds.width = b.width / radius;
        bounds.height = b.height / radius;
      }
      if ( this._geometry.bounds ) this._geometry.bounds.copyFrom(bounds);
      else this._geometry.bounds = bounds;
    }

    /* -------------------------------------------- */

    /**
     * Create a radius constrained polygon from the visual shape polygon.
     * If the visual shape is not created, no polygon is created.
     * @param {number} radius           The radius to constraint to.
     * @returns {PointSourcePolygon} The new polygon or null if no visual shape is present.
     */
    #createShapeFromVisualShape(radius) {
      if ( !this._visualShape ) return null;
      const {x, y} = this.data;
      const circle = new PIXI.Circle(x, y, radius);
      const density = PIXI.Circle.approximateVertexDensity(radius);
      return this._visualShape.applyConstraint(circle, {density, scalingFactor: 100});
    }

    /* -------------------------------------------- */

    /**
     * Create the Edge instances that correspond to this darkness source.
     */
    #createEdges() {
      if ( !this.active || this.isPreview ) return;
      const cls = foundry.canvas.edges.Edge;
      const block = CONST.WALL_SENSE_TYPES.NORMAL;
      const direction = CONST.WALL_DIRECTIONS.LEFT;
      const points = [...this.shape.points];
      let p0 = {x: points[0], y: points[1]};
      points.push(p0.x, p0.y);
      let p1;
      for ( let i=2; i<points.length; i+=2 ) {
        p1 = {x: points[i], y: points[i+1]};
        const id = `${this.sourceId}.${i/2}`;
        const edge = new cls(p0, p1, {type: "darkness", id, object: this.object, direction, light: block, sight: block});
        this.edges.push(edge);
        canvas.edges.set(edge.id, edge);
        p0 = p1;
      }
    }

    /* -------------------------------------------- */

    /**
     * Remove edges from the active Edges collection.
     */
    #deleteEdges() {
      for ( const edge of this.edges ) canvas.edges.delete(edge.id);
      this.edges.length = 0;
    }

    /* -------------------------------------------- */
    /*  Shader Management                           */
    /* -------------------------------------------- */

    /**
     * Update the uniforms of the shader on the darkness layer.
     */
    _updateDarknessUniforms() {
      const u = this.layers.darkness.shader?.uniforms;
      if ( !u ) return;
      u.color = this.colorRGB ?? this.layers.darkness.shader.constructor.defaultUniforms.color;
      u.enableVisionMasking = canvas.effects.visionSources.some(s => s.active) || !game.user.isGM;
      u.borderDistance = this.#borderDistance;
      u.colorationAlpha = this.data.alpha * 2;

      // Passing screenDimensions to use screen size render textures
      u.screenDimensions = canvas.screenDimensions;
      if ( !u.depthTexture ) u.depthTexture = canvas.masks.depth.renderTexture;
      if ( !u.primaryTexture ) u.primaryTexture = canvas.primary.renderTexture;
      if ( !u.visionTexture ) u.visionTexture = canvas.masks.vision.renderTexture;

      // Flag uniforms as updated
      this.layers.darkness.reset = false;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _destroy() {
      this.#deleteEdges();
      super._destroy();
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * @deprecated since v12
     * @ignore
     */
    get isDarkness() {
      const msg = "BaseLightSource#isDarkness is now obsolete. Use DarknessSource instead.";
      foundry.utils.logCompatibilityWarning(msg, { since: 12, until: 14});
      return true;
    }
  }

  /**
   * A specialized subclass of the BaseLightSource which renders a source of light as a point-based effect.
   * @extends {BaseLightSource}
   * @mixes {PointEffectSourceMixin}
   */
  class PointLightSource extends PointEffectSourceMixin(BaseLightSource) {

    /** @override */
    static effectsCollection = "lightSources";

    /* -------------------------------------------- */
    /*  Source Suppression Management               */
    /* -------------------------------------------- */

    /**
     * Update darkness suppression according to darkness sources collection.
     */
    #updateDarknessSuppression() {
      this.suppression.darkness = canvas.effects.testInsideDarkness({x: this.x, y: this.y}, this.elevation);
    }

    /* -------------------------------------------- */
    /*  Light Source Initialization                 */
    /* -------------------------------------------- */

    /** @inheritDoc */
    _initialize(data) {
      super._initialize(data);
      Object.assign(this.data, {
        radius: Math.max(this.data.dim ?? 0, this.data.bright ?? 0)
      });
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _createShapes() {
      this.#updateDarknessSuppression();
      super._createShapes();
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _configure(changes) {
      this.ratio = Math.clamp(Math.abs(this.data.bright) / this.data.radius, 0, 1);
      super._configure(changes);
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _getPolygonConfiguration() {
      return Object.assign(super._getPolygonConfiguration(), {useThreshold: true, includeDarkness: true});
    }

    /* -------------------------------------------- */
    /*  Visibility Testing                          */
    /* -------------------------------------------- */

    /**
     * Test whether this LightSource provides visibility to see a certain target object.
     * @param {object} config               The visibility test configuration
     * @param {CanvasVisibilityTest[]} config.tests  The sequence of tests to perform
     * @param {PlaceableObject} config.object        The target object being tested
     * @returns {boolean}                   Is the target object visible to this source?
     */
    testVisibility({tests, object}={}) {
      if ( !(this.data.vision && this._canDetectObject(object)) ) return false;
      return tests.some(test => this.shape.contains(test.point.x, test.point.y));
    }

    /* -------------------------------------------- */

    /**
     * Can this LightSource theoretically detect a certain object based on its properties?
     * This check should not consider the relative positions of either object, only their state.
     * @param {PlaceableObject} target      The target object being tested
     * @returns {boolean}                   Can the target object theoretically be detected by this vision source?
     */
    _canDetectObject(target) {
      const tgt = target?.document;
      const isInvisible = ((tgt instanceof TokenDocument) && tgt.hasStatusEffect(CONFIG.specialStatusEffects.INVISIBLE));
      return !isInvisible;
    }
  }

  /**
   * A specialized subclass of the BaseEffectSource which describes a movement-based source.
   * @extends {BaseEffectSource}
   * @mixes {PointEffectSource}
   */
  class PointMovementSource extends PointEffectSourceMixin(BaseEffectSource) {

    /** @override */
    static sourceType = "move";
  }

  /**
   * A specialized subclass of the BaseEffectSource which describes a point-based source of sound.
   * @extends {BaseEffectSource}
   * @mixes {PointEffectSource}
   */
  class PointSoundSource extends PointEffectSourceMixin(BaseEffectSource) {

    /** @override */
    static sourceType = "sound";

    /** @override */
    get effectsCollection() {
      return canvas.sounds.sources;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _getPolygonConfiguration() {
      return Object.assign(super._getPolygonConfiguration(), {useThreshold: true});
    }

    /* -------------------------------------------- */

    /**
     * Get the effective volume at which an AmbientSound source should be played for a certain listener.
     * @param {Point} listener
     * @param {object} [options]
     * @param {boolean} [options.easing]
     * @returns {number}
     */
    getVolumeMultiplier(listener, {easing=true}={}) {
      if ( !listener ) return 0;                                        // No listener = 0
      const {x, y, radius} = this.data;
      const distance = Math.hypot(listener.x - x, listener.y - y);
      if ( distance === 0 ) return 1;
      if ( distance > radius ) return 0;                                // Distance outside of radius = 0
      if ( !this.shape?.contains(listener.x, listener.y) ) return 0;    // Point outside of shape = 0
      if ( !easing ) return 1;                                          // No easing = 1
      const dv = Math.clamp(distance, 0, radius) / radius;
      return (Math.cos(Math.PI * dv) + 1) * 0.5;                        // Cosine easing [0, 1]
    }
  }

  /**
   * @typedef {import("./base-effect-source.mjs").BaseEffectSourceData} BaseEffectSourceData
   * @typedef {import("./rendered-effect-source-source.mjs").RenderedEffectSourceData} RenderedEffectSourceData
   */

  /**
   * @typedef {Object}                      VisionSourceData
   * @property {number} contrast            The amount of contrast
   * @property {number} attenuation         Strength of the attenuation between bright, dim, and dark
   * @property {number} saturation          The amount of color saturation
   * @property {number} brightness          The vision brightness.
   * @property {string} visionMode          The vision mode.
   * @property {number} lightRadius         The range of light perception.
   * @property {boolean} blinded            Is this vision source blinded?
   */

  /**
   * A specialized subclass of RenderedEffectSource which represents a source of point-based vision.
   * @extends {RenderedEffectSource<BaseEffectSourceData & RenderedEffectSourceData & VisionSourceData, PointSourcePolygon>}
   */
  class PointVisionSource extends PointEffectSourceMixin(RenderedEffectSource) {

    /** @inheritdoc */
    static sourceType = "sight";

    /** @override */
    static _initializeShaderKeys = ["visionMode", "blinded"];

    /** @override */
    static _refreshUniformsKeys = ["radius", "color", "attenuation", "brightness", "contrast", "saturation", "visionMode"];

    /**
     * The corresponding lighting levels for dim light.
     * @type {number}
     * @protected
     */
    static _dimLightingLevel = LIGHTING_LEVELS.DIM;

    /**
     * The corresponding lighting levels for bright light.
     * @type {string}
     * @protected
     */
    static _brightLightingLevel = LIGHTING_LEVELS.BRIGHT;

    /** @inheritdoc */
    static EDGE_OFFSET = -2;

    /** @override */
    static effectsCollection = "visionSources";

    /** @inheritDoc */
    static defaultData = {
      ...super.defaultData,
      contrast: 0,
      attenuation: 0.5,
      saturation: 0,
      brightness: 0,
      visionMode: "basic",
      lightRadius: null
    }

    /** @override */
    static get _layers() {
      return foundry.utils.mergeObject(super._layers, {
        background: {
          defaultShader: BackgroundVisionShader
        },
        coloration: {
          defaultShader: ColorationVisionShader
        },
        illumination: {
          defaultShader: IlluminationVisionShader
        }
      });
    }

    /* -------------------------------------------- */
    /*  Vision Source Attributes                    */
    /* -------------------------------------------- */

    /**
     * The vision mode linked to this VisionSource
     * @type {VisionMode|null}
     */
    visionMode = null;

    /**
     * The vision mode activation flag for handlers
     * @type {boolean}
     * @internal
     */
    _visionModeActivated = false;

    /**
     * The unconstrained LOS polygon.
     * @type {PointSourcePolygon}
     */
    los;

    /**
     * The polygon of light perception.
     * @type {PointSourcePolygon}
     */
    light;

    /* -------------------------------------------- */

    /**
     * An alias for the shape of the vision source.
     * @type {PointSourcePolygon|PIXI.Polygon}
     */
    get fov() {
      return this.shape;
    }

    /* -------------------------------------------- */

    /**
     * If this vision source background is rendered into the lighting container.
     * @type {boolean}
     */
    get preferred() {
      return this.visionMode?.vision.preferred;
    }

    /* -------------------------------------------- */

    /**
     * Is the rendered source animated?
     * @type {boolean}
     */
    get isAnimated() {
      return this.active && this.data.animation && this.visionMode?.animated;
    }

    /* -------------------------------------------- */

    /**
     * Light perception radius of this vision source, taking into account if the source is blinded.
     * @type {number}
     */
    get lightRadius() {
      return this.#hasBlindedVisionMode ? 0 : (this.data.lightRadius ?? 0);
    }

    /* -------------------------------------------- */

    /** @override */
    get radius() {
      return (this.#hasBlindedVisionMode ? this.data.externalRadius : this.data.radius) ?? 0;
    }

    /* -------------------------------------------- */
    /*  Point Vision Source Blinded Management      */
    /* -------------------------------------------- */

    /**
     * Is this source temporarily blinded?
     * @type {boolean}
     */
    get isBlinded() {
      return (this.data.radius === 0) && ((this.data.lightRadius === 0) || !this.visionMode?.perceivesLight)
        || Object.values(this.blinded).includes(true);
    };

    /**
     * Records of blinding strings with a boolean value.
     * By default, if any of this record is true, the source is blinded.
     * @type {Record<string, boolean>}
     */
    blinded = {};

    /**
     * Data overrides that could happen with blindness vision mode.
     * @type {object}
     */
    visionModeOverrides = {};

    /* -------------------------------------------- */

    /**
     * Update darkness blinding according to darkness sources collection.
     */
    #updateBlindedState() {
      this.blinded.darkness = canvas.effects.testInsideDarkness({x: this.x, y: this.y}, this.elevation);
    }

    /* -------------------------------------------- */

    /**
     * To know if blindness vision mode is configured for this source.
     * Note: Convenient method used to avoid calling this.blinded which is costly.
     * @returns {boolean}
     */
    get #hasBlindedVisionMode() {
      return this.visionMode === CONFIG.Canvas.visionModes.blindness;
    }

    /* -------------------------------------------- */
    /*  Vision Source Initialization                */
    /* -------------------------------------------- */

    /** @inheritDoc */
    _initialize(data) {
      super._initialize(data);
      this.data.lightRadius ??= canvas.dimensions.maxR;
      if ( this.data.lightRadius > 0 ) this.data.lightRadius = Math.max(this.data.lightRadius, this.data.externalRadius);
      if ( this.data.radius > 0 ) this.data.radius = Math.max(this.data.radius, this.data.externalRadius);
      if ( !(this.data.visionMode in CONFIG.Canvas.visionModes) ) this.data.visionMode = "basic";
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _createShapes() {
      this._updateVisionMode();
      super._createShapes();
      this.los = this.shape;
      this.light = this._createLightPolygon();
      this.shape = this._createRestrictedPolygon();
    }

    /* -------------------------------------------- */

    /**
     * Responsible for assigning the Vision Mode and calling the activation and deactivation handlers.
     * @protected
     */
    _updateVisionMode() {
      const previousVM = this.visionMode;
      this.visionMode = CONFIG.Canvas.visionModes[this.data.visionMode];

      // Check blinding conditions
      this.#updateBlindedState();

      // Apply vision mode according to conditions
      if ( this.isBlinded ) this.visionMode = CONFIG.Canvas.visionModes.blindness;

      // Process vision mode overrides for blindness
      const defaults = this.visionMode.vision.defaults;
      const data = this.data;
      const applyOverride = prop => this.#hasBlindedVisionMode && (defaults[prop] !== undefined) ? defaults[prop] : data[prop];
      const blindedColor = applyOverride("color");
      this.visionModeOverrides.colorRGB = blindedColor !== null ? Color.from(blindedColor).rgb : null;
      this.visionModeOverrides.brightness = applyOverride("brightness");
      this.visionModeOverrides.contrast = applyOverride("contrast");
      this.visionModeOverrides.saturation = applyOverride("saturation");
      this.visionModeOverrides.attenuation = applyOverride("attenuation");

      // Process deactivation and activation handlers
      if ( this.visionMode !== previousVM ) previousVM?.deactivate(this);
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _configure(changes) {
      this.visionMode.activate(this);
      super._configure(changes);
      this.animation.animation = this.visionMode.animate;
    }

    /* -------------------------------------------- */

    /** @override */
    _configureLayer(layer, layerId) {
      const vmUniforms = this.visionMode.vision[layerId].uniforms;
      layer.vmUniforms = Object.entries(vmUniforms);
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _getPolygonConfiguration() {
      return Object.assign(super._getPolygonConfiguration(), {
        radius: this.data.disabled || this.suppressed ? 0 : (this.blinded.darkness
          ? this.data.externalRadius : canvas.dimensions.maxR),
        useThreshold: true,
        includeDarkness: true
      });
    }

    /* -------------------------------------------- */

    /**
     * Creates the polygon that represents light perception.
     * If the light perception radius is unconstrained, no new polygon instance is created;
     * instead the LOS polygon of this vision source is returned.
     * @returns {PointSourcePolygon}    The new polygon or `this.los`.
     * @protected
     */
    _createLightPolygon() {
      return this.#createConstrainedPolygon(this.lightRadius);
    }

    /* -------------------------------------------- */

    /**
     * Create a restricted FOV polygon by limiting the radius of the unrestricted LOS polygon.
     * If the vision radius is unconstrained, no new polygon instance is created;
     * instead the LOS polygon of this vision source is returned.
     * @returns {PointSourcePolygon}    The new polygon or `this.los`.
     * @protected
     */
    _createRestrictedPolygon() {
      return this.#createConstrainedPolygon(this.radius || this.data.externalRadius);
    }

    /* -------------------------------------------- */

    /**
     * Create a constrained polygon by limiting the radius of the unrestricted LOS polygon.
     * If the radius is unconstrained, no new polygon instance is created;
     * instead the LOS polygon of this vision source is returned.
     * @param {number} radius           The radius to constraint to.
     * @returns {PointSourcePolygon}    The new polygon or `this.los`.
     */
    #createConstrainedPolygon(radius) {
      if ( radius >= this.los.config.radius ) return this.los;
      const {x, y} = this.data;
      const circle = new PIXI.Circle(x, y, radius);
      const density = PIXI.Circle.approximateVertexDensity(radius);
      return this.los.applyConstraint(circle, {density, scalingFactor: 100});
    }

    /* -------------------------------------------- */
    /*  Shader Management                           */
    /* -------------------------------------------- */

    /** @override */
    _configureShaders() {
      const vm = this.visionMode.vision;
      const shaders = {};
      for ( const layer in this.layers ) {
        shaders[layer] = vm[`${layer.toLowerCase()}`]?.shader || this.layers[layer].defaultShader;
      }
      return shaders;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _updateColorationUniforms() {
      super._updateColorationUniforms();
      const shader = this.layers.coloration.shader;
      if ( !shader ) return;
      const u = shader?.uniforms;
      const d = shader.constructor.defaultUniforms;
      u.colorEffect = this.visionModeOverrides.colorRGB ?? d.colorEffect;
      u.useSampler = true;
      const vmUniforms = this.layers.coloration.vmUniforms;
      if ( vmUniforms.length ) this._updateVisionModeUniforms(shader, vmUniforms);
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _updateIlluminationUniforms() {
      super._updateIlluminationUniforms();
      const shader = this.layers.illumination.shader;
      if ( !shader ) return;
      shader.uniforms.useSampler = false; // We don't need to use the background sampler into vision illumination
      const vmUniforms = this.layers.illumination.vmUniforms;
      if ( vmUniforms.length ) this._updateVisionModeUniforms(shader, vmUniforms);
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _updateBackgroundUniforms() {
      super._updateBackgroundUniforms();
      const shader = this.layers.background.shader;
      if ( !shader ) return;
      const u = shader.uniforms;
      u.technique = 0;
      u.contrast = this.visionModeOverrides.contrast;
      u.useSampler = true;
      const vmUniforms = this.layers.background.vmUniforms;
      if ( vmUniforms.length ) this._updateVisionModeUniforms(shader, vmUniforms);
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _updateCommonUniforms(shader) {
      const u = shader.uniforms;
      const d = shader.constructor.defaultUniforms;
      const c = canvas.colors;

      // Passing common environment values
      u.computeIllumination = true;
      u.darknessLevel = canvas.environment.darknessLevel;
      c.ambientBrightest.applyRGB(u.ambientBrightest);
      c.ambientDarkness.applyRGB(u.ambientDarkness);
      c.ambientDaylight.applyRGB(u.ambientDaylight);
      u.weights[0] = canvas.environment.weights.dark;
      u.weights[1] = canvas.environment.weights.halfdark;
      u.weights[2] = canvas.environment.weights.dim;
      u.weights[3] = canvas.environment.weights.bright;
      u.dimLevelCorrection = this.constructor._dimLightingLevel;
      u.brightLevelCorrection = this.constructor._brightLightingLevel;

      // Vision values
      const attenuation = this.visionModeOverrides.attenuation;
      u.attenuation = Math.max(attenuation, 0.0125);
      const brightness = this.visionModeOverrides.brightness;
      u.brightness = (brightness + 1) / 2;
      u.saturation = this.visionModeOverrides.saturation;
      u.linkedToDarknessLevel = this.visionMode.vision.darkness.adaptive;

      // Other values
      u.elevation = this.data.elevation;
      u.screenDimensions = canvas.screenDimensions;
      u.colorTint = this.visionModeOverrides.colorRGB ?? d.colorTint;

      // Textures
      if ( !u.depthTexture ) u.depthTexture = canvas.masks.depth.renderTexture;
      if ( !u.primaryTexture ) u.primaryTexture = canvas.primary.renderTexture;
      if ( !u.darknessLevelTexture ) u.darknessLevelTexture = canvas.effects.illumination.renderTexture;
    }

    /* -------------------------------------------- */

    /**
     * Update layer uniforms according to vision mode uniforms, if any.
     * @param {AdaptiveVisionShader} shader        The shader being updated.
     * @param {Array} vmUniforms                   The targeted layer.
     * @protected
     */
    _updateVisionModeUniforms(shader, vmUniforms) {
      const shaderUniforms = shader.uniforms;
      for ( const [uniform, value] of vmUniforms ) {
        if ( Array.isArray(value) ) {
          const u = (shaderUniforms[uniform] ??= []);
          for ( const i in value ) u[i] = value[i];
        }
        else shaderUniforms[uniform] = value;
      }
    }
  }

  var _module$2 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    BaseEffectSource: BaseEffectSource,
    BaseLightSource: BaseLightSource,
    GlobalLightSource: GlobalLightSource,
    PointDarknessSource: PointDarknessSource,
    PointEffectSourceMixin: PointEffectSourceMixin,
    PointLightSource: PointLightSource,
    PointMovementSource: PointMovementSource,
    PointSoundSource: PointSoundSource,
    PointVisionSource: PointVisionSource,
    RenderedEffectSource: RenderedEffectSource
  });

  /**
   * Dynamic Token Ring Manager.
   */
  class TokenRing {
    /**
     * A TokenRing is constructed by providing a reference to a Token object.
     * @param {Token} token
     */
    constructor(token) {
      this.#token = new WeakRef(token);
    }

    /* -------------------------------------------- */
    /*  Rings System                                */
    /* -------------------------------------------- */

    /**
     * The start and end radii of the token ring color band.
     * @typedef {Object} RingColorBand
     * @property {number} startRadius   The starting normalized radius of the token ring color band.
     * @property {number} endRadius     The ending normalized radius of the token ring color band.
     */

    /* -------------------------------------------- */

    /**
     * The effects which could be applied to a token ring (using bitwise operations).
     * @type {Readonly<Record<string, number>>}
     */
    static effects = Object.freeze({
      DISABLED: 0x00,
      ENABLED: 0x01,
      RING_PULSE: 0x02,
      RING_GRADIENT: 0x04,
      BKG_WAVE: 0x08,
      INVISIBILITY: 0x10  // or spectral pulse effect
    });

    /* -------------------------------------------- */

    /**
     * Is the token rings framework enabled? Will be `null` if the system hasn't initialized yet.
     * @type {boolean|null}
     */
    static get initialized() {
      return this.#initialized;
    }

    static #initialized = null;

    /* -------------------------------------------- */

    /**
     * Token Rings sprite sheet base texture.
     * @type {PIXI.BaseTexture}
     */
    static baseTexture;

    /**
     * Rings and background textures UVs and center offset.
     * @type {Record<string, {UVs: Float32Array, center: {x: number, y: number}}>}
     */
    static texturesData;

    /**
     * The token ring shader class definition.
     * @type {typeof TokenRingSamplerShader}
     */
    static tokenRingSamplerShader;

    /**
     * Ring data with their ring name, background name and their grid dimension target.
     * @type {{ringName: string, bkgName: string, colorBand: RingColorBand, gridTarget: number,
     * defaultRingColorLittleEndian: number|null, defaultBackgroundColorLittleEndian: number|null,
     * subjectScaleAdjustment: number}[]}
     */
    static #ringData;

    /**
     * Default ring thickness in normalized space.
     * @type {number}
     */
    static #defaultRingThickness = 0.1269848;

    /**
     * Default ring subject thickness in normalized space.
     * @type {number}
     */
    static #defaultSubjectThickness = 0.6666666;

    /* -------------------------------------------- */

    /**
     * Initialize the Token Rings system, registering the batch plugin and patching PrimaryCanvasGroup#addToken.
     */
    static initialize() {
      if ( TokenRing.#initialized ) return;
      TokenRing.#initialized = true;
      // Register batch plugin
      this.tokenRingSamplerShader = CONFIG.Token.ring.shaderClass;
      this.tokenRingSamplerShader.registerPlugin();
    }

    /* -------------------------------------------- */

    /**
     * Create texture UVs for each asset into the token rings sprite sheet.
     */
    static createAssetsUVs() {
      const spritesheet = TextureLoader.loader.getCache(CONFIG.Token.ring.spritesheet);
      if ( !spritesheet ) throw new Error("TokenRing UV generation failed because no spritesheet was loaded!");

      this.baseTexture = spritesheet.baseTexture;
      this.texturesData = {};
      this.#ringData = [];

      const {
        defaultColorBand={startRadius: 0.59, endRadius: 0.7225},
        defaultRingColor: drc,
        defaultBackgroundColor: dbc
      } = spritesheet.data.config ?? {};
      const defaultRingColor = Color.from(drc);
      const defaultBackgroundColor = Color.from(dbc);
      const validDefaultRingColor = defaultRingColor.valid ? defaultRingColor.littleEndian : null;
      const validDefaultBackgroundColor = defaultBackgroundColor.valid ? defaultBackgroundColor.littleEndian : null;

      const frames = Object.keys(spritesheet.data.frames || {});

      for ( const asset of frames ) {
        const assetTexture = PIXI.Assets.cache.get(asset);
        if ( !assetTexture ) continue;

        // Extracting texture UVs
        const frame = assetTexture.frame;
        const textureUvs = new PIXI.TextureUvs();
        textureUvs.set(frame, assetTexture.baseTexture, assetTexture.rotate);
        this.texturesData[asset] = {
          UVs: textureUvs.uvsFloat32,
          center: {
            x: frame.center.x / assetTexture.baseTexture.width,
            y: frame.center.y / assetTexture.baseTexture.height
          }
        };

        // Skip background assets
        if ( asset.includes("-bkg") ) continue;

        // Extracting and determining final colors
        const { ringColor: rc, backgroundColor: bc, colorBand, gridTarget, ringThickness=this.#defaultRingThickness }
          = spritesheet.data.frames[asset] || {};

        const ringColor = Color.from(rc);
        const backgroundColor = Color.from(bc);

        const finalRingColor = ringColor.valid ? ringColor.littleEndian : validDefaultRingColor;
        const finalBackgroundColor = backgroundColor.valid ? backgroundColor.littleEndian : validDefaultBackgroundColor;
        const subjectScaleAdjustment = 1 / (ringThickness + this.#defaultSubjectThickness);

        this.#ringData.push({
          ringName: asset,
          bkgName: `${asset}-bkg`,
          colorBand: foundry.utils.deepClone(colorBand ?? defaultColorBand),
          gridTarget: gridTarget ?? 1,
          defaultRingColorLittleEndian: finalRingColor,
          defaultBackgroundColorLittleEndian: finalBackgroundColor,
          subjectScaleAdjustment
        });
      }

      // Sorting the rings data array
      this.#ringData.sort((a, b) => a.gridTarget - b.gridTarget);
    }

    /* -------------------------------------------- */

    /**
     * Get the UVs array for a given texture name and scale correction.
     * @param {string} name                  Name of the texture we want to get UVs.
     * @param {number} [scaleCorrection=1]   The scale correction applied to UVs.
     * @returns {Float32Array}
     */
    static getTextureUVs(name, scaleCorrection=1) {
      if ( scaleCorrection === 1 ) return this.texturesData[name].UVs;
      const tUVs = this.texturesData[name].UVs;
      const c = this.texturesData[name].center;
      const UVs = new Float32Array(8);
      for ( let i=0; i<8; i+=2 ) {
        UVs[i] = ((tUVs[i] - c.x) * scaleCorrection) + c.x;
        UVs[i+1] = ((tUVs[i+1] - c.y) * scaleCorrection) + c.y;
      }
      return UVs;
    }

    /* -------------------------------------------- */

    /**
     * Get ring and background names for a given size.
     * @param {number} size   The size to match (grid size dimension)
     * @returns {{bkgName: string, ringName: string, colorBand: RingColorBand}}
     */
    static getRingDataBySize(size) {
      if ( !Number.isFinite(size) || !this.#ringData.length ) {
        return {
          ringName: undefined,
          bkgName: undefined,
          colorBand: undefined,
          defaultRingColorLittleEndian: null,
          defaultBackgroundColorLittleEndian: null,
          subjectScaleAdjustment: null
        };
      }
      const rings = this.#ringData.map(r => [Math.abs(r.gridTarget - size), r]);

      // Sort rings on proximity to target size
      rings.sort((a, b) => a[0] - b[0]);

      // Choose the closest ring, access the second element of the first array which is the ring data object
      const closestRing = rings[0][1];

      return {
        ringName: closestRing.ringName,
        bkgName: closestRing.bkgName,
        colorBand: closestRing.colorBand,
        defaultRingColorLittleEndian: closestRing.defaultRingColorLittleEndian,
        defaultBackgroundColorLittleEndian: closestRing.defaultBackgroundColorLittleEndian,
        subjectScaleAdjustment: closestRing.subjectScaleAdjustment
      };
    }

    /* -------------------------------------------- */
    /*  Attributes                                  */
    /* -------------------------------------------- */

    /** @type {string} */
    ringName;

    /** @type {string} */
    bkgName;

    /** @type {Float32Array} */
    ringUVs;

    /** @type {Float32Array} */
    bkgUVs;

    /** @type {number} */
    ringColorLittleEndian = 0xFFFFFF; // Little endian format => BBGGRR

    /** @type {number} */
    bkgColorLittleEndian = 0xFFFFFF; // Little endian format => BBGGRR

    /** @type {number|null} */
    defaultRingColorLittleEndian = null;

    /** @type {number|null} */
    defaultBackgroundColorLittleEndian = null;

    /** @type {number} */
    effects = 0;

    /** @type {number} */
    scaleCorrection = 1;

    /** @type {number} */
    scaleAdjustmentX = 1;

    /** @type {number} */
    scaleAdjustmentY = 1;

    /** @type {number} */
    subjectScaleAdjustment = 1;

    /** @type {number} */
    textureScaleAdjustment = 1;

    /** @type {RingColorBand} */
    colorBand;


    /* -------------------------------------------- */
    /*  Properties                                  */
    /* -------------------------------------------- */

    /**
     * Reference to the token that should be animated.
     * @type {Token|void}
     */
    get token() {
      return this.#token.deref();
    }

    /**
     * Weak reference to the token being animated.
     * @type {WeakRef<Token>}
     */
    #token;

    /* -------------------------------------------- */
    /*  Rendering                                   */
    /* -------------------------------------------- */

    /**
     * Configure the sprite mesh.
     * @param {PrimarySpriteMesh} [mesh]  The mesh to which TokenRing functionality is configured.
     */
    configure(mesh) {
      this.#configureTexture(mesh);
      this.configureSize();
      this.configureVisuals();
    }

    /* -------------------------------------------- */

    /**
     * Clear configuration pertaining to token ring from the mesh.
     */
    clear() {
      this.ringName = undefined;
      this.bkgName = undefined;
      this.ringUVs = undefined;
      this.bkgUVs = undefined;
      this.colorBand = undefined;
      this.ringColorLittleEndian = 0xFFFFFF;
      this.bkgColorLittleEndian = 0xFFFFFF;
      this.defaultRingColorLittleEndian = null;
      this.defaultBackgroundColorLittleEndian = null;
      this.scaleCorrection = 1;
      this.scaleAdjustmentX = 1;
      this.scaleAdjustmentY = 1;
      this.subjectScaleAdjustment = 1;
      this.textureScaleAdjustment = 1;
      const mesh = this.token.mesh;
      if ( mesh ) mesh.padding = 0;
    }

    /* -------------------------------------------- */

    /**
     * Configure token ring size.
     */
    configureSize() {
      const mesh = this.token.mesh;

      // Ring size
      const size = Math.min(this.token.document.width ?? 1, this.token.document.height ?? 1);
      Object.assign(this, this.constructor.getRingDataBySize(size));

      // Subject scale
      const scale = this.token.document.ring.subject.scale ?? this.scaleCorrection ?? 1;
      this.scaleCorrection = scale;
      this.ringUVs = this.constructor.getTextureUVs(this.ringName, scale);
      this.bkgUVs = this.constructor.getTextureUVs(this.bkgName, scale);

      // Determine the longer and shorter sides of the image
      const {width: w, height: h} = this.token.mesh.texture ?? this.token.texture;
      let longSide = Math.max(w, h);
      let shortSide = Math.min(w, h);

      // Calculate the necessary padding
      let padding = (longSide - shortSide) / 2;

      // Determine padding for x and y sides
      let paddingX = (w < h) ? padding : 0;
      let paddingY = (w > h) ? padding : 0;

      // Apply mesh padding
      mesh.paddingX = paddingX;
      mesh.paddingY = paddingY;

      // Apply adjustments
      const adjustment = shortSide / longSide;
      this.scaleAdjustmentX = paddingX ? adjustment : 1.0;
      this.scaleAdjustmentY = paddingY ? adjustment : 1.0;

      // Apply texture scale adjustment for token without a subject texture and in grid fit mode
      const inferred = (this.token.document.texture.src !== this.token.document._inferRingSubjectTexture());
      if ( CONFIG.Token.ring.isGridFitMode && !inferred && !this.token.document._source.ring.subject.texture ) {
        this.textureScaleAdjustment = this.subjectScaleAdjustment;
      }
      else this.textureScaleAdjustment = 1;
    }

    /* -------------------------------------------- */

    /**
     * Configure the token ring visuals properties.
     */
    configureVisuals() {
      const ring = this.token.document.ring;

      // Configure colors
      const colors = foundry.utils.mergeObject(ring.colors, this.token.getRingColors(), {inplace: false});
      const resolveColor = (color, defaultColor) => {
        const resolvedColor = Color.from(color ?? 0xFFFFFF).littleEndian;
        return ((resolvedColor === 0xFFFFFF) && (defaultColor !== null)) ? defaultColor : resolvedColor;
      };
      this.ringColorLittleEndian = resolveColor(colors?.ring, this.defaultRingColorLittleEndian);
      this.bkgColorLittleEndian = resolveColor(colors?.background, this.defaultBackgroundColorLittleEndian);

      // Configure effects
      const effectsToApply = this.token.getRingEffects();
      this.effects = ((ring.effects >= this.constructor.effects.DISABLED)
          ? ring.effects : this.constructor.effects.ENABLED)
        | effectsToApply.reduce((acc, e) => acc |= e, 0x0);

      // Mask with enabled effects for the current token ring configuration
      let mask = this.effects & CONFIG.Token.ring.ringClass.effects.ENABLED;
      for ( const key in CONFIG.Token.ring.effects ) {
        const v = CONFIG.Token.ring.ringClass.effects[key];
        if ( v !== undefined ) {
          mask |= v;
        }
      }
      this.effects &= mask;
    }

    /* -------------------------------------------- */

    /**
     * Configure dynamic token ring subject texture.
     * @param {PrimarySpriteMesh} mesh                  The mesh being configured
     */
    #configureTexture(mesh) {
      const src = this.token.document.ring.subject.texture;
      if ( PIXI.Assets.cache.has(src) ) {
        const subjectTexture = getTexture(src);
        if ( subjectTexture?.valid ) mesh.texture = subjectTexture;
      }
    }

    /* -------------------------------------------- */
    /*  Animations                                  */
    /* -------------------------------------------- */

    /**
     * Flash the ring briefly with a certain color.
     * @param {Color} color                              Color to flash.
     * @param {CanvasAnimationOptions} animationOptions  Options to customize the animation.
     * @returns {Promise<boolean|void>}
     */
    async flashColor(color, animationOptions={}) {
      if ( Number.isNaN(color) ) return;
      const defaultColorFallback = this.token.ring.defaultRingColorLittleEndian ?? 0xFFFFFF;
      const configuredColor = Color.from(foundry.utils.mergeObject(
        this.token.document.ring.colors,
        this.token.getRingColors(),
        {inplace: false}
      ).ring);
      const originalColor = configuredColor.valid ? configuredColor.littleEndian : defaultColorFallback;
      return await CanvasAnimation.animate([{
        attribute: "ringColorLittleEndian",
        parent: this,
        from: originalColor,
        to: new Color(color.littleEndian),
        color: true
      }], foundry.utils.mergeObject({
        duration: 1600,
        priority: PIXI.UPDATE_PRIORITY.HIGH,
        easing: this.constructor.createSpikeEasing(.15)
      }, animationOptions));
    }

    /* -------------------------------------------- */

    /**
     * Create an easing function that spikes in the center. Ideal duration is around 1600ms.
     * @param {number} [spikePct=0.5]  Position on [0,1] where the spike occurs.
     * @returns {Function(number): number}
     */
    static createSpikeEasing(spikePct=0.5) {
      const scaleStart = 1 / spikePct;
      const scaleEnd = 1 / (1 - spikePct);
      return pt => {
        if ( pt < spikePct ) return CanvasAnimation.easeInCircle(pt * scaleStart);
        else return 1 - CanvasAnimation.easeOutCircle(((pt - spikePct) * scaleEnd));
      };
    }

    /* -------------------------------------------- */

    /**
     * Easing function that produces two peaks before returning to the original value. Ideal duration is around 500ms.
     * @param {number} pt     The proportional animation timing on [0,1].
     * @returns {number}      The eased animation progress on [0,1].
     */
    static easeTwoPeaks(pt) {
      return (Math.sin((4 * Math.PI * pt) - (Math.PI / 2)) + 1) / 2;
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * To avoid breaking dnd5e.
     * @deprecated since v12
     * @ignore
     */
    configureMesh() {}

    /**
     * To avoid breaking dnd5e.
     * @deprecated since v12
     * @ignore
     */
    configureNames() {}

  }

  /**
   * @typedef {Object} RingData
   * @property {string} id                        The id of this Token Ring configuration.
   * @property {string} label                     The label of this Token Ring configuration.
   * @property {string} spritesheet               The spritesheet path which provides token ring frames for various sized creatures.
   * @property {Record<string, string>} [effects] Registered special effects which can be applied to a token ring.
   * @property {Object} framework
   * @property {typeof TokenRing} [framework.ringClass=TokenRing] The manager class responsible for rendering token rings.
   * @property {typeof PrimaryBaseSamplerShader} [framework.shaderClass=TokenRingSamplerShader]  The shader class used to render the TokenRing.
   */

  /**
   * A special subclass of DataField used to reference a class definition.
   */
  class ClassReferenceField extends DataField {
    constructor(options) {
      super(options);
      this.#baseClass = options.baseClass;
    }

    /**
     * The base class linked to this data field.
     * @type {typeof Function}
     */
    #baseClass;

    /** @inheritdoc */
    static get _defaults() {
      const defaults = super._defaults;
      defaults.required = true;
      return defaults;
    }

    /** @override */
    _cast(value) {
      if ( !foundry.utils.isSubclass(value, this.#baseClass) ) {
        throw new Error(`The value provided to a ClassReferenceField must be a ${this.#baseClass.name} subclass.`);
      }
      return value;
    }

    /** @override */
    getInitialValue(data) {
      return this.initial;
    }
  }

  /* -------------------------------------------- */

  /**
   * Dynamic Ring configuration data model.
   * @extends {foundry.abstract.DataModel}
   * @implements {RingData}
   */
  class DynamicRingData extends DataModel {
    /** @inheritDoc */
    static defineSchema() {
      const fields = foundry.data.fields;

      // Return model schema
      return {
        id: new fields.StringField({blank: true}),
        label: new fields.StringField({blank: false}),
        spritesheet: new fields.FilePathField({categories: ["TEXT"], required: true}),
        effects: new fields.ObjectField({initial: {
            RING_PULSE: "TOKEN.RING.EFFECTS.RING_PULSE",
            RING_GRADIENT: "TOKEN.RING.EFFECTS.RING_GRADIENT",
            BKG_WAVE: "TOKEN.RING.EFFECTS.BKG_WAVE",
            INVISIBILITY: "TOKEN.RING.EFFECTS.INVISIBILITY"
        }}),
        framework: new fields.SchemaField({
          ringClass: new ClassReferenceField({initial: TokenRing, baseClass: TokenRing}),
          shaderClass: new ClassReferenceField({initial: TokenRingSamplerShader, baseClass: PrimaryBaseSamplerShader})
        })
      };
    }
  }

  /**
   * The start and end radii of the token ring color band.
   * @typedef {Object} RingColorBand
   * @property {number} startRadius The starting normalized radius of the token ring color band.
   * @property {number} endRadius   The ending normalized radius of the token ring color band.
   */

  /**
   * Dynamic ring id.
   * @typedef {string} DynamicRingId
   */

  /**
   * Token Ring configuration Singleton Class.
   *
   * @example Add a new custom ring configuration. Allow only ring pulse, ring gradient and background wave effects.
   * const customConfig = new foundry.canvas.tokens.DynamicRingData({
   *   id: "myCustomRingId",
   *   label: "Custom Ring",
   *   effects: {
   *     RING_PULSE: "TOKEN.RING.EFFECTS.RING_PULSE",
   *     RING_GRADIENT: "TOKEN.RING.EFFECTS.RING_GRADIENT",
   *     BACKGROUND_WAVE: "TOKEN.RING.EFFECTS.BACKGROUND_WAVE"
   *   },
   *   spritesheet: "canvas/tokens/myCustomRings.json",
   *   framework: {
   *     shaderClass: MyCustomTokenRingSamplerShader,
   *     ringClass: TokenRing
   *   }
   * });
   * CONFIG.Token.ring.addConfig(customConfig.id, customConfig);
   *
   * @example Get a specific ring configuration
   * const config = CONFIG.Token.ring.getConfig("myCustomRingId");
   * console.log(config.spritesheet); // Output: canvas/tokens/myCustomRings.json
   *
   * @example Use a specific ring configuration
   * const success = CONFIG.Token.ring.useConfig("myCustomRingId");
   * console.log(success); // Output: true
   *
   * @example Get the labels of all configurations
   * const configLabels = CONFIG.Token.ring.configLabels;
   * console.log(configLabels);
   * // Output:
   * // {
   * //   "coreSteel": "Foundry VTT Steel Ring",
   * //   "coreBronze": "Foundry VTT Bronze Ring",
   * //   "myCustomRingId" : "My Super Power Ring"
   * // }
   *
   * @example Get the IDs of all configurations
   * const configIDs = CONFIG.Token.ring.configIDs;
   * console.log(configIDs); // Output: ["coreSteel", "coreBronze", "myCustomRingId"]
   *
   * @example Create a hook to add a custom token ring configuration. This ring configuration will appear in the settings.
   * Hooks.on("initializeDynamicTokenRingConfig", ringConfig => {
   *   const mySuperPowerRings = new foundry.canvas.tokens.DynamicRingData({
   *     id: "myCustomRingId",
   *     label: "My Super Power Rings",
   *     effects: {
   *       RING_PULSE: "TOKEN.RING.EFFECTS.RING_PULSE",
   *       RING_GRADIENT: "TOKEN.RING.EFFECTS.RING_GRADIENT",
   *       BACKGROUND_WAVE: "TOKEN.RING.EFFECTS.BACKGROUND_WAVE"
   *     },
   *     spritesheet: "canvas/tokens/mySuperPowerRings.json"
   *   });
   *   ringConfig.addConfig("mySuperPowerRings", mySuperPowerRings);
   * });
   *
   * @example Activate color bands debugging visuals to ease configuration
   * CONFIG.Token.ring.debugColorBands = true;
   */
  class TokenRingConfig {
    constructor() {
      if ( TokenRingConfig.#instance ) {
        throw new Error("An instance of TokenRingConfig has already been created. " +
          "Use `CONFIG.Token.ring` to access it.");
      }
      TokenRingConfig.#instance = this;
    }

    /**
     * The token ring config instance.
     * @type {TokenRingConfig}
     */
    static #instance;

    /**
     * To know if the ring config is initialized.
     * @type {boolean}
     */
    static #initialized = false;

    /**
     * To know if a Token Ring registration is possible.
     * @type {boolean}
     */
    static #closedRegistration = true;

    /**
     * Core token rings used in Foundry VTT.
     * Each key is a string identifier for a ring, and the value is an object containing the ring's data.
     * This object is frozen to prevent any modifications.
     * @type {Readonly<Record<DynamicRingId, RingData>>}
     */
    static CORE_TOKEN_RINGS = Object.freeze({
      coreSteel: {
        id: "coreSteel",
        label: "TOKEN.RING.SETTINGS.coreSteel",
        spritesheet: "canvas/tokens/rings-steel.json"
      },
      coreBronze: {
        id: "coreBronze",
        label: "TOKEN.RING.SETTINGS.coreBronze",
        spritesheet: "canvas/tokens/rings-bronze.json"
      }
    });

    /**
     * Core token rings fit modes used in Foundry VTT.
     * @type {Readonly<object>}
     */
    static CORE_TOKEN_RINGS_FIT_MODES = Object.freeze({
      subject: {
        id: "subject",
        label: "TOKEN.RING.SETTINGS.FIT_MODES.subject"
      },
      grid: {
        id: "grid",
        label: "TOKEN.RING.SETTINGS.FIT_MODES.grid"
      }
    });

    /* -------------------------------------------- */

    /**
     * Register the token ring config and initialize it
     */
    static initialize() {
      // If token config is initialized
      if ( this.#initialized ) {
        throw new Error("The token configuration class can be initialized only once!")
      }

      // Open the registration window for the token rings
      this.#closedRegistration = false;

      // Add default rings
      for ( const id in this.CORE_TOKEN_RINGS ) {
        const config = new DynamicRingData(this.CORE_TOKEN_RINGS[id]);
        CONFIG.Token.ring.addConfig(config.id, config);
      }

      // Call an explicit hook for token ring configuration
      Hooks.callAll("initializeDynamicTokenRingConfig", CONFIG.Token.ring);

      // Initialize token rings configuration
      if ( !CONFIG.Token.ring.useConfig(game.settings.get("core", "dynamicTokenRing")) ) {
        CONFIG.Token.ring.useConfig(this.CORE_TOKEN_RINGS.coreSteel.id);
      }

      // Close the registration window for the token rings
      this.#closedRegistration = true;
      this.#initialized = true;
    }

    /* -------------------------------------------- */

    /**
     * Register game settings used by the Token Ring
     */
    static registerSettings() {
      game.settings.register("core", "dynamicTokenRing", {
        name: "TOKEN.RING.SETTINGS.label",
        hint: "TOKEN.RING.SETTINGS.hint",
        scope: "world",
        config: true,
        type: new foundry.data.fields.StringField({required: true, blank: false,
          initial: this.CORE_TOKEN_RINGS.coreSteel.id,
          choices: () => CONFIG.Token.ring.configLabels
        }),
        requiresReload: true
      });

      game.settings.register("core", "dynamicTokenRingFitMode", {
        name: "TOKEN.RING.SETTINGS.FIT_MODES.label",
        hint: "TOKEN.RING.SETTINGS.FIT_MODES.hint",
        scope: "world",
        config: true,
        type: new foundry.data.fields.StringField({
          required: true,
          blank: false,
          initial: this.CORE_TOKEN_RINGS_FIT_MODES.subject.id,
          choices: Object.fromEntries(Object.entries(this.CORE_TOKEN_RINGS_FIT_MODES).map(([key, mode]) => [key, mode.label]))
        }),
        requiresReload: true
      });
    }

    /* -------------------------------------------- */

    /**
     * Ring configurations.
     * @type {Map<string, DynamicRingData>}
     */
    #configs = new Map();

    /**
     * The current ring configuration.
     * @type {DynamicRingData}
     */
    #currentConfig;

    /* -------------------------------------------- */
    /*  Properties                                  */
    /* -------------------------------------------- */

    /**
     * A mapping of token subject paths where modules or systems have configured subject images.
     * @type {Record<string, string>}
     */
    subjectPaths = {};

    /**
     * All color bands visual debug flag.
     * @type {boolean}
     */
    debugColorBands = false;

    /**
     * Get the current ring class.
     * @type {typeof TokenRing} The current ring class.
     */
    get ringClass() {
      return this.#currentConfig.framework.ringClass;
    }

    set ringClass(value) {
      this.#currentConfig.framework.ringClass = value;
    }

    /**
     * Get the current effects.
     * @type {Record<string, string>} The current effects.
     */
    get effects() {
      return this.#currentConfig.effects;
    }

    /**
     * Get the current spritesheet.
     * @type {string} The current spritesheet path.
     */
    get spritesheet() {
      return this.#currentConfig.spritesheet;
    }

    /**
     * Get the current shader class.
     * @type {typeof PrimaryBaseSamplerShader} The current shader class.
     */
    get shaderClass() {
      return this.#currentConfig.framework.shaderClass;
    }

    set shaderClass(value) {
      this.#currentConfig.framework.shaderClass = value;
    }

    /**
     * Get the current localized label.
     * @returns {string}
     */
    get label() {
      return this.#currentConfig.label;
    }

    /**
     * Get the current id.
     * @returns {string}
     */
    get id() {
      return this.#currentConfig.id;
    }

    /* -------------------------------------------- */
    /*  Management                                  */
    /* -------------------------------------------- */

    /**
     * Is a custom fit mode active?
     * @returns {boolean}
     */
    get isGridFitMode() {
      return game.settings.get("core","dynamicTokenRingFitMode")
        === this.constructor.CORE_TOKEN_RINGS_FIT_MODES.grid.id;
    }

    /* -------------------------------------------- */

    /**
     * Add a new ring configuration.
     * @param {string} id         The id of the ring configuration.
     * @param {RingConfig} config The configuration object for the ring.
     */
    addConfig(id, config) {
      if ( this.constructor.#closedRegistration ) {
        throw new Error("Dynamic Rings registration window is closed. You must register a dynamic token ring configuration during" +
          " the `registerDynamicTokenRing` hook.");
      }
      this.#configs.set(id, config);
    }

    /* -------------------------------------------- */

    /**
     * Get a ring configuration.
     * @param {string} id     The id of the ring configuration.
     * @returns {RingConfig}  The ring configuration object.
     */
    getConfig(id) {
      return this.#configs.get(id);
    }

    /* -------------------------------------------- */

    /**
     * Use a ring configuration.
     * @param {string} id  The id of the ring configuration to use.
     * @returns {boolean} True if the configuration was successfully set, false otherwise.
     */
    useConfig(id) {
      if ( this.#configs.has(id) ) {
        this.#currentConfig = this.#configs.get(id);
        return true;
      }
      return false;
    }

    /* -------------------------------------------- */

    /**
     * Get the IDs of all configurations.
     * @returns {string[]} The names of all configurations.
     */
    get configIDs() {
      return Array.from(this.#configs.keys());
    }

    /* -------------------------------------------- */

    /**
     * Get the labels of all configurations.
     * @returns {Record<string, string>} An object with configuration names as keys and localized labels as values.
     */
    get configLabels() {
      const labels = {};
      for ( const [name, config] of this.#configs.entries() ) {
        labels[name] = config.label;
      }
      return labels;
    }

    /* -------------------------------------------- */
    /*  Deprecations and Compatibility              */
    /* -------------------------------------------- */

    /**
     * @deprecated since v11
     * @ignore
     */
    get configNames() {
      const msg = "TokenRingConfig#configNames is deprecated and replaced by TokenRingConfig#configIDs";
      foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
      return this.configIDs;
    }
  }

  var _module$1 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    DynamicRingData: DynamicRingData,
    TokenRing: TokenRing,
    TokenRingConfig: TokenRingConfig
  });

  var canvas$1 = /*#__PURE__*/Object.freeze({
    __proto__: null,
    SMAAFilter: SMAAFilter,
    SceneManager: SceneManager,
    edges: _module$5,
    regions: _module$3,
    sources: _module$2,
    tokens: _module$1
  });

  /**
   * @typedef {object} CompendiumArtInfo
   * @property {string} [actor]         The path to the Actor's portrait image.
   * @property {string|object} [token]  The path to the token image, or an object to merge into the Actor's prototype
   *                                    token.
   * @property {string} [credit]        An optional credit string for use by the game system to apply in an appropriate
   *                                    place.
   */

  /**
   * A mapping of compendium pack IDs to Document IDs to art information.
   * @typedef {Record<string, Record<string, CompendiumArtInfo>>} CompendiumArtMapping
   */

  /**
   * @typedef {object} CompendiumArtDescriptor
   * @property {string} packageId  The ID of the package providing the art.
   * @property {string} title      The title of the package providing the art.
   * @property {string} mapping    The path to the art mapping file.
   * @property {string} [credit]   An optional credit string for use by the game system to apply in an appropriate place.
   * @property {number} priority   The package's user-configured priority.
   */

  var _types$1 = /*#__PURE__*/Object.freeze({
    __proto__: null
  });

  /**
   * @typedef {import("../_types.mjs").CompendiumArtInfo} CompendiumArtInfo
   * @typedef {import("../_types.mjs").CompendiumArtMapping} CompendiumArtMapping
   * @typedef {import("../_types.mjs").CompendiumArtDescriptor} CompendiumArtDescriptor
   */

  /**
   * A class responsible for managing package-provided art and applying it to Documents in compendium packs.
   * @extends {Map<string, CompendiumArtInfo>}
   */
  class CompendiumArt extends Map {
    constructor(iterable) {
      super(iterable);
      if ( game.compendiumArt instanceof this.constructor ) {
        throw new Error("You may not re-initialize the singleton CompendiumArt. Use game.compendiumArt instead.");
      }
    }

    /**
     * The key for the package manifest flag used to store the mapping information.
     * @type {string}
     */
    FLAG = "compendiumArtMappings";

    /**
     * The key for the setting used to store the World's art preferences.
     * @type {string}
     */
    SETTING = "compendiumArtConfiguration";

    /**
     * Whether art application is enabled. This should be switched off when performing client-side compendium migrations
     * in order to avoid persisting injected data.
     * @type {boolean}
     */
    enabled = true;

    /* -------------------------------------------- */

    /**
     * Retrieve all active packages that provide art mappings in priority order.
     * @returns {CompendiumArtDescriptor[]}
     */
    getPackages() {
      const settings = game.settings.get("core", this.SETTING);
      const unsorted = [];
      const configs = [];

      for ( const pkg of [game.system, ...game.modules] ) {
        const isActive = (pkg instanceof System) || pkg.active;
        const flag = pkg.flags?.[this.FLAG]?.[game.system.id];
        if ( !isActive || !flag ) continue;
        const { id: packageId, title } = pkg;
        const { mapping, credit } = flag;
        const config = { packageId, title, mapping, credit };
        configs.push(config);
        const setting = settings[pkg.id] ?? { portraits: true, tokens: true };
        foundry.utils.mergeObject(config, setting);
        if ( config.priority === undefined ) unsorted.push(config);
      }

      const maxPriority = Math.max(...configs.map(({ priority }) => priority ?? -Infinity), CONST.SORT_INTEGER_DENSITY);
      unsorted.forEach((config, i) => config.priority = maxPriority + ((i + 1) * CONST.SORT_INTEGER_DENSITY));
      configs.sort((a, b) => a.priority - b.priority);
      return configs;
    }

    /* -------------------------------------------- */

    /**
     * Collate Document art mappings from active packages.
     * @internal
     */
    async _registerArt() {
      this.clear();
      // Load packages in reverse order so that higher-priority packages overwrite lower-priority ones.
      for ( const { packageId, mapping, credit } of this.getPackages().reverse() ) {
        try {
          const json = await foundry.utils.fetchJsonWithTimeout(mapping);
          await this.#parseArtMapping(packageId, json, credit);
        } catch(e) {
          const pkg = packageId === game.system.id ? game.system : game.modules.get(packageId);
          Hooks.onError("CompendiumArt#_registerArt", e, {
            msg: `Failed to parse compendium art mapping for package '${pkg?.title}'`,
            log: "error"
          });
        }
      }
    }

    /* -------------------------------------------- */

    /**
     * Parse a provided art mapping and store it for reference later, and update compendium indices to use the provided
     * art.
     * @param {string} packageId              The ID of the package providing the mapping.
     * @param {CompendiumArtMapping} mapping  The art mapping information provided by the package.
     * @param {string} [credit]               An optional credit string for use by the game system to apply in an
     *                                        appropriate place.
     */
    async #parseArtMapping(packageId, mapping, credit) {
      const settings = game.settings.get("core", this.SETTING)?.[packageId] ?? { portraits: true, tokens: true };
      for ( const [packName, actors] of Object.entries(mapping) ) {
        const pack = game.packs.get(packName);
        if ( !pack ) continue;
        for ( let [actorId, info] of Object.entries(actors) ) {
          const entry = pack.index.get(actorId);
          if ( !entry || !(settings.portraits || settings.tokens) ) continue;
          if ( settings.portraits ) entry.img = info.actor;
          else delete info.actor;
          if ( !settings.tokens ) delete info.token;
          if ( credit ) info.credit = credit;
          const uuid = pack.getUuid(actorId);
          info = foundry.utils.mergeObject(this.get(uuid) ?? {}, info, { inplace: false });
          this.set(uuid, info);
        }
      }
    }
  }

  /** @module helpers */

  var helpers = /*#__PURE__*/Object.freeze({
    __proto__: null,
    CompendiumArt: CompendiumArt,
    types: _types$1
  });

  /**
   * @typedef {Object} DiceTermResult
   * @property {number} result        The numeric result
   * @property {boolean} [active]     Is this result active, contributing to the total?
   * @property {number} [count]       A value that the result counts as, otherwise the result is not used directly as
   * @property {boolean} [success]    Does this result denote a success?
   * @property {boolean} [failure]    Does this result denote a failure?
   * @property {boolean} [discarded]  Was this result discarded?
   * @property {boolean} [rerolled]   Was this result rerolled?
   * @property {boolean} [exploded]   Was this result exploded?
   */

  /* -------------------------------------------- */
  /*  Roll Parsing Types                          */
  /* -------------------------------------------- */

  /**
   * @typedef {object} RollParseNode
   * @property {string} class    The class name for this node.
   * @property {string} formula  The original matched text for this node.
   */

  /**
   * @typedef {RollParseNode} RollParseTreeNode
   * @property {string} operator                          The binary operator.
   * @property {[RollParseNode, RollParseNode]} operands  The two operands.
   */

  /**
   * @typedef {RollParseNode} FlavorRollParseNode
   * @property {object} options
   * @property {string} options.flavor  Flavor text associated with the node.
   */

  /**
   * @typedef {FlavorRollParseNode} ModifiersRollParseNode
   * @property {string} modifiers  The matched modifiers string.
   */

  /**
   * @typedef {FlavorRollParseNode} NumericRollParseNode
   * @property {number} number  The number.
   */

  /**
   * @typedef {FlavorRollParseNode} FunctionRollParseNode
   * @property {string} fn              The function name.
   * @property {RollParseNode[]} terms  The arguments to the function.
   */

  /**
   * @typedef {ModifiersRollParseNode} PoolRollParseNode
   * @property {RollParseNode[]} terms  The pool terms.
   */

  /**
   * @typedef {FlavorRollParseNode} ParentheticalRollParseNode
   * @property {string} term  The inner parenthetical term.
   */

  /**
   * @typedef {FlavorRollParseNode} StringParseNode
   * @property {string} term  The unclassified string term.
   */

  /**
   * @typedef {ModifiersRollParseNode} DiceRollParseNode
   * @property {number|ParentheticalRollParseNode} number        The number of dice.
   * @property {string|number|ParentheticalRollParseNode} faces  The number of faces or a string denomination like "c" or
   *                                                             "f".
   */

  /**
   * @typedef {null|number|string|RollParseNode|RollParseArg[]} RollParseArg
   */

  var _types = /*#__PURE__*/Object.freeze({
    __proto__: null
  });

  /**
   * @typedef {import("../_types.mjs").RollParseNode} RollParseNode
   */

  /**
   * An abstract class which represents a single token that can be used as part of a Roll formula.
   * Every portion of a Roll formula is parsed into a subclass of RollTerm in order for the Roll to be fully evaluated.
   */
  class RollTerm {
    /**
     * @param {object} [options]  An object of additional options which describes and modifies the term.
     */
    constructor({options={}}={}) {
      this.options = options;
    }

    /**
     * An object of additional options which describes and modifies the term.
     * @type {object}
     */
    options;

    /**
     * An internal flag for whether the term has been evaluated
     * @type {boolean}
     * @internal
     */
    _evaluated = false;

    /**
     * A reference to the Roll at the root of the evaluation tree.
     * @type {Roll}
     * @internal
     */
    _root;

    /**
     * Is this term intermediate, and should be evaluated first as part of the simplification process?
     * @type {boolean}
     */
    isIntermediate = false;

    /**
     * A regular expression pattern which identifies optional term-level flavor text
     * @type {string}
     */
    static FLAVOR_REGEXP_STRING = "(?:\\[([^\\]]+)\\])";

    /**
     * A regular expression which identifies term-level flavor text
     * @type {RegExp}
     */
    static FLAVOR_REGEXP = new RegExp(RollTerm.FLAVOR_REGEXP_STRING, "g");

    /**
     * A regular expression used to match a term of this type
     * @type {RegExp}
     */
    static REGEXP = undefined;

    /**
     * An array of additional attributes which should be retained when the term is serialized
     * @type {string[]}
     */
    static SERIALIZE_ATTRIBUTES = [];

    /* -------------------------------------------- */
    /*  RollTerm Attributes                         */
    /* -------------------------------------------- */

    /**
     * A string representation of the formula expression for this RollTerm, prior to evaluation.
     * @type {string}
     */
    get expression() {
      throw new Error(`The ${this.constructor.name} class must implement the expression attribute`);
    }

    /**
     * A string representation of the formula, including optional flavor text.
     * @type {string}
     */
    get formula() {
      let f = this.expression;
      if ( this.flavor ) f += `[${this.flavor}]`;
      return f;
    }

    /**
     * A string or numeric representation of the final output for this term, after evaluation.
     * @type {number|string}
     */
    get total() {
      throw new Error(`The ${this.constructor.name} class must implement the total attribute`);
    }

    /**
     * Optional flavor text which modifies and describes this term.
     * @type {string}
     */
    get flavor() {
      return this.options.flavor || "";
    }

    /**
     * Whether this term is entirely deterministic or contains some randomness.
     * @type {boolean}
     */
    get isDeterministic() {
      return true;
    }

    /**
     * A reference to the RollResolver app being used to externally resolve this term.
     * @type {RollResolver}
     */
    get resolver() {
      return this._root?._resolver;
    }

    /* -------------------------------------------- */
    /*  RollTerm Methods                            */
    /* -------------------------------------------- */

    /**
     * Evaluate the term, processing its inputs and finalizing its total.
     * @param {object} [options={}]                   Options which modify how the RollTerm is evaluated
     * @param {boolean} [options.minimize=false]      Minimize the result, obtaining the smallest possible value.
     * @param {boolean} [options.maximize=false]      Maximize the result, obtaining the largest possible value.
     * @param {boolean} [options.allowStrings=false]  If true, string terms will not throw an error when evaluated.
     * @returns {Promise<RollTerm>|RollTerm}          Returns a Promise if the term is non-deterministic.
     */
    evaluate(options={}) {
      if ( this._evaluated ) {
        throw new Error(`The ${this.constructor.name} has already been evaluated and is now immutable`);
      }
      this._evaluated = true;
      return this._evaluate(options);
    }

    /* -------------------------------------------- */

    /**
     * Evaluate the term.
     * @param {object} [options={}]           Options which modify how the RollTerm is evaluated, see RollTerm#evaluate
     * @returns {Promise<RollTerm>|RollTerm}  Returns a Promise if the term is non-deterministic.
     * @protected
     */
    _evaluate(options={}) {
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Determine if evaluating a given RollTerm with certain evaluation options can be done so deterministically.
     * @param {RollTerm} term               The term.
     * @param {object} [options]            Options for evaluating the term.
     * @param {boolean} [options.maximize]  Force the result to be maximized.
     * @param {boolean} [options.minimize]  Force the result to be minimized.
     */
    static isDeterministic(term, { maximize, minimize }={}) {
      return maximize || minimize || term.isDeterministic;
    }

    /* -------------------------------------------- */
    /*  Serialization and Loading                   */
    /* -------------------------------------------- */

    /**
     * Construct a RollTerm from a provided data object
     * @param {object} data         Provided data from an un-serialized term
     * @returns {RollTerm}          The constructed RollTerm
     */
    static fromData(data) {
      let cls = CONFIG.Dice.termTypes[data.class];
      if ( !cls ) {
        cls = Object.values(CONFIG.Dice.terms).find(c => c.name === data.class) || foundry.dice.terms.Die;
      }
      return cls._fromData(data);
    }

    /* -------------------------------------------- */

    /**
     * Construct a RollTerm from parser information.
     * @param {RollParseNode} node  The node.
     * @returns {RollTerm}
     */
    static fromParseNode(node) {
      return this.fromData(deepClone(node));
    }

    /* -------------------------------------------- */

    /**
     * Define term-specific logic for how a de-serialized data object is restored as a functional RollTerm
     * @param {object} data         The de-serialized term data
     * @returns {RollTerm}          The re-constructed RollTerm object
     * @protected
     */
    static _fromData(data) {
      if ( data.roll && !(data.roll instanceof Roll) ) data.roll = Roll.fromData(data.roll);
      const term = new this(data);
      term._evaluated = data.evaluated ?? true;
      return term;
    }

    /* -------------------------------------------- */

    /**
     * Reconstruct a RollTerm instance from a provided JSON string
     * @param {string} json   A serialized JSON representation of a DiceTerm
     * @return {RollTerm}     A reconstructed RollTerm from the provided JSON
     */
    static fromJSON(json) {
      let data;
      try {
        data = JSON.parse(json);
      } catch(err) {
        throw new Error("You must pass a valid JSON string");
      }
      return this.fromData(data);
    }

    /* -------------------------------------------- */

    /**
     * Serialize the RollTerm to a JSON string which allows it to be saved in the database or embedded in text.
     * This method should return an object suitable for passing to the JSON.stringify function.
     * @return {object}
     */
    toJSON() {
      const data = {
        class: this.constructor.name,
        options: this.options,
        evaluated: this._evaluated
      };
      for ( let attr of this.constructor.SERIALIZE_ATTRIBUTES ) {
        data[attr] = this[attr];
      }
      return data;
    }
  }

  /**
   * @typedef {import("../_types.mjs").DiceTermResult} DiceTermResult
   */

  /**
   * An abstract base class for any type of RollTerm which involves randomized input from dice, coins, or other devices.
   * @extends RollTerm
   */
  class DiceTerm extends RollTerm {
    /**
     * @param {object} termData                  Data used to create the Dice Term, including the following:
     * @param {number|Roll} [termData.number=1]  The number of dice of this term to roll, before modifiers are applied, or
     *                                           a Roll instance that will be evaluated to a number.
     * @param {number|Roll} termData.faces       The number of faces on each die of this type, or a Roll instance that
     *                                           will be evaluated to a number.
     * @param {string[]} [termData.modifiers]    An array of modifiers applied to the results
     * @param {object[]} [termData.results]      An optional array of pre-cast results for the term
     * @param {object} [termData.options]        Additional options that modify the term
     */
    constructor({number=1, faces=6, method, modifiers=[], results=[], options={}}) {
      super({options});

      this._number = number;
      this._faces = faces;
      this.method = method;
      this.modifiers = modifiers;
      this.results = results;

      // If results were explicitly passed, the term has already been evaluated
      if ( results.length ) this._evaluated = true;
    }

    /* -------------------------------------------- */

    /**
     * The resolution method used to resolve this DiceTerm.
     * @type {string}
     */
    get method() {
      return this.#method;
    }

    set method(method) {
      if ( this.#method || !(method in CONFIG.Dice.fulfillment.methods) ) return;
      this.#method = method;
    }

    #method;

    /**
     * An Array of dice term modifiers which are applied
     * @type {string[]}
     */
    modifiers;

    /**
     * The array of dice term results which have been rolled
     * @type {DiceTermResult[]}
     */
    results;

    /**
     * Define the denomination string used to register this DiceTerm type in CONFIG.Dice.terms
     * @type {string}
     */
    static DENOMINATION = "";

    /**
     * Define the named modifiers that can be applied for this particular DiceTerm type.
     * @type {Record<string, string|Function>}
     */
    static MODIFIERS = {};

    /**
     * A regular expression pattern which captures the full set of term modifiers
     * Anything until a space, group symbol, or arithmetic operator
     * @type {string}
     */
    static MODIFIERS_REGEXP_STRING = "([^ (){}[\\]+\\-*/]+)";

    /**
     * A regular expression used to separate individual modifiers
     * @type {RegExp}
     */
    static MODIFIER_REGEXP = /([A-z]+)([^A-z\s()+\-*\/]+)?/g

    /** @inheritdoc */
    static REGEXP = new RegExp(`^([0-9]+)?[dD]([A-z]|[0-9]+)${this.MODIFIERS_REGEXP_STRING}?${this.FLAVOR_REGEXP_STRING}?$`);

    /** @inheritdoc */
    static SERIALIZE_ATTRIBUTES = ["number", "faces", "modifiers", "results", "method"];

    /* -------------------------------------------- */
    /*  Dice Term Attributes                        */
    /* -------------------------------------------- */

    /**
     * The number of dice of this term to roll. Returns undefined if the number is a complex term that has not yet been
     * evaluated.
     * @type {number|void}
     */
    get number() {
      if ( typeof this._number === "number" ) return this._number;
      else if ( this._number?._evaluated ) return this._number.total;
    }

    /**
     * The number of dice of this term to roll, before modifiers are applied, or a Roll instance that will be evaluated to
     * a number.
     * @type {number|Roll}
     * @protected
     */
    _number;

    set number(value) {
      this._number = value;
    }

    /* -------------------------------------------- */

    /**
     * The number of faces on the die. Returns undefined if the faces are represented as a complex term that has not yet
     * been evaluated.
     * @type {number|void}
     */
    get faces() {
      if ( typeof this._faces === "number" ) return this._faces;
      else if ( this._faces?._evaluated ) return this._faces.total;
    }

    /**
     * The number of faces on the die, or a Roll instance that will be evaluated to a number.
     * @type {number|Roll}
     * @protected
     */
    _faces;

    set faces(value) {
      this._faces = value;
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    get expression() {
      const x = this.constructor.DENOMINATION === "d" ? this._faces : this.constructor.DENOMINATION;
      return `${this._number}d${x}${this.modifiers.join("")}`;
    }

    /* -------------------------------------------- */

    /**
     * The denomination of this DiceTerm instance.
     * @type {string}
     */
    get denomination() {
      return this.constructor.DENOMINATION;
    }

    /* -------------------------------------------- */

    /**
     * An array of additional DiceTerm instances involved in resolving this DiceTerm.
     * @type {DiceTerm[]}
     */
    get dice() {
      const dice = [];
      if ( this._number instanceof Roll ) dice.push(...this._number.dice);
      if ( this._faces instanceof Roll ) dice.push(...this._faces.dice);
      return dice;
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    get total() {
      if ( !this._evaluated ) return undefined;
      let total = this.results.reduce((t, r) => {
        if ( !r.active ) return t;
        if ( r.count !== undefined ) return t + r.count;
        else return t + r.result;
      }, 0);
      if ( this.number < 0 ) total *= -1;
      return total;
    }

    /* -------------------------------------------- */

    /**
     * Return an array of rolled values which are still active within this term
     * @type {number[]}
     */
    get values() {
      return this.results.reduce((arr, r) => {
        if ( !r.active ) return arr;
        arr.push(r.result);
        return arr;
      }, []);
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    get isDeterministic() {
      return false;
    }

    /* -------------------------------------------- */
    /*  Dice Term Methods                           */
    /* -------------------------------------------- */

    /**
     * Alter the DiceTerm by adding or multiplying the number of dice which are rolled
     * @param {number} multiply   A factor to multiply. Dice are multiplied before any additions.
     * @param {number} add        A number of dice to add. Dice are added after multiplication.
     * @returns {DiceTerm}        The altered term
     */
    alter(multiply, add) {
      if ( this._evaluated ) throw new Error(`You may not alter a DiceTerm after it has already been evaluated`);
      multiply = Number.isFinite(multiply) && (multiply >= 0) ? multiply : 1;
      add = Number.isInteger(add) ? add : 0;
      if ( multiply >= 0 ) {
        if ( this._number instanceof Roll ) this._number = Roll.create(`(${this._number} * ${multiply})`);
        else this._number = Math.round(this.number * multiply);
      }
      if ( add ) {
        if ( this._number instanceof Roll ) this._number = Roll.create(`(${this._number} + ${add})`);
        else this._number += add;
      }
      return this;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    _evaluate(options={}) {
      if ( RollTerm.isDeterministic(this, options) ) return this._evaluateSync(options);
      return this._evaluateAsync(options);
    }

    /* -------------------------------------------- */

    /**
     * Evaluate this dice term asynchronously.
     * @param {object} [options]  Options forwarded to inner Roll evaluation.
     * @returns {Promise<DiceTerm>}
     * @protected
     */
    async _evaluateAsync(options={}) {
      for ( const roll of [this._faces, this._number] ) {
        if ( !(roll instanceof Roll) ) continue;
        if ( this._root ) roll._root = this._root;
        await roll.evaluate(options);
      }
      if ( Math.abs(this.number) > 999 ) {
        throw new Error("You may not evaluate a DiceTerm with more than 999 requested results");
      }
      // If this term was an intermediate term, it has not yet been added to the resolver, so we add it here.
      if ( this.resolver && !this._id ) await this.resolver.addTerm(this);
      for ( let n = this.results.length; n < Math.abs(this.number); n++ ) await this.roll(options);
      await this._evaluateModifiers();
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Evaluate deterministic values of this term synchronously.
     * @param {object} [options]
     * @param {boolean} [options.maximize]  Force the result to be maximized.
     * @param {boolean} [options.minimize]  Force the result to be minimized.
     * @param {boolean} [options.strict]    Throw an error if attempting to evaluate a die term in a way that cannot be
     *                                      done synchronously.
     * @returns {DiceTerm}
     * @protected
     */
    _evaluateSync(options={}) {
      if ( this._faces instanceof Roll ) this._faces.evaluateSync(options);
      if ( this._number instanceof Roll ) this._number.evaluateSync(options);
      if ( Math.abs(this.number) > 999 ) {
        throw new Error("You may not evaluate a DiceTerm with more than 999 requested results");
      }
      for ( let n = this.results.length; n < Math.abs(this.number); n++ ) {
        const roll = { active: true };
        if ( options.minimize ) roll.result = Math.min(1, this.faces);
        else if ( options.maximize ) roll.result = this.faces;
        else if ( options.strict ) throw new Error("Cannot synchronously evaluate a non-deterministic term.");
        else continue;
        this.results.push(roll);
      }
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Roll the DiceTerm by mapping a random uniform draw against the faces of the dice term.
     * @param {object} [options={}]                 Options which modify how a random result is produced
     * @param {boolean} [options.minimize=false]    Minimize the result, obtaining the smallest possible value.
     * @param {boolean} [options.maximize=false]    Maximize the result, obtaining the largest possible value.
     * @returns {Promise<DiceTermResult>}           The produced result
     */
    async roll({minimize=false, maximize=false, ...options}={}) {
      const roll = {result: undefined, active: true};
      roll.result = await this._roll(options);
      if ( minimize ) roll.result = Math.min(1, this.faces);
      else if ( maximize ) roll.result = this.faces;
      else if ( roll.result === undefined ) roll.result = this.randomFace();
      this.results.push(roll);
      return roll;
    }

    /* -------------------------------------------- */

    /**
     * Generate a roll result value for this DiceTerm based on its fulfillment method.
     * @param {object} [options]        Options forwarded to the fulfillment method handler.
     * @returns {Promise<number|void>}  Returns a Promise that resolves to the fulfilled number, or undefined if it could
     *                                  not be fulfilled.
     * @protected
     */
    async _roll(options={}) {
      return this.#invokeFulfillmentHandler(options);
    }

    /* -------------------------------------------- */

    /**
     * Invoke the configured fulfillment handler for this term to produce a result value.
     * @param {object} [options]        Options forwarded to the fulfillment method handler.
     * @returns {Promise<number|void>}  Returns a Promise that resolves to the fulfilled number, or undefined if it could
     *                                  not be fulfilled.
     */
    async #invokeFulfillmentHandler(options={}) {
      const config = game.settings.get("core", "diceConfiguration");
      const method = config[this.denomination] || CONFIG.Dice.fulfillment.defaultMethod;
      if ( (method === "manual") && !game.user.hasPermission("MANUAL_ROLLS") ) return;
      const { handler, interactive } = CONFIG.Dice.fulfillment.methods[method] ?? {};
      if ( interactive && this.resolver ) return this.resolver.resolveResult(this, method, options);
      return handler?.(this, options);
    }

    /* -------------------------------------------- */

    /**
     * Maps a randomly-generated value in the interval [0, 1) to a face value on the die.
     * @param {number} randomUniform  A value to map. Must be in the interval [0, 1).
     * @returns {number}              The face value.
     */
    mapRandomFace(randomUniform) {
      return Math.ceil((1 - randomUniform) * this.faces);
    }

    /* -------------------------------------------- */

    /**
     * Generate a random face value for this die using the configured PRNG.
     * @returns {number}
     */
    randomFace() {
      return this.mapRandomFace(CONFIG.Dice.randomUniform());
    }

    /* -------------------------------------------- */

    /**
     * Return a string used as the label for each rolled result
     * @param {DiceTermResult} result     The rolled result
     * @returns {string}                   The result label
     */
    getResultLabel(result) {
      return String(result.result);
    }

    /* -------------------------------------------- */

    /**
     * Get the CSS classes that should be used to display each rolled result
     * @param {DiceTermResult} result     The rolled result
     * @returns {string[]}                 The desired classes
     */
    getResultCSS(result) {
      const hasSuccess = result.success !== undefined;
      const hasFailure = result.failure !== undefined;
      const isMax = result.result === this.faces;
      const isMin = result.result === 1;
      return [
        this.constructor.name.toLowerCase(),
        "d" + this.faces,
        result.success ? "success" : null,
        result.failure ? "failure" : null,
        result.rerolled ? "rerolled" : null,
        result.exploded ? "exploded" : null,
        result.discarded ? "discarded" : null,
        !(hasSuccess || hasFailure) && isMin ? "min" : null,
        !(hasSuccess || hasFailure) && isMax ? "max" : null
      ]
    }

    /* -------------------------------------------- */

    /**
     * Render the tooltip HTML for a Roll instance
     * @returns {object}      The data object used to render the default tooltip template for this DiceTerm
     */
    getTooltipData() {
      const { total, faces, flavor } = this;
      const method = CONFIG.Dice.fulfillment.methods[this.method];
      const icon = method?.interactive ? (method.icon ?? '<i class="fas fa-bluetooth"></i>') : null;
      return {
        total, faces, flavor, icon,
        method: method?.label,
        formula: this.expression,
        rolls: this.results.map(r => {
          return {
            result: this.getResultLabel(r),
            classes: this.getResultCSS(r).filterJoin(" ")
          };
        })
      };
    }

    /* -------------------------------------------- */
    /*  Modifier Methods                            */
    /* -------------------------------------------- */

    /**
     * Sequentially evaluate each dice roll modifier by passing the term to its evaluation function
     * Augment or modify the results array.
     * @internal
     */
    async _evaluateModifiers() {
      const cls = this.constructor;
      const requested = foundry.utils.deepClone(this.modifiers);
      this.modifiers = [];

      // Sort modifiers from longest to shortest to ensure that the matching algorithm greedily matches the longest
      // prefixes first.
      const allModifiers = Object.keys(cls.MODIFIERS).sort((a, b) => b.length - a.length);

      // Iterate over requested modifiers
      for ( const m of requested ) {
        let command = m.match(/[A-z]+/)[0].toLowerCase();

        // Matched command
        if ( command in cls.MODIFIERS ) {
          await this._evaluateModifier(command, m);
          continue;
        }

        // Unmatched compound command
        while ( command ) {
          let matched = false;
          for ( const modifier of allModifiers ) {
            if ( command.startsWith(modifier) ) {
              matched = true;
              await this._evaluateModifier(modifier, modifier);
              command = command.replace(modifier, "");
              break;
            }
          }
          if ( !matched ) command = "";
        }
      }
    }

    /* -------------------------------------------- */

    /**
     * Asynchronously evaluate a single modifier command, recording it in the array of evaluated modifiers
     * @param {string} command        The parsed modifier command
     * @param {string} modifier       The full modifier request
     * @internal
     */
    async _evaluateModifier(command, modifier) {
      let fn = this.constructor.MODIFIERS[command];
      if ( typeof fn === "string" ) fn = this[fn];
      if ( fn instanceof Function ) {
        const result = await fn.call(this, modifier);
        const earlyReturn = (result === false) || (result === this); // handling this is backwards compatibility
        if ( !earlyReturn ) this.modifiers.push(modifier.toLowerCase());
      }
    }

    /* -------------------------------------------- */

    /**
     * A helper comparison function.
     * Returns a boolean depending on whether the result compares favorably against the target.
     * @param {number} result         The result being compared
     * @param {string} comparison     The comparison operator in [=,&lt;,&lt;=,>,>=]
     * @param {number} target         The target value
     * @returns {boolean}             Is the comparison true?
     */
    static compareResult(result, comparison, target) {
      switch ( comparison ) {
        case "=":
          return result === target;
        case "<":
          return result < target;
        case "<=":
          return result <= target;
        case ">":
          return result > target;
        case ">=":
          return result >= target;
      }
    }

    /* -------------------------------------------- */

    /**
     * A helper method to modify the results array of a dice term by flagging certain results are kept or dropped.
     * @param {object[]} results      The results array
     * @param {number} number         The number to keep or drop
     * @param {boolean} [keep]        Keep results?
     * @param {boolean} [highest]     Keep the highest?
     * @returns {object[]}            The modified results array
     */
    static _keepOrDrop(results, number, {keep=true, highest=true}={}) {

      // Sort remaining active results in ascending (keep) or descending (drop) order
      const ascending = keep === highest;
      const values = results.reduce((arr, r) => {
        if ( r.active ) arr.push(r.result);
        return arr;
      }, []).sort((a, b) => ascending ? a - b : b - a);

      // Determine the cut point, beyond which to discard
      number = Math.clamp(keep ? values.length - number : number, 0, values.length);
      const cut = values[number];

      // Track progress
      let discarded = 0;
      const ties = [];
      let comp = ascending ? "<" : ">";

      // First mark results on the wrong side of the cut as discarded
      results.forEach(r => {
        if ( !r.active ) return;  // Skip results which have already been discarded
        let discard = this.compareResult(r.result, comp, cut);
        if ( discard ) {
          r.discarded = true;
          r.active = false;
          discarded++;
        }
        else if ( r.result === cut ) ties.push(r);
      });

      // Next discard ties until we have reached the target
      ties.forEach(r => {
        if ( discarded < number ) {
          r.discarded = true;
          r.active = false;
          discarded++;
        }
      });
      return results;
    }

    /* -------------------------------------------- */

    /**
     * A reusable helper function to handle the identification and deduction of failures
     */
    static _applyCount(results, comparison, target, {flagSuccess=false, flagFailure=false}={}) {
      for ( let r of results ) {
        let success = this.compareResult(r.result, comparison, target);
        if (flagSuccess) {
          r.success = success;
          if (success) delete r.failure;
        }
        else if (flagFailure ) {
          r.failure = success;
          if (success) delete r.success;
        }
        r.count = success ? 1 : 0;
      }
    }

    /* -------------------------------------------- */

    /**
     * A reusable helper function to handle the identification and deduction of failures
     */
    static _applyDeduct(results, comparison, target, {deductFailure=false, invertFailure=false}={}) {
      for ( let r of results ) {

        // Flag failures if a comparison was provided
        if (comparison) {
          const fail = this.compareResult(r.result, comparison, target);
          if ( fail ) {
            r.failure = true;
            delete r.success;
          }
        }

        // Otherwise treat successes as failures
        else {
          if ( r.success === false ) {
            r.failure = true;
            delete r.success;
          }
        }

        // Deduct failures
        if ( deductFailure ) {
          if ( r.failure ) r.count = -1;
        }
        else if ( invertFailure ) {
          if ( r.failure ) r.count = -1 * r.result;
        }
      }
    }

    /* -------------------------------------------- */
    /*  Factory Methods                             */
    /* -------------------------------------------- */

    /**
     * Determine whether a string expression matches this type of term
     * @param {string} expression               The expression to parse
     * @param {object} [options={}]             Additional options which customize the match
     * @param {boolean} [options.imputeNumber=true]  Allow the number of dice to be optional, i.e. "d6"
     * @returns {RegExpMatchArray|null}
     */
    static matchTerm(expression, {imputeNumber=true}={}) {
      const match = expression.match(this.REGEXP);
      if ( !match ) return null;
      if ( (match[1] === undefined) && !imputeNumber ) return null;
      return match;
    }

    /* -------------------------------------------- */

    /**
     * Construct a term of this type given a matched regular expression array.
     * @param {RegExpMatchArray} match          The matched regular expression array
     * @returns {DiceTerm}                      The constructed term
     */
    static fromMatch(match) {
      let [number, denomination, modifiers, flavor] = match.slice(1);

      // Get the denomination of DiceTerm
      denomination = denomination.toLowerCase();
      const cls = denomination in CONFIG.Dice.terms ? CONFIG.Dice.terms[denomination] : CONFIG.Dice.terms.d;
      if ( !foundry.utils.isSubclass(cls, foundry.dice.terms.DiceTerm) ) {
        throw new Error(`DiceTerm denomination ${denomination} not registered to CONFIG.Dice.terms as a valid DiceTerm class`);
      }

      // Get the term arguments
      number = Number.isNumeric(number) ? parseInt(number) : 1;
      const faces = Number.isNumeric(denomination) ? parseInt(denomination) : null;

      // Match modifiers
      modifiers = Array.from((modifiers || "").matchAll(this.MODIFIER_REGEXP)).map(m => m[0]);

      // Construct a term of the appropriate denomination
      return new cls({number, faces, modifiers, options: {flavor}});
    }

    /* -------------------------------------------- */

    /** @override */
    static fromParseNode(node) {
      let { number, faces } = node;
      let denomination = "d";
      if ( number === null ) number = 1;
      if ( number.class ) {
        number = Roll.defaultImplementation.fromTerms(Roll.defaultImplementation.instantiateAST(number));
      }
      if ( typeof faces === "string" ) denomination = faces.toLowerCase();
      else if ( faces.class ) {
        faces = Roll.defaultImplementation.fromTerms(Roll.defaultImplementation.instantiateAST(faces));
      }
      const modifiers = Array.from((node.modifiers || "").matchAll(this.MODIFIER_REGEXP)).map(([m]) => m);
      const cls = CONFIG.Dice.terms[denomination];
      const data = { ...node, number, modifiers, class: cls.name };
      if ( denomination === "d" ) data.faces = faces;
      return this.fromData(data);
    }

    /* -------------------------------------------- */
    /*  Serialization & Loading                     */
    /* -------------------------------------------- */

    /** @inheritDoc */
    toJSON() {
      const data = super.toJSON();
      if ( this._number instanceof Roll ) data._number = this._number.toJSON();
      if ( this._faces instanceof Roll ) data._faces = this._faces.toJSON();
      return data;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    static _fromData(data) {
      if ( data._number ) data.number = Roll.fromData(data._number);
      if ( data._faces ) data.faces = Roll.fromData(data._faces);
      return super._fromData(data);
    }
  }

  /**
   * A type of DiceTerm used to represent flipping a two-sided coin.
   * @implements {DiceTerm}
   */
  class Coin extends DiceTerm {
    constructor(termData) {
      termData.faces = 2;
      super(termData);
    }

    /** @inheritdoc */
    static DENOMINATION = "c";

    /** @inheritdoc */
    static MODIFIERS = {
      "c": "call"
    };

    /* -------------------------------------------- */

    /** @inheritdoc */
    async roll({minimize=false, maximize=false, ...options}={}) {
      const roll = {result: undefined, active: true};
      if ( minimize ) roll.result = 0;
      else if ( maximize ) roll.result = 1;
      else roll.result = await this._roll(options);
      if ( roll.result === undefined ) roll.result = this.randomFace();
      this.results.push(roll);
      return roll;
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    getResultLabel(result) {
      return {
        "0": "T",
        "1": "H"
      }[result.result];
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    getResultCSS(result) {
      return [
        this.constructor.name.toLowerCase(),
        result.result === 1 ? "heads" : "tails",
        result.success ? "success" : null,
        result.failure ? "failure" : null
      ]
    }

    /* -------------------------------------------- */

    /** @override */
    mapRandomFace(randomUniform) {
      return Math.round(randomUniform);
    }

    /* -------------------------------------------- */
    /*  Term Modifiers                              */
    /* -------------------------------------------- */

    /**
     * Call the result of the coin flip, marking any coins that matched the called target as a success
     * 3dcc1      Flip 3 coins and treat "heads" as successes
     * 2dcc0      Flip 2 coins and treat "tails" as successes
     * @param {string} modifier     The matched modifier query
     */
    call(modifier) {

      // Match the modifier
      const rgx = /c([01])/i;
      const match = modifier.match(rgx);
      if ( !match ) return false;
      let [target] = match.slice(1);
      target = parseInt(target);

      // Treat each result which matched the call as a success
      for ( let r of this.results ) {
        const match = r.result === target;
        r.count = match ? 1 : 0;
        r.success = match;
      }
    }
  }

  /**
   * A type of DiceTerm used to represent rolling a fair n-sided die.
   * @implements {DiceTerm}
   *
   * @example Roll four six-sided dice
   * ```js
   * let die = new Die({faces: 6, number: 4}).evaluate();
   * ```
   */
  class Die extends DiceTerm {
    /** @inheritdoc */
    static DENOMINATION = "d";

    /** @inheritdoc */
    static MODIFIERS = {
      r: "reroll",
      rr: "rerollRecursive",
      x: "explode",
      xo: "explodeOnce",
      k: "keep",
      kh: "keep",
      kl: "keep",
      d: "drop",
      dh: "drop",
      dl: "drop",
      min: "minimum",
      max: "maximum",
      even: "countEven",
      odd: "countOdd",
      cs: "countSuccess",
      cf: "countFailures",
      df: "deductFailures",
      sf: "subtractFailures",
      ms: "marginSuccess"
    };

    /* -------------------------------------------- */

    /** @inheritdoc */
    get total() {
      const total = super.total;
      if ( this.options.marginSuccess ) return total - parseInt(this.options.marginSuccess);
      else if ( this.options.marginFailure ) return parseInt(this.options.marginFailure) - total;
      else return total;
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    get denomination() {
      return `d${this.faces}`;
    }

    /* -------------------------------------------- */
    /*  Term Modifiers                              */
    /* -------------------------------------------- */

    /**
     * Re-roll the Die, rolling additional results for any values which fall within a target set.
     * If no target number is specified, re-roll the lowest possible result.
     *
     * 20d20r         reroll all 1s
     * 20d20r1        reroll all 1s
     * 20d20r=1       reroll all 1s
     * 20d20r1=1      reroll a single 1
     *
     * @param {string} modifier        The matched modifier query
     * @param {boolean} recursive      Reroll recursively, continuing to reroll until the condition is no longer met
     * @returns {Promise<false|void>}  False if the modifier was unmatched
     */
    async reroll(modifier, {recursive=false}={}) {

      // Match the re-roll modifier
      const rgx = /rr?([0-9]+)?([<>=]+)?([0-9]+)?/i;
      const match = modifier.match(rgx);
      if ( !match ) return false;
      let [max, comparison, target] = match.slice(1);

      // If no comparison or target are provided, treat the max as the target
      if ( max && !(target || comparison) ) {
        target = max;
        max = null;
      }

      // Determine target values
      max = Number.isNumeric(max) ? parseInt(max) : null;
      target = Number.isNumeric(target) ? parseInt(target) : 1;
      comparison = comparison || "=";

      // Recursively reroll until there are no remaining results to reroll
      let checked = 0;
      const initial = this.results.length;
      while ( checked < this.results.length ) {
        const r = this.results[checked];
        checked++;
        if ( !r.active ) continue;

        // Maybe we have run out of rerolls
        if ( (max !== null) && (max <= 0) ) break;

        // Determine whether to re-roll the result
        if ( DiceTerm.compareResult(r.result, comparison, target) ) {
          r.rerolled = true;
          r.active = false;
          await this.roll({ reroll: true });
          if ( max !== null ) max -= 1;
        }

        // Limit recursion
        if ( !recursive && (checked >= initial) ) checked = this.results.length;
        if ( checked > 1000 ) throw new Error("Maximum recursion depth for exploding dice roll exceeded");
      }
    }

    /**
     * @see {@link Die#reroll}
     */
    async rerollRecursive(modifier) {
      return this.reroll(modifier, {recursive: true});
    }

    /* -------------------------------------------- */

    /**
     * Explode the Die, rolling additional results for any values which match the target set.
     * If no target number is specified, explode the highest possible result.
     * Explosion can be a "small explode" using a lower-case x or a "big explode" using an upper-case "X"
     *
     * @param {string} modifier        The matched modifier query
     * @param {boolean} recursive      Explode recursively, such that new rolls can also explode?
     * @returns {Promise<false|void>}  False if the modifier was unmatched.
     */
    async explode(modifier, {recursive=true}={}) {

      // Match the "explode" or "explode once" modifier
      const rgx = /xo?([0-9]+)?([<>=]+)?([0-9]+)?/i;
      const match = modifier.match(rgx);
      if ( !match ) return false;
      let [max, comparison, target] = match.slice(1);

      // If no comparison or target are provided, treat the max as the target value
      if ( max && !(target || comparison) ) {
        target = max;
        max = null;
      }

      // Determine target values
      target = Number.isNumeric(target) ? parseInt(target) : this.faces;
      comparison = comparison || "=";

      // Determine the number of allowed explosions
      max = Number.isNumeric(max) ? parseInt(max) : null;

      // Recursively explode until there are no remaining results to explode
      let checked = 0;
      const initial = this.results.length;
      while ( checked < this.results.length ) {
        const r = this.results[checked];
        checked++;
        if ( !r.active ) continue;

        // Maybe we have run out of explosions
        if ( (max !== null) && (max <= 0) ) break;

        // Determine whether to explode the result and roll again!
        if ( DiceTerm.compareResult(r.result, comparison, target) ) {
          r.exploded = true;
          await this.roll({ explode: true });
          if ( max !== null ) max -= 1;
        }

        // Limit recursion
        if ( !recursive && (checked === initial) ) break;
        if ( checked > 1000 ) throw new Error("Maximum recursion depth for exploding dice roll exceeded");
      }
    }

    /**
     * @see {@link Die#explode}
     */
    async explodeOnce(modifier) {
      return this.explode(modifier, {recursive: false});
    }

    /* -------------------------------------------- */

    /**
     * Keep a certain number of highest or lowest dice rolls from the result set.
     *
     * 20d20k       Keep the 1 highest die
     * 20d20kh      Keep the 1 highest die
     * 20d20kh10    Keep the 10 highest die
     * 20d20kl      Keep the 1 lowest die
     * 20d20kl10    Keep the 10 lowest die
     *
     * @param {string} modifier     The matched modifier query
     */
    keep(modifier) {
      const rgx = /k([hl])?([0-9]+)?/i;
      const match = modifier.match(rgx);
      if ( !match ) return false;
      let [direction, number] = match.slice(1);
      direction = direction ? direction.toLowerCase() : "h";
      number = parseInt(number) || 1;
      DiceTerm._keepOrDrop(this.results, number, {keep: true, highest: direction === "h"});
    }

    /* -------------------------------------------- */

    /**
     * Drop a certain number of highest or lowest dice rolls from the result set.
     *
     * 20d20d       Drop the 1 lowest die
     * 20d20dh      Drop the 1 highest die
     * 20d20dl      Drop the 1 lowest die
     * 20d20dh10    Drop the 10 highest die
     * 20d20dl10    Drop the 10 lowest die
     *
     * @param {string} modifier     The matched modifier query
     */
    drop(modifier) {
      const rgx = /d([hl])?([0-9]+)?/i;
      const match = modifier.match(rgx);
      if ( !match ) return false;
      let [direction, number] = match.slice(1);
      direction = direction ? direction.toLowerCase() : "l";
      number = parseInt(number) || 1;
      DiceTerm._keepOrDrop(this.results, number, {keep: false, highest: direction !== "l"});
    }

    /* -------------------------------------------- */

    /**
     * Count the number of successful results which occurred in a given result set.
     * Successes are counted relative to some target, or relative to the maximum possible value if no target is given.
     * Applying a count-success modifier to the results re-casts all results to 1 (success) or 0 (failure)
     *
     * 20d20cs      Count the number of dice which rolled a 20
     * 20d20cs>10   Count the number of dice which rolled higher than 10
     * 20d20cs<10   Count the number of dice which rolled less than 10
     *
     * @param {string} modifier     The matched modifier query
     */
    countSuccess(modifier) {
      const rgx = /(?:cs)([<>=]+)?([0-9]+)?/i;
      const match = modifier.match(rgx);
      if ( !match ) return false;
      let [comparison, target] = match.slice(1);
      comparison = comparison || "=";
      target = parseInt(target) ?? this.faces;
      DiceTerm._applyCount(this.results, comparison, target, {flagSuccess: true});
    }

    /* -------------------------------------------- */

    /**
     * Count the number of failed results which occurred in a given result set.
     * Failures are counted relative to some target, or relative to the lowest possible value if no target is given.
     * Applying a count-failures modifier to the results re-casts all results to 1 (failure) or 0 (non-failure)
     *
     * 6d6cf      Count the number of dice which rolled a 1 as failures
     * 6d6cf<=3   Count the number of dice which rolled less than 3 as failures
     * 6d6cf>4    Count the number of dice which rolled greater than 4 as failures
     *
     * @param {string} modifier     The matched modifier query
     */
    countFailures(modifier) {
      const rgx = /(?:cf)([<>=]+)?([0-9]+)?/i;
      const match = modifier.match(rgx);
      if ( !match ) return false;
      let [comparison, target] = match.slice(1);
      comparison = comparison || "=";
      target = parseInt(target) ?? 1;
      DiceTerm._applyCount(this.results, comparison, target, {flagFailure: true});
    }

    /* -------------------------------------------- */

    /**
     * Count the number of even results which occurred in a given result set.
     * Even numbers are marked as a success and counted as 1
     * Odd numbers are marked as a non-success and counted as 0.
     *
     * 6d6even    Count the number of even numbers rolled
     *
     * @param {string} modifier     The matched modifier query
     */
    countEven(modifier) {
      for ( let r of this.results ) {
        r.success = ( (r.result % 2) === 0 );
        r.count = r.success ? 1 : 0;
      }
    }

    /* -------------------------------------------- */

    /**
     * Count the number of odd results which occurred in a given result set.
     * Odd numbers are marked as a success and counted as 1
     * Even numbers are marked as a non-success and counted as 0.
     *
     * 6d6odd    Count the number of odd numbers rolled
     *
     * @param {string} modifier     The matched modifier query
     */
    countOdd(modifier) {
      for ( let r of this.results ) {
        r.success = ( (r.result % 2) !== 0 );
        r.count = r.success ? 1 : 0;
      }
    }

    /* -------------------------------------------- */

    /**
     * Deduct the number of failures from the dice result, counting each failure as -1
     * Failures are identified relative to some target, or relative to the lowest possible value if no target is given.
     * Applying a deduct-failures modifier to the results counts all failed results as -1.
     *
     * 6d6df      Subtract the number of dice which rolled a 1 from the non-failed total.
     * 6d6cs>3df  Subtract the number of dice which rolled a 3 or less from the non-failed count.
     * 6d6cf<3df  Subtract the number of dice which rolled less than 3 from the non-failed count.
     *
     * @param {string} modifier     The matched modifier query
     */
    deductFailures(modifier) {
      const rgx = /(?:df)([<>=]+)?([0-9]+)?/i;
      const match = modifier.match(rgx);
      if ( !match ) return false;
      let [comparison, target] = match.slice(1);
      if ( comparison || target ) {
        comparison = comparison || "=";
        target = parseInt(target) ?? 1;
      }
      DiceTerm._applyDeduct(this.results, comparison, target, {deductFailure: true});
    }

    /* -------------------------------------------- */

    /**
     * Subtract the value of failed dice from the non-failed total, where each failure counts as its negative value.
     * Failures are identified relative to some target, or relative to the lowest possible value if no target is given.
     * Applying a deduct-failures modifier to the results counts all failed results as -1.
     *
     * 6d6df<3    Subtract the value of results which rolled less than 3 from the non-failed total.
     *
     * @param {string} modifier     The matched modifier query
     */
    subtractFailures(modifier) {
      const rgx = /(?:sf)([<>=]+)?([0-9]+)?/i;
      const match = modifier.match(rgx);
      if ( !match ) return false;
      let [comparison, target] = match.slice(1);
      if ( comparison || target ) {
        comparison = comparison || "=";
        target = parseInt(target) ?? 1;
      }
      DiceTerm._applyDeduct(this.results, comparison, target, {invertFailure: true});
    }

    /* -------------------------------------------- */

    /**
     * Subtract the total value of the DiceTerm from a target value, treating the difference as the final total.
     * Example: 6d6ms>12    Roll 6d6 and subtract 12 from the resulting total.
     * @param {string} modifier     The matched modifier query
     */
    marginSuccess(modifier) {
      const rgx = /(?:ms)([<>=]+)?([0-9]+)?/i;
      const match = modifier.match(rgx);
      if ( !match ) return false;
      let [comparison, target] = match.slice(1);
      target = parseInt(target);
      if ( [">", ">=", "=", undefined].includes(comparison) ) this.options.marginSuccess = target;
      else if ( ["<", "<="].includes(comparison) ) this.options.marginFailure = target;
    }

    /* -------------------------------------------- */

    /**
     * Constrain each rolled result to be at least some minimum value.
     * Example: 6d6min2    Roll 6d6, each result must be at least 2
     * @param {string} modifier     The matched modifier query
     */
    minimum(modifier) {
      const rgx = /(?:min)([0-9]+)/i;
      const match = modifier.match(rgx);
      if ( !match ) return false;
      let [target] = match.slice(1);
      target = parseInt(target);
      for ( let r of this.results ) {
        if ( r.result < target ) {
          r.count = target;
          r.rerolled = true;
        }
      }
    }

    /* -------------------------------------------- */

    /**
     * Constrain each rolled result to be at most some maximum value.
     * Example: 6d6max5    Roll 6d6, each result must be at most 5
     * @param {string} modifier     The matched modifier query
     */
    maximum(modifier) {
      const rgx = /(?:max)([0-9]+)/i;
      const match = modifier.match(rgx);
      if ( !match ) return false;
      let [target] = match.slice(1);
      target = parseInt(target);
      for ( let r of this.results ) {
        if ( r.result > target ) {
          r.count = target;
          r.rerolled = true;
        }
      }
    }
  }

  /**
   * A type of DiceTerm used to represent a three-sided Fate/Fudge die.
   * Mathematically behaves like 1d3-2
   * @extends {DiceTerm}
   */
  class FateDie extends DiceTerm {
    constructor(termData) {
      termData.faces = 3;
      super(termData);
    }

    /** @inheritdoc */
    static DENOMINATION = "f";

    /** @inheritdoc */
    static MODIFIERS = {
      "r": Die.prototype.reroll,
      "rr": Die.prototype.rerollRecursive,
      "k": Die.prototype.keep,
      "kh": Die.prototype.keep,
      "kl": Die.prototype.keep,
      "d": Die.prototype.drop,
      "dh": Die.prototype.drop,
      "dl": Die.prototype.drop
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    async roll({minimize=false, maximize=false, ...options}={}) {
      const roll = {result: undefined, active: true};
      if ( minimize ) roll.result = -1;
      else if ( maximize ) roll.result = 1;
      else roll.result = await this._roll(options);
      if ( roll.result === undefined ) roll.result = this.randomFace();
      if ( roll.result === -1 ) roll.failure = true;
      if ( roll.result === 1 ) roll.success = true;
      this.results.push(roll);
      return roll;
    }

    /* -------------------------------------------- */

    /** @override */
    mapRandomFace(randomUniform) {
      return Math.ceil((randomUniform * this.faces) - 2);
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    getResultLabel(result) {
      return {
        "-1": "-",
        "0": "&nbsp;",
        "1": "+"
      }[result.result];
    }
  }

  /**
   * A type of RollTerm used to apply a function.
   * @extends {RollTerm}
   */
  class FunctionTerm extends RollTerm {
    constructor({fn, terms=[], rolls=[], result, options}={}) {
      super({options});
      this.fn = fn;
      this.terms = terms;
      this.rolls = (rolls.length === terms.length) ? rolls : this.terms.map(t => Roll.create(t));
      this.result = result;
      if ( result !== undefined ) this._evaluated = true;
    }

    /**
     * The name of the configured function, or one in the Math environment, which should be applied to the term
     * @type {string}
     */
    fn;

    /**
     * An array of string argument terms for the function
     * @type {string[]}
     */
    terms;

    /**
     * The cached Roll instances for each function argument
     * @type {Roll[]}
     */
    rolls = [];

    /**
     * The cached result of evaluating the method arguments
     * @type {string|number}
     */
    result;

    /** @inheritdoc */
    isIntermediate = true;

    /** @inheritdoc */
    static SERIALIZE_ATTRIBUTES = ["fn", "terms", "rolls", "result"];

    /* -------------------------------------------- */
    /*  Function Term Attributes                    */
    /* -------------------------------------------- */

    /**
     * An array of evaluated DiceTerm instances that should be bubbled up to the parent Roll
     * @type {DiceTerm[]}
     */
    get dice() {
      return this.rolls.flatMap(r => r.dice);
    }

    /** @inheritdoc */
    get total() {
      return this.result;
    }

    /** @inheritdoc */
    get expression() {
      return `${this.fn}(${this.terms.join(",")})`;
    }

    /**
     * The function this term represents.
     * @returns {RollFunction}
     */
    get function() {
      return CONFIG.Dice.functions[this.fn] ?? Math[this.fn];
    }

    /** @inheritdoc */
    get isDeterministic() {
      if ( this.function?.constructor.name === "AsyncFunction" ) return false;
      return this.terms.every(t => Roll.create(t).isDeterministic);
    }

    /* -------------------------------------------- */
    /*  Math Term Methods                           */
    /* -------------------------------------------- */

    /** @inheritdoc */
    _evaluate(options={}) {
      if ( RollTerm.isDeterministic(this, options) ) return this._evaluateSync(options);
      return this._evaluateAsync(options);
    }

    /* -------------------------------------------- */

    /**
     * Evaluate this function when it contains any non-deterministic sub-terms.
     * @param {object} [options]
     * @returns {Promise<RollTerm>}
     * @protected
     */
    async _evaluateAsync(options={}) {
      const args = await Promise.all(this.rolls.map(async roll => {
        if ( this._root ) roll._root = this._root;
        await roll.evaluate({ ...options, allowStrings: true });
        roll.propagateFlavor(this.flavor);
        return this.#parseArgument(roll);
      }));
      this.result = await this.function(...args);
      if ( !options.allowStrings ) this.result = Number(this.result);
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Evaluate this function when it contains only deterministic sub-terms.
     * @param {object} [options]
     * @returns {RollTerm}
     * @protected
     */
    _evaluateSync(options={}) {
      const args = [];
      for ( const roll of this.rolls ) {
        roll.evaluateSync({ ...options, allowStrings: true });
        roll.propagateFlavor(this.flavor);
        args.push(this.#parseArgument(roll));
      }
      this.result = this.function(...args);
      if ( !options.allowStrings ) this.result = Number(this.result);
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Parse a function argument from its evaluated Roll instance.
     * @param {Roll} roll  The evaluated Roll instance that wraps the argument.
     * @returns {string|number}
     */
    #parseArgument(roll) {
      const { product } = roll;
      if ( typeof product !== "string" ) return product;
      const [, value] = product.match(/^\$([^$]+)\$$/) || [];
      return value ? JSON.parse(value) : product;
    }

    /* -------------------------------------------- */
    /*  Saving and Loading                          */
    /* -------------------------------------------- */

    /** @inheritDoc */
    static _fromData(data) {
      data.rolls = (data.rolls || []).map(r => r instanceof Roll ? r : Roll.fromData(r));
      return super._fromData(data);
    }

    /* -------------------------------------------- */

    /** @inheritDoc */
    toJSON() {
      const data = super.toJSON();
      data.rolls = data.rolls.map(r => r.toJSON());
      return data;
    }

    /* -------------------------------------------- */

    /** @override */
    static fromParseNode(node) {
      const rolls = node.terms.map(t => {
        return Roll.defaultImplementation.fromTerms(Roll.defaultImplementation.instantiateAST(t));
      });
      const modifiers = Array.from((node.modifiers || "").matchAll(DiceTerm.MODIFIER_REGEXP)).map(([m]) => m);
      return this.fromData({ ...node, rolls, modifiers, terms: rolls.map(r => r.formula) });
    }
  }

  /**
   * A type of RollTerm used to represent static numbers.
   * @extends {RollTerm}
   */
  class NumericTerm extends RollTerm {
    constructor({number, options}={}) {
      super({options});
      this.number = Number(number);
    }

    /**
     * The term's numeric value.
     * @type {number}
     */
    number;

    /** @inheritdoc */
    static REGEXP = new RegExp(`^([0-9]+(?:\\.[0-9]+)?)${RollTerm.FLAVOR_REGEXP_STRING}?$`);

    /** @inheritdoc */
    static SERIALIZE_ATTRIBUTES = ["number"];

    /** @inheritdoc */
    get expression() {
      return String(this.number);
    }

    /** @inheritdoc */
    get total() {
      return this.number;
    }

    /* -------------------------------------------- */
    /*  Factory Methods                             */
    /* -------------------------------------------- */

    /**
     * Determine whether a string expression matches a NumericTerm
     * @param {string} expression               The expression to parse
     * @returns {RegExpMatchArray|null}
     */
    static matchTerm(expression) {
      return expression.match(this.REGEXP) || null;
    }

    /* -------------------------------------------- */

    /**
     * Construct a term of this type given a matched regular expression array.
     * @param {RegExpMatchArray} match          The matched regular expression array
     * @returns {NumericTerm}                   The constructed term
     */
    static fromMatch(match) {
      const [number, flavor] = match.slice(1);
      return new this({number, options: {flavor}});
    }
  }

  /**
   * A type of RollTerm used to denote and perform an arithmetic operation.
   * @extends {RollTerm}
   */
  class OperatorTerm extends RollTerm {
    constructor({operator, options}={}) {
      super({options});
      this.operator = operator;
    }

    /**
     * The term's operator value.
     * @type {string}
     */
    operator;

    /**
     * An object of operators with their precedence values.
     * @type {Readonly<Record<string, number>>}
     */
    static PRECEDENCE = Object.freeze({
      "+": 10,
      "-": 10,
      "*": 20,
      "/": 20,
      "%": 20
    });

    /**
     * An array of operators which represent arithmetic operations
     * @type {string[]}
     */
    static OPERATORS = Object.keys(this.PRECEDENCE);

    /** @inheritdoc */
    static REGEXP = new RegExp(this.OPERATORS.map(o => "\\"+o).join("|"), "g");

    /** @inheritdoc */
    static SERIALIZE_ATTRIBUTES = ["operator"];

    /** @inheritdoc */
    get flavor() {
      return ""; // Operator terms cannot have flavor text
    }

    /** @inheritdoc */
    get expression() {
      return ` ${this.operator} `;
    }

    /** @inheritdoc */
    get total() {
      return ` ${this.operator} `;
    }
  }

  /**
   * A type of RollTerm used to enclose a parenthetical expression to be recursively evaluated.
   * @extends {RollTerm}
   */
  class ParentheticalTerm extends RollTerm {
    constructor({term, roll, options}) {
      super({options});
      this.term = term;
      this.roll = roll;

      // If a roll was explicitly passed in, the parenthetical may have already been evaluated
      if ( this.roll ) {
        this.term = roll.formula;
        this._evaluated = this.roll._evaluated;
      }
    }

    /**
     * The original provided string term used to construct the parenthetical
     * @type {string}
     */
    term;

    /**
     * An already-evaluated Roll instance used instead of the string term.
     * @type {Roll}
     */
    roll;

    /** @inheritdoc */
    isIntermediate = true;

    /**
     * The regular expression pattern used to identify the opening of a parenthetical expression.
     * This could also identify the opening of a math function.
     * @type {RegExp}
     */
    static OPEN_REGEXP = /([A-z][A-z0-9]+)?\(/g;

    /**
     * A regular expression pattern used to identify the closing of a parenthetical expression.
     * @type {RegExp}
     */
    static CLOSE_REGEXP = new RegExp("\\)(?:\\$\\$F[0-9]+\\$\\$)?", "g");

    /** @inheritdoc */
    static SERIALIZE_ATTRIBUTES = ["term", "roll"];

    /* -------------------------------------------- */
    /*  Parenthetical Term Attributes               */
    /* -------------------------------------------- */

    /**
     * An array of evaluated DiceTerm instances that should be bubbled up to the parent Roll
     * @type {DiceTerm[]}
     */
    get dice() {
      return this.roll?.dice;
    }

    /** @inheritdoc */
    get total() {
      return this.roll.total;
    }

    /** @inheritdoc */
    get expression() {
      return `(${this.term})`;
    }

    /** @inheritdoc */
    get isDeterministic() {
      return Roll.create(this.term).isDeterministic;
    }

    /* -------------------------------------------- */
    /*  Parenthetical Term Methods                  */
    /* -------------------------------------------- */

    /** @inheritdoc */
    _evaluate(options={}) {
      const roll = this.roll || Roll.create(this.term);
      if ( this._root ) roll._root = this._root;
      if ( options.maximize || options.minimize || roll.isDeterministic ) return this._evaluateSync(roll, options);
      return this._evaluateAsync(roll, options);
    }

    /* -------------------------------------------- */

    /**
     * Evaluate this parenthetical when it contains any non-deterministic sub-terms.
     * @param {Roll} roll  The inner Roll instance to evaluate.
     * @param {object} [options]
     * @returns {Promise<RollTerm>}
     * @protected
     */
    async _evaluateAsync(roll, options={}) {
      this.roll = await roll.evaluate(options);
      this.roll.propagateFlavor(this.flavor);
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Evaluate this parenthetical when it contains only deterministic sub-terms.
     * @param {Roll} roll  The inner Roll instance to evaluate.
     * @param {object} [options]
     * @returns {RollTerm}
     * @protected
     */
    _evaluateSync(roll, options={}) {
      this.roll = roll.evaluateSync(options);
      this.roll.propagateFlavor(this.flavor);
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Construct a ParentheticalTerm from an Array of component terms which should be wrapped inside the parentheses.
     * @param {RollTerm[]} terms      The array of terms to use as internal parts of the parenthetical
     * @param {object} [options={}]   Additional options passed to the ParentheticalTerm constructor
     * @returns {ParentheticalTerm}   The constructed ParentheticalTerm instance
     *
     * @example Create a Parenthetical Term from an array of component RollTerm instances
     * ```js
     * const d6 = new Die({number: 4, faces: 6});
     * const plus = new OperatorTerm({operator: "+"});
     * const bonus = new NumericTerm({number: 4});
     * t = ParentheticalTerm.fromTerms([d6, plus, bonus]);
     * t.formula; // (4d6 + 4)
     * ```
     */
    static fromTerms(terms, options) {
      const roll = Roll.defaultImplementation.fromTerms(terms);
      return new this({roll, options});
    }

    /* -------------------------------------------- */

    /** @override */
    static fromParseNode(node) {
      const roll = Roll.defaultImplementation.fromTerms(Roll.defaultImplementation.instantiateAST(node.term));
      return this.fromData({ ...node, roll, term: roll.formula });
    }
  }

  /**
   * @typedef {import("../_types.mjs").DiceTermResult} DiceTermResult
   */

  /**
   * A type of RollTerm which encloses a pool of multiple inner Rolls which are evaluated jointly.
   *
   * A dice pool represents a set of Roll expressions which are collectively modified to compute an effective total
   * across all Rolls in the pool. The final total for the pool is defined as the sum over kept rolls, relative to any
   * success count or margin.
   *
   * @example Keep the highest of the 3 roll expressions
   * ```js
   * let pool = new PoolTerm({
   *   terms: ["4d6", "3d8 - 1", "2d10 + 3"],
   *   modifiers: ["kh"]
   * });
   * pool.evaluate();
   * ```
   */
  class PoolTerm extends RollTerm {
    constructor({terms=[], modifiers=[], rolls=[], results=[], options={}}={}) {
      super({options});
      this.terms = terms;
      this.modifiers = modifiers;
      this.rolls = (rolls.length === terms.length) ? rolls : this.terms.map(t => Roll.create(t));
      this.results = results;

      // If rolls and results were explicitly passed, the term has already been evaluated
      if ( rolls.length && results.length ) this._evaluated = true;
    }

    /* -------------------------------------------- */

    /**
     * The original provided terms to the Dice Pool
     * @type {string[]}
     */
    terms;

    /**
     * The string modifiers applied to resolve the pool
     * @type {string[]}
     */
    modifiers;

    /**
     * Each component term of the dice pool as a Roll instance.
     * @type {Roll[]}
     */
    rolls;

    /**
     * The array of dice pool results which have been rolled
     * @type {DiceTermResult[]}
     */
    results;

    /**
     * Define the modifiers that can be used for this particular DiceTerm type.
     * @type {Record<string, function|string>}
     */
    static MODIFIERS = {
      "k": "keep",
      "kh": "keep",
      "kl": "keep",
      "d": "drop",
      "dh": "drop",
      "dl": "drop",
      "cs": "countSuccess",
      "cf": "countFailures"
    };

    /**
     * The regular expression pattern used to identify the opening of a dice pool expression.
     * @type {RegExp}
     */
    static OPEN_REGEXP = /{/g;

    /**
     * A regular expression pattern used to identify the closing of a dice pool expression.
     * @type {RegExp}
     */
    static CLOSE_REGEXP = new RegExp(`}${DiceTerm.MODIFIERS_REGEXP_STRING}?(?:\\$\\$F[0-9]+\\$\\$)?`, "g");

    /**
     * A regular expression pattern used to match the entirety of a DicePool expression.
     * @type {RegExp}
     */
    static REGEXP = new RegExp(`{([^}]+)}${DiceTerm.MODIFIERS_REGEXP_STRING}?(?:\\$\\$F[0-9]+\\$\\$)?`);

    /** @inheritdoc */
    static SERIALIZE_ATTRIBUTES = ["terms", "modifiers", "rolls", "results"];

    /* -------------------------------------------- */
    /*  Dice Pool Attributes                        */
    /* -------------------------------------------- */

    /**
     * Return an Array of each individual DiceTerm instances contained within the PoolTerm.
     * @type {DiceTerm[]}
     */
    get dice() {
      return this.rolls.flatMap(r => r.dice);
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    get expression() {
      return `{${this.terms.join(",")}}${this.modifiers.join("")}`;
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    get total() {
      if ( !this._evaluated ) return undefined;
      return this.results.reduce((t, r) => {
        if ( !r.active ) return t;
        if ( r.count !== undefined ) return t + r.count;
        else return t + r.result;
      }, 0);
    }

    /* -------------------------------------------- */

    /**
     * Return an array of rolled values which are still active within the PoolTerm
     * @type {number[]}
     */
    get values() {
      return this.results.reduce((arr, r) => {
        if ( !r.active ) return arr;
        arr.push(r.result);
        return arr;
      }, []);
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    get isDeterministic() {
      return this.terms.every(t => Roll.create(t).isDeterministic);
    }

    /* -------------------------------------------- */

    /**
     * Alter the DiceTerm by adding or multiplying the number of dice which are rolled
     * @param {any[]} args        Arguments passed to each contained Roll#alter method.
     * @returns {PoolTerm}        The altered pool
     */
    alter(...args) {
      this.rolls.forEach(r => r.alter(...args));
      return this;
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    _evaluate(options={}) {
      if ( RollTerm.isDeterministic(this, options) ) return this._evaluateSync(options);
      return this._evaluateAsync(options);
    }

    /* -------------------------------------------- */

    /**
     * Evaluate this pool term when it contains any non-deterministic sub-terms.
     * @param {object} [options]
     * @returns {Promise<PoolTerm>}
     * @protected
     */
    async _evaluateAsync(options={}) {
      for ( const roll of this.rolls ) {
        if ( this._root ) roll._root = this._root;
        await roll.evaluate(options);
        roll.propagateFlavor(this.flavor);
        this.results.push({ result: roll.total, active: true });
      }
      await this._evaluateModifiers();
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Evaluate this pool term when it contains only deterministic sub-terms.
     * @param {object} [options]
     * @returns {PoolTerm}
     * @protected
     */
    _evaluateSync(options={}) {
      for ( const roll of this.rolls ) {
        if ( this._root ) roll._root = this._root;
        roll.evaluateSync(options);
        roll.propagateFlavor(this.flavor);
        this.results.push({ result: roll.total, active: true });
      }
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Use the same logic as for the DiceTerm to avoid duplication
     * @see DiceTerm#_evaluateModifiers
     */
    _evaluateModifiers() {
      return DiceTerm.prototype._evaluateModifiers.call(this);
    }

    /* -------------------------------------------- */

    /**
     * Use the same logic as for the DiceTerm to avoid duplication
     * @see DiceTerm#_evaluateModifier
     */
    _evaluateModifier(command, modifier) {
      return DiceTerm.prototype._evaluateModifier.call(this, command, modifier);
    }

    /* -------------------------------------------- */
    /*  Saving and Loading                          */
    /* -------------------------------------------- */

    /** @inheritdoc */
    static _fromData(data) {
      data.rolls = (data.rolls || []).map(r => r instanceof Roll ? r : Roll.fromData(r));
      return super._fromData(data);
    }

    /* -------------------------------------------- */

    /** @inheritdoc */
    toJSON() {
      const data = super.toJSON();
      data.rolls = data.rolls.map(r => r.toJSON());
      return data;
    }

    /* -------------------------------------------- */

    /**
     * Given a string formula, create and return an evaluated PoolTerm object
     * @param {string} formula    The string formula to parse
     * @param {object} [options]  Additional options applied to the PoolTerm
     * @returns {PoolTerm|null}   The evaluated PoolTerm object or null if the formula is invalid
     */
    static fromExpression(formula, options={}) {
      const rgx = formula.trim().match(this.REGEXP);
      if ( !rgx ) return null;
      let [terms, modifiers] = rgx.slice(1);
      terms = terms.split(",");
      modifiers = Array.from((modifiers || "").matchAll(DiceTerm.MODIFIER_REGEXP)).map(m => m[0]);
      return new this({terms, modifiers, options});
    }

    /* -------------------------------------------- */

    /**
     * Create a PoolTerm by providing an array of existing Roll objects
     * @param {Roll[]} rolls      An array of Roll objects from which to create the pool
     * @returns {RollTerm}        The constructed PoolTerm comprised of the provided rolls
     */
    static fromRolls(rolls=[]) {
      const allEvaluated = rolls.every(t => t._evaluated);
      const noneEvaluated = !rolls.some(t => t._evaluated);
      if ( !(allEvaluated || noneEvaluated) ) {
        throw new Error("You can only call PoolTerm.fromRolls with an array of Roll instances which are either all evaluated, or none evaluated");
      }
      const pool = new this({
        terms: rolls.map(r => r.formula),
        modifiers: [],
        rolls: rolls,
        results: allEvaluated ? rolls.map(r => ({result: r.total, active: true})) : []
      });
      pool._evaluated = allEvaluated;
      return pool;
    }

    /* -------------------------------------------- */

    /** @override */
    static fromParseNode(node) {
      const rolls = node.terms.map(t => {
        return Roll.defaultImplementation.fromTerms(Roll.defaultImplementation.instantiateAST(t)).toJSON();
      });
      const modifiers = Array.from((node.modifiers || "").matchAll(DiceTerm.MODIFIER_REGEXP)).map(([m]) => m);
      return this.fromData({ ...node, rolls, modifiers, terms: rolls.map(r => r.formula) });
    }

    /* -------------------------------------------- */
    /*  Modifiers                                   */
    /* -------------------------------------------- */

    /**
     * Keep a certain number of highest or lowest dice rolls from the result set.
     *
     * {1d6,1d8,1d10,1d12}kh2       Keep the 2 best rolls from the pool
     * {1d12,6}kl                   Keep the lowest result in the pool
     *
     * @param {string} modifier     The matched modifier query
     */
    keep(modifier) {
      return Die.prototype.keep.call(this, modifier);
    }

    /* -------------------------------------------- */

    /**
     * Keep a certain number of highest or lowest dice rolls from the result set.
     *
     * {1d6,1d8,1d10,1d12}dl3       Drop the 3 worst results in the pool
     * {1d12,6}dh                   Drop the highest result in the pool
     *
     * @param {string} modifier     The matched modifier query
     */
    drop(modifier) {
      return Die.prototype.drop.call(this, modifier);
    }

    /* -------------------------------------------- */

    /**
     * Count the number of successful results which occurred in the pool.
     * Successes are counted relative to some target, or relative to the maximum possible value if no target is given.
     * Applying a count-success modifier to the results re-casts all results to 1 (success) or 0 (failure)
     *
     * 20d20cs      Count the number of dice which rolled a 20
     * 20d20cs>10   Count the number of dice which rolled higher than 10
     * 20d20cs<10   Count the number of dice which rolled less than 10
     *
     * @param {string} modifier     The matched modifier query
     */
    countSuccess(modifier) {
      return Die.prototype.countSuccess.call(this, modifier);
    }

    /* -------------------------------------------- */

    /**
     * Count the number of failed results which occurred in a given result set.
     * Failures are counted relative to some target, or relative to the lowest possible value if no target is given.
     * Applying a count-failures modifier to the results re-casts all results to 1 (failure) or 0 (non-failure)
     *
     * 6d6cf      Count the number of dice which rolled a 1 as failures
     * 6d6cf<=3   Count the number of dice which rolled less than 3 as failures
     * 6d6cf>4    Count the number of dice which rolled greater than 4 as failures
     *
     * @param {string} modifier     The matched modifier query
     */
    countFailures(modifier) {
      return Die.prototype.countFailures.call(this, modifier);
    }
  }

  /**
   * A type of RollTerm used to represent strings which have not yet been matched.
   * @extends {RollTerm}
   */
  class StringTerm extends RollTerm {
    constructor({term, options}={}) {
      super({options});
      this.term = term;
    }

    /**
     * The term's string value.
     * @type {string}
     */
    term;

    /** @inheritdoc */
    static SERIALIZE_ATTRIBUTES = ["term"];

    /** @inheritdoc */
    get expression() {
      return this.term;
    }

    /** @inheritdoc */
    get total() {
      return this.term;
    }

    /** @inheritdoc */
    get isDeterministic() {
      const classified = Roll.defaultImplementation._classifyStringTerm(this.term, {intermediate: false});
      if ( classified instanceof StringTerm ) return true;
      return classified.isDeterministic;
    }

    /** @inheritdoc */
    evaluate({ allowStrings=false }={}) {
      if ( !allowStrings ) throw new Error(`Unresolved StringTerm ${this.term} requested for evaluation`);
      return this;
    }
  }

  var _module = /*#__PURE__*/Object.freeze({
    __proto__: null,
    Coin: Coin,
    DiceTerm: DiceTerm,
    Die: Die,
    FateDie: FateDie,
    FunctionTerm: FunctionTerm,
    NumericTerm: NumericTerm,
    OperatorTerm: OperatorTerm,
    ParentheticalTerm: ParentheticalTerm,
    PoolTerm: PoolTerm,
    RollTerm: RollTerm,
    StringTerm: StringTerm
  });

  /**
   * @typedef {import("../_types.mjs").RollParseNode} RollParseNode
   */

  /**
   * An interface and API for constructing and evaluating dice rolls.
   * The basic structure for a dice roll is a string formula and an object of data against which to parse it.
   *
   * @example Attack with advantage
   * ```js
   * // Construct the Roll instance
   * let r = new Roll("2d20kh + @prof + @strMod", {prof: 2, strMod: 4});
   *
   * // The parsed terms of the roll formula
   * console.log(r.terms);    // [Die, OperatorTerm, NumericTerm, OperatorTerm, NumericTerm]
   *
   * // Execute the roll
   * await r.evaluate();
   *
   * // The resulting equation after it was rolled
   * console.log(r.result);   // 16 + 2 + 4
   *
   * // The total resulting from the roll
   * console.log(r.total);    // 22
   * ```
   */
  let Roll$1 = class Roll {
    /**
     * @param {string} formula    The string formula to parse
     * @param {object} data       The data object against which to parse attributes within the formula
     * @param {object} [options]  Options which modify or describe the Roll
     */
    constructor(formula, data={}, options={}) {
      this.data = this._prepareData(data);
      this.options = options;
      this.terms = this.constructor.parse(formula, this.data);
      this._formula = this.resetFormula();
    }

    /**
     * The original provided data object which substitutes into attributes of the roll formula.
     * @type {object}
     */
    data;

    /**
     * Options which modify or describe the Roll
     * @type {object}
     */
    options;

    /**
     * The identified terms of the Roll
     * @type {RollTerm[]}
     */
    terms;

    /**
     * An array of inner DiceTerms that were evaluated as part of the Roll evaluation
     * @type {DiceTerm[]}
     * @internal
     */
    _dice = [];

    /**
     * Store the original cleaned formula for the Roll, prior to any internal evaluation or simplification
     * @type {string}
     * @internal
     */
    _formula;

    /**
     * Track whether this Roll instance has been evaluated or not. Once evaluated the Roll is immutable.
     * @type {boolean}
     * @internal
     */
    _evaluated = false;

    /**
     * Cache the numeric total generated through evaluation of the Roll.
     * @type {number}
     * @internal
     */
    _total;

    /**
     * A reference to the Roll at the root of the evaluation tree.
     * @type {Roll}
     * @internal
     */
    _root;

    /**
     * A reference to the RollResolver app being used to externally resolve this Roll.
     * @type {RollResolver}
     * @internal
     */
    _resolver;

    /**
     * A Proxy environment for safely evaluating a string using only available Math functions
     * @type {Math}
     */
    static MATH_PROXY = new Proxy(Math, {
      has: () => true, // Include everything
      get: (t, k) => k === Symbol.unscopables ? undefined : t[k],
      set: () => console.error("You may not set properties of the Roll.MATH_PROXY environment") // No-op
    });

    /**
     * The HTML template path used to render a complete Roll object to the chat log
     * @type {string}
     */
    static CHAT_TEMPLATE = "templates/dice/roll.html";

    /**
     * The HTML template used to render an expanded Roll tooltip to the chat log
     * @type {string}
     */
    static TOOLTIP_TEMPLATE = "templates/dice/tooltip.html";

    /**
     * A mapping of Roll instances to currently-active resolvers.
     * @type {Map<Roll, RollResolver>}
     */
    static RESOLVERS = new Map();

    /* -------------------------------------------- */

    /**
     * Prepare the data structure used for the Roll.
     * This is factored out to allow for custom Roll classes to do special data preparation using provided input.
     * @param {object} data   Provided roll data
     * @returns {object}      The prepared data object
     * @protected
     */
    _prepareData(data) {
      return data;
    }

    /* -------------------------------------------- */
    /*  Roll Attributes                             */
    /* -------------------------------------------- */

    /**
     * Return an Array of the individual DiceTerm instances contained within this Roll.
     * @type {DiceTerm[]}
     */
    get dice() {
      return this._dice.concat(this.terms.flatMap(t => {
        const dice = [];
        dice.push(...(t.dice ?? []));
        if ( t instanceof DiceTerm ) dice.push(t);
        return dice;
      }));
    }

    /* -------------------------------------------- */

    /**
     * Return a standardized representation for the displayed formula associated with this Roll.
     * @type {string}
     */
    get formula() {
      return this.constructor.getFormula(this.terms);
    }

    /* -------------------------------------------- */

    /**
     * The resulting arithmetic expression after rolls have been evaluated
     * @type {string}
     */
    get result() {
      return this.terms.map(t => t.total).join("");
    }

    /* -------------------------------------------- */

    /**
     * Return the total result of the Roll expression if it has been evaluated.
     * @type {number}
     */
    get total() {
      return Number(this._total) || 0;
    }

    /* -------------------------------------------- */

    /**
     * Return the arbitrary product of evaluating this Roll.
     * @returns {any}
     */
    get product() {
      return this._total;
    }

    /* -------------------------------------------- */

    /**
     * Whether this Roll contains entirely deterministic terms or whether there is some randomness.
     * @type {boolean}
     */
    get isDeterministic() {
      return this.terms.every(t => t.isDeterministic);
    }

    /* -------------------------------------------- */
    /*  Roll Instance Methods                       */
    /* -------------------------------------------- */

    /**
     * Alter the Roll expression by adding or multiplying the number of dice which are rolled
     * @param {number} multiply   A factor to multiply. Dice are multiplied before any additions.
     * @param {number} add        A number of dice to add. Dice are added after multiplication.
     * @param {boolean} [multiplyNumeric]  Apply multiplication factor to numeric scalar terms
     * @returns {Roll}            The altered Roll expression
     */
    alter(multiply, add, {multiplyNumeric=false}={}) {
      if ( this._evaluated ) throw new Error("You may not alter a Roll which has already been evaluated");

      // Alter dice and numeric terms
      this.terms = this.terms.map(term => {
        if ( term instanceof DiceTerm ) return term.alter(multiply, add);
        else if ( (term instanceof NumericTerm) && multiplyNumeric ) term.number *= multiply;
        return term;
      });

      // Update the altered formula and return the altered Roll
      this.resetFormula();
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Clone the Roll instance, returning a new Roll instance that has not yet been evaluated.
     * @returns {Roll}
     */
    clone() {
      return new this.constructor(this._formula, this.data, this.options);
    }

    /* -------------------------------------------- */

    /**
     * Execute the Roll asynchronously, replacing dice and evaluating the total result
     * @param {object} [options={}]                      Options which inform how the Roll is evaluated
     * @param {boolean} [options.minimize=false]         Minimize the result, obtaining the smallest possible value.
     * @param {boolean} [options.maximize=false]         Maximize the result, obtaining the largest possible value.
     * @param {boolean} [options.allowStrings=false]     If true, string terms will not cause an error to be thrown during
     *                                                   evaluation.
     * @param {boolean} [options.allowInteractive=true]  If false, force the use of non-interactive rolls and do not
     *                                                   prompt the user to make manual rolls.
     * @returns {Promise<Roll>}                          The evaluated Roll instance
     *
     * @example Evaluate a Roll expression
     * ```js
     * let r = new Roll("2d6 + 4 + 1d4");
     * await r.evaluate();
     * console.log(r.result); // 5 + 4 + 2
     * console.log(r.total);  // 11
     * ```
     */
    async evaluate({minimize=false, maximize=false, allowStrings=false, allowInteractive=true, ...options}={}) {
      if ( this._evaluated ) {
        throw new Error(`The ${this.constructor.name} has already been evaluated and is now immutable`);
      }
      this._evaluated = true;
      if ( CONFIG.debug.dice ) console.debug(`Evaluating roll with formula "${this.formula}"`);

      // Migration path for async rolls
      if ( "async" in options ) {
        foundry.utils.logCompatibilityWarning("The async option for Roll#evaluate has been removed. "
          + "Use Roll#evaluateSync for synchronous roll evaluation.");
      }
      return this._evaluate({minimize, maximize, allowStrings, allowInteractive});
    }

    /* -------------------------------------------- */

    /**
     * Execute the Roll synchronously, replacing dice and evaluating the total result.
     * @param {object} [options={}]
     * @param {boolean} [options.minimize=false]      Minimize the result, obtaining the smallest possible value.
     * @param {boolean} [options.maximize=false]      Maximize the result, obtaining the largest possible value.
     * @param {boolean} [options.strict=true]         Throw an Error if the Roll contains non-deterministic terms that
     *                                                cannot be evaluated synchronously. If this is set to false,
     *                                                non-deterministic terms will be ignored.
     * @param {boolean} [options.allowStrings=false]  If true, string terms will not cause an error to be thrown during
     *                                                evaluation.
     * @returns {Roll}                                The evaluated Roll instance.
     */
    evaluateSync({minimize=false, maximize=false, allowStrings=false, strict=true}={}) {
      if ( this._evaluated ) {
        throw new Error(`The ${this.constructor.name} has already been evaluated and is now immutable.`);
      }
      this._evaluated = true;
      if ( CONFIG.debug.dice ) console.debug(`Synchronously evaluating roll with formula "${this.formula}"`);
      return this._evaluateSync({minimize, maximize, allowStrings, strict});
    }

    /* -------------------------------------------- */

    /**
     * Evaluate the roll asynchronously.
     * @param {object} [options]                    Options which inform how evaluation is performed
     * @param {boolean} [options.minimize]          Force the result to be minimized
     * @param {boolean} [options.maximize]          Force the result to be maximized
     * @param {boolean} [options.allowStrings]      If true, string terms will not cause an error to be thrown during
     *                                              evaluation.
     * @param {boolean} [options.allowInteractive]  If false, force the use of digital rolls and do not prompt the user to
     *                                              make manual rolls.
     * @returns {Promise<Roll>}
     * @protected
     */
    async _evaluate(options={}) {
      // If the user has configured alternative dice fulfillment methods, prompt for the first pass of fulfillment here.
      let resolver;
      const { allowInteractive, minimize, maximize } = options;
      if ( !this._root && (allowInteractive !== false) && (maximize !== true) && (minimize !== true) ) {
        resolver = new this.constructor.resolverImplementation(this);
        this._resolver = resolver;
        await resolver.awaitFulfillment();
      }

      const ast = CONFIG.Dice.parser.toAST(this.terms);
      this._total = await this._evaluateASTAsync(ast, options);
      resolver?.close();
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Evaluate an AST asynchronously.
     * @param {RollParseNode|RollTerm} node     The root node or term.
     * @param {object} [options]                Options which inform how evaluation is performed
     * @param {boolean} [options.minimize]      Force the result to be minimized
     * @param {boolean} [options.maximize]      Force the result to be maximized
     * @param {boolean} [options.allowStrings]  If true, string terms will not cause an error to be thrown during
     *                                          evaluation.
     * @returns {Promise<string|number>}
     * @protected
     */
    async _evaluateASTAsync(node, options={}) {
      if ( node.class !== "Node" ) {
        if ( !node._evaluated ) {
          node._root = this._root ?? this;
          await node.evaluate(options);
        }
        return node.total;
      }

      let [left, right] = node.operands;
      [left, right] = [await this._evaluateASTAsync(left, options), await this._evaluateASTAsync(right, options)];

      switch ( node.operator ) {
        case "-": return left - right;
        case "*": return left * right;
        case "/": return left / right;
        case "%": return left % right;

        // Treat an unknown operator as addition.
        default: return left + right;
      }
    }

    /* -------------------------------------------- */

    /**
     * Evaluate the roll synchronously.
     * @param {object} [options]                Options which inform how evaluation is performed
     * @param {boolean} [options.minimize]      Force the result to be minimized
     * @param {boolean} [options.maximize]      Force the result to be maximized
     * @param {boolean} [options.strict]        Throw an error if encountering a term that cannot be synchronously
     *                                          evaluated.
     * @param {boolean} [options.allowStrings]  If true, string terms will not cause an error to be thrown during
     *                                          evaluation.
     * @returns {Roll}
     * @protected
     */
    _evaluateSync(options={}) {
      const ast = CONFIG.Dice.parser.toAST(this.terms);
      this._total = this._evaluateASTSync(ast, options);
      return this;
    }

    /* -------------------------------------------- */

    /**
     * Evaluate an AST synchronously.
     * @param {RollParseNode|RollTerm} node     The root node or term.
     * @param {object} [options]                Options which inform how evaluation is performed
     * @param {boolean} [options.minimize]      Force the result to be minimized
     * @param {boolean} [options.maximize]      Force the result to be maximized
     * @param {boolean} [options.strict]        Throw an error if encountering a term that cannot be synchronously
     *                                          evaluated.
     * @param {boolean} [options.allowStrings]  If true, string terms will not cause an error to be thrown during
     *                                          evaluation.
     * @returns {string|number}
     * @protected
     */
    _evaluateASTSync(node, options={}) {
      const { maximize, minimize, strict } = options;
      if ( node.class !== "Node" ) {
        if ( node._evaluated ) return node.total;
        if ( RollTerm.isDeterministic(node, { maximize, minimize }) ) {
          node.evaluate(options);
          return node.total;
        }
        if ( strict ) throw new Error("This Roll contains terms that cannot be synchronously evaluated.");
        return 0;
      }

      let [left, right] = node.operands;
      [left, right] = [this._evaluateASTSync(left, options), this._evaluateASTSync(right, options)];

      switch ( node.operator ) {
        case "-": return left - right;
        case "*": return left * right;
        case "/": return left / right;
        case "%": return left % right;

        // Treat an unknown operator as addition.
        default: return left + right;
      }
    }

    /* -------------------------------------------- */

    /**
     * Safely evaluate the final total result for the Roll using its component terms.
     * @returns {number}    The evaluated total
     * @protected
     */
    _evaluateTotal() {
      const expression = this.terms.map(t => t.total).join(" ");
      const total = this.constructor.safeEval(expression);
      if ( !Number.isNumeric(total) ) {
        throw new Error(game.i18n.format("DICE.ErrorNonNumeric", {formula: this.formula}));
      }
      return total;
    }

    /* -------------------------------------------- */

    /**
     * Alias for evaluate.
     * @see {Roll#evaluate}
     * @param {object} options    Options passed to Roll#evaluate
     * @returns {Promise<Roll>}
     */
    async roll(options={}) {
      return this.evaluate(options);
    }

    /* -------------------------------------------- */

    /**
     * Create a new Roll object using the original provided formula and data.
     * Each roll is immutable, so this method returns a new Roll instance using the same data.
     * @param {object} [options={}]  Evaluation options passed to Roll#evaluate
     * @returns {Promise<Roll>}      A new Roll object, rolled using the same formula and data
     */
    async reroll(options={}) {
      const r = this.clone();
      return r.evaluate(options);
    }

    /* -------------------------------------------- */

    /**
     * Recompile the formula string that represents this Roll instance from its component terms.
     * @returns {string}                The re-compiled formula
     */
    resetFormula() {
      return this._formula = this.constructor.getFormula(this.terms);
    }

    /* -------------------------------------------- */

    /**
     * Propagate flavor text across all terms that do not have any.
     * @param {string} flavor  The flavor text.
     */
    propagateFlavor(flavor) {
      if ( !flavor ) return;
      this.terms.forEach(t => t.options.flavor ??= flavor);
    }

    /* -------------------------------------------- */

    /** @override */
    toString() {
      return this._formula;
    }

    /* -------------------------------------------- */
    /*  Static Class Methods                        */
    /* -------------------------------------------- */

    /**
     * A factory method which constructs a Roll instance using the default configured Roll class.
     * @param {string} formula        The formula used to create the Roll instance
     * @param {object} [data={}]      The data object which provides component data for the formula
     * @param {object} [options={}]   Additional options which modify or describe this Roll
     * @returns {Roll}                The constructed Roll instance
     */
    static create(formula, data={}, options={}) {
      const cls = CONFIG.Dice.rolls[0];
      return new cls(formula, data, options);
    }

    /* -------------------------------------------- */

    /**
     * Get the default configured Roll class.
     * @returns {typeof Roll}
     */
    static get defaultImplementation() {
      return CONFIG.Dice.rolls[0];
    }

    /* -------------------------------------------- */

    /**
     * Retrieve the appropriate resolver implementation based on the user's configuration.
     * @returns {typeof RollResolver}
     */
    static get resolverImplementation() {
      const config = game.settings.get("core", "diceConfiguration");
      const methods = new Set(Object.values(config).filter(method => {
        if ( !method || (method === "manual") ) return false;
        return CONFIG.Dice.fulfillment.methods[method]?.interactive;
      }));

      // If there is more than one interactive method configured, use the default resolver which has a combined, method-
      // agnostic interface.
      if ( methods.size !== 1 ) return foundry.applications.dice.RollResolver;

      // Otherwise use the specific resolver configured for that method, if any.
      const method = CONFIG.Dice.fulfillment.methods[methods.first()];
      return method.resolver ?? foundry.applications.dice.RollResolver;
    }

    /* -------------------------------------------- */

    /**
     * Transform an array of RollTerm objects into a cleaned string formula representation.
     * @param {RollTerm[]} terms      An array of terms to represent as a formula
     * @returns {string}              The string representation of the formula
     */
    static getFormula(terms) {
      return terms.map(t => t.formula).join("");
    }

    /* -------------------------------------------- */

    /**
     * A sandbox-safe evaluation function to execute user-input code with access to scoped Math methods.
     * @param {string} expression   The input string expression
     * @returns {number}            The numeric evaluated result
     */
    static safeEval(expression) {
      let result;
      try {
        // eslint-disable-next-line no-new-func
        const evl = new Function("sandbox", `with (sandbox) { return ${expression}}`);
        result = evl(this.MATH_PROXY);
      } catch(err) {
        result = undefined;
      }
      if ( !Number.isNumeric(result) ) {
        throw new Error(`Roll.safeEval produced a non-numeric result from expression "${expression}"`);
      }
      return result;
    }

    /* -------------------------------------------- */

    /**
     * After parenthetical and arithmetic terms have been resolved, we need to simplify the remaining expression.
     * Any remaining string terms need to be combined with adjacent non-operators in order to construct parsable terms.
     * @param {RollTerm[]} terms      An array of terms which is eligible for simplification
     * @returns {RollTerm[]}          An array of simplified terms
     */
    static simplifyTerms(terms) {

      // Simplify terms by combining with pending strings
      let simplified = terms.reduce((terms, term) => {
        const prior = terms[terms.length - 1];
        const isOperator = term instanceof OperatorTerm;

        // Combine a non-operator term with prior StringTerm
        if ( !isOperator && (prior instanceof StringTerm) ) {
          prior.term += term.total;
          foundry.utils.mergeObject(prior.options, term.options);
          return terms;
        }

        // Combine StringTerm with a prior non-operator term
        const priorOperator = prior instanceof OperatorTerm;
        if ( prior && !priorOperator && (term instanceof StringTerm) ) {
          term.term = String(prior.total) + term.term;
          foundry.utils.mergeObject(term.options, prior.options);
          terms[terms.length - 1] = term;
          return terms;
        }

        // Otherwise continue
        terms.push(term);
        return terms;
      }, []);

      // Convert remaining String terms to a RollTerm which can be evaluated
      simplified = simplified.map(term => {
        if ( !(term instanceof StringTerm) ) return term;
        const t = this._classifyStringTerm(term.formula, {intermediate: false});
        t.options = foundry.utils.mergeObject(term.options, t.options, {inplace: false});
        return t;
      });

      // Eliminate leading or trailing arithmetic
      if ( (simplified[0] instanceof OperatorTerm) && (simplified[0].operator !== "-") ) simplified.shift();
      if ( simplified.at(-1) instanceof OperatorTerm ) simplified.pop();
      return simplified;
    }

    /* -------------------------------------------- */

    /**
     * Simulate a roll and evaluate the distribution of returned results
     * @param {string} formula      The Roll expression to simulate
     * @param {number} n            The number of simulations
     * @returns {Promise<number[]>} The rolled totals
     */
    static async simulate(formula, n=10000) {
      const results = await Promise.all([...Array(n)].map(async () => {
        const r = new this(formula);
        return (await r.evaluate({allowInteractive: false})).total;
      }, []));
      const summary = results.reduce((sum, v) => {
        sum.total = sum.total + v;
        if ( (sum.min === null) || (v < sum.min) ) sum.min = v;
        if ( (sum.max === null) || (v > sum.max) ) sum.max = v;
        return sum;
      }, {total: 0, min: null, max: null});
      summary.mean = summary.total / n;
      console.log(`Formula: ${formula} | Iterations: ${n} | Mean: ${summary.mean} | Min: ${summary.min} | Max: ${summary.max}`);
      return results;
    }

    /* -------------------------------------------- */

    /**
     * Register an externally-fulfilled result with an active RollResolver.
     * @param {string} method        The fulfillment method.
     * @param {string} denomination  The die denomination being fulfilled.
     * @param {number} result        The obtained result.
     * @returns {boolean|void}       Whether the result was consumed. Returns undefined if no resolver was available.
     */
    static registerResult(method, denomination, result) {
      // TODO: Currently this only takes the first Resolver, but the logic for which Resolver to use could be improved.
      for ( const app of foundry.applications.instances.values() ) {
        if ( (app instanceof foundry.applications.dice.RollResolver) && app.rendered ) {
          return app.registerResult(method, denomination, result);
        }
      }
    }

    /* -------------------------------------------- */
    /*  Roll Formula Parsing                        */
    /* -------------------------------------------- */

    /**
     * Parse a formula expression using the compiled peggy grammar.
     * @param {string} formula  The original string expression to parse.
     * @param {object} data     A data object used to substitute for attributes in the formula.
     * @returns {RollTerm[]}
     */
    static parse(formula, data) {
      if ( !formula ) return [];

      // Step 1: Replace formula and remove all spaces.
      const replaced = this.replaceFormulaData(formula, data, { missing: "0" });

      // Step 2: Use configured RollParser to parse the formula into a parse tree.
      const tree = foundry.dice.RollGrammar.parse(replaced);

      // Step 3: Flatten the tree into infix notation and instantiate all the nodes as RollTerm instances.
      return this.instantiateAST(tree);
    }

    /* -------------------------------------------- */

    /**
     * Instantiate the nodes in an AST sub-tree into RollTerm instances.
     * @param {RollParseNode} ast  The root of the AST sub-tree.
     * @returns {RollTerm[]}
     */
    static instantiateAST(ast) {
      return CONFIG.Dice.parser.flattenTree(ast).map(node => {
        const cls = foundry.dice.terms[node.class] ?? RollTerm;
        return cls.fromParseNode(node);
      });
    }

    /* -------------------------------------------- */

    /**
     * Replace referenced data attributes in the roll formula with values from the provided data.
     * Data references in the formula use the @attr syntax and would reference the corresponding attr key.
     *
     * @param {string} formula          The original formula within which to replace
     * @param {object} data             The data object which provides replacements
     * @param {object} [options]        Options which modify formula replacement
     * @param {string} [options.missing]      The value that should be assigned to any unmatched keys.
     *                                        If null, the unmatched key is left as-is.
     * @param {boolean} [options.warn=false]  Display a warning notification when encountering an un-matched key.
     * @static
     */
    static replaceFormulaData(formula, data, {missing, warn=false}={}) {
      let dataRgx = new RegExp(/@([a-z.0-9_-]+)/gi);
      return formula.replace(dataRgx, (match, term) => {
        let value = foundry.utils.getProperty(data, term);
        if ( value == null ) {
          if ( warn && ui.notifications ) ui.notifications.warn(game.i18n.format("DICE.WarnMissingData", {match}));
          return (missing !== undefined) ? String(missing) : match;
        }
        switch ( foundry.utils.getType(value) ) {
          case "string": return value.trim();
          case "number": case "boolean": return String(value);
          case "Object":
            if ( value.constructor.name !== "Object" ) return value.toString();
            break;
          case "Set": value = Array.from(value); break;
          case "Map": value = Object.fromEntries(Array.from(value)); break;
        }
        return `$${JSON.stringify(value)}$`;
      });
    }

    /* -------------------------------------------- */

    /**
     * Validate that a provided roll formula can represent a valid
     * @param {string} formula    A candidate formula to validate
     * @returns {boolean}         Is the provided input a valid dice formula?
     */
    static validate(formula) {

      // Replace all data references with an arbitrary number
      formula = formula.replace(/@([a-z.0-9_-]+)/gi, "1");

      // Attempt to evaluate the roll
      try {
        const r = new this(formula);
        r.evaluateSync({ strict: false });
        return true;
      }

        // If we weren't able to evaluate, the formula is invalid
      catch(err) {
        return false;
      }
    }

    /* -------------------------------------------- */

    /**
     * Determine which of the given terms require external fulfillment.
     * @param {RollTerm[]} terms  The terms.
     * @returns {DiceTerm[]}
     */
    static identifyFulfillableTerms(terms) {
      const fulfillable = [];
      const config = game.settings.get("core", "diceConfiguration");
      const allowManual = game.user.hasPermission("MANUAL_ROLLS");

      /**
       * Determine if a given term should be externally fulfilled.
       * @param {RollTerm} term  The term.
       */
      const identifyTerm = term => {
        if ( !(term instanceof DiceTerm) || !term.number || !term.faces ) return;
        const method = config[term.denomination] || CONFIG.Dice.fulfillment.defaultMethod;
        if ( (method === "manual") && !allowManual ) return;
        if ( CONFIG.Dice.fulfillment.methods[method]?.interactive ) fulfillable.push(term);
      };

      /**
       * Identify any DiceTerms in the provided list of terms.
       * @param {RollTerm[]} terms  The terms.
       */
      const identifyDice = (terms=[]) => {
        terms.forEach(term => {
          identifyTerm(term);
          if ( "dice" in term ) identifyDice(term.dice);
        });
      };

      identifyDice(terms);
      return fulfillable;
    }

    /* -------------------------------------------- */

    /**
     * Classify a remaining string term into a recognized RollTerm class
     * @param {string} term         A remaining un-classified string
     * @param {object} [options={}] Options which customize classification
     * @param {boolean} [options.intermediate=true]  Allow intermediate terms
     * @param {RollTerm|string} [options.prior]       The prior classified term
     * @param {RollTerm|string} [options.next]        The next term to classify
     * @returns {RollTerm}          A classified RollTerm instance
     * @internal
     */
    static _classifyStringTerm(term, {intermediate=true, prior, next}={}) {

      // Terms already classified
      if ( term instanceof RollTerm ) return term;

      // Numeric terms
      const numericMatch = NumericTerm.matchTerm(term);
      if ( numericMatch ) return NumericTerm.fromMatch(numericMatch);

      // Dice terms
      const diceMatch = DiceTerm.matchTerm(term, {imputeNumber: !intermediate});
      if ( diceMatch ) {
        if ( intermediate && (prior?.isIntermediate || next?.isIntermediate) ) return new StringTerm({term});
        return DiceTerm.fromMatch(diceMatch);
      }

      // Remaining strings
      return new StringTerm({term});
    }

    /* -------------------------------------------- */
    /*  Chat Messages                               */
    /* -------------------------------------------- */

    /**
     * Render the tooltip HTML for a Roll instance
     * @returns {Promise<string>}     The rendered HTML tooltip as a string
     */
    async getTooltip() {
      const parts = this.dice.map(d => d.getTooltipData());
      return renderTemplate(this.constructor.TOOLTIP_TEMPLATE, { parts });
    }

    /* -------------------------------------------- */

    /**
     * Render a Roll instance to HTML
     * @param {object} [options={}]               Options which affect how the Roll is rendered
     * @param {string} [options.flavor]             Flavor text to include
     * @param {string} [options.template]           A custom HTML template path
     * @param {boolean} [options.isPrivate=false]   Is the Roll displayed privately?
     * @returns {Promise<string>}                 The rendered HTML template as a string
     */
    async render({flavor, template=this.constructor.CHAT_TEMPLATE, isPrivate=false}={}) {
      if ( !this._evaluated ) await this.evaluate({allowInteractive: !isPrivate});
      const chatData = {
        formula: isPrivate ? "???" : this._formula,
        flavor: isPrivate ? null : flavor ?? this.options.flavor,
        user: game.user.id,
        tooltip: isPrivate ? "" : await this.getTooltip(),
        total: isPrivate ? "?" : Math.round(this.total * 100) / 100
      };
      return renderTemplate(template, chatData);
    }

    /* -------------------------------------------- */

    /**
     * Transform a Roll instance into a ChatMessage, displaying the roll result.
     * This function can either create the ChatMessage directly, or return the data object that will be used to create.
     *
     * @param {object} messageData          The data object to use when creating the message
     * @param {options} [options]           Additional options which modify the created message.
     * @param {string} [options.rollMode]   The template roll mode to use for the message from CONFIG.Dice.rollModes
     * @param {boolean} [options.create=true]   Whether to automatically create the chat message, or only return the
     *                                          prepared chatData object.
     * @returns {Promise<ChatMessage|object>} A promise which resolves to the created ChatMessage document if create is
     *                                        true, or the Object of prepared chatData otherwise.
     */
    async toMessage(messageData={}, {rollMode, create=true}={}) {
      if ( rollMode === "roll" ) rollMode = undefined;
      rollMode ||= game.settings.get("core", "rollMode");

      // Perform the roll, if it has not yet been rolled
      if ( !this._evaluated ) await this.evaluate({allowInteractive: rollMode !== CONST.DICE_ROLL_MODES.BLIND});

      // Prepare chat data
      messageData = foundry.utils.mergeObject({
        user: game.user.id,
        content: String(this.total),
        sound: CONFIG.sounds.dice
      }, messageData);
      messageData.rolls = [this];

      // Either create the message or just return the chat data
      const cls = getDocumentClass("ChatMessage");
      const msg = new cls(messageData);

      // Either create or return the data
      if ( create ) return cls.create(msg.toObject(), { rollMode });
      else {
        msg.applyRollMode(rollMode);
        return msg.toObject();
      }
    }

    /* -------------------------------------------- */
    /*  Interface Helpers                           */
    /* -------------------------------------------- */

    /**
     * Expand an inline roll element to display its contained dice result as a tooltip.
     * @param {HTMLAnchorElement} a     The inline-roll button
     * @returns {Promise<void>}
     */
    static async expandInlineResult(a) {
      if ( !a.classList.contains("inline-roll") ) return;
      if ( a.classList.contains("expanded") ) return;

      // Create a new tooltip
      const roll = this.fromJSON(unescape(a.dataset.roll));
      const tip = document.createElement("div");
      tip.innerHTML = await roll.getTooltip();

      // Add the tooltip
      const tooltip = tip.querySelector(".dice-tooltip");
      if ( !tooltip ) return;
      a.appendChild(tooltip);
      a.classList.add("expanded");

      // Set the position
      const pa = a.getBoundingClientRect();
      const pt = tooltip.getBoundingClientRect();
      tooltip.style.left = `${Math.min(pa.x, window.innerWidth - (pt.width + 3))}px`;
      tooltip.style.top = `${Math.min(pa.y + pa.height + 3, window.innerHeight - (pt.height + 3))}px`;
      const zi = getComputedStyle(a).zIndex;
      tooltip.style.zIndex = Number.isNumeric(zi) ? zi + 1 : 100;

      // Disable tooltip while expanded
      delete a.dataset.tooltip;
      game.tooltip.deactivate();
    }

    /* -------------------------------------------- */

    /**
     * Collapse an expanded inline roll to conceal its tooltip.
     * @param {HTMLAnchorElement} a     The inline-roll button
     */
    static collapseInlineResult(a) {
      if ( !a.classList.contains("inline-roll") ) return;
      if ( !a.classList.contains("expanded") ) return;
      const tooltip = a.querySelector(".dice-tooltip");
      if ( tooltip ) tooltip.remove();
      const roll = this.fromJSON(unescape(a.dataset.roll));
      a.dataset.tooltip = roll.formula;
      return a.classList.remove("expanded");
    }

    /* -------------------------------------------- */

    /**
     * Construct an inline roll link for this Roll.
     * @param {object} [options]                  Additional options to configure how the link is constructed.
     * @param {string} [options.label]            A custom label for the total.
     * @param {Record<string, string>} [options.attrs]    Attributes to set on the link.
     * @param {Record<string, string>} [options.dataset]  Custom data attributes to set on the link.
     * @param {string[]} [options.classes]        Additional classes to add to the link. The classes `inline-roll`
     *                                            and `inline-result` are added by default.
     * @param {string} [options.icon]             A font-awesome icon class to use as the icon instead of a d20.
     * @returns {HTMLAnchorElement}
     */
    toAnchor({attrs={}, dataset={}, classes=[], label, icon}={}) {
      dataset = foundry.utils.mergeObject({roll: escape(JSON.stringify(this))}, dataset);
      const a = document.createElement("a");
      a.classList.add("inline-roll", "inline-result", ...classes);
      a.dataset.tooltip = this.formula;
      Object.entries(attrs).forEach(([k, v]) => a.setAttribute(k, v));
      Object.entries(dataset).forEach(([k, v]) => a.dataset[k] = v);
      label = label ? `${label}: ${this.total}` : this.total;
      a.innerHTML = `<i class="${icon ?? "fas fa-dice-d20"}"></i>${label}`;
      return a;
    }

    /* -------------------------------------------- */
    /*  Serialization and Loading                   */
    /* -------------------------------------------- */

    /**
     * Represent the data of the Roll as an object suitable for JSON serialization.
     * @returns {object}     Structured data which can be serialized into JSON
     */
    toJSON() {
      return {
        class: this.constructor.name,
        options: this.options,
        dice: this._dice,
        formula: this._formula,
        terms: this.terms.map(t => t.toJSON()),
        total: this._total,
        evaluated: this._evaluated
      };
    }

    /* -------------------------------------------- */

    /**
     * Recreate a Roll instance using a provided data object
     * @param {object} data   Unpacked data representing the Roll
     * @returns {Roll}         A reconstructed Roll instance
     */
    static fromData(data) {

      // Redirect to the proper Roll class definition
      if ( data.class && (data.class !== this.name) ) {
        const cls = CONFIG.Dice.rolls.find(cls => cls.name === data.class);
        if ( !cls ) throw new Error(`Unable to recreate ${data.class} instance from provided data`);
        return cls.fromData(data);
      }

      // Create the Roll instance
      const roll = new this(data.formula, data.data, data.options);

      // Expand terms
      roll.terms = data.terms.map(t => {
        if ( t.class ) {
          if ( t.class === "DicePool" ) t.class = "PoolTerm"; // Backwards compatibility
          if ( t.class === "MathTerm" ) t.class = "FunctionTerm";
          return RollTerm.fromData(t);
        }
        return t;
      });

      // Repopulate evaluated state
      if ( data.evaluated ?? true ) {
        roll._total = data.total;
        roll._dice = (data.dice || []).map(t => DiceTerm.fromData(t));
        roll._evaluated = true;
      }
      return roll;
    }

    /* -------------------------------------------- */

    /**
     * Recreate a Roll instance using a provided JSON string
     * @param {string} json   Serialized JSON data representing the Roll
     * @returns {Roll}        A reconstructed Roll instance
     */
    static fromJSON(json) {
      return this.fromData(JSON.parse(json));
    }

    /* -------------------------------------------- */

    /**
     * Manually construct a Roll object by providing an explicit set of input terms
     * @param {RollTerm[]} terms      The array of terms to use as the basis for the Roll
     * @param {object} [options={}]   Additional options passed to the Roll constructor
     * @returns {Roll}                The constructed Roll instance
     *
     * @example Construct a Roll instance from an array of component terms
     * ```js
     * const t1 = new Die({number: 4, faces: 8};
     * const plus = new OperatorTerm({operator: "+"});
     * const t2 = new NumericTerm({number: 8});
     * const roll = Roll.fromTerms([t1, plus, t2]);
     * roll.formula; // 4d8 + 8
     * ```
     */
    static fromTerms(terms, options={}) {

      // Validate provided terms
      if ( !terms.every(t => t instanceof RollTerm ) ) {
        throw new Error("All provided terms must be RollTerm instances");
      }
      const allEvaluated = terms.every(t => t._evaluated);
      const noneEvaluated = !terms.some(t => t._evaluated);
      if ( !(allEvaluated || noneEvaluated) ) {
        throw new Error("You can only call Roll.fromTerms with an array of terms which are either all evaluated, or none evaluated");
      }

      // Construct the roll
      const formula = this.getFormula(terms);
      const roll = new this(formula, {}, options);
      roll.terms = terms;
      roll._evaluated = allEvaluated;
      if ( roll._evaluated ) roll._total = roll._evaluateTotal();
      return roll;
    }
  };

  var grammar = // @generated by Peggy 4.0.2.
  //
  // https://peggyjs.org/
  (function() {

  function peg$subclass(child, parent) {
    function C() { this.constructor = child; }
    C.prototype = parent.prototype;
    child.prototype = new C();
  }

  function peg$SyntaxError(message, expected, found, location) {
    var self = Error.call(this, message);
    // istanbul ignore next Check is a necessary evil to support older environments
    if (Object.setPrototypeOf) {
      Object.setPrototypeOf(self, peg$SyntaxError.prototype);
    }
    self.expected = expected;
    self.found = found;
    self.location = location;
    self.name = "SyntaxError";
    return self;
  }

  peg$subclass(peg$SyntaxError, Error);

  function peg$padEnd(str, targetLength, padString) {
    padString = padString || " ";
    if (str.length > targetLength) { return str; }
    targetLength -= str.length;
    padString += padString.repeat(targetLength);
    return str + padString.slice(0, targetLength);
  }

  peg$SyntaxError.prototype.format = function(sources) {
    var str = "Error: " + this.message;
    if (this.location) {
      var src = null;
      var k;
      for (k = 0; k < sources.length; k++) {
        if (sources[k].source === this.location.source) {
          src = sources[k].text.split(/\r\n|\n|\r/g);
          break;
        }
      }
      var s = this.location.start;
      var offset_s = (this.location.source && (typeof this.location.source.offset === "function"))
        ? this.location.source.offset(s)
        : s;
      var loc = this.location.source + ":" + offset_s.line + ":" + offset_s.column;
      if (src) {
        var e = this.location.end;
        var filler = peg$padEnd("", offset_s.line.toString().length, ' ');
        var line = src[s.line - 1];
        var last = s.line === e.line ? e.column : line.length + 1;
        var hatLen = (last - s.column) || 1;
        str += "\n --> " + loc + "\n"
            + filler + " |\n"
            + offset_s.line + " | " + line + "\n"
            + filler + " | " + peg$padEnd("", s.column - 1, ' ')
            + peg$padEnd("", hatLen, "^");
      } else {
        str += "\n at " + loc;
      }
    }
    return str;
  };

  peg$SyntaxError.buildMessage = function(expected, found) {
    var DESCRIBE_EXPECTATION_FNS = {
      literal: function(expectation) {
        return "\"" + literalEscape(expectation.text) + "\"";
      },

      class: function(expectation) {
        var escapedParts = expectation.parts.map(function(part) {
          return Array.isArray(part)
            ? classEscape(part[0]) + "-" + classEscape(part[1])
            : classEscape(part);
        });

        return "[" + (expectation.inverted ? "^" : "") + escapedParts.join("") + "]";
      },

      any: function() {
        return "any character";
      },

      end: function() {
        return "end of input";
      },

      other: function(expectation) {
        return expectation.description;
      }
    };

    function hex(ch) {
      return ch.charCodeAt(0).toString(16).toUpperCase();
    }

    function literalEscape(s) {
      return s
        .replace(/\\/g, "\\\\")
        .replace(/"/g,  "\\\"")
        .replace(/\0/g, "\\0")
        .replace(/\t/g, "\\t")
        .replace(/\n/g, "\\n")
        .replace(/\r/g, "\\r")
        .replace(/[\x00-\x0F]/g,          function(ch) { return "\\x0" + hex(ch); })
        .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x"  + hex(ch); });
    }

    function classEscape(s) {
      return s
        .replace(/\\/g, "\\\\")
        .replace(/\]/g, "\\]")
        .replace(/\^/g, "\\^")
        .replace(/-/g,  "\\-")
        .replace(/\0/g, "\\0")
        .replace(/\t/g, "\\t")
        .replace(/\n/g, "\\n")
        .replace(/\r/g, "\\r")
        .replace(/[\x00-\x0F]/g,          function(ch) { return "\\x0" + hex(ch); })
        .replace(/[\x10-\x1F\x7F-\x9F]/g, function(ch) { return "\\x"  + hex(ch); });
    }

    function describeExpectation(expectation) {
      return DESCRIBE_EXPECTATION_FNS[expectation.type](expectation);
    }

    function describeExpected(expected) {
      var descriptions = expected.map(describeExpectation);
      var i, j;

      descriptions.sort();

      if (descriptions.length > 0) {
        for (i = 1, j = 1; i < descriptions.length; i++) {
          if (descriptions[i - 1] !== descriptions[i]) {
            descriptions[j] = descriptions[i];
            j++;
          }
        }
        descriptions.length = j;
      }

      switch (descriptions.length) {
        case 1:
          return descriptions[0];

        case 2:
          return descriptions[0] + " or " + descriptions[1];

        default:
          return descriptions.slice(0, -1).join(", ")
            + ", or "
            + descriptions[descriptions.length - 1];
      }
    }

    function describeFound(found) {
      return found ? "\"" + literalEscape(found) + "\"" : "end of input";
    }

    return "Expected " + describeExpected(expected) + " but " + describeFound(found) + " found.";
  };

  function peg$parse(input, options) {
    options = options !== undefined ? options : {};

    var peg$FAILED = {};
    var peg$source = options.grammarSource;

    var peg$startRuleFunctions = { Expression: peg$parseExpression };
    var peg$startRuleFunction = peg$parseExpression;

    var peg$c0 = "(";
    var peg$c1 = ",";
    var peg$c2 = ")";
    var peg$c3 = "{";
    var peg$c4 = "}";
    var peg$c5 = "$";
    var peg$c6 = ".";
    var peg$c7 = "[";
    var peg$c8 = "]";

    var peg$r0 = /^[dD]/;
    var peg$r1 = /^[a-z]/i;
    var peg$r2 = /^[^$]/;
    var peg$r3 = /^[^ (){}[\]$,+\-*%\/]/;
    var peg$r4 = /^[a-z$_]/i;
    var peg$r5 = /^[a-z$_0-9]/i;
    var peg$r6 = /^[^ (){}[\]$+\-*\/,]/;
    var peg$r7 = /^[0-9]/;
    var peg$r8 = /^[%*\/]/;
    var peg$r9 = /^[+\-]/;
    var peg$r10 = /^[^[\]]/;
    var peg$r11 = /^[ ]/;

    var peg$e0 = peg$literalExpectation("(", false);
    var peg$e1 = peg$literalExpectation(",", false);
    var peg$e2 = peg$literalExpectation(")", false);
    var peg$e3 = peg$classExpectation(["d", "D"], false, false);
    var peg$e4 = peg$classExpectation([["a", "z"]], false, true);
    var peg$e5 = peg$literalExpectation("{", false);
    var peg$e6 = peg$literalExpectation("}", false);
    var peg$e7 = peg$literalExpectation("$", false);
    var peg$e8 = peg$classExpectation(["$"], true, false);
    var peg$e9 = peg$classExpectation([" ", "(", ")", "{", "}", "[", "]", "$", ",", "+", "-", "*", "%", "/"], true, false);
    var peg$e10 = peg$classExpectation([["a", "z"], "$", "_"], false, true);
    var peg$e11 = peg$classExpectation([["a", "z"], "$", "_", ["0", "9"]], false, true);
    var peg$e12 = peg$classExpectation([" ", "(", ")", "{", "}", "[", "]", "$", "+", "-", "*", "/", ","], true, false);
    var peg$e13 = peg$classExpectation([["0", "9"]], false, false);
    var peg$e14 = peg$literalExpectation(".", false);
    var peg$e15 = peg$classExpectation(["%", "*", "/"], false, false);
    var peg$e16 = peg$classExpectation(["+", "-"], false, false);
    var peg$e17 = peg$literalExpectation("[", false);
    var peg$e18 = peg$classExpectation(["[", "]"], true, false);
    var peg$e19 = peg$literalExpectation("]", false);
    var peg$e20 = peg$otherExpectation("whitespace");
    var peg$e21 = peg$classExpectation([" "], false, false);

    var peg$f0 = function(leading, head, tail) {

    /*

    The grammar rules are matched in order of precedence starting at the top of the file, so the rules that match the
    largest portions of a string should generally go at the top, with matches for smaller substrings going at the bottom.

    Here we have a rule that matches the overall roll formula. If a given formula does not match this rule, it means that
    it is an invalid formula and will throw a parsing error.

    Prefixing a pattern with 'foo:' is a way to give a name to the sub-match in the associated javascript code. We use
    this fairly heavily since we want to forward these sub-matches onto the 'parser'. We can think of them like named
    capture groups.

    Prefixing a pattern with '@' is called 'plucking', and is used to identify sub-matches that should be assigned to the
    overall capture name (like 'foo:'), ignoring any that are not 'plucked'.

    For example 'tail:(_ @Operators _ @Term)*' matches operators followed by terms, with any amount of whitespace
    in-between, however only the operator and term matches are assigned to the 'tail' variable, the whitespace is ignored.

    The 'head:A tail:(Delimiter B)*' pattern is a way of representing a string of things separated by a delimiter,
    like 'A + B + C' for formulas, or 'A, B, C' for Pool and Math terms.

    In each of these cases we follow the same pattern: We match a term, then we call 'parser.on*', and that method is
    responsible for taking the raw matched sub-strings and returning a 'parse node', i.e. a plain javascript object that
    contains all the information we need to instantiate a real `RollTerm`.

    */

    return parser._onExpression(head, tail, leading, text(), error);
  };
    var peg$f1 = function(fn, head, tail, flavor) {
    return parser._onFunctionTerm(fn, head, tail, flavor, text());
  };
    var peg$f2 = function(number, faces, modifiers, flavor) {
    return parser._onDiceTerm(number, faces, modifiers, flavor, text());
  };
    var peg$f3 = function(number, flavor) { return parser._onNumericTerm(number, flavor); };
    var peg$f4 = function(head, tail, modifiers, flavor) {
    return parser._onPoolTerm(head, tail, modifiers, flavor, text());
  };
    var peg$f5 = function(term, flavor) { return parser._onParenthetical(term, flavor, text()); };
    var peg$f6 = function(term, flavor) { return parser._onStringTerm(term, flavor); };
    var peg$f7 = function() { return Number(text()); };
    var peg$f8 = function(head, tail) { return [head, ...tail]; };
    var peg$f9 = function(head, tail) { return [null, head, ...tail]; };
    var peg$currPos = options.peg$currPos | 0;
    var peg$savedPos = peg$currPos;
    var peg$posDetailsCache = [{ line: 1, column: 1 }];
    var peg$maxFailPos = peg$currPos;
    var peg$maxFailExpected = options.peg$maxFailExpected || [];
    var peg$silentFails = options.peg$silentFails | 0;

    var peg$result;

    if (options.startRule) {
      if (!(options.startRule in peg$startRuleFunctions)) {
        throw new Error("Can't start parsing from rule \"" + options.startRule + "\".");
      }

      peg$startRuleFunction = peg$startRuleFunctions[options.startRule];
    }

    function text() {
      return input.substring(peg$savedPos, peg$currPos);
    }

    function error(message, location) {
      location = location !== undefined
        ? location
        : peg$computeLocation(peg$savedPos, peg$currPos);

      throw peg$buildSimpleError(message, location);
    }

    function peg$literalExpectation(text, ignoreCase) {
      return { type: "literal", text: text, ignoreCase: ignoreCase };
    }

    function peg$classExpectation(parts, inverted, ignoreCase) {
      return { type: "class", parts: parts, inverted: inverted, ignoreCase: ignoreCase };
    }

    function peg$endExpectation() {
      return { type: "end" };
    }

    function peg$otherExpectation(description) {
      return { type: "other", description: description };
    }

    function peg$computePosDetails(pos) {
      var details = peg$posDetailsCache[pos];
      var p;

      if (details) {
        return details;
      } else {
        if (pos >= peg$posDetailsCache.length) {
          p = peg$posDetailsCache.length - 1;
        } else {
          p = pos;
          while (!peg$posDetailsCache[--p]) {}
        }

        details = peg$posDetailsCache[p];
        details = {
          line: details.line,
          column: details.column
        };

        while (p < pos) {
          if (input.charCodeAt(p) === 10) {
            details.line++;
            details.column = 1;
          } else {
            details.column++;
          }

          p++;
        }

        peg$posDetailsCache[pos] = details;

        return details;
      }
    }

    function peg$computeLocation(startPos, endPos, offset) {
      var startPosDetails = peg$computePosDetails(startPos);
      var endPosDetails = peg$computePosDetails(endPos);

      var res = {
        source: peg$source,
        start: {
          offset: startPos,
          line: startPosDetails.line,
          column: startPosDetails.column
        },
        end: {
          offset: endPos,
          line: endPosDetails.line,
          column: endPosDetails.column
        }
      };
      return res;
    }

    function peg$fail(expected) {
      if (peg$currPos < peg$maxFailPos) { return; }

      if (peg$currPos > peg$maxFailPos) {
        peg$maxFailPos = peg$currPos;
        peg$maxFailExpected = [];
      }

      peg$maxFailExpected.push(expected);
    }

    function peg$buildSimpleError(message, location) {
      return new peg$SyntaxError(message, null, null, location);
    }

    function peg$buildStructuredError(expected, found, location) {
      return new peg$SyntaxError(
        peg$SyntaxError.buildMessage(expected, found),
        expected,
        found,
        location
      );
    }

    function peg$parseExpression() {
      var s0, s2, s3, s4, s5, s6, s8, s10;

      s0 = peg$currPos;
      peg$parse_();
      s2 = [];
      s3 = peg$currPos;
      s4 = peg$parse_();
      s5 = peg$parseAdditive();
      if (s5 !== peg$FAILED) {
        s3 = s5;
      } else {
        peg$currPos = s3;
        s3 = peg$FAILED;
      }
      while (s3 !== peg$FAILED) {
        s2.push(s3);
        s3 = peg$currPos;
        s4 = peg$parse_();
        s5 = peg$parseAdditive();
        if (s5 !== peg$FAILED) {
          s3 = s5;
        } else {
          peg$currPos = s3;
          s3 = peg$FAILED;
        }
      }
      s3 = peg$parse_();
      s4 = peg$parseTerm();
      if (s4 !== peg$FAILED) {
        s5 = [];
        s6 = peg$currPos;
        peg$parse_();
        s8 = peg$parseOperators();
        if (s8 !== peg$FAILED) {
          peg$parse_();
          s10 = peg$parseTerm();
          if (s10 !== peg$FAILED) {
            s6 = [ s8, s10 ];
          } else {
            peg$currPos = s6;
            s6 = peg$FAILED;
          }
        } else {
          peg$currPos = s6;
          s6 = peg$FAILED;
        }
        while (s6 !== peg$FAILED) {
          s5.push(s6);
          s6 = peg$currPos;
          peg$parse_();
          s8 = peg$parseOperators();
          if (s8 !== peg$FAILED) {
            peg$parse_();
            s10 = peg$parseTerm();
            if (s10 !== peg$FAILED) {
              s6 = [ s8, s10 ];
            } else {
              peg$currPos = s6;
              s6 = peg$FAILED;
            }
          } else {
            peg$currPos = s6;
            s6 = peg$FAILED;
          }
        }
        s6 = peg$parse_();
        peg$savedPos = s0;
        s0 = peg$f0(s2, s4, s5);
      } else {
        peg$currPos = s0;
        s0 = peg$FAILED;
      }

      return s0;
    }

    function peg$parseTerm() {
      var s0;

      s0 = peg$parseFunctionTerm();
      if (s0 === peg$FAILED) {
        s0 = peg$parseDiceTerm();
        if (s0 === peg$FAILED) {
          s0 = peg$parseNumericTerm();
          if (s0 === peg$FAILED) {
            s0 = peg$parsePoolTerm();
            if (s0 === peg$FAILED) {
              s0 = peg$parseParenthetical();
              if (s0 === peg$FAILED) {
                s0 = peg$parseStringTerm();
              }
            }
          }
        }
      }

      return s0;
    }

    function peg$parseFunctionTerm() {
      var s0, s1, s2, s4, s6, s7, s8, s9, s11;

      s0 = peg$currPos;
      s1 = peg$parseFunctionName();
      if (s1 !== peg$FAILED) {
        if (input.charCodeAt(peg$currPos) === 40) {
          s2 = peg$c0;
          peg$currPos++;
        } else {
          s2 = peg$FAILED;
          if (peg$silentFails === 0) { peg$fail(peg$e0); }
        }
        if (s2 !== peg$FAILED) {
          peg$parse_();
          s4 = peg$parseExpression();
          if (s4 === peg$FAILED) {
            s4 = null;
          }
          peg$parse_();
          s6 = [];
          s7 = peg$currPos;
          s8 = peg$parse_();
          if (input.charCodeAt(peg$currPos) === 44) {
            s9 = peg$c1;
            peg$currPos++;
          } else {
            s9 = peg$FAILED;
            if (peg$silentFails === 0) { peg$fail(peg$e1); }
          }
          if (s9 !== peg$FAILED) {
            peg$parse_();
            s11 = peg$parseExpression();
            if (s11 !== peg$FAILED) {
              s7 = s11;
            } else {
              peg$currPos = s7;
              s7 = peg$FAILED;
            }
          } else {
            peg$currPos = s7;
            s7 = peg$FAILED;
          }
          while (s7 !== peg$FAILED) {
            s6.push(s7);
            s7 = peg$currPos;
            s8 = peg$parse_();
            if (input.charCodeAt(peg$currPos) === 44) {
              s9 = peg$c1;
              peg$currPos++;
            } else {
              s9 = peg$FAILED;
              if (peg$silentFails === 0) { peg$fail(peg$e1); }
            }
            if (s9 !== peg$FAILED) {
              peg$parse_();
              s11 = peg$parseExpression();
              if (s11 !== peg$FAILED) {
                s7 = s11;
              } else {
                peg$currPos = s7;
                s7 = peg$FAILED;
              }
            } else {
              peg$currPos = s7;
              s7 = peg$FAILED;
            }
          }
          s7 = peg$parse_();
          if (input.charCodeAt(peg$currPos) === 41) {
            s8 = peg$c2;
            peg$currPos++;
          } else {
            s8 = peg$FAILED;
            if (peg$silentFails === 0) { peg$fail(peg$e2); }
          }
          if (s8 !== peg$FAILED) {
            s9 = peg$parseFlavor();
            if (s9 === peg$FAILED) {
              s9 = null;
            }
            peg$savedPos = s0;
            s0 = peg$f1(s1, s4, s6, s9);
          } else {
            peg$currPos = s0;
            s0 = peg$FAILED;
          }
        } else {
          peg$currPos = s0;
          s0 = peg$FAILED;
        }
      } else {
        peg$currPos = s0;
        s0 = peg$FAILED;
      }

      return s0;
    }

    function peg$parseDiceTerm() {
      var s0, s1, s2, s3, s4, s5;

      s0 = peg$currPos;
      s1 = peg$parseParenthetical();
      if (s1 === peg$FAILED) {
        s1 = peg$parseConstant();
      }
      if (s1 === peg$FAILED) {
        s1 = null;
      }
      s2 = input.charAt(peg$currPos);
      if (peg$r0.test(s2)) {
        peg$currPos++;
      } else {
        s2 = peg$FAILED;
        if (peg$silentFails === 0) { peg$fail(peg$e3); }
      }
      if (s2 !== peg$FAILED) {
        s3 = input.charAt(peg$currPos);
        if (peg$r1.test(s3)) {
          peg$currPos++;
        } else {
          s3 = peg$FAILED;
          if (peg$silentFails === 0) { peg$fail(peg$e4); }
        }
        if (s3 === peg$FAILED) {
          s3 = peg$parseParenthetical();
          if (s3 === peg$FAILED) {
            s3 = peg$parseConstant();
          }
        }
        if (s3 !== peg$FAILED) {
          s4 = peg$parseModifiers();
          if (s4 === peg$FAILED) {
            s4 = null;
          }
          s5 = peg$parseFlavor();
          if (s5 === peg$FAILED) {
            s5 = null;
          }
          peg$savedPos = s0;
          s0 = peg$f2(s1, s3, s4, s5);
        } else {
          peg$currPos = s0;
          s0 = peg$FAILED;
        }
      } else {
        peg$currPos = s0;
        s0 = peg$FAILED;
      }

      return s0;
    }

    function peg$parseNumericTerm() {
      var s0, s1, s2, s3, s4;

      s0 = peg$currPos;
      s1 = peg$parseConstant();
      if (s1 !== peg$FAILED) {
        s2 = peg$parseFlavor();
        if (s2 === peg$FAILED) {
          s2 = null;
        }
        s3 = peg$currPos;
        peg$silentFails++;
        s4 = peg$parseStringTerm();
        peg$silentFails--;
        if (s4 === peg$FAILED) {
          s3 = undefined;
        } else {
          peg$currPos = s3;
          s3 = peg$FAILED;
        }
        if (s3 !== peg$FAILED) {
          peg$savedPos = s0;
          s0 = peg$f3(s1, s2);
        } else {
          peg$currPos = s0;
          s0 = peg$FAILED;
        }
      } else {
        peg$currPos = s0;
        s0 = peg$FAILED;
      }

      return s0;
    }

    function peg$parsePoolTerm() {
      var s0, s1, s3, s4, s5, s6, s7, s8;

      s0 = peg$currPos;
      if (input.charCodeAt(peg$currPos) === 123) {
        s1 = peg$c3;
        peg$currPos++;
      } else {
        s1 = peg$FAILED;
        if (peg$silentFails === 0) { peg$fail(peg$e5); }
      }
      if (s1 !== peg$FAILED) {
        peg$parse_();
        s3 = peg$parseExpression();
        if (s3 !== peg$FAILED) {
          s4 = [];
          s5 = peg$currPos;
          if (input.charCodeAt(peg$currPos) === 44) {
            s6 = peg$c1;
            peg$currPos++;
          } else {
            s6 = peg$FAILED;
            if (peg$silentFails === 0) { peg$fail(peg$e1); }
          }
          if (s6 !== peg$FAILED) {
            s7 = peg$parse_();
            s8 = peg$parseExpression();
            if (s8 !== peg$FAILED) {
              s5 = s8;
            } else {
              peg$currPos = s5;
              s5 = peg$FAILED;
            }
          } else {
            peg$currPos = s5;
            s5 = peg$FAILED;
          }
          while (s5 !== peg$FAILED) {
            s4.push(s5);
            s5 = peg$currPos;
            if (input.charCodeAt(peg$currPos) === 44) {
              s6 = peg$c1;
              peg$currPos++;
            } else {
              s6 = peg$FAILED;
              if (peg$silentFails === 0) { peg$fail(peg$e1); }
            }
            if (s6 !== peg$FAILED) {
              s7 = peg$parse_();
              s8 = peg$parseExpression();
              if (s8 !== peg$FAILED) {
                s5 = s8;
              } else {
                peg$currPos = s5;
                s5 = peg$FAILED;
              }
            } else {
              peg$currPos = s5;
              s5 = peg$FAILED;
            }
          }
          if (input.charCodeAt(peg$currPos) === 125) {
            s5 = peg$c4;
            peg$currPos++;
          } else {
            s5 = peg$FAILED;
            if (peg$silentFails === 0) { peg$fail(peg$e6); }
          }
          if (s5 !== peg$FAILED) {
            s6 = peg$parseModifiers();
            if (s6 === peg$FAILED) {
              s6 = null;
            }
            s7 = peg$parseFlavor();
            if (s7 === peg$FAILED) {
              s7 = null;
            }
            peg$savedPos = s0;
            s0 = peg$f4(s3, s4, s6, s7);
          } else {
            peg$currPos = s0;
            s0 = peg$FAILED;
          }
        } else {
          peg$currPos = s0;
          s0 = peg$FAILED;
        }
      } else {
        peg$currPos = s0;
        s0 = peg$FAILED;
      }

      return s0;
    }

    function peg$parseParenthetical() {
      var s0, s1, s3, s5, s6;

      s0 = peg$currPos;
      if (input.charCodeAt(peg$currPos) === 40) {
        s1 = peg$c0;
        peg$currPos++;
      } else {
        s1 = peg$FAILED;
        if (peg$silentFails === 0) { peg$fail(peg$e0); }
      }
      if (s1 !== peg$FAILED) {
        peg$parse_();
        s3 = peg$parseExpression();
        if (s3 !== peg$FAILED) {
          peg$parse_();
          if (input.charCodeAt(peg$currPos) === 41) {
            s5 = peg$c2;
            peg$currPos++;
          } else {
            s5 = peg$FAILED;
            if (peg$silentFails === 0) { peg$fail(peg$e2); }
          }
          if (s5 !== peg$FAILED) {
            s6 = peg$parseFlavor();
            if (s6 === peg$FAILED) {
              s6 = null;
            }
            peg$savedPos = s0;
            s0 = peg$f5(s3, s6);
          } else {
            peg$currPos = s0;
            s0 = peg$FAILED;
          }
        } else {
          peg$currPos = s0;
          s0 = peg$FAILED;
        }
      } else {
        peg$currPos = s0;
        s0 = peg$FAILED;
      }

      return s0;
    }

    function peg$parseStringTerm() {
      var s0, s1, s2;

      s0 = peg$currPos;
      s1 = peg$parseReplacedData();
      if (s1 === peg$FAILED) {
        s1 = peg$parsePlainString();
      }
      if (s1 !== peg$FAILED) {
        s2 = peg$parseFlavor();
        if (s2 === peg$FAILED) {
          s2 = null;
        }
        peg$savedPos = s0;
        s0 = peg$f6(s1, s2);
      } else {
        peg$currPos = s0;
        s0 = peg$FAILED;
      }

      return s0;
    }

    function peg$parseReplacedData() {
      var s0, s1, s2, s3, s4, s5;

      s0 = peg$currPos;
      s1 = peg$currPos;
      if (input.charCodeAt(peg$currPos) === 36) {
        s2 = peg$c5;
        peg$currPos++;
      } else {
        s2 = peg$FAILED;
        if (peg$silentFails === 0) { peg$fail(peg$e7); }
      }
      if (s2 !== peg$FAILED) {
        s3 = peg$currPos;
        s4 = [];
        s5 = input.charAt(peg$currPos);
        if (peg$r2.test(s5)) {
          peg$currPos++;
        } else {
          s5 = peg$FAILED;
          if (peg$silentFails === 0) { peg$fail(peg$e8); }
        }
        if (s5 !== peg$FAILED) {
          while (s5 !== peg$FAILED) {
            s4.push(s5);
            s5 = input.charAt(peg$currPos);
            if (peg$r2.test(s5)) {
              peg$currPos++;
            } else {
              s5 = peg$FAILED;
              if (peg$silentFails === 0) { peg$fail(peg$e8); }
            }
          }
        } else {
          s4 = peg$FAILED;
        }
        if (s4 !== peg$FAILED) {
          s3 = input.substring(s3, peg$currPos);
        } else {
          s3 = s4;
        }
        if (s3 !== peg$FAILED) {
          if (input.charCodeAt(peg$currPos) === 36) {
            s4 = peg$c5;
            peg$currPos++;
          } else {
            s4 = peg$FAILED;
            if (peg$silentFails === 0) { peg$fail(peg$e7); }
          }
          if (s4 !== peg$FAILED) {
            s2 = [s2, s3, s4];
            s1 = s2;
          } else {
            peg$currPos = s1;
            s1 = peg$FAILED;
          }
        } else {
          peg$currPos = s1;
          s1 = peg$FAILED;
        }
      } else {
        peg$currPos = s1;
        s1 = peg$FAILED;
      }
      if (s1 !== peg$FAILED) {
        s0 = input.substring(s0, peg$currPos);
      } else {
        s0 = s1;
      }

      return s0;
    }

    function peg$parsePlainString() {
      var s0, s1, s2;

      s0 = peg$currPos;
      s1 = [];
      s2 = input.charAt(peg$currPos);
      if (peg$r3.test(s2)) {
        peg$currPos++;
      } else {
        s2 = peg$FAILED;
        if (peg$silentFails === 0) { peg$fail(peg$e9); }
      }
      if (s2 !== peg$FAILED) {
        while (s2 !== peg$FAILED) {
          s1.push(s2);
          s2 = input.charAt(peg$currPos);
          if (peg$r3.test(s2)) {
            peg$currPos++;
          } else {
            s2 = peg$FAILED;
            if (peg$silentFails === 0) { peg$fail(peg$e9); }
          }
        }
      } else {
        s1 = peg$FAILED;
      }
      if (s1 !== peg$FAILED) {
        s0 = input.substring(s0, peg$currPos);
      } else {
        s0 = s1;
      }

      return s0;
    }

    function peg$parseFunctionName() {
      var s0, s1, s2, s3, s4;

      s0 = peg$currPos;
      s1 = peg$currPos;
      s2 = input.charAt(peg$currPos);
      if (peg$r4.test(s2)) {
        peg$currPos++;
      } else {
        s2 = peg$FAILED;
        if (peg$silentFails === 0) { peg$fail(peg$e10); }
      }
      if (s2 !== peg$FAILED) {
        s3 = [];
        s4 = input.charAt(peg$currPos);
        if (peg$r5.test(s4)) {
          peg$currPos++;
        } else {
          s4 = peg$FAILED;
          if (peg$silentFails === 0) { peg$fail(peg$e11); }
        }
        while (s4 !== peg$FAILED) {
          s3.push(s4);
          s4 = input.charAt(peg$currPos);
          if (peg$r5.test(s4)) {
            peg$currPos++;
          } else {
            s4 = peg$FAILED;
            if (peg$silentFails === 0) { peg$fail(peg$e11); }
          }
        }
        s2 = [s2, s3];
        s1 = s2;
      } else {
        peg$currPos = s1;
        s1 = peg$FAILED;
      }
      if (s1 !== peg$FAILED) {
        s0 = input.substring(s0, peg$currPos);
      } else {
        s0 = s1;
      }

      return s0;
    }

    function peg$parseModifiers() {
      var s0, s1, s2;

      s0 = peg$currPos;
      s1 = [];
      s2 = input.charAt(peg$currPos);
      if (peg$r6.test(s2)) {
        peg$currPos++;
      } else {
        s2 = peg$FAILED;
        if (peg$silentFails === 0) { peg$fail(peg$e12); }
      }
      if (s2 !== peg$FAILED) {
        while (s2 !== peg$FAILED) {
          s1.push(s2);
          s2 = input.charAt(peg$currPos);
          if (peg$r6.test(s2)) {
            peg$currPos++;
          } else {
            s2 = peg$FAILED;
            if (peg$silentFails === 0) { peg$fail(peg$e12); }
          }
        }
      } else {
        s1 = peg$FAILED;
      }
      if (s1 !== peg$FAILED) {
        s0 = input.substring(s0, peg$currPos);
      } else {
        s0 = s1;
      }

      return s0;
    }

    function peg$parseConstant() {
      var s0, s2, s3, s4, s5, s6;

      s0 = peg$currPos;
      peg$parse_();
      s2 = [];
      s3 = input.charAt(peg$currPos);
      if (peg$r7.test(s3)) {
        peg$currPos++;
      } else {
        s3 = peg$FAILED;
        if (peg$silentFails === 0) { peg$fail(peg$e13); }
      }
      if (s3 !== peg$FAILED) {
        while (s3 !== peg$FAILED) {
          s2.push(s3);
          s3 = input.charAt(peg$currPos);
          if (peg$r7.test(s3)) {
            peg$currPos++;
          } else {
            s3 = peg$FAILED;
            if (peg$silentFails === 0) { peg$fail(peg$e13); }
          }
        }
      } else {
        s2 = peg$FAILED;
      }
      if (s2 !== peg$FAILED) {
        s3 = peg$currPos;
        if (input.charCodeAt(peg$currPos) === 46) {
          s4 = peg$c6;
          peg$currPos++;
        } else {
          s4 = peg$FAILED;
          if (peg$silentFails === 0) { peg$fail(peg$e14); }
        }
        if (s4 !== peg$FAILED) {
          s5 = [];
          s6 = input.charAt(peg$currPos);
          if (peg$r7.test(s6)) {
            peg$currPos++;
          } else {
            s6 = peg$FAILED;
            if (peg$silentFails === 0) { peg$fail(peg$e13); }
          }
          if (s6 !== peg$FAILED) {
            while (s6 !== peg$FAILED) {
              s5.push(s6);
              s6 = input.charAt(peg$currPos);
              if (peg$r7.test(s6)) {
                peg$currPos++;
              } else {
                s6 = peg$FAILED;
                if (peg$silentFails === 0) { peg$fail(peg$e13); }
              }
            }
          } else {
            s5 = peg$FAILED;
          }
          if (s5 !== peg$FAILED) {
            s4 = [s4, s5];
            s3 = s4;
          } else {
            peg$currPos = s3;
            s3 = peg$FAILED;
          }
        } else {
          peg$currPos = s3;
          s3 = peg$FAILED;
        }
        if (s3 === peg$FAILED) {
          s3 = null;
        }
        peg$savedPos = s0;
        s0 = peg$f7();
      } else {
        peg$currPos = s0;
        s0 = peg$FAILED;
      }

      return s0;
    }

    function peg$parseOperators() {
      var s0;

      s0 = peg$parseMultiplicativeFirst();
      if (s0 === peg$FAILED) {
        s0 = peg$parseAdditiveOnly();
      }

      return s0;
    }

    function peg$parseMultiplicativeFirst() {
      var s0, s1, s2, s3, s5;

      s0 = peg$currPos;
      s1 = peg$parseMultiplicative();
      if (s1 !== peg$FAILED) {
        s2 = [];
        s3 = peg$currPos;
        peg$parse_();
        s5 = peg$parseAdditive();
        if (s5 !== peg$FAILED) {
          s3 = s5;
        } else {
          peg$currPos = s3;
          s3 = peg$FAILED;
        }
        while (s3 !== peg$FAILED) {
          s2.push(s3);
          s3 = peg$currPos;
          peg$parse_();
          s5 = peg$parseAdditive();
          if (s5 !== peg$FAILED) {
            s3 = s5;
          } else {
            peg$currPos = s3;
            s3 = peg$FAILED;
          }
        }
        peg$savedPos = s0;
        s0 = peg$f8(s1, s2);
      } else {
        peg$currPos = s0;
        s0 = peg$FAILED;
      }

      return s0;
    }

    function peg$parseAdditiveOnly() {
      var s0, s1, s2, s3, s5;

      s0 = peg$currPos;
      s1 = peg$parseAdditive();
      if (s1 !== peg$FAILED) {
        s2 = [];
        s3 = peg$currPos;
        peg$parse_();
        s5 = peg$parseAdditive();
        if (s5 !== peg$FAILED) {
          s3 = s5;
        } else {
          peg$currPos = s3;
          s3 = peg$FAILED;
        }
        while (s3 !== peg$FAILED) {
          s2.push(s3);
          s3 = peg$currPos;
          peg$parse_();
          s5 = peg$parseAdditive();
          if (s5 !== peg$FAILED) {
            s3 = s5;
          } else {
            peg$currPos = s3;
            s3 = peg$FAILED;
          }
        }
        peg$savedPos = s0;
        s0 = peg$f9(s1, s2);
      } else {
        peg$currPos = s0;
        s0 = peg$FAILED;
      }

      return s0;
    }

    function peg$parseMultiplicative() {
      var s0;

      s0 = input.charAt(peg$currPos);
      if (peg$r8.test(s0)) {
        peg$currPos++;
      } else {
        s0 = peg$FAILED;
        if (peg$silentFails === 0) { peg$fail(peg$e15); }
      }

      return s0;
    }

    function peg$parseAdditive() {
      var s0;

      s0 = input.charAt(peg$currPos);
      if (peg$r9.test(s0)) {
        peg$currPos++;
      } else {
        s0 = peg$FAILED;
        if (peg$silentFails === 0) { peg$fail(peg$e16); }
      }

      return s0;
    }

    function peg$parseFlavor() {
      var s0, s1, s2, s3, s4;

      s0 = peg$currPos;
      if (input.charCodeAt(peg$currPos) === 91) {
        s1 = peg$c7;
        peg$currPos++;
      } else {
        s1 = peg$FAILED;
        if (peg$silentFails === 0) { peg$fail(peg$e17); }
      }
      if (s1 !== peg$FAILED) {
        s2 = peg$currPos;
        s3 = [];
        s4 = input.charAt(peg$currPos);
        if (peg$r10.test(s4)) {
          peg$currPos++;
        } else {
          s4 = peg$FAILED;
          if (peg$silentFails === 0) { peg$fail(peg$e18); }
        }
        if (s4 !== peg$FAILED) {
          while (s4 !== peg$FAILED) {
            s3.push(s4);
            s4 = input.charAt(peg$currPos);
            if (peg$r10.test(s4)) {
              peg$currPos++;
            } else {
              s4 = peg$FAILED;
              if (peg$silentFails === 0) { peg$fail(peg$e18); }
            }
          }
        } else {
          s3 = peg$FAILED;
        }
        if (s3 !== peg$FAILED) {
          s2 = input.substring(s2, peg$currPos);
        } else {
          s2 = s3;
        }
        if (s2 !== peg$FAILED) {
          if (input.charCodeAt(peg$currPos) === 93) {
            s3 = peg$c8;
            peg$currPos++;
          } else {
            s3 = peg$FAILED;
            if (peg$silentFails === 0) { peg$fail(peg$e19); }
          }
          if (s3 !== peg$FAILED) {
            s0 = s2;
          } else {
            peg$currPos = s0;
            s0 = peg$FAILED;
          }
        } else {
          peg$currPos = s0;
          s0 = peg$FAILED;
        }
      } else {
        peg$currPos = s0;
        s0 = peg$FAILED;
      }

      return s0;
    }

    function peg$parse_() {
      var s0, s1;

      peg$silentFails++;
      s0 = [];
      s1 = input.charAt(peg$currPos);
      if (peg$r11.test(s1)) {
        peg$currPos++;
      } else {
        s1 = peg$FAILED;
        if (peg$silentFails === 0) { peg$fail(peg$e21); }
      }
      while (s1 !== peg$FAILED) {
        s0.push(s1);
        s1 = input.charAt(peg$currPos);
        if (peg$r11.test(s1)) {
          peg$currPos++;
        } else {
          s1 = peg$FAILED;
          if (peg$silentFails === 0) { peg$fail(peg$e21); }
        }
      }
      peg$silentFails--;
      s1 = peg$FAILED;
      if (peg$silentFails === 0) { peg$fail(peg$e20); }

      return s0;
    }


    /*

    This is a per-parser initialization block. It runs whenever the parser is invoked. Any variables declared here are
    available in any javascript blocks in the rest of the grammar.

    In addition to the parser generated by peggy, we allow for certain parts of the process to be delegated to client
    code. A class implementing this 'parser' API may be passed-in here as an option when the peggy parser is invoked,
    otherwise we use the one configured at CONFIG.Dice.parser.

    */

    const Parser = options.parser ?? CONFIG.Dice.parser;
    const parser = new Parser(input);

    peg$result = peg$startRuleFunction();

    if (options.peg$library) {
      return /** @type {any} */ ({
        peg$result,
        peg$currPos,
        peg$FAILED,
        peg$maxFailExpected,
        peg$maxFailPos
      });
    }
    if (peg$result !== peg$FAILED && peg$currPos === input.length) {
      return peg$result;
    } else {
      if (peg$result !== peg$FAILED && peg$currPos < input.length) {
        peg$fail(peg$endExpectation());
      }

      throw peg$buildStructuredError(
        peg$maxFailExpected,
        peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null,
        peg$maxFailPos < input.length
          ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1)
          : peg$computeLocation(peg$maxFailPos, peg$maxFailPos)
      );
    }
  }

    return {
      StartRules: ["Expression"],
      SyntaxError: peg$SyntaxError,
      parse: peg$parse
    };
  })()
  ;

  /**
   * @typedef {import("../_types.mjs").RollParseNode} RollParseNode
   * @typedef {import("../_types.mjs").RollParseTreeNode} RollParseTreeNode
   * @typedef {import("../_types.mjs").NumericRollParseNode} NumericRollParseNode
   * @typedef {import("../_types.mjs").FunctionRollParseNode} FunctionRollParseNode
   * @typedef {import("../_types.mjs").PoolRollParseNode} PoolRollParseNode
   * @typedef {import("../_types.mjs").ParentheticalRollParseNode} ParentheticalRollParseNode
   * @typedef {import("../_types.mjs").DiceRollParseNode} DiceRollParseNode
   * @typedef {import("../_types.mjs").RollParseArg} RollParseArg
   */

  /**
   * A class for transforming events from the Peggy grammar lexer into various formats.
   */
  class RollParser {
    /**
     * @param {string} formula  The full formula.
     */
    constructor(formula) {
      this.formula = formula;
    }

    /**
     * The full formula.
     * @type {string}
     */
    formula;

    /* -------------------------------------------- */
    /*  Parse Events                                */
    /* -------------------------------------------- */

    /**
     * Handle a base roll expression.
     * @param {RollParseNode} head                The first operand.
     * @param {[string[], RollParseNode][]} tail  Zero or more subsequent (operators, operand) tuples.
     * @param {string} [leading]                  A leading operator.
     * @param {string} formula                    The original matched text.
     * @param {function} error                    The peggy error callback to invoke on a parse error.
     * @returns {RollParseTreeNode}
     * @internal
     * @protected
     */
    _onExpression(head, tail, leading, formula, error) {
      if ( CONFIG.debug.rollParsing ) console.debug(this.constructor.formatDebug("onExpression", head, tail));
      if ( leading.length ) leading = this._collapseOperators(leading);
      if ( leading === "-" ) head = this._wrapNegativeTerm(head);

      // We take the list of (operator, operand) tuples and arrange them into a left-skewed binary tree.
      return tail.reduce((acc, [operators, operand]) => {
        let operator;
        let [multiplicative, ...additive] = operators;
        if ( additive.length ) additive = this._collapseOperators(additive);
        if ( multiplicative ) {
          operator = multiplicative;
          if ( additive === "-" ) operand = this._wrapNegativeTerm(operand);
        }
        else operator = additive;
        if ( typeof operator !== "string" ) error(`Failed to parse ${formula}. Unexpected operator.`);
        const operands = [acc, operand];
        return { class: "Node", formula: `${acc.formula} ${operator} ${operand.formula}`, operands, operator };
      }, head);
    }

    /* -------------------------------------------- */

    /**
     * Handle a dice term.
     * @param {NumericRollParseNode|ParentheticalRollParseNode|null} number  The number of dice.
     * @param {string|NumericRollParseNode|ParentheticalRollParseNode|null} faces  The number of die faces or a string
     *                                                                             denomination like "c" or "f".
     * @param {string|null} modifiers                                        The matched modifiers string.
     * @param {string|null} flavor                                           Associated flavor text.
     * @param {string} formula                                               The original matched text.
     * @returns {DiceRollParseNode}
     * @internal
     * @protected
     */
    _onDiceTerm(number, faces, modifiers, flavor, formula) {
      if ( CONFIG.debug.rollParsing )  {
        console.debug(this.constructor.formatDebug("onDiceTerm", number, faces, modifiers, flavor, formula));
      }
      return { class: "DiceTerm", formula, modifiers, number, faces, evaluated: false, options: { flavor } };
    }

    /* -------------------------------------------- */

    /**
     * Handle a numeric term.
     * @param {number} number  The number.
     * @param {string} flavor  Associated flavor text.
     * @returns {NumericRollParseNode}
     * @internal
     * @protected
     */
    _onNumericTerm(number, flavor) {
      if ( CONFIG.debug.rollParsing ) console.debug(this.constructor.formatDebug("onNumericTerm", number, flavor));
      return {
        class: "NumericTerm", number,
        formula: `${number}${flavor ? `[${flavor}]` : ""}`,
        evaluated: false,
        options: { flavor }
      };
    }

    /* -------------------------------------------- */

    /**
     * Handle a math term.
     * @param {string} fn             The Math function.
     * @param {RollParseNode} head    The first term.
     * @param {RollParseNode[]} tail  Zero or more additional terms.
     * @param {string} flavor         Associated flavor text.
     * @param {string} formula        The original matched text.
     * @returns {FunctionRollParseNode}
     * @internal
     * @protected
     */
    _onFunctionTerm(fn, head, tail, flavor, formula) {
      if ( CONFIG.debug.rollParsing ) {
        console.debug(this.constructor.formatDebug("onFunctionTerm", fn, head, tail, flavor, formula));
      }
      const terms = [];
      if ( head ) terms.push(head, ...tail);
      return { class: "FunctionTerm", fn, terms, formula, evaluated: false, options: { flavor } };
    }

    /* -------------------------------------------- */

    /**
     * Handle a pool term.
     * @param {RollParseNode} head     The first term.
     * @param {RollParseNode[]} tail   Zero or more additional terms.
     * @param {string|null} modifiers  The matched modifiers string.
     * @param {string|null} flavor     Associated flavor text.
     * @param {string} formula         The original matched text.
     * @returns {PoolRollParseNode}
     * @internal
     * @protected
     */
    _onPoolTerm(head, tail, modifiers, flavor, formula) {
      if ( CONFIG.debug.rollParsing ) {
        console.debug(this.constructor.formatDebug("onPoolTerm", head, tail, modifiers, flavor, formula));
      }
      const terms = [];
      if ( head ) terms.push(head, ...tail);
      return { class: "PoolTerm", terms, formula, modifiers, evaluated: false, options: { flavor } };
    }

    /* -------------------------------------------- */

    /**
     * Handle a parenthetical.
     * @param {RollParseNode} term  The inner term.
     * @param {string|null} flavor  Associated flavor text.
     * @param {string} formula      The original matched text.
     * @returns {ParentheticalRollParseNode}
     * @internal
     * @protected
     */
    _onParenthetical(term, flavor, formula) {
      if ( CONFIG.debug.rollParsing ) {
        console.debug(this.constructor.formatDebug("onParenthetical", term, flavor, formula));
      }
      return { class: "ParentheticalTerm", term, formula, evaluated: false, options: { flavor } };
    }

    /* -------------------------------------------- */

    /**
     * Handle some string that failed to be classified.
     * @param {string} term  The term.
     * @param {string|null} [flavor]  Associated flavor text.
     * @returns {StringParseNode}
     * @protected
     */
    _onStringTerm(term, flavor) {
      return { class: "StringTerm", term, evaluated: false, options: { flavor } };
    }

    /* -------------------------------------------- */

    /**
     * Collapse multiple additive operators into a single one.
     * @param {string[]} operators  A sequence of additive operators.
     * @returns {string}
     * @protected
     */
    _collapseOperators(operators) {
      let head = operators.pop();
      for ( const operator of operators ) {
        if ( operator === "-" ) head = head === "+" ? "-" : "+";
      }
      return head;
    }

    /* -------------------------------------------- */

    /**
     * Wrap a term with a leading minus.
     * @param {RollParseNode} term  The term to wrap.
     * @returns {RollParseNode}
     * @protected
     */
    _wrapNegativeTerm(term) {
      // Special case when we have a numeric term, otherwise we wrap it in a parenthetical.
      if ( term.class === "NumericTerm" ) {
        term.number *= -1;
        term.formula = `-${term.formula}`;
        return term;
      }

      return foundry.dice.RollGrammar.parse(`(${term.formula} * -1)`, { parser: this.constructor });
    }

    /* -------------------------------------------- */
    /*  Tree Manipulation                           */
    /* -------------------------------------------- */

    /**
     * Flatten a tree structure (either a parse tree or AST) into an array with operators in infix notation.
     * @param {RollParseNode} root  The root of the tree.
     * @returns {RollParseNode[]}
     */
    static flattenTree(root) {
      const list = [];

      /**
       * Flatten the given node.
       * @param {RollParseNode} node  The node.
       */
      function flattenNode(node) {
        if ( node.class !== "Node" ) {
          list.push(node);
          return;
        }

        const [left, right] = node.operands;
        flattenNode(left);
        list.push({ class: "OperatorTerm", operator: node.operator, evaluated: false });
        flattenNode(right);
      }

      flattenNode(root);
      return list;
    }

    /* -------------------------------------------- */

    /**
     * Use the Shunting Yard algorithm to convert a parse tree or list of terms into an AST with correct operator
     * precedence.
     * See https://en.wikipedia.org/wiki/Shunting_yard_algorithm for a description of the algorithm in detail.
     * @param {RollParseNode|RollTerm[]} root  The root of the parse tree or a list of terms.
     * @returns {RollParseNode}                The root of the AST.
     */
    static toAST(root) {
      // Flatten the parse tree to an array representing the original formula in infix notation.
      const list = Array.isArray(root) ? root : this.flattenTree(root);
      const operators = [];
      const output = [];

      /**
       * Pop operators from the operator stack and push them onto the output stack until we reach an operator with lower
       * or equal precedence and left-associativity.
       * @param {RollParseNode} op  The target operator to push.
       */
      function pushOperator(op) {
        let peek = operators.at(-1);
        // We assume all our operators are left-associative, so we only check if the precedence is lower or equal here.
        while ( peek && ((OperatorTerm.PRECEDENCE[peek.operator] ?? 0) >= (OperatorTerm.PRECEDENCE[op.operator] ?? 0)) ) {
          output.push(operators.pop());
          peek = operators.at(-1);
        }
        operators.push(op);
      }

      for ( const node of list ) {
        // If this is an operator, push it onto the operators stack.
        if ( this.isOperatorTerm(node) ) {
          pushOperator(node);
          continue;
        }

        // Recursively reorganize inner terms to AST sub-trees.
        if ( node.class === "ParentheticalTerm" ) node.term = this.toAST(node.term);
        else if ( (node.class === "FunctionTerm") || (node.class === "PoolTerm") ) {
          node.terms = node.terms.map(term => this.toAST(term));
        }

        // Push the node onto the output stack.
        output.push(node);
      }

      // Pop remaining operators off the operator stack and onto the output stack.
      while ( operators.length ) output.push(operators.pop());

      // The output now contains the formula in postfix notation, with correct operator precedence applied. We recombine
      // it into a tree by matching each postfix operator with two operands.
      const ast = [];
      for ( const node of output ) {
        if ( !this.isOperatorTerm(node) ) {
          ast.push(node);
          continue;
        }
        const right = ast.pop();
        const left = ast.pop();
        ast.push({ class: "Node", operator: node.operator, operands: [left, right] });
      }

      // The postfix array has been recombined into an array of one element, which is the root of the new AST.
      return ast.pop();
    }

    /* -------------------------------------------- */

    /**
     * Determine if a given node is an operator term.
     * @param {RollParseNode|RollTerm} node
     */
    static isOperatorTerm(node) {
      return (node instanceof OperatorTerm) || (node.class === "OperatorTerm");
    }

    /* -------------------------------------------- */
    /*  Debug Formatting                            */
    /* -------------------------------------------- */

    /**
     * Format a list argument.
     * @param {RollParseArg[]} list  The list to format.
     * @returns {string}
     */
    static formatList(list) {
      if ( !list ) return "[]";
      return `[${list.map(RollParser.formatArg).join(", ")}]`;
    }

    /* -------------------------------------------- */

    /**
     * Format a parser argument.
     * @param {RollParseArg} arg  The argument.
     * @returns {string}
     */
    static formatArg(arg) {
      switch ( getType(arg) ) {
        case "null": return "null";
        case "number": return `${arg}`;
        case "string": return `"${arg}"`;
        case "Object": return arg.class;
        case "Array": return RollParser.formatList(arg);
      }
    }

    /* -------------------------------------------- */

    /**
     * Format arguments for debugging.
     * @param {string} method         The method name.
     * @param {...RollParseArg} args  The arguments.
     * @returns {string}
     */
    static formatDebug(method, ...args) {
      return `${method}(${args.map(RollParser.formatArg).join(", ")})`;
    }
  }

  /**
   * A standalone, pure JavaScript implementation of the Mersenne Twister pseudo random number generator.
   *
   * @author Raphael Pigulla <pigulla@four66.com>
   * @version 0.2.3
   * @license
   * Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura,
   * All rights reserved.
   *
   * Redistribution and use in source and binary forms, with or without
   * modification, are permitted provided that the following conditions
   * are met:
   *
   * 1. Redistributions of source code must retain the above copyright
   * notice, this list of conditions and the following disclaimer.
   *
   * 2. Redistributions in binary form must reproduce the above copyright
   * notice, this list of conditions and the following disclaimer in the
   * documentation and/or other materials provided with the distribution.
   *
   * 3. The names of its contributors may not be used to endorse or promote
   * products derived from this software without specific prior written
   * permission.
   *
   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
   * A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
   * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
   * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
   * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
   * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
   * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   */
  class MersenneTwister {
    /**
     * Instantiates a new Mersenne Twister.
     * @param {number} [seed]   The initial seed value, if not provided the current timestamp will be used.
     * @constructor
     */
    constructor(seed) {

      // Initial values
      this.MAX_INT = 4294967296.0;
      this.N = 624;
      this.M = 397;
      this.UPPER_MASK = 0x80000000;
      this.LOWER_MASK = 0x7fffffff;
      this.MATRIX_A = 0x9908b0df;

      // Initialize sequences
      this.mt = new Array(this.N);
      this.mti = this.N + 1;
      this.SEED = this.seed(seed ?? new Date().getTime());
    };

    /**
     * Initializes the state vector by using one unsigned 32-bit integer "seed", which may be zero.
     *
     * @since 0.1.0
     * @param {number} seed The seed value.
     */
    seed(seed) {
      this.SEED = seed;
      let s;
      this.mt[0] = seed >>> 0;

      for (this.mti = 1; this.mti < this.N; this.mti++) {
        s = this.mt[this.mti - 1] ^ (this.mt[this.mti - 1] >>> 30);
        this.mt[this.mti] =
          (((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253) + this.mti;
        this.mt[this.mti] >>>= 0;
      }
      return seed;
    };

    /**
     * Initializes the state vector by using an array key[] of unsigned 32-bit integers of the specified length. If
     * length is smaller than 624, then each array of 32-bit integers gives distinct initial state vector. This is
     * useful if you want a larger seed space than 32-bit word.
     *
     * @since 0.1.0
     * @param {array} vector The seed vector.
     */
    seedArray(vector) {
      let i = 1, j = 0, k = this.N > vector.length ? this.N : vector.length, s;
      this.seed(19650218);
      for (; k > 0; k--) {
        s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30);

        this.mt[i] = (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1664525) << 16) + ((s & 0x0000ffff) * 1664525))) +
          vector[j] + j;
        this.mt[i] >>>= 0;
        i++;
        j++;
        if (i >= this.N) {
          this.mt[0] = this.mt[this.N-1];
          i = 1;
        }
        if (j >= vector.length) {
          j = 0;
        }
      }

      for (k = this.N-1; k; k--) {
        s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30);
        this.mt[i] =
          (this.mt[i] ^ (((((s & 0xffff0000) >>> 16) * 1566083941) << 16) + (s & 0x0000ffff) * 1566083941)) - i;
        this.mt[i] >>>= 0;
        i++;
        if (i >= this.N) {
          this.mt[0] = this.mt[this.N - 1];
          i = 1;
        }
      }
      this.mt[0] = 0x80000000;
    };

    /**
     * Generates a random unsigned 32-bit integer.
     *
     * @since 0.1.0
     * @returns {number}
     */
    int() {
      let y, kk, mag01 = [0, this.MATRIX_A];

      if (this.mti >= this.N) {
        if (this.mti === this.N+1) {
          this.seed(5489);
        }

        for (kk = 0; kk < this.N - this.M; kk++) {
          y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK);
          this.mt[kk] = this.mt[kk + this.M] ^ (y >>> 1) ^ mag01[y & 1];
        }

        for (; kk < this.N - 1; kk++) {
          y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK);
          this.mt[kk] = this.mt[kk + (this.M - this.N)] ^ (y >>> 1) ^ mag01[y & 1];
        }

        y = (this.mt[this.N - 1] & this.UPPER_MASK) | (this.mt[0] & this.LOWER_MASK);
        this.mt[this.N - 1] = this.mt[this.M - 1] ^ (y >>> 1) ^ mag01[y & 1];
        this.mti = 0;
      }

      y = this.mt[this.mti++];

      y ^= (y >>> 11);
      y ^= (y << 7) & 0x9d2c5680;
      y ^= (y << 15) & 0xefc60000;
      y ^= (y >>> 18);

      return y >>> 0;
    };

    /**
     * Generates a random unsigned 31-bit integer.
     *
     * @since 0.1.0
     * @returns {number}
     */
    int31() {
      return this.int() >>> 1;
    };

    /**
     * Generates a random real in the interval [0;1] with 32-bit resolution.
     *
     * @since 0.1.0
     * @returns {number}
     */
    real() {
      return this.int() * (1.0 / (this.MAX_INT - 1));
    };

    /**
     * Generates a random real in the interval ]0;1[ with 32-bit resolution.
     *
     * @since 0.1.0
     * @returns {number}
     */
    realx() {
      return (this.int() + 0.5) * (1.0 / this.MAX_INT);
    };

    /**
     * Generates a random real in the interval [0;1[ with 32-bit resolution.
     *
     * @since 0.1.0
     * @returns {number}
     */
    rnd() {
      return this.int() * (1.0 / this.MAX_INT);
    };

    /**
     * Generates a random real in the interval [0;1[ with 32-bit resolution.
     *
     * Same as .rnd() method - for consistency with Math.random() interface.
     *
     * @since 0.2.0
     * @returns {number}
     */
    random() {
      return this.rnd();
    };

    /**
     * Generates a random real in the interval [0;1[ with 53-bit resolution.
     *
     * @since 0.1.0
     * @returns {number}
     */
    rndHiRes() {
      const a = this.int() >>> 5;
      const b = this.int() >>> 6;
      return (a * 67108864.0 + b) * (1.0 / 9007199254740992.0);
    };

    /**
     * A pseudo-normal distribution using the Box-Muller transform.
     * @param {number} mu     The normal distribution mean
     * @param {number} sigma  The normal distribution standard deviation
     * @returns {number}
     */
    normal(mu, sigma) {
      let u = 0;
      while (u === 0) u = this.random(); // Converting [0,1) to (0,1)
      let v = 0;
      while (v === 0) v = this.random(); // Converting [0,1) to (0,1)
      let n = Math.sqrt( -2.0 * Math.log(u) ) * Math.cos(2.0 * Math.PI * v);
      return (n * sigma) + mu;
    }

    /**
     * A factory method for generating random uniform rolls
     * @returns {number}
     */
    static random() {
      return twist.random();
    }

    /**
     * A factory method for generating random normal rolls
     * @return {number}
     */
    static normal(...args) {
      return twist.normal(...args);
    }
  }

  // Global singleton
  const twist = new MersenneTwister(Date.now());

  /** @module dice */

  var dice = /*#__PURE__*/Object.freeze({
    __proto__: null,
    MersenneTwister: MersenneTwister,
    Roll: Roll$1,
    RollGrammar: grammar,
    RollParser: RollParser,
    terms: _module,
    types: _types
  });

  /** @module types */

  /* ----------------------------------------- */
  /*  Data Model                               */
  /* ----------------------------------------- */

  /**
   * @typedef {Object} DocumentConstructionContext
   * @property {Document|null} [parent=null]    The parent Document of this one, if this one is embedded
   * @property {string|null} [pack=null]        The compendium collection ID which contains this Document, if any
   * @property {boolean} [strict=true]          Whether to validate initial data strictly?
   */

  /* ----------------------------------------- */
  /*  Reusable Type Definitions                */
  /* ----------------------------------------- */

  /**
   * Make all properties in T recursively readonly.
   * @template T
   * @typedef {Readonly<{
   *   [K in keyof T]:
   *     T[K] extends (undefined | null | boolean | number | string | symbol | bigint | Function) ? T[K] :
   *     T[K] extends Array<infer V> ? ReadonlyArray<DeepReadonly<V>> :
   *     T[K] extends Map<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> :
   *     T[K] extends Set<infer V> ? ReadonlySet<DeepReadonly<V>> : DeepReadonly<T[K]>
   * }>} DeepReadonly
   */

  /**
   * A class constructor.
   * Used for functions with generic class constructor parameters.
   * @typedef {new (...args: any[]) => any} Constructor
   */

  /**
   * A single point, expressed as an object {x, y}
   * @typedef {PIXI.Point|{x: number, y: number}} Point
   */

  /**
   * A single point, expressed as an array [x,y]
   * @typedef {number[]} PointArray
   */

  /**
   * A standard rectangle interface.
   * @typedef {{x: number, y: number, width: number, height: number}} Rectangle
   */

  /**
   * @typedef {typeof Number|typeof String|typeof Boolean} BuiltinTypes
   */

  /**
   * @typedef {number|[red: number, green: number, blue: number]|string|Color} ColorSource
   */

  /* ----------------------------------------- */
  /*  Settings Type Definitions                */
  /* ----------------------------------------- */

  /**
   * A Client Setting
   * @typedef {Object} SettingConfig
   * @property {string} key             A unique machine-readable id for the setting
   * @property {string} namespace       The namespace the setting belongs to
   * @property {string} name            The human-readable name
   * @property {string} hint            An additional human-readable hint
   * @property {string} scope           The scope the Setting is stored in, either World or Client
   * @property {boolean} config         Indicates if this Setting should render in the Config application
   * @property {BuiltinTypes|DataField|DataModel} type  The type of data stored by this Setting
   * @property {Object} [choices]       For string Types, defines the allowable values
   * @property {Object} [range]         For numeric Types, defines the allowable range
   * @property {any} [default]          The default value
   * @property {function} [onChange]    Executes when the value of this Setting changes
   * @property {CustomFormInput} [input] A custom form field input used in conjunction with a DataField type
   */

  /**
   * A Client Setting Submenu
   * @typedef {Object} SettingSubmenuConfig
   * @property {string} name             The human readable name
   * @property {string} label            The human readable label
   * @property {string} hint             An additional human readable hint
   * @property {string} icon             The classname of an Icon to render
   * @property {any} type                 The FormApplication class to render TODO better typing
   * @property {boolean} restricted      If true, only a GM can edit this Setting
   */

  /**
   * A Client Keybinding Action Configuration
   * @typedef {Object} KeybindingActionConfig
   * @property {string} [namespace]                       The namespace within which the action was registered
   * @property {string} name                              The human-readable name
   * @property {string} [hint]                            An additional human-readable hint
   * @property {KeybindingActionBinding[]} [uneditable]   The default bindings that can never be changed nor removed.
   * @property {KeybindingActionBinding[]} [editable]     The default bindings that can be changed by the user.
   * @property {Function} [onDown]                        A function to execute when a key down event occurs. If True is returned, the event is consumed and no further keybinds execute.
   * @property {Function} [onUp]                          A function to execute when a key up event occurs. If True is returned, the event is consumed and no further keybinds execute.
   * @property {boolean} [repeat=false]                   If True, allows Repeat events to execute the Action's onDown. Defaults to false.
   * @property {boolean} [restricted=false]               If true, only a GM can edit and execute this Action
   * @property {string[]} [reservedModifiers]             Modifiers such as [ "CONTROL" ] that can be also pressed when executing this Action. Prevents using one of these modifiers as a Binding.
   * @property {number} [precedence=0]                    The preferred precedence of running this Keybinding Action
   * @property {number} [order]                           The recorded registration order of the action
   */

  /**
   * A Client Keybinding Action Binding
   * @typedef {Object} KeybindingActionBinding
   * @property {number} [index]           A numeric index which tracks this bindings position during form rendering
   * @property {string} key               The KeyboardEvent#code value from https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values
   * @property {string[]} [modifiers]     An array of modifiers keys from KeyboardManager.MODIFIER_KEYS which are required for this binding to be activated
   */

  /**
  * @typedef {Object} KeybindingAction      An action that can occur when a key is pressed
  * @property {string} action               The namespaced machine identifier of the Action
  * @property {string} key                  The Keyboard key
  * @property {string} name                 The human-readable name
  * @property {string[]} requiredModifiers  Required modifiers
  * @property {string[]} optionalModifiers  Optional (reserved) modifiers
  * @property {Function} onDown             The handler that executes onDown
  * @property {Function} onUp               The handler that executes onUp
  * @property {boolean} repeat              If True, allows Repeat events to execute this Action's onDown
  * @property {boolean} restricted          If true, only a GM can execute this Action
  * @property {number} precedence           The registration precedence
  * @property {number} order                The registration order
  */

  /**
   * Keyboard event context
   * @typedef {Object} KeyboardEventContext
   * @property {string} key                  The normalized string key, such as "A"
   * @property {KeyboardEvent} event         The originating keypress event
   * @property {boolean} isShift             Is the Shift modifier being pressed
   * @property {boolean} isControl           Is the Control or Meta modifier being processed
   * @property {boolean} isAlt               Is the Alt modifier being pressed
   * @property {boolean} hasModifier         Are any of the modifiers being pressed
   * @property {string[]} modifiers          A list of string modifiers applied to this context, such as [ "CONTROL" ]
   * @property {boolean} up                  True if the Key is Up, else False if down
   * @property {boolean} repeat              True if the given key is being held down such that it is automatically repeating.
   * @property {string} [action]             The executing Keybinding Action. May be undefined until the action is known.
   */

  /**
   * Connected Gamepad info
   * @typedef {Object} ConnectedGamepad
   * @property {Map<string, Number>} axes         A map of axes values
   * @property {Set.<string>} activeButtons       The Set of pressed Buttons
   */

  /* ----------------------------------------- */
  /*  Socket Requests and Responses            */
  /* ----------------------------------------- */

  /**
   * @typedef {object|object[]|string|string[]} RequestData
   */

  /**
   * @typedef {Object} SocketRequest
   * @property {object} [options]
   * @property {boolean} [broadcast]
   */

  /**
   * @typedef {Object} SocketResponse
   * @property {SocketRequest} request  The initial request
   * @property {Error} [error]          An error, if one occurred
   * @property {string} [status]        The status of the request
   * @property {string} [userId]        The ID of the requesting User
   * @property {RequestData} [data]     Data returned as a result of the request
   */

  var types = /*#__PURE__*/Object.freeze({
    __proto__: null
  });

  /**
   * The Foundry Virtual Tabletop client-side ESModule API.
   * @module foundry
   */


  /* ----------------------------------------- */
  /*  Client-Side Globals                      */
  /* ----------------------------------------- */

  // Global foundry namespace
  globalThis.foundry = {
    CONST: CONST$1,            // Commons
    abstract,
    utils,
    documents,
    packages,
    config,
    prosemirror,
    grid,
    applications,     // Client
    audio,
    canvas: canvas$1,
    helpers,
    data,
    dice
  };
  globalThis.CONST = CONST$1;

  // Specifically expose some global classes
  Object.assign(globalThis, {
    Color: Color$1,
    Collection: Collection,
    ProseMirror: prosemirror,
    Roll: Roll$1
  });

  // Immutable constants
  for ( const c of Object.values(CONST$1) ) {
    Object.freeze(c);
  }

  /* ----------------------------------------- */
  /*  Backwards Compatibility                  */
  /* ----------------------------------------- */

  /** @deprecated since v12 */
  addBackwardsCompatibilityReferences({
    AudioHelper: "audio.AudioHelper",
    AmbientSoundConfig: "applications.sheets.AmbientSoundConfig",
    AmbientLightConfig: "applications.sheets.AmbientLightConfig",
    Sound: "audio.Sound",
    RollTerm: "dice.terms.RollTerm",
    MersenneTwister: "dice.MersenneTwister",
    DiceTerm: "dice.terms.DiceTerm",
    MathTerm: "dice.terms.FunctionTerm",
    NumericTerm: "dice.terms.NumericTerm",
    OperatorTerm: "dice.terms.OperatorTerm",
    ParentheticalTerm: "dice.terms.ParentheticalTerm",
    PoolTerm: "dice.terms.PoolTerm",
    StringTerm: "dice.terms.StringTerm",
    Coin: "dice.terms.Coin",
    Die: "dice.terms.Die",
    FateDie: "dice.terms.FateDie",
    twist: "dice.MersenneTwister",
    LightSource: "canvas.sources.PointLightSource",
    DarknessSource: "canvas.sources.PointDarknessSource",
    GlobalLightSource: "canvas.sources.GlobalLightSource",
    VisionSource: "canvas.sources.PointVisionSource",
    SoundSource: "canvas.sources.PointSoundSource",
    MovementSource: "canvas.sources.PointMovementSource",
    PermissionConfig: "applications.apps.PermissionConfig",
    BaseGrid: "grid.GridlessGrid",
    SquareGrid: "grid.SquareGrid",
    HexagonalGrid: "grid.HexagonalGrid",
    GridHex: "grid.GridHex",
    UserConfig: "applications.sheets.UserConfig",
    WordTree: "utils.WordTree"
  }, {since: 12, until: 14});

  /** @deprecated since v12 */
  for ( let [k, v] of Object.entries(utils) ) {
    if ( !(k in globalThis) ) {
      Object.defineProperty(globalThis, k, {
        get() {
          foundry.utils.logCompatibilityWarning(`You are accessing globalThis.${k} which must now be accessed via `
            + `foundry.utils.${k}`, {since: 12, until: 14, once: true});
          return v;
        }
      });
    }
  }

  /**
   * Add Foundry Virtual Tabletop ESModule exports to the global scope for backwards compatibility
   * @param {object} mapping      A mapping of class name to ESModule export path
   * @param {object} [options]    Options which modify compatible references
   * @param {number} [options.since]  Deprecated since generation
   * @param {number} [options.until]  Backwards compatibility provided until generation
   */
  function addBackwardsCompatibilityReferences(mapping, {since, until}={}) {
    const properties = Object.fromEntries(Object.entries(mapping).map(([name, path]) => {
      return [name, {
        get() {
          foundry.utils.logCompatibilityWarning(`You are accessing the global "${name}" which is now namespaced under `
            + `foundry.${path}`, {since, until, once: true});
          return foundry.utils.getProperty(globalThis.foundry, path);
        }
      }]
    }));
    Object.defineProperties(globalThis, properties);
  }

  /* ----------------------------------------- */
  /*  Dispatch Ready Event                     */
  /* ----------------------------------------- */

  if ( globalThis.window ) {
    console.log(`${vtt$1} | Foundry Virtual Tabletop ESModule loaded`);
    const ready = new Event("FoundryFrameworkLoaded");
    globalThis.dispatchEvent(ready);
  }

  exports.CONST = CONST$1;
  exports.abstract = abstract;
  exports.applications = applications;
  exports.audio = audio;
  exports.canvas = canvas$1;
  exports.config = config;
  exports.data = data;
  exports.dice = dice;
  exports.documents = documents;
  exports.grid = grid;
  exports.helpers = helpers;
  exports.packages = packages;
  exports.prosemirror = prosemirror;
  exports.types = types;
  exports.utils = utils;

  return exports;

})({});
