All files / src/dm ve.dm.BranchNode.js

100% Statements 73/73
94.87% Branches 37/39
100% Functions 12/12
100% Lines 72/72

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                                      1x   15985x     15985x     15985x     15985x 38x                           1x   1x                   1x 1x 1x               1x 1x 1x 1x 1x                   1x 1x 1x               1x 1x 1x 1x 1x                         1x 16295x   16295x 16295x 1169x 1169x     16295x 20008x 20008x     16295x 16295x 16295x   16295x               1x 16861x   16861x   16861x   4636x                     12225x 12225x 12225x 12225x     29834x 780x       29834x     29834x   29834x 2438x 2438x   2438x 2436x       29834x 29834x                   1x 584x   584x 200x   384x 722x 722x 370x     14x               1x 20x 20x 79x 22x         20x    
/*!
 * VisualEditor DataModel BranchNode class.
 *
 * @copyright See AUTHORS.txt
 */
 
/**
 * DataModel branch node.
 *
 * Branch nodes can have branch or leaf nodes as children.
 *
 * @abstract
 * @extends ve.dm.Node
 * @mixes ve.BranchNode
 *
 * @constructor
 * @param {Object} [element] Reference to element in linear model
 * @param {ve.dm.Node[]} [children] Child nodes to attach
 */
ve.dm.BranchNode = function VeDmBranchNode( element, children ) {
	// Mixin constructor
	ve.BranchNode.call( this );
 
	// Parent constructor
	ve.dm.BranchNode.super.call( this, element );
 
	// Properties
	this.slugPositions = {};
 
	// TODO: children is only ever used in tests
	if ( Array.isArray( children ) && children.length ) {
		this.splice( 0, 0, ...children );
	}
};
 
/**
 * @event ve.dm.BranchNode#splice
 * @see #method-splice
 * @param {number} index
 * @param {number} deleteCount
 * @param {...ve.dm.BranchNode} [nodes]
 */
 
/* Inheritance */
 
OO.inheritClass( ve.dm.BranchNode, ve.dm.Node );
 
OO.mixinClass( ve.dm.BranchNode, ve.BranchNode );
 
/* Methods */
 
/**
 * Add a child node to the end of the list.
 *
 * @param {ve.dm.BranchNode} childModel Item to add
 * @return {number} New number of children
 */
ve.dm.BranchNode.prototype.push = function ( childModel ) {
	this.splice( this.children.length, 0, childModel );
	return this.children.length;
};
 
/**
 * Remove a child node from the end of the list.
 *
 * @return {ve.dm.BranchNode|undefined} Removed childModel
 */
ve.dm.BranchNode.prototype.pop = function () {
	Eif ( this.children.length ) {
		const childModel = this.children[ this.children.length - 1 ];
		this.splice( this.children.length - 1, 1 );
		return childModel;
	}
};
 
/**
 * Add a child node to the beginning of the list.
 *
 * @param {ve.dm.BranchNode} childModel Item to add
 * @return {number} New number of children
 */
ve.dm.BranchNode.prototype.unshift = function ( childModel ) {
	this.splice( 0, 0, childModel );
	return this.children.length;
};
 
/**
 * Remove a child node from the beginning of the list.
 *
 * @return {ve.dm.BranchNode|undefined} Removed childModel
 */
ve.dm.BranchNode.prototype.shift = function () {
	Eif ( this.children.length ) {
		const childModel = this.children[ 0 ];
		this.splice( 0, 1 );
		return childModel;
	}
};
 
/**
 * Add and/or remove child nodes at an offset.
 *
 * @param {number} index Index to remove and or insert nodes at
 * @param {number} deleteCount Number of nodes to remove
 * @param {...ve.dm.BranchNode} [nodes] Variadic list of nodes to insert
 * @fires ve.dm.BranchNode#splice
 * @return {ve.dm.BranchNode[]} Removed nodes
 */
ve.dm.BranchNode.prototype.splice = function ( index, deleteCount, ...nodes ) {
	let diff = 0;
 
	const removals = this.children.splice( index, deleteCount, ...nodes );
	removals.forEach( ( node ) => {
		node.detach();
		diff -= node.getOuterLength();
	} );
 
	nodes.forEach( ( node ) => {
		node.attach( this );
		diff += node.getOuterLength();
	} );
 
	this.adjustLength( diff, true );
	this.setupBlockSlugs();
	this.emit( 'splice', index, deleteCount, ...nodes );
 
	return removals;
};
 
/**
 * Setup a sparse array of booleans indicating where to place slugs
 *
 * TODO: The function name is misleading: in ContentBranchNodes it sets up inline slugs
 */
ve.dm.BranchNode.prototype.setupBlockSlugs = function () {
	const isBlock = this.canHaveChildrenNotContent();
 
	this.slugPositions = {};
 
	if ( isBlock && !this.isAllowedChildNodeType( 'paragraph' ) ) {
		// Don't put slugs in nodes which can't contain paragraphs
		return;
	}
 
	// Consider every position between two child nodes, before first child and after last child.
	// Skip over metadata children. Add slugs in appropriate places.
 
	// Support: Firefox
	// Note that this inserts a slug at position 0 if this content branch has no items or only
	// internal items, keeping the node from becoming invisible/unfocusable. In Firefox, backspace
	// after Ctrl+A leaves the document completely empty, so this ensures DocumentNode gets a slug.
 
	const len = this.children.length;
	let i = -1; // from -1 to len-1
	let j = 0; // from 0 to len
	while ( i < len ) {
		// If the next node is a meta item, find the first non-meta node after it, and consider that
		// one instead when deciding to insert a slug. Meta nodes themselves don't have slugs.
		while ( j < len && this.children[ j ].isMetaData() ) {
			j++;
		}
 
		// Can have slug at the beginning, or after every node which allows it (except internal nodes)
		const canHaveSlugAfter = i === -1 || ( this.children[ i ].canHaveSlugAfter() &&
			!this.children[ i ].isInternal() );
		// Can have slug at the end, or before every node which allows it
		const canHaveSlugBefore = j === len || this.children[ j ].canHaveSlugBefore();
 
		if ( canHaveSlugAfter && canHaveSlugBefore ) {
			const suppressSlugTypeAfter = this.children[ j ] && this.children[ j ].suppressSlugType();
			const suppressSlugTypeBefore = this.children[ i ] && this.children[ i ].suppressSlugType();
			// Slugs are suppressed if they have the same string type, e.g. for adjacent floated images
			if ( !( typeof suppressSlugTypeAfter === 'string' && suppressSlugTypeAfter === suppressSlugTypeBefore ) ) {
				this.slugPositions[ j ] = true;
			}
		}
 
		i = j;
		j++;
	}
};
 
/**
 * Check in the branch node has a slug at a particular offset
 *
 * @param {number} offset Offset to check for a slug at
 * @return {boolean} There is a slug at the offset
 */
ve.dm.BranchNode.prototype.hasSlugAtOffset = function ( offset ) {
	let startOffset = this.getOffset() + ( this.isWrapped() ? 1 : 0 );
 
	if ( offset === startOffset ) {
		return !!this.slugPositions[ 0 ];
	}
	for ( let i = 0; i < this.children.length; i++ ) {
		startOffset += this.children[ i ].getOuterLength();
		if ( offset === startOffset ) {
			return !!this.slugPositions[ i + 1 ];
		}
	}
	return false;
};
 
/**
 * Get all annotations and the ranges they cover
 *
 * @return {ve.dm.ElementLinearData.AnnotationRange[]} Contiguous annotation ranges, ordered by start then end
 */
ve.dm.BranchNode.prototype.getAnnotationRanges = function () {
	const annotationRanges = [];
	this.traverse( ( node ) => {
		if ( node.canContainContent() ) {
			annotationRanges.push(
				...this.getDocument().data.getAnnotationRanges( node.getRange() )
			);
		}
	} );
	return annotationRanges;
};