"use strict";

const DOMException = require("domexception");

const EventTargetImpl = require("../events/EventTarget-impl").implementation;
const { simultaneousIterators } = require("../../utils");
const NODE_TYPE = require("../node-type");
const NODE_DOCUMENT_POSITION = require("../node-document-position");
const NodeList = require("../generated/NodeList");
const { clone, locateNamespacePrefix, locateNamespace } = require("../node");
const attributes = require("../attributes");

const { domSymbolTree } = require("../helpers/internal-constants");
const { documentBaseURLSerialized } = require("../helpers/document-base-url");
const { queueTreeMutationRecord } = require("../helpers/mutation-observers");
const {
  isShadowRoot, getRoot, shadowIncludingRoot, assignSlot, assignSlotableForTree, assignSlotable,
  signalSlotChange, isSlot
} = require("../helpers/shadow-dom");

function isObsoleteNodeType(node) {
  return node.nodeType === NODE_TYPE.ENTITY_NODE ||
    node.nodeType === NODE_TYPE.ENTITY_REFERENCE_NODE ||
    node.nodeType === NODE_TYPE.NOTATION_NODE ||
  //  node.nodeType === NODE_TYPE.ATTRIBUTE_NODE ||  // this is missing how do we handle?
    node.nodeType === NODE_TYPE.CDATA_SECTION_NODE;
}

function nodeEquals(a, b) {
  if (a.nodeType !== b.nodeType) {
    return false;
  }

  switch (a.nodeType) {
    case NODE_TYPE.DOCUMENT_TYPE_NODE:
      if (a.name !== b.name || a.publicId !== b.publicId ||
          a.systemId !== b.systemId) {
        return false;
      }
      break;
    case NODE_TYPE.ELEMENT_NODE:
      if (a._namespaceURI !== b._namespaceURI || a._prefix !== b._prefix || a._localName !== b._localName ||
          a._attributes.length !== b._attributes.length) {
        return false;
      }
      break;
    case NODE_TYPE.PROCESSING_INSTRUCTION_NODE:
      if (a._target !== b._target || a._data !== b._data) {
        return false;
      }
      break;
    case NODE_TYPE.TEXT_NODE:
    case NODE_TYPE.COMMENT_NODE:
      if (a._data !== b._data) {
        return false;
      }
      break;
  }

  if (a.nodeType === NODE_TYPE.ELEMENT_NODE && !attributes.attributeListsEqual(a, b)) {
    return false;
  }

  for (const nodes of simultaneousIterators(domSymbolTree.childrenIterator(a), domSymbolTree.childrenIterator(b))) {
    if (!nodes[0] || !nodes[1]) {
      // mismatch in the amount of childNodes
      return false;
    }

    if (!nodeEquals(nodes[0], nodes[1])) {
      return false;
    }
  }

  return true;
}

// https://dom.spec.whatwg.org/#concept-tree-host-including-inclusive-ancestor
function isHostInclusiveAncestor(nodeImplA, nodeImplB) {
  for (const ancestor of domSymbolTree.ancestorsIterator(nodeImplB)) {
    if (ancestor === nodeImplA) {
      return true;
    }
  }

  const rootImplB = getRoot(nodeImplB);
  if (rootImplB._host) {
    return isHostInclusiveAncestor(nodeImplA, rootImplB._host);
  }

  return false;
}

class NodeImpl extends EventTargetImpl {
  constructor(args, privateData) {
    super();

    domSymbolTree.initialize(this);

    this._ownerDocument = privateData.ownerDocument;

    this._childNodesList = null;
    this._childrenList = null;
    this._version = 0;
    this._memoizedQueries = {};
    this._registeredObserverList = [];
  }

  _getTheParent() {
    if (this._assignedSlot) {
      return this._assignedSlot;
    }

    return domSymbolTree.parent(this);
  }

  get parentNode() {
    return domSymbolTree.parent(this);
  }

  getRootNode(options) {
    return options.composed ? shadowIncludingRoot(this) : getRoot(this);
  }

  get nodeName() {
    switch (this.nodeType) {
      case NODE_TYPE.ELEMENT_NODE:
        return this.tagName;
      case NODE_TYPE.TEXT_NODE:
        return "#text";
      case NODE_TYPE.CDATA_SECTION_NODE:
        return "#cdata-section";
      case NODE_TYPE.PROCESSING_INSTRUCTION_NODE:
        return this.target;
      case NODE_TYPE.COMMENT_NODE:
        return "#comment";
      case NODE_TYPE.DOCUMENT_NODE:
        return "#document";
      case NODE_TYPE.DOCUMENT_TYPE_NODE:
        return this.name;
      case NODE_TYPE.DOCUMENT_FRAGMENT_NODE:
        return "#document-fragment";
    }

    // should never happen
    return null;
  }

  get firstChild() {
    return domSymbolTree.firstChild(this);
  }

  // https://dom.spec.whatwg.org/#connected
  // https://dom.spec.whatwg.org/#dom-node-isconnected
  get isConnected() {
    const root = shadowIncludingRoot(this);
    return root && root.nodeType === NODE_TYPE.DOCUMENT_NODE;
  }

  get ownerDocument() {
    return this.nodeType === NODE_TYPE.DOCUMENT_NODE ? null : this._ownerDocument;
  }

  get lastChild() {
    return domSymbolTree.lastChild(this);
  }

  get childNodes() {
    if (!this._childNodesList) {
      this._childNodesList = NodeList.createImpl([], {
        element: this,
        query: () => domSymbolTree.childrenToArray(this)
      });
    } else {
      this._childNodesList._update();
    }

    return this._childNodesList;
  }

  get nextSibling() {
    return domSymbolTree.nextSibling(this);
  }

  get previousSibling() {
    return domSymbolTree.previousSibling(this);
  }

  _modified() {
    this._version++;
    for (const ancestor of domSymbolTree.ancestorsIterator(this)) {
      ancestor._version++;
    }

    if (this._childrenList) {
      this._childrenList._update();
    }
    if (this._childNodesList) {
      this._childNodesList._update();
    }
    this._clearMemoizedQueries();
  }

  _childTextContentChangeSteps() {
    // Default: do nothing
  }

  _clearMemoizedQueries() {
    this._memoizedQueries = {};
    const myParent = domSymbolTree.parent(this);
    if (myParent) {
      myParent._clearMemoizedQueries();
    }
  }

  _descendantRemoved(parent, child) {
    const myParent = domSymbolTree.parent(this);
    if (myParent) {
      myParent._descendantRemoved(parent, child);
    }
  }

  _descendantAdded(parent, child) {
    const myParent = domSymbolTree.parent(this);
    if (myParent) {
      myParent._descendantAdded(parent, child);
    }
  }

  _attach() {
    this._attached = true;

    for (const child of domSymbolTree.childrenIterator(this)) {
      if (child._attach) {
        child._attach();
      }
    }
  }

  _detach() {
    this._attached = false;

    if (this._ownerDocument && this._ownerDocument._lastFocusedElement === this) {
      this._ownerDocument._lastFocusedElement = null;
    }

    for (const child of domSymbolTree.childrenIterator(this)) {
      if (child._detach) {
        child._detach();
      }
    }
  }

  hasChildNodes() {
    return domSymbolTree.hasChildren(this);
  }

  normalize() {
    for (const child of domSymbolTree.childrenIterator(this)) {
      if (child.normalize) {
        child.normalize();
      }

      // Normalize should only transform Text nodes, and nothing else.
      if (child.nodeType !== NODE_TYPE.TEXT_NODE) {
        continue;
      }

      if (child.nodeValue === "") {
        this._remove(child);
        continue;
      }

      const prevChild = domSymbolTree.previousSibling(child);

      if (prevChild && prevChild.nodeType === NODE_TYPE.TEXT_NODE) {
        // merge text nodes
        prevChild.appendData(child.nodeValue);
        this._remove(child);
      }
    }
  }

  get parentElement() {
    const parentNode = domSymbolTree.parent(this);
    return parentNode !== null && parentNode.nodeType === NODE_TYPE.ELEMENT_NODE ? parentNode : null;
  }

  get baseURI() {
    return documentBaseURLSerialized(this._ownerDocument);
  }

  compareDocumentPosition(otherImpl) {
    // Let reference be the context object.
    const reference = this;

    if (isObsoleteNodeType(reference) || isObsoleteNodeType(otherImpl)) {
      throw new Error("Obsolete node type");
    }

    const result = domSymbolTree.compareTreePosition(reference, otherImpl);

    // “If other and reference are not in the same tree, return the result of adding DOCUMENT_POSITION_DISCONNECTED,
    //  DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, and either DOCUMENT_POSITION_PRECEDING or
    // DOCUMENT_POSITION_FOLLOWING, with the constraint that this is to be consistent, together.”
    if (result === NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_DISCONNECTED) {
      // symbol-tree does not add these bits required by the spec:
      return NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_DISCONNECTED |
        NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC |
        NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_FOLLOWING;
    }

    return result;
  }

  lookupPrefix(namespace) {
    if (namespace === null || namespace === "") {
      return null;
    }

    switch (this.nodeType) {
      case NODE_TYPE.ELEMENT_NODE: {
        return locateNamespacePrefix(this, namespace);
      }
      case NODE_TYPE.DOCUMENT_NODE: {
        return this.documentElement !== null ? locateNamespacePrefix(this.documentElement, namespace) : null;
      }
      case NODE_TYPE.DOCUMENT_TYPE_NODE:
      case NODE_TYPE.DOCUMENT_FRAGMENT_NODE: {
        return null;
      }
      case NODE_TYPE.ATTRIBUTE_NODE: {
        return this._element !== null ? locateNamespacePrefix(this._element, namespace) : null;
      }
      default: {
        return this.parentElement !== null ? locateNamespacePrefix(this.parentElement, namespace) : null;
      }
    }
  }

  lookupNamespaceURI(prefix) {
    if (prefix === "") {
      prefix = null;
    }

    return locateNamespace(this, prefix);
  }

  isDefaultNamespace(namespace) {
    if (namespace === "") {
      namespace = null;
    }

    const defaultNamespace = locateNamespace(this, null);
    return defaultNamespace === namespace;
  }

  contains(other) {
    if (other === null) {
      return false;
    } else if (this === other) {
      return true;
    }
    return Boolean(this.compareDocumentPosition(other) & NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_CONTAINED_BY);
  }

  isEqualNode(node) {
    if (node === null) {
      return false;
    }

    // Fast-path, not in the spec
    if (this === node) {
      return true;
    }

    return nodeEquals(this, node);
  }

  isSameNode(node) {
    if (this === node) {
      return true;
    }

    return false;
  }

  cloneNode(deep) {
    if (isShadowRoot(this)) {
      throw new DOMException("ShadowRoot nodes are not clonable.", "NotSupportedError");
    }

    deep = Boolean(deep);

    return clone(this, undefined, deep);
  }

  get nodeValue() {
    switch (this.nodeType) {
      case NODE_TYPE.ATTRIBUTE_NODE: {
        return this._value;
      }
      case NODE_TYPE.TEXT_NODE:
      case NODE_TYPE.CDATA_SECTION_NODE: // CDATASection is a subclass of Text
      case NODE_TYPE.PROCESSING_INSTRUCTION_NODE:
      case NODE_TYPE.COMMENT_NODE: {
        return this._data;
      }
      default: {
        return null;
      }
    }
  }

  set nodeValue(value) {
    if (value === null) {
      value = "";
    }

    switch (this.nodeType) {
      case NODE_TYPE.ATTRIBUTE_NODE: {
        attributes.setAnExistingAttributeValue(this, value);
        break;
      }
      case NODE_TYPE.TEXT_NODE:
      case NODE_TYPE.CDATA_SECTION_NODE: // CDATASection is a subclass of Text
      case NODE_TYPE.PROCESSING_INSTRUCTION_NODE:
      case NODE_TYPE.COMMENT_NODE: {
        this.replaceData(0, this.length, value);
        break;
      }
    }
  }

  get textContent() {
    switch (this.nodeType) {
      case NODE_TYPE.DOCUMENT_FRAGMENT_NODE:
      case NODE_TYPE.ELEMENT_NODE: {
        let text = "";
        for (const child of domSymbolTree.treeIterator(this)) {
          if (child.nodeType === NODE_TYPE.TEXT_NODE || child.nodeType === NODE_TYPE.CDATA_SECTION_NODE) {
            text += child.nodeValue;
          }
        }
        return text;
      }

      case NODE_TYPE.ATTRIBUTE_NODE: {
        return this._value;
      }

      case NODE_TYPE.TEXT_NODE:
      case NODE_TYPE.CDATA_SECTION_NODE: // CDATASection is a subclass of Text
      case NODE_TYPE.PROCESSING_INSTRUCTION_NODE:
      case NODE_TYPE.COMMENT_NODE: {
        return this._data;
      }

      default: {
        return null;
      }
    }
  }

  set textContent(value) {
    switch (this.nodeType) {
      case NODE_TYPE.DOCUMENT_FRAGMENT_NODE:
      case NODE_TYPE.ELEMENT_NODE: {
        let nodeImpl = null;

        if (value !== null && value !== "") {
          nodeImpl = this._ownerDocument.createTextNode(value);
        }

        this._replaceAll(nodeImpl);
        break;
      }

      case NODE_TYPE.ATTRIBUTE_NODE: {
        attributes.setAnExistingAttributeValue(this, value);
        break;
      }

      case NODE_TYPE.TEXT_NODE:
      case NODE_TYPE.CDATA_SECTION_NODE: // CDATASection is a subclass of Text
      case NODE_TYPE.PROCESSING_INSTRUCTION_NODE:
      case NODE_TYPE.COMMENT_NODE: {
        this.replaceData(0, this.length, value);
        break;
      }
    }
  }

  // https://dom.spec.whatwg.org/#dom-node-insertbefore
  insertBefore(nodeImpl, childImpl) {
    return this._preInsert(nodeImpl, childImpl);
  }

  // https://dom.spec.whatwg.org/#dom-node-appendchild
  appendChild(nodeImpl) {
    return this._append(nodeImpl);
  }

  // https://dom.spec.whatwg.org/#dom-node-replacechild
  replaceChild(nodeImpl, childImpl) {
    return this._replace(nodeImpl, childImpl);
  }

  // https://dom.spec.whatwg.org/#dom-node-removechild
  removeChild(oldChildImpl) {
    return this._preRemove(oldChildImpl);
  }

  // https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
  _preInsertValidity(nodeImpl, childImpl) {
    const { nodeType, nodeName } = nodeImpl;
    const { nodeType: parentType, nodeName: parentName } = this;

    if (
      parentType !== NODE_TYPE.DOCUMENT_NODE &&
      parentType !== NODE_TYPE.DOCUMENT_FRAGMENT_NODE &&
      parentType !== NODE_TYPE.ELEMENT_NODE
    ) {
      throw new DOMException(`Node can't be inserted in a ${parentName} parent.`, "HierarchyRequestError");
    }

    if (isHostInclusiveAncestor(nodeImpl, this)) {
      throw new DOMException("The operation would yield an incorrect node tree.", "HierarchyRequestError");
    }

    if (childImpl && domSymbolTree.parent(childImpl) !== this) {
      throw new DOMException("The child can not be found in the parent.", "NotFoundError");
    }

    if (
      nodeType !== NODE_TYPE.DOCUMENT_FRAGMENT_NODE &&
      nodeType !== NODE_TYPE.DOCUMENT_TYPE_NODE &&
      nodeType !== NODE_TYPE.ELEMENT_NODE &&
      nodeType !== NODE_TYPE.TEXT_NODE &&
      nodeType !== NODE_TYPE.CDATA_SECTION_NODE && // CData section extends from Text
      nodeType !== NODE_TYPE.PROCESSING_INSTRUCTION_NODE &&
      nodeType !== NODE_TYPE.COMMENT_NODE
    ) {
      throw new DOMException(`${nodeName} node can't be inserted in parent node.`, "HierarchyRequestError");
    }

    if (
      (nodeType === NODE_TYPE.TEXT_NODE && parentType === NODE_TYPE.DOCUMENT_NODE) ||
      (nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE && parentType !== NODE_TYPE.DOCUMENT_NODE)
    ) {
      throw new DOMException(`${nodeName} node can't be inserted in ${parentName} parent.`, "HierarchyRequestError");
    }

    if (parentType === NODE_TYPE.DOCUMENT_NODE) {
      const nodeChildren = domSymbolTree.childrenToArray(nodeImpl);
      const parentChildren = domSymbolTree.childrenToArray(this);

      switch (nodeType) {
        case NODE_TYPE.DOCUMENT_FRAGMENT_NODE: {
          const nodeChildrenElements = nodeChildren.filter(child => child.nodeType === NODE_TYPE.ELEMENT_NODE);
          if (nodeChildrenElements.length > 1) {
            throw new DOMException(
              `Invalid insertion of ${nodeName} node in ${parentName} node.`,
              "HierarchyRequestError"
            );
          }

          const hasNodeTextChildren = nodeChildren.some(child => child.nodeType === NODE_TYPE.TEXT_NODE);
          if (hasNodeTextChildren) {
            throw new DOMException(
              `Invalid insertion of ${nodeName} node in ${parentName} node.`,
              "HierarchyRequestError"
            );
          }

          if (
            nodeChildrenElements.length === 1 &&
            (
              parentChildren.some(child => child.nodeType === NODE_TYPE.ELEMENT_NODE) ||
              (childImpl && childImpl.nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE) ||
              (
                childImpl &&
                domSymbolTree.nextSibling(childImpl) &&
                domSymbolTree.nextSibling(childImpl).nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE
              )
            )
          ) {
            throw new DOMException(
              `Invalid insertion of ${nodeName} node in ${parentName} node.`,
              "HierarchyRequestError"
            );
          }
          break;
        }

        case NODE_TYPE.ELEMENT_NODE:
          if (
            parentChildren.some(child => child.nodeType === NODE_TYPE.ELEMENT_NODE) ||
            (childImpl && childImpl.nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE) ||
            (
              childImpl &&
              domSymbolTree.nextSibling(childImpl) &&
              domSymbolTree.nextSibling(childImpl).nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE
            )
          ) {
            throw new DOMException(
              `Invalid insertion of ${nodeName} node in ${parentName} node.`,
              "HierarchyRequestError"
            );
          }
          break;

        case NODE_TYPE.DOCUMENT_TYPE_NODE:
          if (
            parentChildren.some(child => child.nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE) ||
            (
              childImpl &&
              domSymbolTree.previousSibling(childImpl) &&
              domSymbolTree.previousSibling(childImpl).nodeType === NODE_TYPE.ELEMENT_NODE
            ) ||
            (!childImpl && parentChildren.some(child => child.nodeType === NODE_TYPE.ELEMENT_NODE))
          ) {
            throw new DOMException(
              `Invalid insertion of ${nodeName} node in ${parentName} node.`,
              "HierarchyRequestError"
            );
          }
          break;
      }
    }
  }

  // https://dom.spec.whatwg.org/#concept-node-pre-insert
  _preInsert(nodeImpl, childImpl) {
    this._preInsertValidity(nodeImpl, childImpl);

    let referenceChildImpl = childImpl;
    if (referenceChildImpl === nodeImpl) {
      referenceChildImpl = domSymbolTree.nextSibling(nodeImpl);
    }

    this._ownerDocument._adoptNode(nodeImpl);

    this._insert(nodeImpl, referenceChildImpl);

    return nodeImpl;
  }

  // https://dom.spec.whatwg.org/#concept-node-insert
  _insert(nodeImpl, childImpl, suppressObservers) {
    const nodesImpl = nodeImpl.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE ?
      domSymbolTree.childrenToArray(nodeImpl) :
      [nodeImpl];

    if (nodeImpl.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE) {
      let grandChildImpl;
      while ((grandChildImpl = domSymbolTree.firstChild(nodeImpl))) {
        nodeImpl._remove(grandChildImpl, true);
      }
    }

    if (nodeImpl.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE) {
      queueTreeMutationRecord(nodeImpl, [], nodesImpl, null, null);
    }

    const previousChildImpl = childImpl ?
      domSymbolTree.previousSibling(childImpl) :
      domSymbolTree.lastChild(this);

    for (const node of nodesImpl) {
      if (!childImpl) {
        domSymbolTree.appendChild(this, node);
      } else {
        domSymbolTree.insertBefore(childImpl, node);
      }

      if (
        (this.nodeType === NODE_TYPE.ELEMENT_NODE && this._shadowRoot !== null) &&
        (node.nodeType === NODE_TYPE.ELEMENT_NODE || node.nodeType === NODE_TYPE.TEXT_NODE)
      ) {
        assignSlot(node);
      }

      this._modified();

      if (node.nodeType === NODE_TYPE.TEXT_NODE ||
          node.nodeType === NODE_TYPE.CDATA_SECTION_NODE) {
        this._childTextContentChangeSteps();
      }

      if (isSlot(this) && this._assignedNodes.length === 0 && isShadowRoot(getRoot(this))) {
        signalSlotChange(this);
      }

      const root = getRoot(node);
      if (isShadowRoot(root)) {
        assignSlotableForTree(root);
      }

      if (this._attached && nodeImpl._attach) {
        node._attach();
      }

      this._descendantAdded(this, node);
    }

    if (!suppressObservers) {
      queueTreeMutationRecord(this, nodesImpl, [], previousChildImpl, childImpl);
    }
  }

  // https://dom.spec.whatwg.org/#concept-node-append
  _append(nodeImpl) {
    return this._preInsert(nodeImpl, null);
  }

  // https://dom.spec.whatwg.org/#concept-node-replace
  _replace(nodeImpl, childImpl) {
    const { nodeType, nodeName } = nodeImpl;
    const { nodeType: parentType, nodeName: parentName } = this;

    // Note: This section differs from the pre-insert validation algorithm.
    if (
      parentType !== NODE_TYPE.DOCUMENT_NODE &&
      parentType !== NODE_TYPE.DOCUMENT_FRAGMENT_NODE &&
      parentType !== NODE_TYPE.ELEMENT_NODE
    ) {
      throw new DOMException(`Node can't be inserted in a ${parentName} parent.`, "HierarchyRequestError");
    }

    if (isHostInclusiveAncestor(nodeImpl, this)) {
      throw new DOMException("The operation would yield an incorrect node tree.", "HierarchyRequestError");
    }

    if (childImpl && domSymbolTree.parent(childImpl) !== this) {
      throw new DOMException("The child can not be found in the parent.", "NotFoundError");
    }

    if (
      nodeType !== NODE_TYPE.DOCUMENT_FRAGMENT_NODE &&
      nodeType !== NODE_TYPE.DOCUMENT_TYPE_NODE &&
      nodeType !== NODE_TYPE.ELEMENT_NODE &&
      nodeType !== NODE_TYPE.TEXT_NODE &&
      nodeType !== NODE_TYPE.CDATA_SECTION_NODE && // CData section extends from Text
      nodeType !== NODE_TYPE.PROCESSING_INSTRUCTION_NODE &&
      nodeType !== NODE_TYPE.COMMENT_NODE
    ) {
      throw new DOMException(`${nodeName} node can't be inserted in parent node.`, "HierarchyRequestError");
    }

    if (
      (nodeType === NODE_TYPE.TEXT_NODE && parentType === NODE_TYPE.DOCUMENT_NODE) ||
      (nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE && parentType !== NODE_TYPE.DOCUMENT_NODE)
    ) {
      throw new DOMException(`${nodeName} node can't be inserted in ${parentName} parent.`, "HierarchyRequestError");
    }

    if (parentType === NODE_TYPE.DOCUMENT_NODE) {
      const nodeChildren = domSymbolTree.childrenToArray(nodeImpl);
      const parentChildren = domSymbolTree.childrenToArray(this);

      switch (nodeType) {
        case NODE_TYPE.DOCUMENT_FRAGMENT_NODE: {
          const nodeChildrenElements = nodeChildren.filter(child => child.nodeType === NODE_TYPE.ELEMENT_NODE);
          if (nodeChildrenElements.length > 1) {
            throw new DOMException(
              `Invalid insertion of ${nodeName} node in ${parentName} node.`,
              "HierarchyRequestError"
            );
          }

          const hasNodeTextChildren = nodeChildren.some(child => child.nodeType === NODE_TYPE.TEXT_NODE);
          if (hasNodeTextChildren) {
            throw new DOMException(
              `Invalid insertion of ${nodeName} node in ${parentName} node.`,
              "HierarchyRequestError"
            );
          }

          const parentChildElements = parentChildren.filter(child => child.nodeType === NODE_TYPE.ELEMENT_NODE);
          if (
            nodeChildrenElements.length === 1 &&
            (
              (parentChildElements.length === 1 && parentChildElements[0] !== childImpl) ||
              (
                childImpl &&
                domSymbolTree.nextSibling(childImpl) &&
                domSymbolTree.nextSibling(childImpl).nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE
              )
            )
          ) {
            throw new DOMException(
              `Invalid insertion of ${nodeName} node in ${parentName} node.`,
              "HierarchyRequestError"
            );
          }
          break;
        }

        case NODE_TYPE.ELEMENT_NODE:
          if (
            parentChildren.some(child => child.nodeType === NODE_TYPE.ELEMENT_NODE && child !== childImpl) ||
            (
              childImpl &&
              domSymbolTree.nextSibling(childImpl) &&
              domSymbolTree.nextSibling(childImpl).nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE
            )
          ) {
            throw new DOMException(
              `Invalid insertion of ${nodeName} node in ${parentName} node.`,
              "HierarchyRequestError"
            );
          }
          break;

        case NODE_TYPE.DOCUMENT_TYPE_NODE:
          if (
            parentChildren.some(child => child.nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE && child !== childImpl) ||
            (
              childImpl &&
              domSymbolTree.previousSibling(childImpl) &&
              domSymbolTree.previousSibling(childImpl).nodeType === NODE_TYPE.ELEMENT_NODE
            )
          ) {
            throw new DOMException(
              `Invalid insertion of ${nodeName} node in ${parentName} node.`,
              "HierarchyRequestError"
            );
          }
          break;
      }
    }

    let referenceChildImpl = domSymbolTree.nextSibling(childImpl);
    if (referenceChildImpl === nodeImpl) {
      referenceChildImpl = domSymbolTree.nextSibling(nodeImpl);
    }

    const previousSiblingImpl = domSymbolTree.previousSibling(childImpl);

    this._ownerDocument._adoptNode(nodeImpl);

    let removedNodesImpl = [];

    if (domSymbolTree.parent(childImpl)) {
      removedNodesImpl = [childImpl];
      this._remove(childImpl, true);
    }

    const nodesImpl = nodeImpl.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE ?
      domSymbolTree.childrenToArray(nodeImpl) :
      [nodeImpl];

    this._insert(nodeImpl, referenceChildImpl, true);

    queueTreeMutationRecord(this, nodesImpl, removedNodesImpl, previousSiblingImpl, referenceChildImpl);

    return childImpl;
  }

  // https://dom.spec.whatwg.org/#concept-node-replace-all
  _replaceAll(nodeImpl) {
    if (nodeImpl !== null) {
      this._ownerDocument._adoptNode(nodeImpl);
    }

    const removedNodesImpl = domSymbolTree.childrenToArray(this);

    let addedNodesImpl;
    if (nodeImpl === null) {
      addedNodesImpl = [];
    } else if (nodeImpl.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE) {
      addedNodesImpl = domSymbolTree.childrenToArray(nodeImpl);
    } else {
      addedNodesImpl = [nodeImpl];
    }

    for (const childImpl of domSymbolTree.childrenIterator(this)) {
      this._remove(childImpl, true);
    }

    if (nodeImpl) {
      this._insert(nodeImpl, null, true);
    }

    queueTreeMutationRecord(this, addedNodesImpl, removedNodesImpl, null, null);
  }

  // https://dom.spec.whatwg.org/#concept-node-pre-remove
  _preRemove(childImpl) {
    if (domSymbolTree.parent(childImpl) !== this) {
      throw new DOMException("The node to be removed is not a child of this node.", "NotFoundError");
    }

    this._remove(childImpl);

    return childImpl;
  }

  // https://dom.spec.whatwg.org/#concept-node-remove
  _remove(nodeImpl, suppressObservers) {
    if (this._ownerDocument) {
      this._ownerDocument._runPreRemovingSteps(nodeImpl);
    }

    const oldPreviousSiblingImpl = domSymbolTree.previousSibling(nodeImpl);
    const oldNextSiblingImpl = domSymbolTree.nextSibling(nodeImpl);

    domSymbolTree.remove(nodeImpl);

    if (nodeImpl._assignedSlot) {
      assignSlotable(nodeImpl._assignedSlot);
    }

    if (isSlot(this) && this._assignedNodes.length === 0 && isShadowRoot(getRoot(this))) {
      signalSlotChange(this);
    }

    let hasSlotDescendant = isSlot(nodeImpl);
    if (!hasSlotDescendant) {
      for (const child of domSymbolTree.treeIterator(nodeImpl)) {
        if (isSlot(child)) {
          hasSlotDescendant = true;
          break;
        }
      }
    }

    if (hasSlotDescendant) {
      assignSlotableForTree(getRoot(this));
      assignSlotableForTree(nodeImpl);
    }

    this._modified();
    nodeImpl._detach();
    this._descendantRemoved(this, nodeImpl);

    if (!suppressObservers) {
      queueTreeMutationRecord(this, [], [nodeImpl], oldPreviousSiblingImpl, oldNextSiblingImpl);
    }

    if (nodeImpl.nodeType === NODE_TYPE.TEXT_NODE) {
      this._childTextContentChangeSteps();
    }
  }
}

module.exports = {
  implementation: NodeImpl
};


"use strict";

const idlUtils = require("../generated/utils");
const UIEventInit = require("../generated/UIEventInit");
const EventImpl = require("./Event-impl").implementation;

// Until webidl2js gains support for checking for Window, this would have to do.
function isWindow(val) {
  if (typeof val !== "object") {
    return false;
  }
  const wrapper = idlUtils.wrapperForImpl(val);
  if (typeof wrapper === "object") {
    return wrapper === wrapper._globalProxy;
  }

  // `val` may be either impl or wrapper currently, because webidl2js currently unwraps Window objects (and their global
  // proxies) to their underlying EventTargetImpl during conversion, which is not what we want. But at the same time,
  // some internal usage call this constructor with the actual global proxy.
  return isWindow(idlUtils.implForWrapper(val));
}

class UIEventImpl extends EventImpl {
  constructor(args, privateData) {
    const eventInitDict = args[1];

    // undefined check included so that we can omit the property in internal usage.
    if (eventInitDict && eventInitDict.view !== null && eventInitDict.view !== undefined) {
      if (!isWindow(eventInitDict.view)) {
        throw new TypeError(`Failed to construct '${new.target.name.replace(/Impl$/, "")}': member view is not of ` +
                            "type Window.");
      }
    }

    super(args, privateData);
  }

  initUIEvent(type, bubbles, cancelable, view, detail) {
    if (view !== null) {
      if (!isWindow(view)) {
        throw new TypeError(`Failed to execute 'initUIEvent' on '${this.constructor.name.replace(/Impl$/, "")}': ` +
                            "parameter 4 is not of type 'Window'.");
      }
    }

    if (this._dispatchFlag) {
      return;
    }

    this.initEvent(type, bubbles, cancelable);
    this.view = view;
    this.detail = detail;
  }
}
UIEventImpl.defaultInit = UIEventInit.convert(undefined);

module.exports = {
  implementation: UIEventImpl
};


"use strict";
const request = require("request");
const { EventEmitter } = require("events");
const ProgressEvent = require("./generated/ProgressEvent");
const fs = require("fs");
const { URL } = require("whatwg-url");
const parseDataURL = require("data-urls");

const DOMException = require("domexception");
const xhrSymbols = require("./xmlhttprequest-symbols");
const wrapCookieJarForRequest = require("./helpers/wrap-cookie-jar-for-request");
const { fireAnEvent } = require("./helpers/events");

const headerListSeparatorRegexp = /,[ \t]*/;
const simpleMethods = new Set(["GET", "HEAD", "POST"]);
const simpleHeaders = new Set(["accept", "accept-language", "content-language", "content-type"]);
const preflightHeaders = new Set([
  "access-control-expose-headers",
  "access-control-allow-headers",
  "access-control-allow-credentials",
  "access-control-allow-origin"
]);

function getRequestHeader(requestHeaders, header) {
  const lcHeader = header.toLowerCase();
  const keys = Object.keys(requestHeaders);
  let n = keys.length;
  while (n--) {
    const key = keys[n];
    if (key.toLowerCase() === lcHeader) {
      return requestHeaders[key];
    }
  }
  return null;
}

function updateRequestHeader(requestHeaders, header, newValue) {
  const lcHeader = header.toLowerCase();
  const keys = Object.keys(requestHeaders);
  let n = keys.length;
  while (n--) {
    const key = keys[n];
    if (key.toLowerCase() === lcHeader) {
      requestHeaders[key] = newValue;
    }
  }
}

function mergeHeaders(lhs, rhs) {
  const rhsParts = rhs.split(",");
  const lhsParts = lhs.split(",");
  return rhsParts.concat(lhsParts.filter(p => rhsParts.indexOf(p) < 0)).join(",");
}

function dispatchError(xhr) {
  const errMessage = xhr[xhrSymbols.properties].error;
  requestErrorSteps(xhr, "error", new DOMException(errMessage, "NetworkError"));

  if (xhr._ownerDocument) {
    const error = new Error(errMessage);
    error.type = "XMLHttpRequest"; // TODO this should become "resource loading" when XHR goes through resource loader

    xhr._ownerDocument._defaultView._virtualConsole.emit("jsdomError", error);
  }
}

function validCORSHeaders(xhr, response, flag, properties, origin) {
  const acaoStr = response.headers["access-control-allow-origin"];
  const acao = acaoStr ? acaoStr.trim() : null;
  if (acao !== "*" && acao !== origin) {
    properties.error = "Cross origin " + origin + " forbidden";
    dispatchError(xhr);
    return false;
  }
  const acacStr = response.headers["access-control-allow-credentials"];
  const acac = acacStr ? acacStr.trim() : null;
  if (flag.withCredentials && acac !== "true") {
    properties.error = "Credentials forbidden";
    dispatchError(xhr);
    return false;
  }
  return true;
}

function validCORSPreflightHeaders(xhr, response, flag, properties) {
  if (!validCORSHeaders(xhr, response, flag, properties, properties.origin)) {
    return false;
  }
  const acahStr = response.headers["access-control-allow-headers"];
  const acah = new Set(acahStr ? acahStr.trim().toLowerCase().split(headerListSeparatorRegexp) : []);
  const forbiddenHeaders = Object.keys(flag.requestHeaders).filter(header => {
    const lcHeader = header.toLowerCase();
    return !simpleHeaders.has(lcHeader) && !acah.has(lcHeader);
  });
  if (forbiddenHeaders.length > 0) {
    properties.error = "Headers " + forbiddenHeaders + " forbidden";
    dispatchError(xhr);
    return false;
  }
  return true;
}

function requestErrorSteps(xhr, event, exception) {
  const properties = xhr[xhrSymbols.properties];
  const flag = xhr[xhrSymbols.flag];

  properties.readyState = xhr.DONE;
  properties.send = false;

  setResponseToNetworkError(xhr);

  if (flag.synchronous) {
    throw exception;
  }

  fireAnEvent("readystatechange", xhr);

  if (!properties.uploadComplete) {
    properties.uploadComplete = true;

    if (properties.uploadListener) {
      fireAnEvent(event, xhr.upload, ProgressEvent, { loaded: 0, total: 0, lengthComputable: false });
      fireAnEvent("loadend", xhr.upload, ProgressEvent, { loaded: 0, total: 0, lengthComputable: false });
    }
  }

  fireAnEvent(event, xhr, ProgressEvent, { loaded: 0, total: 0, lengthComputable: false });
  fireAnEvent("loadend", xhr, ProgressEvent, { loaded: 0, total: 0, lengthComputable: false });
}

function setResponseToNetworkError(xhr) {
  const properties = xhr[xhrSymbols.properties];
  properties.responseCache = properties.responseTextCache = properties.responseXMLCache = null;
  properties.responseHeaders = {};
  properties.status = 0;
  properties.statusText = "";
}

// return a "request" client object or an event emitter matching the same behaviour for unsupported protocols
// the callback should be called with a "request" response object or an event emitter matching the same behaviour too
function createClient(xhr) {
  const flag = xhr[xhrSymbols.flag];
  const properties = xhr[xhrSymbols.properties];
  const urlObj = new URL(flag.uri);
  const uri = urlObj.href;
  const ucMethod = flag.method.toUpperCase();

  const { requestManager } = flag;

  if (urlObj.protocol === "file:") {
    const response = new EventEmitter();
    response.statusCode = 200;
    response.rawHeaders = [];
    response.headers = {};
    response.request = { uri: urlObj };
    const filePath = urlObj.pathname
      .replace(/^file:\/\//, "")
      .replace(/^\/([a-z]):\//i, "$1:/")
      .replace(/%20/g, " ");

    const client = new EventEmitter();

    const readableStream = fs.createReadStream(filePath, { encoding: null });

    readableStream.on("data", chunk => {
      response.emit("data", chunk);
      client.emit("data", chunk);
    });

    readableStream.on("end", () => {
      response.emit("end");
      client.emit("end");
    });

    readableStream.on("error", err => {
      client.emit("error", err);
    });

    client.abort = function () {
      readableStream.destroy();
      client.emit("abort");
    };

    if (requestManager) {
      const req = {
        abort() {
          properties.abortError = true;
          xhr.abort();
        }
      };
      requestManager.add(req);
      const rmReq = requestManager.remove.bind(requestManager, req);
      client.on("abort", rmReq);
      client.on("error", rmReq);
      client.on("end", rmReq);
    }

    process.nextTick(() => client.emit("response", response));

    return client;
  }

  if (urlObj.protocol === "data:") {
    const response = new EventEmitter();

    response.request = { uri: urlObj };

    const client = new EventEmitter();

    let buffer;
    try {
      const parsed = parseDataURL(uri);
      const contentType = parsed.mimeType.toString();
      buffer = parsed.body;
      response.statusCode = 200;
      response.rawHeaders = ["Content-Type", contentType];
      response.headers = { "content-type": contentType };
    } catch (err) {
      process.nextTick(() => client.emit("error", err));
      return client;
    }

    client.abort = () => {
      // do nothing
    };

    process.nextTick(() => {
      client.emit("response", response);
      process.nextTick(() => {
        response.emit("data", buffer);
        client.emit("data", buffer);
        response.emit("end");
        client.emit("end");
      });
    });

    return client;
  }

  const requestHeaders = {};

  for (const header in flag.requestHeaders) {
    requestHeaders[header] = flag.requestHeaders[header];
  }

  if (getRequestHeader(flag.requestHeaders, "referer") === null) {
    requestHeaders.Referer = flag.referrer;
  }
  if (getRequestHeader(flag.requestHeaders, "user-agent") === null) {
    requestHeaders["User-Agent"] = flag.userAgent;
  }
  if (getRequestHeader(flag.requestHeaders, "accept-language") === null) {
    requestHeaders["Accept-Language"] = "en";
  }
  if (getRequestHeader(flag.requestHeaders, "accept") === null) {
    requestHeaders.Accept = "*/*";
  }

  const crossOrigin = flag.origin !== urlObj.origin;
  if (crossOrigin) {
    requestHeaders.Origin = flag.origin;
  }

  const options = {
    uri,
    method: flag.method,
    headers: requestHeaders,
    gzip: true,
    maxRedirects: 21,
    followAllRedirects: true,
    encoding: null,
    strictSSL: flag.strictSSL,
    proxy: flag.proxy,
    forever: true
  };
  if (flag.auth) {
    options.auth = {
      user: flag.auth.user || "",
      pass: flag.auth.pass || "",
      sendImmediately: false
    };
  }
  if (flag.cookieJar && (!crossOrigin || flag.withCredentials)) {
    options.jar = wrapCookieJarForRequest(flag.cookieJar);
  }

  const { body } = flag;
  const hasBody = body !== undefined &&
                  body !== null &&
                  body !== "" &&
                  !(ucMethod === "HEAD" || ucMethod === "GET");

  if (hasBody && !flag.formData) {
    options.body = body;
  }

  if (hasBody && getRequestHeader(flag.requestHeaders, "content-type") === null) {
    requestHeaders["Content-Type"] = "text/plain;charset=UTF-8";
  }

  function doRequest() {
    try {
      const client = request(options);

      if (hasBody && flag.formData) {
        const form = client.form();
        for (const entry of body) {
          form.append(entry.name, entry.value, entry.options);
        }
      }

      return client;
    } catch (e) {
      const client = new EventEmitter();
      process.nextTick(() => client.emit("error", e));
      return client;
    }
  }

  let client;

  const nonSimpleHeaders = Object.keys(flag.requestHeaders)
    .filter(header => !simpleHeaders.has(header.toLowerCase()));

  if (crossOrigin && (!simpleMethods.has(ucMethod) || nonSimpleHeaders.length > 0 || properties.uploadListener)) {
    client = new EventEmitter();

    const preflightRequestHeaders = [];
    for (const header in requestHeaders) {
      // the only existing request headers the cors spec allows on the preflight request are Origin and Referrer
      const lcHeader = header.toLowerCase();
      if (lcHeader === "origin" || lcHeader === "referrer") {
        preflightRequestHeaders[header] = requestHeaders[header];
      }
    }

    preflightRequestHeaders["Access-Control-Request-Method"] = flag.method;
    if (nonSimpleHeaders.length > 0) {
      preflightRequestHeaders["Access-Control-Request-Headers"] = nonSimpleHeaders.join(", ");
    }

    preflightRequestHeaders["User-Agent"] = flag.userAgent;

    flag.preflight = true;

    const preflightOptions = {
      uri,
      method: "OPTIONS",
      headers: preflightRequestHeaders,
      followRedirect: false,
      encoding: null,
      pool: flag.pool,
      strictSSL: flag.strictSSL,
      proxy: flag.proxy,
      forever: true
    };

    const preflightClient = request(preflightOptions);

    preflightClient.on("response", resp => {
      // don't send the real request if the preflight request returned an error
      if (resp.statusCode < 200 || resp.statusCode > 299) {
        client.emit("error", new Error("Response for preflight has invalid HTTP status code " + resp.statusCode));
        return;
      }
      // don't send the real request if we aren't allowed to use the headers
      if (!validCORSPreflightHeaders(xhr, resp, flag, properties)) {
        setResponseToNetworkError(xhr);
        return;
      }
      const realClient = doRequest();
      realClient.on("response", res => {
        for (const header in resp.headers) {
          if (preflightHeaders.has(header)) {
            res.headers[header] = Object.prototype.hasOwnProperty.call(res.headers, header) ?
                                  mergeHeaders(res.headers[header], resp.headers[header]) :
                                  resp.headers[header];
          }
        }
        client.emit("response", res);
      });
      realClient.on("data", chunk => client.emit("data", chunk));
      realClient.on("end", () => client.emit("end"));
      realClient.on("abort", () => client.emit("abort"));
      realClient.on("request", req => {
        client.headers = realClient.headers;
        client.emit("request", req);
      });
      realClient.on("redirect", () => {
        client.response = realClient.response;
        client.emit("redirect");
      });
      realClient.on("error", err => client.emit("error", err));
      client.abort = () => {
        realClient.abort();
      };
    });

    preflightClient.on("error", err => client.emit("error", err));

    client.abort = () => {
      preflightClient.abort();
    };
  } else {
    client = doRequest();
  }

  if (requestManager) {
    const req = {
      abort() {
        properties.abortError = true;
        xhr.abort();
      }
    };
    requestManager.add(req);
    const rmReq = requestManager.remove.bind(requestManager, req);
    client.on("abort", rmReq);
    client.on("error", rmReq);
    client.on("end", rmReq);
  }

  return client;
}

exports.headerListSeparatorRegexp = headerListSeparatorRegexp;
exports.simpleHeaders = simpleHeaders;
exports.preflightHeaders = preflightHeaders;
exports.getRequestHeader = getRequestHeader;
exports.updateRequestHeader = updateRequestHeader;
exports.dispatchError = dispatchError;
exports.validCORSHeaders = validCORSHeaders;
exports.requestErrorSteps = requestErrorSteps;
exports.setResponseToNetworkError = setResponseToNetworkError;
exports.createClient = createClient;


"use strict";

const xmlParser = require("./xml");
const htmlParser = require("./html");

// https://w3c.github.io/DOM-Parsing/#dfn-fragment-parsing-algorithm
function parseFragment(markup, contextElement) {
  const { _parsingMode } = contextElement._ownerDocument;

  let parseAlgorithm;
  if (_parsingMode === "html") {
    parseAlgorithm = htmlParser.parseFragment;
  } else if (_parsingMode === "xml") {
    parseAlgorithm = xmlParser.parseFragment;
  }

  // Note: HTML and XML fragment parsing algorithm already return a document fragments; no need to do steps 3 and 4
  return parseAlgorithm(markup, contextElement);
}

function parseIntoDocument(markup, ownerDocument) {
  const { _parsingMode } = ownerDocument;

  let parseAlgorithm;
  if (_parsingMode === "html") {
    parseAlgorithm = htmlParser.parseIntoDocument;
  } else if (_parsingMode === "xml") {
    parseAlgorithm = xmlParser.parseIntoDocument;
  }

  return parseAlgorithm(markup, ownerDocument);
}

module.exports = {
  parseFragment,
  parseIntoDocument
};


"use strict";
/* eslint-disable no-console */
/* eslint-disable no-invalid-this */

function onError(event) {
  const bench = event.target;
  console.error("Error in benchmark", bench.name, ":", bench.error);
}

function onStart() {
  console.log("# " + this.name + " #");
}

function onCycle(event) {
  console.log(String(event.target));
}

function onComplete() {
  if (this.length > 1) {
    const fastest = this.filter("fastest")[0];
    const slowest = this.filter("slowest")[0];
    if (fastest) {
      const relativeDiff = slowest && Math.round(slowest.stats.mean / fastest.stats.mean * 100) / 100;
      console.info("Fastest is \"" +
                   fastest.name.trim() +
                   "\", which is " +
                   relativeDiff +
                   "x faster than the slowest \"" +
                   slowest.name +
                   "\"");
    }
  }

  console.log("");
}

module.exports = function consoleReporter(suite) {
  suite.off("error", onError);
  suite.off("start", onStart);
  suite.off("cycle", onCycle);
  suite.off("complete", onComplete);
  suite.on("error", onError);
  suite.on("start", onStart);
  suite.on("cycle", onCycle);
  suite.on("complete", onComplete);
  return suite;
};


"use strict";
const util = require("util");
const idlUtils = require("../generated/utils");
const ErrorEvent = require("../generated/ErrorEvent");
const { createAnEvent } = require("../helpers/events");

const errorReportingMode = Symbol("error reporting mode");

// https://html.spec.whatwg.org/multipage/webappapis.html#report-the-error
// Omits script parameter and any check for muted errors.
// Takes target as an EventTarget impl.
// Takes error object, message, and location as params, unlike the spec.
// Returns whether the event was handled or not.
function reportAnError(line, col, target, errorObject, message, location) {
  if (target[errorReportingMode]) {
    return false;
  }

  target[errorReportingMode] = true;

  const event = createAnEvent("error", ErrorEvent, {
    cancelable: true,
    message,
    filename: location,
    lineno: line,
    colno: col,
    error: errorObject
  });

  try {
    target._dispatch(event);
  } finally {
    target[errorReportingMode] = false;
    return event.defaultPrevented;
  }
}

module.exports = function reportException(window, error, filenameHint) {
  // This function will give good results on real Error objects with stacks; poor ones otherwise

  const stack = error && error.stack;
  const lines = stack && stack.split("\n");

  // Find the first line that matches; important for multi-line messages
  let pieces;
  if (lines) {
    for (let i = 1; i < lines.length && !pieces; ++i) {
      pieces = lines[i].match(/at (?:(.+)\s+)?\(?(?:(.+?):(\d+):(\d+)|([^)]+))\)?/);
    }
  }

  const fileName = (pieces && pieces[2]) || filenameHint || window._document.URL;
  const lineNumber = (pieces && parseInt(pieces[3])) || 0;
  const columnNumber = (pieces && parseInt(pieces[4])) || 0;

  const windowImpl = idlUtils.implForWrapper(window);

  const handled = reportAnError(lineNumber, columnNumber, windowImpl, error, error.message, fileName);

  if (!handled) {
    const errorString = shouldBeDisplayedAsError(error) ? `[${error.name}: ${error.message}]` : util.inspect(error);
    const jsdomError = new Error(`Uncaught ${errorString}`);
    jsdomError.detail = error;
    jsdomError.type = "unhandled exception";

    window._virtualConsole.emit("jsdomError", jsdomError);
  }
};

function shouldBeDisplayedAsError(x) {
  return x.name && x.message !== undefined && x.stack;
}


"use strict";
const { reflectURLAttribute } = require("../../utils");
const DOMTokenList = require("../generated/DOMTokenList");
const HTMLElementImpl = require("./HTMLElement-impl").implementation;
const idlUtils = require("../generated/utils");
const { fetchStylesheet } = require("../helpers/stylesheets");
const { parseURLToResultingURLRecord } = require("../helpers/document-base-url");
const whatwgURL = require("whatwg-url");

// Important reading: "appropriate times to obtain the resource" in
// https://html.spec.whatwg.org/multipage/semantics.html#link-type-stylesheet

class HTMLLinkElementImpl extends HTMLElementImpl {
  constructor(args, privateData) {
    super(args, privateData);

    this.sheet = null;
  }

  get relList() {
    if (this._relList === undefined) {
      this._relList = DOMTokenList.createImpl([], {
        element: this,
        attributeLocalName: "rel",
        supportedTokens: new Set(["stylesheet"])
      });
    }
    return this._relList;
  }

  _attach() {
    super._attach();
    maybeFetchAndProcess(this);
  }

  _attrModified(name, value, oldValue) {
    super._attrModified(name, value, oldValue);

    if (name === "href") { // TODO crossorigin="" or type=""
      maybeFetchAndProcess(this);
    }

    if (name === "rel" && this._relList !== undefined) {
      this._relList.attrModified();
    }
  }

  get _accept() {
    return "text/css,*/*;q=0.1";
  }

  get href() {
    return reflectURLAttribute(this, "href");
  }

  set href(value) {
    this.setAttributeNS(null, "href", value);
  }
}

module.exports = {
  implementation: HTMLLinkElementImpl
};

// https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet
function maybeFetchAndProcess(el) {
  if (!isExternalResourceLink(el)) {
    return;
  }

  // Browsing-context connected
  if (!el.isConnected || !el._ownerDocument._defaultView) {
    return;
  }

  fetchAndProcess(el);
}

// https://html.spec.whatwg.org/multipage/semantics.html#default-fetch-and-process-the-linked-resource
// TODO: refactor into general link-fetching like the spec.
function fetchAndProcess(el) {
  const href = el.getAttributeNS(null, "href");

  if (href === null || href === "") {
    return;
  }

  const url = parseURLToResultingURLRecord(href, el._ownerDocument);
  if (url === null) {
    return;
  }

  // TODO handle crossorigin="", nonce, integrity="", referrerpolicy=""

  const serialized = whatwgURL.serializeURL(url);

  fetchStylesheet(el, serialized);
}

function isExternalResourceLink(el) {
  // for our purposes, only stylesheets can be external resource links
  const wrapper = idlUtils.wrapperForImpl(el);
  if (!/(?:[ \t\n\r\f]|^)stylesheet(?:[ \t\n\r\f]|$)/i.test(wrapper.rel)) {
    // rel is a space-separated list of tokens, and the original rel types
    // are case-insensitive.
    return false;
  }

  return Boolean(el.href);
}


"use strict";

const NODE_TYPE = require("../node-type");

const { domSymbolTree } = require("./internal-constants");
const { HTML_NS } = require("./namespaces");
const { signalSlotList, queueMutationObserverMicrotask } = require("./mutation-observers");

// Valid host element for ShadowRoot.
// Defined in: https://dom.spec.whatwg.org/#dom-element-attachshadow
const VALID_HOST_ELEMENT_NAME = new Set([
  "article",
  "aside",
  "blockquote",
  "body",
  "div",
  "footer",
  "h1",
  "h2",
  "h3",
  "h4",
  "h5",
  "h6",
  "header",
  "main",
  "nav",
  "p",
  "section",
  "span"
]);

function isValidHostElementName(name) {
  return VALID_HOST_ELEMENT_NAME.has(name);
}

// Use an approximation by checking the presence of nodeType instead of instead of using the isImpl from
// "../generated/Node" to avoid introduction of circular dependencies.
function isNode(nodeImpl) {
  return Boolean(nodeImpl && "nodeType" in nodeImpl);
}

// Use an approximation by checking the value of nodeType and presence of nodeType host instead of instead
// of using the isImpl from "../generated/ShadowRoot" to avoid introduction of circular dependencies.
function isShadowRoot(nodeImpl) {
  return Boolean(nodeImpl && nodeImpl.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE && "host" in nodeImpl);
}

// https://dom.spec.whatwg.org/#concept-slotable
function isSlotable(nodeImpl) {
  return nodeImpl && (nodeImpl.nodeType === NODE_TYPE.ELEMENT_NODE || nodeImpl.nodeType === NODE_TYPE.TEXT_NODE);
}

function isSlot(nodeImpl) {
  return nodeImpl && nodeImpl.localName === "slot" && nodeImpl._namespaceURI === HTML_NS;
}

// https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor
function isShadowInclusiveAncestor(ancestor, node) {
  while (isNode(node)) {
    if (node === ancestor) {
      return true;
    }

    if (isShadowRoot(node)) {
      node = node.host;
    } else {
      node = domSymbolTree.parent(node);
    }
  }

  return false;
}

// https://dom.spec.whatwg.org/#retarget
function retarget(a, b) {
  while (true) {
    if (!isNode(a)) {
      return a;
    }

    const aRoot = getRoot(a);
    if (
      !isShadowRoot(aRoot) ||
      (isNode(b) && isShadowInclusiveAncestor(aRoot, b))
    ) {
      return a;
    }

    a = getRoot(a).host;
  }
}

// https://dom.spec.whatwg.org/#get-the-parent
function getEventTargetParent(eventTarget, event) {
  // _getTheParent will be missing for Window, since it doesn't have an impl class and we don't want to pollute the
  // user-visible global scope with a _getTheParent value. TODO: remove this entire function and use _getTheParent
  // directly, once Window gets split into impl/wrapper.
  return eventTarget._getTheParent ? eventTarget._getTheParent(event) : null;
}

function getRoot(node) {
  let root;

  for (const ancestor of domSymbolTree.ancestorsIterator(node)) {
    root = ancestor;
  }

  return root;
}

// https://dom.spec.whatwg.org/#concept-shadow-including-root
function shadowIncludingRoot(node) {
  const root = getRoot(node);
  return isShadowRoot(root) ? shadowIncludingRoot(root.host) : root;
}

// https://dom.spec.whatwg.org/#assign-a-slot
function assignSlot(slotable) {
  const slot = findSlot(slotable);

  if (slot) {
    assignSlotable(slot);
  }
}

// https://dom.spec.whatwg.org/#assign-slotables
function assignSlotable(slot) {
  const slotables = findSlotable(slot);

  let shouldFireSlotChange = false;

  if (slotables.length !== slot._assignedNodes.length) {
    shouldFireSlotChange = true;
  } else {
    for (let i = 0; i < slotables.length; i++) {
      if (slotables[i] !== slot._assignedNodes[i]) {
        shouldFireSlotChange = true;
        break;
      }
    }
  }

  if (shouldFireSlotChange) {
    signalSlotChange(slot);
  }

  slot._assignedNodes = slotables;

  for (const slotable of slotables) {
    slotable._assignedSlot = slot;
  }
}

// https://dom.spec.whatwg.org/#assign-slotables-for-a-tree
function assignSlotableForTree(root) {
  for (const slot of domSymbolTree.treeIterator(root)) {
    if (isSlot(slot)) {
      assignSlotable(slot);
    }
  }
}

// https://dom.spec.whatwg.org/#find-slotables
function findSlotable(slot) {
  const result = [];

  const root = getRoot(slot);
  if (!isShadowRoot(root)) {
    return result;
  }

  for (const slotable of domSymbolTree.treeIterator(root.host)) {
    const foundSlot = findSlot(slotable);

    if (foundSlot === slot) {
      result.push(slotable);
    }
  }

  return result;
}

// https://dom.spec.whatwg.org/#find-flattened-slotables
function findFlattenedSlotables(slot) {
  const result = [];

  const root = getRoot(slot);
  if (!isShadowRoot(root)) {
    return result;
  }

  const slotables = findSlotable(slot);

  if (slotables.length === 0) {
    for (const child of domSymbolTree.childrenIterator(slot)) {
      if (isSlotable(child)) {
        slotables.push(child);
      }
    }
  }

  for (const node of slotables) {
    if (isSlot(node) && isShadowRoot(getRoot(node))) {
      const temporaryResult = findFlattenedSlotables(node);
      result.push(...temporaryResult);
    } else {
      result.push(node);
    }
  }

  return result;
}

// https://dom.spec.whatwg.org/#find-a-slot
function findSlot(slotable, openFlag) {
  const { parentNode: parent } = slotable;

  if (!parent) {
    return null;
  }

  const shadow = parent._shadowRoot;

  if (!shadow || (openFlag && shadow.mode !== "open")) {
    return null;
  }

  for (const child of domSymbolTree.treeIterator(shadow)) {
    if (isSlot(child) && child.name === slotable._slotableName) {
      return child;
    }
  }

  return null;
}

// https://dom.spec.whatwg.org/#signal-a-slot-change
function signalSlotChange(slot) {
  if (!signalSlotList.some(entry => entry === slot)) {
    signalSlotList.push(slot);
  }

  queueMutationObserverMicrotask();
}

// https://dom.spec.whatwg.org/#concept-shadow-including-descendant
function* shadowIncludingInclusiveDescendantsIterator(node) {
  yield node;

  if (node._shadowRoot) {
    yield* shadowIncludingInclusiveDescendantsIterator(node._shadowRoot);
  }

  for (const child of domSymbolTree.childrenIterator(node)) {
    yield* shadowIncludingInclusiveDescendantsIterator(child);
  }
}

// https://dom.spec.whatwg.org/#concept-shadow-including-descendant
function* shadowIncludingDescendantsIterator(node) {
  if (node._shadowRoot) {
    yield* shadowIncludingInclusiveDescendantsIterator(node._shadowRoot);
  }

  for (const child of domSymbolTree.childrenIterator(node)) {
    yield* shadowIncludingInclusiveDescendantsIterator(child);
  }
}

module.exports = {
  isValidHostElementName,

  isNode,
  isSlotable,
  isSlot,
  isShadowRoot,

  isShadowInclusiveAncestor,
  retarget,
  getEventTargetParent,
  getRoot,
  shadowIncludingRoot,

  assignSlot,
  assignSlotable,
  assignSlotableForTree,

  findSlot,
  findFlattenedSlotables,

  signalSlotChange,

  shadowIncludingInclusiveDescendantsIterator,
  shadowIncludingDescendantsIterator
};


"use strict";
const whatwgURL = require("whatwg-url");
const arrayEqual = require("array-equal");
const notImplemented = require("../../browser/not-implemented.js");
const reportException = require("../helpers/runtime-script-errors.js");
const idlUtils = require("../generated/utils.js");

exports.evaluateJavaScriptURL = (window, urlRecord) => {
  const urlString = whatwgURL.serializeURL(urlRecord);
  const scriptSource = whatwgURL.percentDecode(Buffer.from(urlString)).toString();
  if (window._runScripts === "dangerously") {
    try {
      return window.eval(scriptSource);
    } catch (e) {
      reportException(window, e, urlString);
    }
  }
  return undefined;
};

// https://html.spec.whatwg.org/#navigating-across-documents
exports.navigate = (window, newURL, flags) => {
  // This is NOT a spec-compliant implementation of navigation in any way. It implements a few selective steps that
  // are nice for jsdom users, regarding hash changes and JavaScript URLs. Full navigation support is being worked on
  // and will likely require some additional hooks to be implemented.
  if (!window._document) {
    return;
  }

  const document = idlUtils.implForWrapper(window._document);
  const currentURL = document._URL;

  if (!flags.reloadTriggered && urlEquals(currentURL, newURL, { excludeFragments: true })) {
    if (newURL.fragment !== currentURL.fragment) {
      navigateToFragment(window, newURL, flags);
    }
    return;
  }

  // NOT IMPLEMENTED: Prompt to unload the active document of browsingContext.

  // NOT IMPLEMENTED: form submission algorithm
  // const navigationType = 'other';

  // NOT IMPLEMENTED: if resource is a response...
  if (newURL.scheme === "javascript") {
    setTimeout(() => {
      const result = exports.evaluateJavaScriptURL(window, newURL);
      if (typeof result === "string") {
        notImplemented("string results from 'javascript:' URLs", window);
      }
    }, 0);
    return;
  }
  navigateFetch(window);
};

// https://html.spec.whatwg.org/#scroll-to-fragid
function navigateToFragment(window, newURL, flags) {
  const document = idlUtils.implForWrapper(window._document);

  window._sessionHistory.clearHistoryTraversalTasks();

  if (!flags.replacement) {
    // handling replacement=true here deviates from spec, but matches real browser behaviour
    // see https://github.com/whatwg/html/issues/2796 for spec bug
    window._sessionHistory.removeAllEntriesAfterCurrentEntry();
  }
  const newEntry = { document, url: newURL };
  window._sessionHistory.addEntryAfterCurrentEntry(newEntry);
  window._sessionHistory.traverseHistory(newEntry, { nonBlockingEvents: true, replacement: flags.replacement });
}

// https://html.spec.whatwg.org/#process-a-navigate-fetch
function navigateFetch(window) {
  // TODO:
  notImplemented("navigation (except hash changes)", window);
}

function urlEquals(a, b, flags) {
  if (a.scheme !== b.scheme ||
      a.username !== b.username ||
      a.password !== b.password ||
      a.host !== b.host ||
      a.port !== b.port ||
      !arrayEqual(a.path, b.path) ||
      a.query !== b.query ||
      // Omitted per spec: url.fragment !== this._url.fragment ||
      a.cannotBeABaseURL !== b.cannotBeABaseURL) {
    return false;
  }
  return flags.excludeFragments || a.fragment === b.fragment;
}


"use strict";
const DOMException = require("domexception");

const reportException = require("../helpers/runtime-script-errors");
const idlUtils = require("../generated/utils");
const {
  isNode, isShadowRoot, isSlotable, getRoot, getEventTargetParent,
  isShadowInclusiveAncestor, retarget
} = require("../helpers/shadow-dom");

const Event = require("../generated/Event").interface;
const MouseEvent = require("../generated/MouseEvent");

class EventTargetImpl {
  constructor() {
    this._eventListeners = Object.create(null);
  }

  addEventListener(type, callback, options) {
    // webidl2js currently can't handle neither optional arguments nor callback interfaces
    if (callback === undefined || callback === null) {
      callback = null;
    } else if (typeof callback !== "object" && typeof callback !== "function") {
      throw new TypeError("Only undefined, null, an object, or a function are allowed for the callback parameter");
    }

    options = normalizeEventHandlerOptions(options, ["capture", "once", "passive"]);

    if (callback === null) {
      return;
    }

    if (!this._eventListeners[type]) {
      this._eventListeners[type] = [];
    }

    for (let i = 0; i < this._eventListeners[type].length; ++i) {
      const listener = this._eventListeners[type][i];
      if (listener.options.capture === options.capture && listener.callback === callback) {
        return;
      }
    }

    this._eventListeners[type].push({
      callback,
      options
    });
  }

  removeEventListener(type, callback, options) {
    if (callback === undefined || callback === null) {
      callback = null;
    } else if (typeof callback !== "object" && typeof callback !== "function") {
      throw new TypeError("Only undefined, null, an object, or a function are allowed for the callback parameter");
    }

    options = normalizeEventHandlerOptions(options, ["capture"]);

    if (callback === null) {
      // Optimization, not in the spec.
      return;
    }

    if (!this._eventListeners[type]) {
      return;
    }

    for (let i = 0; i < this._eventListeners[type].length; ++i) {
      const listener = this._eventListeners[type][i];
      if (listener.callback === callback && listener.options.capture === options.capture) {
        this._eventListeners[type].splice(i, 1);
        break;
      }
    }
  }

  dispatchEvent(eventImpl) {
    if (eventImpl._dispatchFlag || !eventImpl._initializedFlag) {
      throw new DOMException("Tried to dispatch an uninitialized event", "InvalidStateError");
    }
    if (eventImpl.eventPhase !== Event.NONE) {
      throw new DOMException("Tried to dispatch a dispatching event", "InvalidStateError");
    }

    eventImpl.isTrusted = false;

    return this._dispatch(eventImpl);
  }

  // https://dom.spec.whatwg.org/#get-the-parent
  _getTheParent() {
    return null;
  }

  // https://dom.spec.whatwg.org/#concept-event-dispatch
  // legacyOutputDidListenersThrowFlag optional parameter is not necessary here since it is only used by indexDB.
  _dispatch(eventImpl, targetOverride /* , legacyOutputDidListenersThrowFlag */) {
    let targetImpl = this;
    let clearTargets = false;
    let activationTarget = null;

    eventImpl._dispatchFlag = true;

    targetOverride = targetOverride || targetImpl;
    let relatedTarget = retarget(eventImpl.relatedTarget, targetImpl);

    if (targetImpl !== relatedTarget || targetImpl === eventImpl.relatedTarget) {
      const touchTargets = [];

      appendToEventPath(eventImpl, targetImpl, targetOverride, relatedTarget, touchTargets, false);

      const isActivationEvent = MouseEvent.isImpl(eventImpl) && eventImpl.type === "click";

      if (isActivationEvent && targetImpl._hasActivationBehavior) {
        activationTarget = targetImpl;
      }

      let slotInClosedTree = false;
      let slotable = isSlotable(targetImpl) && targetImpl._assignedSlot ? targetImpl : null;
      let parent = getEventTargetParent(targetImpl, eventImpl);

      // Populate event path
      // https://dom.spec.whatwg.org/#event-path
      while (parent !== null) {
        if (slotable !== null) {
          if (parent.localName !== "slot") {
            throw new Error(`JSDOM Internal Error: Expected parent to be a Slot`);
          }

          slotable = null;

          const parentRoot = getRoot(parent);
          if (isShadowRoot(parentRoot) && parentRoot.mode === "closed") {
            slotInClosedTree = true;
          }
        }

        if (isSlotable(parent) && parent._assignedSlot) {
          slotable = parent;
        }

        relatedTarget = retarget(eventImpl.relatedTarget, parent);

        if (
          (isNode(parent) && isShadowInclusiveAncestor(getRoot(targetImpl), parent)) ||
          idlUtils.wrapperForImpl(parent).constructor.name === "Window"
        ) {
          if (isActivationEvent && eventImpl.bubbles && activationTarget === null &&
              parent._hasActivationBehavior) {
            activationTarget = parent;
          }

          appendToEventPath(eventImpl, parent, null, relatedTarget, touchTargets, slotInClosedTree);
        } else if (parent === relatedTarget) {
          parent = null;
        } else {
          targetImpl = parent;

          if (isActivationEvent && activationTarget === null && targetImpl._hasActivationBehavior) {
            activationTarget = targetImpl;
          }

          appendToEventPath(eventImpl, parent, targetImpl, relatedTarget, touchTargets, slotInClosedTree);
        }

        if (parent !== null) {
          parent = getEventTargetParent(parent, eventImpl);
        }

        slotInClosedTree = false;
      }

      let clearTargetsTupleIndex = -1;
      for (let i = eventImpl._path.length - 1; i >= 0 && clearTargetsTupleIndex === -1; i--) {
        if (eventImpl._path[i].target !== null) {
          clearTargetsTupleIndex = i;
        }
      }
      const clearTargetsTuple = eventImpl._path[clearTargetsTupleIndex];

      clearTargets =
          (isNode(clearTargetsTuple.target) && isShadowRoot(getRoot(clearTargetsTuple.target))) ||
          (isNode(clearTargetsTuple.relatedTarget) && isShadowRoot(getRoot(clearTargetsTuple.relatedTarget)));

      eventImpl.eventPhase = Event.CAPTURING_PHASE;

      if (activationTarget !== null && activationTarget._legacyPreActivationBehavior) {
        activationTarget._legacyPreActivationBehavior();
      }

      for (let i = eventImpl._path.length - 1; i >= 0; --i) {
        const tuple = eventImpl._path[i];

        if (tuple.target === null) {
          invokeEventListeners(tuple, eventImpl);
        }
      }

      for (let i = 0; i < eventImpl._path.length; i++) {
        const tuple = eventImpl._path[i];

        if (tuple.target !== null) {
          eventImpl.eventPhase = Event.AT_TARGET;
        } else {
          eventImpl.eventPhase = Event.BUBBLING_PHASE;
        }

        if (
          (eventImpl.eventPhase === Event.BUBBLING_PHASE && eventImpl.bubbles) ||
          eventImpl.eventPhase === Event.AT_TARGET
        ) {
          invokeEventListeners(tuple, eventImpl);
        }
      }
    }

    eventImpl.eventPhase = Event.NONE;

    eventImpl.currentTarget = null;
    eventImpl._path = [];
    eventImpl._dispatchFlag = false;
    eventImpl._stopPropagationFlag = false;
    eventImpl._stopImmediatePropagationFlag = false;

    if (clearTargets) {
      eventImpl.target = null;
      eventImpl.relatedTarget = null;
    }

    if (activationTarget !== null) {
      if (!eventImpl._canceledFlag) {
        activationTarget._activationBehavior();
      } else if (activationTarget._legacyCanceledActivationBehavior) {
        activationTarget._legacyCanceledActivationBehavior();
      }
    }

    return !eventImpl._canceledFlag;
  }
}

module.exports = {
  implementation: EventTargetImpl
};

// https://dom.spec.whatwg.org/#concept-event-listener-invoke
function invokeEventListeners(tuple, eventImpl) {
  const tupleIndex = eventImpl._path.indexOf(tuple);
  for (let i = tupleIndex; i >= 0; i--) {
    const t = eventImpl._path[i];
    if (t.target) {
      eventImpl.target = t.target;
      break;
    }
  }

  eventImpl.relatedTarget = idlUtils.wrapperForImpl(tuple.relatedTarget);

  if (eventImpl._stopPropagationFlag) {
    return;
  }

  eventImpl.currentTarget = idlUtils.wrapperForImpl(tuple.item);

  const listeners = tuple.item._eventListeners;
  innerInvokeEventListeners(eventImpl, listeners);
}

// https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke
function innerInvokeEventListeners(eventImpl, listeners) {
  let found = false;

  const { type, target } = eventImpl;
  const wrapper = idlUtils.wrapperForImpl(target);

  if (!listeners || !listeners[type]) {
    return found;
  }

  // Copy event listeners before iterating since the list can be modified during the iteration.
  const handlers = listeners[type].slice();

  for (let i = 0; i < handlers.length; i++) {
    const listener = handlers[i];
    const { capture, once, passive } = listener.options;

    // Check if the event listener has been removed since the listeners has been cloned.
    if (!listeners[type].includes(listener)) {
      continue;
    }

    found = true;

    if (
      (eventImpl.eventPhase === Event.CAPTURING_PHASE && !capture) ||
      (eventImpl.eventPhase === Event.BUBBLING_PHASE && capture)
    ) {
      continue;
    }

    if (once) {
      listeners[type].splice(listeners[type].indexOf(listener), 1);
    }

    if (passive) {
      eventImpl._inPassiveListenerFlag = true;
    }

    try {
      if (typeof listener.callback === "object") {
        if (typeof listener.callback.handleEvent === "function") {
          listener.callback.handleEvent(idlUtils.wrapperForImpl(eventImpl));
        }
      } else {
        listener.callback.call(eventImpl.currentTarget, idlUtils.wrapperForImpl(eventImpl));
      }
    } catch (e) {
      let window = null;
      if (wrapper && wrapper._document) {
        // Triggered by Window
        window = wrapper;
      } else if (target._ownerDocument) {
        // Triggered by most webidl2js'ed instances
        window = target._ownerDocument._defaultView;
      } else if (wrapper._ownerDocument) {
        // Currently triggered by XHR and some other non-webidl2js things
        window = wrapper._ownerDocument._defaultView;
      }

      if (window) {
        reportException(window, e);
      }
      // Errors in window-less documents just get swallowed... can you think of anything better?
    }

    eventImpl._inPassiveListenerFlag = false;

    if (eventImpl._stopImmediatePropagationFlag) {
      return found;
    }
  }

  return found;
}

/**
 * Normalize the event listeners options argument in order to get always a valid options object
 * @param   {Object} options         - user defined options
 * @param   {Array} defaultBoolKeys  - boolean properties that should belong to the options object
 * @returns {Object} object containing at least the "defaultBoolKeys"
 */
function normalizeEventHandlerOptions(options, defaultBoolKeys) {
  const returnValue = {};

  // no need to go further here
  if (typeof options === "boolean" || options === null || typeof options === "undefined") {
    returnValue.capture = Boolean(options);
    return returnValue;
  }

  // non objects options so we typecast its value as "capture" value
  if (typeof options !== "object") {
    returnValue.capture = Boolean(options);
    // at this point we don't need to loop the "capture" key anymore
    defaultBoolKeys = defaultBoolKeys.filter(k => k !== "capture");
  }

  for (const key of defaultBoolKeys) {
    returnValue[key] = Boolean(options[key]);
  }

  return returnValue;
}

// https://dom.spec.whatwg.org/#concept-event-path-append
function appendToEventPath(eventImpl, target, targetOverride, relatedTarget, touchTargets, slotInClosedTree) {
  const itemInShadowTree = isNode(target) && isShadowRoot(getRoot(target));
  const rootOfClosedTree = isShadowRoot(target) && target.mode === "closed";

  eventImpl._path.push({
    item: target,
    itemInShadowTree,
    target: targetOverride,
    relatedTarget,
    touchTargets,
    rootOfClosedTree,
    slotInClosedTree
  });
}


"use strict";

const { domSymbolTree } = require("./internal-constants");
const reportException = require("./runtime-script-errors");

const Event = require("../generated/Event");
const idlUtils = require("../generated/utils");
const MutationRecord = require("../generated/MutationRecord");

const MUTATION_TYPE = {
  ATTRIBUTES: "attributes",
  CHARACTER_DATA: "characterData",
  CHILD_LIST: "childList"
};

// Note:
// Since jsdom doesn't currently implement the concept of "unit of related similar-origin browsing contexts"
// (https://html.spec.whatwg.org/multipage/browsers.html#unit-of-related-similar-origin-browsing-contexts)
// we will approximate that the following properties are global for now.

// https://dom.spec.whatwg.org/#mutation-observer-compound-microtask-queued-flag
let mutationObserverMicrotaskQueueFlag = false;

// Non-spec compliant: List of all the mutation observers with mutation records enqueued. It's a replacement for
// mutation observer list (https://dom.spec.whatwg.org/#mutation-observer-list) but without leaking since it's empty
// before notifying the mutation observers.
const activeMutationObservers = new Set();

// https://dom.spec.whatwg.org/#signal-slot-list
const signalSlotList = [];

// https://dom.spec.whatwg.org/#queue-a-mutation-record
function queueMutationRecord(
  type,
  target,
  name,
  namespace,
  oldValue,
  addedNodes,
  removedNodes,
  previousSibling,
  nextSibling
) {
  const interestedObservers = new Map();

  const nodes = domSymbolTree.ancestorsToArray(target);

  for (const node of nodes) {
    for (const registered of node._registeredObserverList) {
      const { options, observer: mo } = registered;

      if (
        !(node !== target && options.subtree === false) &&
        !(type === MUTATION_TYPE.ATTRIBUTES && options.attributes !== true) &&
        !(type === MUTATION_TYPE.ATTRIBUTES && options.attributeFilter &&
          !options.attributeFilter.some(value => value === name || value === namespace)) &&
        !(type === MUTATION_TYPE.CHARACTER_DATA && options.characterData !== true) &&
        !(type === MUTATION_TYPE.CHILD_LIST && options.childList === false)
      ) {
        if (!interestedObservers.has(mo)) {
          interestedObservers.set(mo, null);
        }

        if (
          (type === MUTATION_TYPE.ATTRIBUTES && options.attributeOldValue === true) ||
          (type === MUTATION_TYPE.CHARACTER_DATA && options.characterDataOldValue === true)
        ) {
          interestedObservers.set(mo, oldValue);
        }
      }
    }
  }

  for (const [observer, mappedOldValue] of interestedObservers.entries()) {
    const record = MutationRecord.createImpl([], {
      type,
      target,
      attributeName: name,
      attributeNamespace: namespace,
      oldValue: mappedOldValue,
      addedNodes,
      removedNodes,
      previousSibling,
      nextSibling
    });

    observer._recordQueue.push(record);
    activeMutationObservers.add(observer);
  }

  queueMutationObserverMicrotask();
}

// https://dom.spec.whatwg.org/#queue-a-tree-mutation-record
function queueTreeMutationRecord(target, addedNodes, removedNodes, previousSibling, nextSibling) {
  queueMutationRecord(
    MUTATION_TYPE.CHILD_LIST,
    target,
    null,
    null,
    null,
    addedNodes,
    removedNodes,
    previousSibling,
    nextSibling
  );
}

// https://dom.spec.whatwg.org/#queue-an-attribute-mutation-record
function queueAttributeMutationRecord(target, name, namespace, oldValue) {
  queueMutationRecord(
    MUTATION_TYPE.ATTRIBUTES,
    target,
    name,
    namespace,
    oldValue,
    [],
    [],
    null,
    null
  );
}

// https://dom.spec.whatwg.org/#queue-a-mutation-observer-compound-microtask
function queueMutationObserverMicrotask() {
  if (mutationObserverMicrotaskQueueFlag) {
    return;
  }

  mutationObserverMicrotaskQueueFlag = true;

  Promise.resolve().then(() => {
    notifyMutationObservers();
  });
}

// https://dom.spec.whatwg.org/#notify-mutation-observers
function notifyMutationObservers() {
  mutationObserverMicrotaskQueueFlag = false;

  const notifyList = [...activeMutationObservers].sort((a, b) => a._id - b._id);
  activeMutationObservers.clear();

  const signalList = [...signalSlotList];
  signalSlotList.splice(0, signalSlotList.length);

  for (const mo of notifyList) {
    const records = [...mo._recordQueue];
    mo._recordQueue = [];

    for (const node of mo._nodeList) {
      node._registeredObserverList = node._registeredObserverList.filter(registeredObserver => {
        return registeredObserver.source !== mo;
      });

      if (records.length) {
        try {
          mo._callback(
            records.map(idlUtils.wrapperForImpl),
            idlUtils.wrapperForImpl(mo)
          );
        } catch (e) {
          const { target } = records[0];
          const window = target._ownerDocument._defaultView;

          reportException(window, e);
        }
      }
    }
  }

  for (const slot of signalList) {
    const slotChangeEvent = Event.createImpl(
      [
        "slotchange",
        { bubbles: true }
      ],
      { isTrusted: true }
    );

    slot._dispatch(slotChangeEvent);
  }
}

module.exports = {
  MUTATION_TYPE,

  queueMutationRecord,
  queueTreeMutationRecord,
  queueAttributeMutationRecord,

  queueMutationObserverMicrotask,

  signalSlotList
};


"use strict";
const cssom = require("cssom");
const whatwgEncoding = require("whatwg-encoding");
const whatwgURL = require("whatwg-url");

// TODO: this should really implement https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet
// It (and the things it calls) is nowhere close right now.
exports.fetchStylesheet = (elementImpl, urlString) => {
  const parsedURL = whatwgURL.parseURL(urlString);
  return fetchStylesheetInternal(elementImpl, urlString, parsedURL);
};

// https://drafts.csswg.org/cssom/#remove-a-css-style-sheet
exports.removeStylesheet = (sheet, elementImpl) => {
  const { styleSheets } = elementImpl._ownerDocument;
  styleSheets.splice(styleSheets.indexOf(sheet, 1));

  // Remove the association explicitly; in the spec it's implicit so this step doesn't exist.
  elementImpl.sheet = null;

  // TODO: "Set the CSS style sheet’s parent CSS style sheet, owner node and owner CSS rule to null."
  // Probably when we have a real CSSOM implementation.
};

// https://drafts.csswg.org/cssom/#create-a-css-style-sheet kinda:
// - Parsing failures are not handled gracefully like they should be
// - The import rules stuff seems out of place, and probably should affect the load event...
exports.createStylesheet = (sheetText, elementImpl, baseURL) => {
  let sheet;
  try {
    sheet = cssom.parse(sheetText);
  } catch (e) {
    if (elementImpl._ownerDocument._defaultView) {
      const error = new Error("Could not parse CSS stylesheet");
      error.detail = sheetText;
      error.type = "css parsing";

      elementImpl._ownerDocument._defaultView._virtualConsole.emit("jsdomError", error);
    }
    return;
  }

  scanForImportRules(elementImpl, sheet.cssRules, baseURL);

  addStylesheet(sheet, elementImpl);
};

// https://drafts.csswg.org/cssom/#add-a-css-style-sheet
function addStylesheet(sheet, elementImpl) {
  elementImpl._ownerDocument.styleSheets.push(sheet);

  // Set the association explicitly; in the spec it's implicit.
  elementImpl.sheet = sheet;

  // TODO: title and disabled stuff
}

function fetchStylesheetInternal(elementImpl, urlString, parsedURL) {
  const document = elementImpl._ownerDocument;
  let defaultEncoding = document._encoding;
  const resourceLoader = document._resourceLoader;

  if (elementImpl.localName === "link" && elementImpl.hasAttributeNS(null, "charset")) {
    defaultEncoding = whatwgEncoding.labelToName(elementImpl.getAttributeNS(null, "charset"));
  }

  function onStylesheetLoad(data) {
    const css = whatwgEncoding.decode(data, defaultEncoding);

    // TODO: MIME type checking?
    if (elementImpl.sheet) {
      exports.removeStylesheet(elementImpl.sheet, elementImpl);
    }
    exports.createStylesheet(css, elementImpl, parsedURL);
  }

  resourceLoader.fetch(urlString, {
    element: elementImpl,
    onLoad: onStylesheetLoad
  });
}

// TODO this is actually really messed up and overwrites the sheet on elementImpl
// Tracking in https://github.com/tmpvar/jsdom/issues/2124
function scanForImportRules(elementImpl, cssRules, baseURL) {
  if (!cssRules) {
    return;
  }

  for (let i = 0; i < cssRules.length; ++i) {
    if (cssRules[i].cssRules) {
      // @media rule: keep searching inside it.
      scanForImportRules(elementImpl, cssRules[i].cssRules, baseURL);
    } else if (cssRules[i].href) {
      // @import rule: fetch the resource and evaluate it.
      // See http://dev.w3.org/csswg/cssom/#css-import-rule
      //     If loading of the style sheet fails its cssRules list is simply
      //     empty. I.e. an @import rule always has an associated style sheet.
      const parsed = whatwgURL.parseURL(cssRules[i].href, { baseURL });
      if (parsed === null) {
        const window = elementImpl._ownerDocument._defaultView;
        if (window) {
          const error = new Error(`Could not parse CSS @import URL ${cssRules[i].href} relative to base URL ` +
                                  `"${whatwgURL.serializeURL(baseURL)}"`);
          error.type = "css @import URL parsing";
          window._virtualConsole.emit("jsdomError", error);
        }
      } else {
        fetchStylesheetInternal(elementImpl, whatwgURL.serializeURL(parsed), parsed);
      }
    }
  }
}