all files / src/dm/ ve.dm.Node.js

95.76% Statements 158/165
85.94% Branches 55/64
94.87% Functions 37/39
95.76% Lines 158/165
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761                                    17014×     17014× 17014×     17014× 17014×                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   74×   74× 81× 43× 43×         74×                                     937×       937× 152×   937×   937×     405× 405× 405×   405×   405×   405×     937×               1863×                   711× 711×     711×           8171×           6365×                       837×           23447×           2617×           14691×           148521×                     22090×           3096×           1251×           10×           3071×           574×           93×           96×               17398×                                     896×           1603×           742×           2405×                                   29×   29× 72×   72× 13×     16×                   88×   88× 72×       16×             16×           144965×                           25647× 25647×     25646×   25646×   25646× 7295×     25646× 25646×                           21349×           87402×   87402× 34195×       53207× 53207× 53207× 86963× 53207×   33756×   53207×     53207×                           396×             396× 392×     396× 67×           24×     43× 43×   372×    
/*!
 * VisualEditor DataModel Node class.
 *
 * @copyright 2011-2019 VisualEditor Team and others; see http://ve.mit-license.org
 */
 
/**
 * Generic DataModel node.
 *
 * @abstract
 * @extends ve.dm.Model
 * @mixins OO.EventEmitter
 * @mixins ve.Node
 *
 * @constructor
 * @param {Object} [element] Reference to element in linear model
 */
ve.dm.Node = function VeDmNode( element ) {
	// Parent constructor
	ve.dm.Node.super.apply( this, arguments );
 
	// Mixin constructors
	ve.Node.call( this );
	OO.EventEmitter.call( this );
 
	// Properties
	this.length = 0;
	this.element = element;
};
 
/**
 * @event attributeChange
 * @param {string} key
 * @param {Mixed} oldValue
 * @param {Mixed} newValue
 */
 
/**
 * @event lengthChange
 * @param {number} diff
 */
 
/**
 * @event update
 * @param {boolean} staged Transaction was applied in staging mode
 */
 
/* Inheritance */
 
OO.inheritClass( ve.dm.Node, ve.dm.Model );
 
OO.mixinClass( ve.dm.Node, ve.Node );
 
OO.mixinClass( ve.dm.Node, OO.EventEmitter );
 
/* Static Properties */
 
/**
 * Whether this node handles its own children. After converting a DOM node to a linear model
 * node of this type, the converter checks this property. If it's false, the converter will descend
 * into the DOM node's children, recursively convert them, and attach the resulting nodes as
 * children of the linear model node. If it's true, the converter will not descend, and will
 * expect the node's toDataElement() to have handled the entire DOM subtree.
 *
 * The same is true when converting from linear model data to DOM: if this property is true,
 * toDomElements() will be passed the node's data element and all of its children and will be
 * expected to convert the entire subtree. If it's false, the converter will descend into the
 * child nodes and convert each one individually.
 *
 * If .static.childNodeTypes is set to [], this property is ignored and will be assumed to be true.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.handlesOwnChildren = false;
 
/**
 * Whether this node's children should be ignored. If true, this node will be treated as a leaf
 * node even if it has children. Often used in combination with handlesOwnChildren.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.ignoreChildren = false;
 
/**
 * Whether this node can be deleted. If false, ve.dm.Transaction#newFromRemoval will silently
 * ignore any attepts to delete this node.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isDeletable = true;
 
/**
 * Whether this node type is internal. Internal node types are ignored by the converter.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isInternal = false;
 
/**
 * Whether this node type has a wrapping element in the linear model. Most node types are wrapped,
 * only special node types are not wrapped.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isWrapped = true;
 
/**
 * Whether this node type can be unwrapped by user input (e.g. backspace to unwrap a list item)
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isUnwrappable = true;
 
/**
 * Whether this node type is a content node type. This means the node represents content, cannot
 * have children, and can only appear as children of a content container node. Content nodes are
 * also known as inline nodes.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isContent = false;
 
/**
 * Whether this node type is a metadata node. This means the node represents a leaf node that
 * has no explicit view representation, and should be treated differently for the purposes of
 * round-tripping, copy/paste etc.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isMetaData = false;
 
/**
 * For a non-content node type, whether this node type can be serialized in a content
 * position (e.g. for round tripping). This value is ignored if isContent is true.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.canSerializeAsContent = false;
 
/**
 * Whether this node type can be focused. Focusable nodes react to selections differently.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isFocusable = false;
 
/**
 * Whether this node type is alignable.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isAlignable = false;
 
/**
 * Whether this node type can behave as a table cell.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isCellable = false;
 
/**
 * Whether this node type can contain content. The children of content container nodes must be
 * content nodes.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.canContainContent = false;
 
/**
 * Whether this node type behaves like a list when diffing.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isDiffedAsList = false;
 
/**
 * Whether this node type behaves like a leaf when diffing.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.isDiffedAsLeaf = false;
 
/**
 * Whether this node type has significant whitespace. Only applies to content container nodes
 * (i.e. can only be true if canContainContent is also true).
 *
 * If a content node has significant whitespace, the text inside it is not subject to whitespace
 * stripping and preservation.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Node.static.hasSignificantWhitespace = false;
 
/**
 * Array of allowed child node types for this node type.
 *
 * An empty array means no children are allowed. null means any node type is allowed as a child.
 *
 * @static
 * @property {string[]|null}
 * @inheritable
 */
ve.dm.Node.static.childNodeTypes = null;
 
/**
 * Array of allowed parent node types for this node type.
 *
 * An empty array means this node type cannot be the child of any node. null means this node type
 * can be the child of any node type.
 *
 * @static
 * @property {string[]|null}
 * @inheritable
 */
ve.dm.Node.static.parentNodeTypes = null;
 
/**
 * Array of suggested parent node types for this node type.
 *
 * These parent node types are allowed but the editor will avoid creating them.
 *
 * An empty array means this node type should not be the child of any node. null means this node type
 * can be the child of any node type.
 *
 * @static
 * @property {string[]|null}
 * @inheritable
 */
ve.dm.Node.static.suggestedParentNodeTypes = null;
 
/**
 * Array of annotation types which can't be applied to this node
 *
 * @static
 * @property {string[]}
 * @inheritable
 */
ve.dm.Node.static.blacklistedAnnotationTypes = [];
 
/**
 * Default attributes to set for newly created linear model elements. These defaults will be used
 * when creating a new element in ve.dm.NodeFactory#getDataElement when there is no DOM node or
 * existing linear model element to base the attributes on.
 *
 * This property is an object with attribute names as keys and attribute values as values.
 * Attributes may be omitted, in which case they'll simply be undefined.
 *
 * @static
 * @property {Object}
 * @inheritable
 */
ve.dm.Node.static.defaultAttributes = {};
 
/**
 * Sanitize the node's linear model data, typically if it was generated from an external source (e.g. copied HTML)
 *
 * @param {Object} dataElement Linear model element, modified in place
 */
ve.dm.Node.static.sanitize = function () {
};
 
/**
 * Remap the internal list indexes stored in a linear model data element.
 *
 * The default implementation is empty. Nodes should override this if they store internal list
 * indexes in attributes. To remap, do something like
 * dataElement.attributes.foo = mapping[dataElement.attributes.foo];
 *
 * @static
 * @inheritable
 * @param {Object} dataElement Data element (opening) to remap. Will be modified.
 * @param {Object} mapping Object mapping old internal list indexes to new internal list indexes
 * @param {ve.dm.InternalList} internalList Internal list the indexes are being mapped into.
 *  Used for refreshing attribute values that were computed with getNextUniqueNumber().
 */
ve.dm.Node.static.remapInternalListIndexes = function () {
};
 
/**
 * Remap the internal list keys stored in a linear model data element.
 *
 * The default implementation is empty. Nodes should override this if they store internal list
 * keys in attributes.
 *
 * @static
 * @inheritable
 * @param {Object} dataElement Data element (opening) to remap. Will be modified.
 * @param {ve.dm.InternalList} internalList Internal list the keys are being mapped into.
 */
ve.dm.Node.static.remapInternalListKeys = function () {
};
 
/**
 * Determine if a hybrid element is inline and allowed to be inline in this context
 *
 * We generate block elements for block tags and inline elements for inline
 * tags; unless we're in a content location, in which case we have no choice
 * but to generate an inline element.
 *
 * @static
 * @param {HTMLElement[]} domElements DOM elements being converted
 * @param {ve.dm.Converter} converter Converter object
 * @return {boolean} The element is inline
 */
ve.dm.Node.static.isHybridInline = function ( domElements, converter ) {
	var i, length, allTagsInline = true;
 
	for ( i = 0, length = domElements.length; i < length; i++ ) {
		if ( ve.isBlockElement( domElements[ i ] ) ) {
			allTagsInline = false;
			break;
		}
	}
 
	// Force inline in content locations (but not wrappers)
	return ( converter.isExpectingContent() && !converter.isInWrapper() ) ||
		// ..also force inline in wrappers that we can't close
		( converter.isInWrapper() && !converter.canCloseWrapper() ) ||
		// ..otherwise just look at the tag names
		allTagsInline;
};
 
/**
 * Get a clone of the node's document data element.
 *
 * The attributes object will be deep-copied and the .internal.generated
 * property will be removed if present.
 *
 * @static
 * @param {Object} element Element object
 * @param {ve.dm.HashValueStore} store Hash-value store used by element
 * @param {boolean} preserveGenerated Preserve internal.generated property of element
 * @return {Object} Cloned element object
 */
ve.dm.Node.static.cloneElement = function ( element, store, preserveGenerated ) {
	var about, originalDomElements, domElements,
		modified = false,
		clone = ve.copy( element );
 
	if ( !preserveGenerated ) {
		ve.deleteProp( clone, 'internal', 'generated' );
	}
	originalDomElements = store.value( clone.originalDomElementsHash );
	// Generate a new about attribute to prevent about grouping of cloned nodes
	if ( originalDomElements ) {
		// TODO: The '#mwtNNN' is required by Parsoid. Make the name used here
		// more generic and specify the #mwt pattern in MW code.
		about = '#mwt' + Math.floor( 1000000000 * Math.random() );
		domElements = originalDomElements.map( function ( el ) {
			var elClone = el.cloneNode( true );
			// Check for hasAttribute as comments don't have them
			if ( elClone.hasAttribute && elClone.hasAttribute( 'about' ) ) {
				elClone.setAttribute( 'about', about );
				modified = true;
			}
			return elClone;
		} );
		if ( modified ) {
			clone.originalDomElementsHash = store.hash( domElements, domElements.map( ve.getNodeHtml ).join( '' ) );
		}
	}
	return clone;
};
 
/* Methods */
 
/**
 * @inheritdoc
 */
ve.dm.Node.prototype.getStore = function () {
	return this.doc && this.doc.store;
};
 
/**
 * @see #static-cloneElement
 * Implementations should override the static method, not this one
 *
 * @param {boolean} preserveGenerated Preserve internal.generated property of element
 * @return {Object} Cloned element object
 */
ve.dm.Node.prototype.getClonedElement = function ( preserveGenerated ) {
	var store = this.getStore();
	Iif ( !store ) {
		throw new Error( 'Node must be attached to the document to be cloned.' );
	}
	return this.constructor.static.cloneElement( this.element, store, preserveGenerated );
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.getChildNodeTypes = function () {
	return this.constructor.static.childNodeTypes;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.getParentNodeTypes = function () {
	return this.constructor.static.parentNodeTypes;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.getSuggestedParentNodeTypes = function () {
	return this.constructor.static.suggestedParentNodeTypes;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.canHaveChildren = function () {
	return ve.dm.nodeFactory.canNodeHaveChildren( this.type );
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.canHaveChildrenNotContent = function () {
	return ve.dm.nodeFactory.canNodeHaveChildrenNotContent( this.type );
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.isInternal = function () {
	return this.constructor.static.isInternal;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.isMetaData = function () {
	return this.constructor.static.isMetaData;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.isWrapped = function () {
	return this.constructor.static.isWrapped;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.isUnwrappable = function () {
	return this.isWrapped() && this.constructor.static.isUnwrappable;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.canContainContent = function () {
	return this.constructor.static.canContainContent;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.isContent = function () {
	return this.constructor.static.isContent;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.isFocusable = function () {
	return this.constructor.static.isFocusable;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.isAlignable = function () {
	return this.constructor.static.isAlignable;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.isCellable = function () {
	return this.constructor.static.isCellable;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.isCellEditable = function () {
	return this.constructor.static.isCellEditable;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.isDiffedAsList = function () {
	return this.constructor.static.isDiffedAsList;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.isDiffedAsLeaf = function () {
	return this.constructor.static.isDiffedAsLeaf;
};
 
/**
 * Check if the node can have a slug before it.
 *
 * @return {boolean} Whether the node can have a slug before it
 */
ve.dm.Node.prototype.canHaveSlugBefore = function () {
	return !this.canContainContent() && this.getParentNodeTypes() === null;
};
 
/**
 * Check if the node can have a slug after it.
 *
 * @method
 * @return {boolean} Whether the node can have a slug after it
 */
ve.dm.Node.prototype.canHaveSlugAfter = ve.dm.Node.prototype.canHaveSlugBefore;
 
/**
 * A string identifier used to suppress slugs
 *
 * If sequential nodes have the same non-null suppressSlugType, then
 * no slug is shown, e.g. two floated images can return 'float' to
 * suppress the slug between them.
 *
 * @return {string|null} Type
 */
ve.dm.Node.prototype.suppressSlugType = function () {
	return null;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.hasSignificantWhitespace = function () {
	return this.constructor.static.hasSignificantWhitespace;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.handlesOwnChildren = function () {
	return this.constructor.static.handlesOwnChildren;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.shouldIgnoreChildren = function () {
	return this.constructor.static.ignoreChildren;
};
 
/**
 * Check if the node can be the root of a branch exposed in a ve.ce.Surface
 *
 * @return {boolean} Node can be the root of a surfaced branch
 */
ve.dm.Node.prototype.isSurfaceable = function () {
	return this.hasChildren() && !this.canContainContent() && !this.isMetaData() && !this.getChildNodeTypes();
};
 
/**
 * Check if the node has an ancestor with matching type and attribute values.
 *
 * @param {string} type Node type to match
 * @param {Object} [attributes] Node attributes to match
 * @return {boolean} Node has an ancestor with matching type and attribute values
 */
ve.dm.Node.prototype.hasMatchingAncestor = function ( type, attributes ) {
	var node = this;
	// Traverse up to matching node
	while ( node && !node.matches( type, attributes ) ) {
		node = node.getParent();
		// Return false if we reach the root without finding anything
		if ( node === null ) {
			return false;
		}
	}
	return true;
};
 
/**
 * Check if the node matches type and attribute values.
 *
 * @param {string} type Node type to match
 * @param {Object} [attributes] Node attributes to match
 * @return {boolean} Node matches type and attribute values
 */
ve.dm.Node.prototype.matches = function ( type, attributes ) {
	var key;
 
	if ( this.getType() !== type ) {
		return false;
	}
 
	// Check attributes
	Iif ( attributes ) {
		for ( key in attributes ) {
			if ( this.getAttribute( key ) !== attributes[ key ] ) {
				return false;
			}
		}
	}
	return true;
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.getLength = function () {
	return this.length;
};
 
/**
 * Set the inner length of the node.
 *
 * This should only be called after a relevant change to the document data. Calling this method will
 * not change the document data.
 *
 * @param {number} length Length of content
 * @fires lengthChange
 * @fires update
 * @throws {Error} Invalid content length error if length is less than 0
 */
ve.dm.Node.prototype.setLength = function ( length ) {
	var diff;
	if ( length < 0 ) {
		throw new Error( 'Length cannot be negative' );
	}
	// Compute length adjustment from old length
	diff = length - this.length;
	// Set new length
	this.length = length;
	// Adjust the parent's length
	if ( this.parent ) {
		this.parent.adjustLength( diff );
	}
	// Emit events
	this.emit( 'lengthChange', diff );
	this.emit( 'update' );
};
 
/**
 * Adjust the length.
 *
 * This should only be called after a relevant change to the document data. Calling this method will
 * not change the document data.
 *
 * @param {number} adjustment Amount to adjust length by
 * @fires lengthChange
 * @fires update
 * @throws {Error} Invalid adjustment error if resulting length is less than 0
 */
ve.dm.Node.prototype.adjustLength = function ( adjustment ) {
	this.setLength( this.length + adjustment );
};
 
/**
 * @inheritdoc ve.Node
 */
ve.dm.Node.prototype.getOffset = function () {
	var i, len, siblings, offset;
 
	if ( !this.parent ) {
		return 0;
	}
 
	// Find our index in the parent and add up lengths while we do so
	siblings = this.parent.children;
	offset = this.parent.getOffset() + ( this.parent === this.root ? 0 : 1 );
	for ( i = 0, len = siblings.length; i < len; i++ ) {
		if ( siblings[ i ] === this ) {
			break;
		}
		offset += siblings[ i ].getOuterLength();
	}
	Iif ( i === len ) {
		throw new Error( 'Node not found in parent\'s children array' );
	}
	return offset;
};
 
/**
 * Check if the node can be merged with another.
 *
 * For two nodes to be mergeable, the two nodes must either be the same node or:
 *  - Have the same type
 *  - Have the same depth
 *  - Have similar ancestry (each node upstream must have the same type)
 *
 * @param {ve.dm.Node} node Node to consider merging with
 * @return {boolean} Nodes can be merged
 */
ve.dm.Node.prototype.canBeMergedWith = function ( node ) {
	var n1 = this,
		n2 = node;
 
	// Content node can be merged with node that can contain content, for instance: TextNode
	// and ParagraphNode. When this method is called for such case (one node is a content node and
	// the other one can contain content) make sure to start traversal from node that can contain
	// content (instead of content node itself).
	if ( n1.canContainContent() && n2.isContent() ) {
		n2 = n2.getParent();
	} else if ( n2.canContainContent() && n1.isContent() ) {
		n1 = n1.getParent();
	}
	// Move up from n1 and n2 simultaneously until we find a common ancestor
	while ( n1 !== n2 ) {
		if (
			// Check if we have reached a root (means there's no common ancestor or unequal depth)
			( n1 === null || n2 === null ) ||
			// Ensure that types match
			n1.getType() !== n2.getType()
		) {
			return false;
		}
		// Move up
		n1 = n1.getParent();
		n2 = n2.getParent();
	}
	return true;
};