Source: diff/patch/DeltaNode.js

import {Node} from '../../tree/Node.js';
import {Dsl} from '../../config/Dsl.js';
import xmldom from '@xmldom/xmldom';

/**
 * A node inside a CPEE process tree annotated with change related information.
 * This class serves as the basis for diff visualization as well as merging.
 *
 * @implements {XmlSerializable<DeltaNode>}
 * @extends {Node}
 */
export class DeltaNode extends Node {
  /**
   * The type of change this node was affected by.
   * @type {String}
   */
  type;
  /**
   * The updates applied to the attributes and text content of this node.
   * @type {Map<String, Update>}
   * @const
   */
  updates;
  /**
   * Placeholder children of this node that were deleted or moved away.
   * @type {Array<DeltaNode>}
   * @const
   */
  placeholders;
  /**
   * The ID of the node in the base tree, if it exists, that this node
   * corresponds to. Null indicates an inserted node
   * @type {?Number}
   */
  baseNode;

  /**
   * Construct a new DeltaNode instance.
   * @param {String} label The label of the node.
   * @param {?String} text The text content of the node.
   * @param {String} type The type of change this node was affected by.
   * @param {?Number} baseNode The base node ID.
   */
  constructor(
      label,
      text = null,
      type = Dsl.CHANGE_MODEL.NIL.label,
      baseNode = null,
  ) {
    super(label, text);
    this.baseNode = baseNode;
    this.type = type;
    this.updates = new Map();
    this.placeholders = [];
  }

  /**
   * Insert a new child and adjust placeholder indices.
   * @param {Number} index The position at which to insert the new child.
   * @param {Node} node The new child.
   * @override
   */
  insertChild(index, node) {
    super.insertChild(index, node);
    // Adjust placeholders
    for (const placeholder of this.placeholders) {
      if (placeholder._index >= index) {
        placeholder._index++;
      }
    }
  }

  /**
   * Remove a node from the child list of its parent. Also adjust the indices
   * of all placeholders. Note: The parent attribute is not cleared by this
   * function.
   * @override
   */
  removeFromParent() {
    super.removeFromParent();
    if (this._parent != null) {
      for (const placeholder of this._parent.placeholders) {
        if (placeholder._index > this.index) {
          placeholder._index--;
        }
      }
    }
  }

  /**
   * @param {Object} ownerDocument The owner document of the generated XML
   *     element.
   * @return {Object} XML DOM object for this delta node and its children.
   * @override
   */
  toXmlDom(ownerDocument = xmldom
      .DOMImplementation
      .prototype
      .createDocument(Dsl.DEFAULT_NAMESPACE)) {
    const prefix =
        Object
            .values(Dsl.CHANGE_MODEL)
            .find((changeType) => changeType.label === this.type)
            .prefix + ':';
    const xmlElement = ownerDocument.createElement(prefix + this.label);
    xmlElement.localName = this.label;

    if (this.isMoved() || this.isMovedFrom()) {
      xmlElement.setAttribute(prefix + 'move_id', this.baseNode);
    }

    if (this.isRoot()) {
      xmlElement.setAttribute('xmlns', Dsl.DEFAULT_NAMESPACE);
      for (const type of Object.values(Dsl.CHANGE_MODEL)) {
        xmlElement.setAttribute('xmlns:' + type.prefix, type.uri);
      }
    }

    // Append regular attributes
    for (const [key, value] of this.attributes) {
      if (!this.updates.has(key)) {
        xmlElement.setAttribute(key, value);
      }
    }

    // Append updated attributes
    for (const [key, update] of this.updates) {
      const oldVal = update.oldVal;
      const newVal = update.newVal;
      if (oldVal == null) {
        xmlElement.setAttribute(
            Dsl.CHANGE_MODEL.INSERTION.prefix + ':' + key,
            newVal,
        );
      } else if (newVal == null) {
        xmlElement.setAttribute(
            Dsl.CHANGE_MODEL.DELETION.prefix + ':' + key,
            oldVal,
        );
      } else {
        xmlElement.setAttribute(
            Dsl.CHANGE_MODEL.UPDATE_FROM.prefix + ':' + key,
            oldVal,
        );
        xmlElement.setAttribute(
            Dsl.CHANGE_MODEL.UPDATE.prefix + ':' + key,
            newVal,
        );
      }
    }

    const textKey = 'text';
    // Changes in text content are also modelled as updates
    if (this.updates.has(textKey)) {
      const oldVal = this.updates.get(textKey).oldVal;
      const newVal = this.updates.get(textKey).newVal;
      if (oldVal == null) {
        xmlElement.setAttribute(
            Dsl.CHANGE_MODEL.INSERTION.prefix + ':' + textKey,
            'true',
        );
      } else if (newVal == null) {
        xmlElement.setAttribute(
            Dsl.CHANGE_MODEL.DELETION.prefix + ':' + textKey,
            'true',
        );
      } else {
        xmlElement.setAttribute(
            Dsl.CHANGE_MODEL.UPDATE_FROM.prefix + ':' + textKey,
            oldVal,
        );
        xmlElement.setAttribute(
            Dsl.CHANGE_MODEL.UPDATE.prefix + ':' + textKey,
            'true',
        );
      }
    }

    if (this.hasText()) {
      xmlElement.appendChild(ownerDocument.createTextNode(this.text));
    }

    for (const child of this) {
      xmlElement.appendChild(child.toXmlDom(ownerDocument));
    }

    return xmlElement;
  }

  /**
   * Create a new DeltaNode instance from an existing node.
   * @param {Node} node
   * @param {Boolean} includeChildren
   * @return {DeltaNode}
   * @override
   */
  static fromNode(node, includeChildren) {
    const deltaNode = new DeltaNode(node.label, node.text);
    for (const [key, value] of node.attributes) {
      deltaNode.attributes.set(key, value);
    }
    if (includeChildren) {
      for (const child of node) {
        deltaNode.appendChild(this.fromNode(child, includeChildren));
      }
    }
    if (node instanceof DeltaNode) {
      deltaNode.type = node.type;
      deltaNode.baseNode = node.baseNode;
      for (const placeholder of node.placeholders) {
        deltaNode.placeholders.push(this.fromNode(placeholder, true));
      }
      for (const [key, update] of node.updates) {
        deltaNode.updates.set(key, update.copy());
      }
    }
    return deltaNode;
  }

  /**
   * @return {Boolean} If this node was deleted.
   */
  isDeleted() {
    return this.type === Dsl.CHANGE_MODEL.DELETION.label;
  }

  /**
   * @return {Boolean} If this node was inserted.
   */
  isInserted() {
    return this.type === Dsl.CHANGE_MODEL.INSERTION.label;
  }

  /**
   * @return {Boolean} If this node was moved.
   */
  isMoved() {
    return this.type === Dsl.CHANGE_MODEL.MOVE.label;
  }

  /**
   * @return {Boolean} If this node is a placeholder for a moved node before it
   *     was moved.
   */
  isMovedFrom() {
    return this.type === Dsl.CHANGE_MODEL.MOVE_FROM.label;
  }

  /**
   * @return {Boolean} If this node was deleted or moved away
   */
  isPlaceholder() {
    return this.isMovedFrom() || this.isDeleted();
  }

  /**
   * @return {Boolean} If this node was not changed in any way regarding
   *     position or content.
   */
  isUnchanged() {
    return this.type === Dsl.CHANGE_MODEL.NIL.label && !this.isUpdated();
  }

  /**
   * @return {Boolean} If this node was updated.
   */
  isUpdated() {
    return this.updates.size > 0;
  }
}