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

91.26% Statements 94/103
83.63% Branches 46/55
100% Functions 24/24
91.26% Lines 94/103

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 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                              1x   24167x 24167x         1x                     1x                     1x                       1x                       1x                                   1x                                                                                                               1x 2136x                                             1x 1313x 1313x                                     1x                               1x                     1x 3988x       3988x 1945x   3988x                 1x 10236x                 1x 4749x                     1x 61x 61x 53x 53x 40x     61x                   1x 26x   26x 9x 17x 2x 15x   10x 7x 3x 3x       5x 5x     5x                             1x 10x     10x 10x   10x 10x   10x 25x   10x 10x   10x 10x   5x 5x 5x       10x                   1x 65x 65x 65x                         1x 647x                         1x 46x                     1x 24x               1x 4021x               1x 347x               1x 1693x                     1x 13743x                     1x 959x 959x                 959x               1x 3400x                 1x 3046x                   1x 51x                         1x 2550x                       1x 659x    
/*!
 * VisualEditor DataModel Model class.
 *
 * @copyright See AUTHORS.txt
 */
 
/**
 * Base class for DM models.
 *
 * @class
 * @abstract
 *
 * @constructor
 * @param {Object} element Reference to plain object in linear model
 */
ve.dm.Model = function VeDmModel( element ) {
	// Properties
	this.element = element || { type: this.constructor.static.name };
	this.store = null;
};
 
/* Inheritance */
 
OO.initClass( ve.dm.Model );
 
/* Static Properties */
 
/**
 * Symbolic name for this model class. Must be set to a unique string by every subclass.
 *
 * @static
 * @property {string}
 * @inheritable
 */
ve.dm.Model.static.name = null;
 
/**
 * Array of HTML tag names that this model should be a match candidate for.
 * Empty array means none, null means any.
 * For more information about element matching, see ve.dm.ModelRegistry.
 *
 * @static
 * @property {string[]}
 * @inheritable
 */
ve.dm.Model.static.matchTagNames = null;
 
/**
 * Array of RDFa types that this model should be a match candidate for.
 * Any other types the element might have must be specified in allowedRdfaTypes.
 * Empty array means none, null means any.
 * For more information about element matching, see ve.dm.ModelRegistry.
 *
 * @static
 * @property {Array.<string|RegExp>|null}
 * @inheritable
 */
ve.dm.Model.static.matchRdfaTypes = null;
 
/**
 * Extra RDFa types that the element is allowed to have (but don't by
 * themselves trigger a match).
 * Empty array means none, null means any.
 * For more information about element matching, see ve.dm.ModelRegistry.
 *
 * @static
 * @property {Array.<string|RegExp>|null}
 * @inheritable
 */
ve.dm.Model.static.allowedRdfaTypes = [];
 
/**
 * Optional function to determine whether this model should match a given element.
 * Takes a Node and returns true or false.
 * This function is only called if this model has a chance of "winning"; see
 * ve.dm.ModelRegistry for more information about element matching.
 * If set to null, this property is ignored. Setting this to null is not the same as unconditionally
 * returning true, because the presence or absence of a matchFunction affects the model's
 * specificity.
 *
 * NOTE: This function is NOT a method, within this function "this" will not refer to an instance
 * of this class (or to anything reasonable, for that matter).
 *
 * @static
 * @property {Function}
 * @inheritable
 */
ve.dm.Model.static.matchFunction = null;
 
/**
 * Static function to convert a DOM element or set of sibling DOM elements to a linear model element
 * for this model type.
 *
 * This function is only called if this model "won" the matching for the first DOM element, so
 * domElements[0] will match this model's matching rule. There is usually only one DOM node in
 * domElements[]. Multiple elements will only be passed if this model supports about groups.
 * If there are multiple nodes, the nodes are all adjacent siblings in the same about group
 * (i.e. they are grouped together because they have the same value for the about attribute).
 *
 * The converter has some state variables that can be obtained by this function:
 * - if converter.isExpectingContent() returns true, the converter expects a content element
 * - if converter.isInWrapper() returns true, the returned element will be put in a wrapper
 *   paragraph generated by the converter (this is only relevant if isExpectingContent() is true)
 * - converter.canCloseWrapper() returns true if the current wrapper paragraph can be closed,
 *   and false if it can't be closed or if there is no active wrapper
 *
 * This function is allowed to return a content element when context indicates that a non-content
 * element is expected or vice versa. If that happens, the converter deals with it in the following way:
 *
 * - if a non-content element is expected but a content element is returned:
 *     - open a wrapper paragraph
 *     - put the returned element in the wrapper
 * - if a content element is expected but a non-content element is returned:
 *     - if we are in a wrapper paragraph:
 *         - if we can close the wrapper:
 *             - close the wrapper
 *             - insert the returned element right after the end of the wrapper
 *         - if we can't close the wrapper:
 *             - alienate the element
 *     - if we aren't in a wrapper paragraph:
 *         - alienate the element
 *
 * For these purposes, annotations are considered content. Meta-items can occur anywhere, so if
 * a meta-element is returned no special action is taken. Note that "alienate" always means an alien
 * **node** (ve.dm.AlienNode) will be generated, never an alien meta-item (ve.dm.AlienMetaItem),
 * regardless of whether the subclass attempting the conversion is a node or a meta-item.
 *
 * The returned linear model element must have a type property set to a registered model name
 * (usually the model's own .static.name, but that's not required). It may optionally have an attributes
 * property set to an object with key-value pairs. Any other properties are not allowed.
 *
 * This function may return a single linear model element, or an array of balanced linear model
 * data. If this function needs to recursively convert a DOM node (e.g. a child of one of the
 * DOM elements passed in), it can call converter.getDataFromDomSubtree( domElement ). Note that
 * if an array is returned, the converter will not descend into the DOM node's children; the model
 * will be assumed to have handled those children.
 *
 * @static
 * @inheritable
 * @param {Node[]} domElements DOM elements to convert. Usually only one element
 * @param {ve.dm.Converter} converter
 * @return {Object|Array|null} Linear model element, or array with linear model data, or null to alienate
 */
ve.dm.Model.static.toDataElement = function () {
	return { type: this.name };
};
 
/**
 * Static function to convert a linear model data element for this model type back to one or more
 * DOM elements.
 *
 * If this model is a node with handlesOwnChildren set to true, dataElement will be an array of
 * the linear model data of this node and all of its children, rather than a single element.
 * In this case, this function may want to recursively convert linear model data to DOM, which can
 * be done with ve.dm.Converter#getDomSubtreeFromData.
 *
 * NOTE: If this function returns multiple DOM elements, the DOM elements produced by the children
 * of this model (if it's a node and has children) will be attached to the first DOM element in the array.
 * For annotations, only the first element is used, and any additional elements are ignored.
 *
 * @static
 * @inheritable
 * @param {Object|Array} dataElement Linear model element or array of linear model data
 * @param {HTMLDocument} doc HTML document for creating elements
 * @param {ve.dm.Converter} converter Converter object to optionally call `getDomSubtreeFromData` on
 * @return {Node[]} DOM elements
 */
ve.dm.Model.static.toDomElements = function ( dataElement, doc ) {
	Eif ( this.matchTagNames && this.matchTagNames.length === 1 ) {
		return [ doc.createElement( this.matchTagNames[ 0 ] ) ];
	}
	throw new Error( 've.dm.Model subclass must match a single tag name or implement toDomElements' );
};
 
/**
 * Whether this model supports about grouping. When a DOM element matches a model type that has
 * about grouping enabled, the converter will look for adjacent siblings with the same value for
 * the about attribute, and ask #toDataElement to produce a single data element for all of those
 * DOM nodes combined.
 *
 * The converter doesn't descend into about groups, i.e. it doesn't convert the children of the
 * DOM elements that make up the about group. This means the resulting linear model element will
 * be childless.
 *
 * @static
 * @property {boolean}
 * @inheritable
 */
ve.dm.Model.static.enableAboutGrouping = false;
 
/**
 * Which HTML attributes should be preserved for this model type. When converting back to DOM,
 * these HTML attributes will be restored except for attributes that were already set by #toDomElements.
 *
 * The value of this property can be one of the following:
 *
 * - true, to preserve all attributes (default)
 * - false, to preserve none
 * - a function that takes an attribute name and returns true or false
 *
 * @static
 * @property {boolean|Function}
 * @inheritable
 */
ve.dm.Model.static.preserveHtmlAttributes = true;
 
/* Static methods */
 
/**
 * Get hash object of a linear model data element.
 *
 * @static
 * @param {Object} dataElement Data element
 * @return {Object} Hash object
 */
ve.dm.Model.static.getHashObject = function ( dataElement ) {
	const hash = {
		type: dataElement.type,
		attributes: dataElement.attributes
	};
	if ( dataElement.originalDomElementsHash !== undefined ) {
		hash.originalDomElementsHash = dataElement.originalDomElementsHash;
	}
	return hash;
};
 
/**
 * Array of RDFa types that this model should be a match candidate for.
 *
 * @static
 * @return {Array.<string|RegExp>|null} Array of strings or regular expressions
 */
ve.dm.Model.static.getMatchRdfaTypes = function () {
	return this.matchRdfaTypes;
};
 
/**
 * Extra RDFa types that the element is allowed to have.
 *
 * @static
 * @return {Array.<string|RegExp>|null} Array of strings or regular expressions
 */
ve.dm.Model.static.getAllowedRdfaTypes = function () {
	return this.allowedRdfaTypes;
};
 
/**
 * Describe attribute changes in the model
 *
 * @param {Object} attributeChanges Attribute changes, keyed list containing objects with from and to properties
 * @param {Object} attributes New attributes
 * @param {Object} element New element
 * @return {Array} Descriptions, list of strings or Node arrays
 */
ve.dm.Model.static.describeChanges = function ( attributeChanges ) {
	const descriptions = [];
	for ( const key in attributeChanges ) {
		const change = this.describeChange( key, attributeChanges[ key ] );
		if ( change ) {
			descriptions.push( change );
		}
	}
	return descriptions;
};
 
/**
 * Describe a single attribute change in the model
 *
 * @param {string} key Attribute key
 * @param {Object} change Change object with from and to properties
 * @return {string|Node[]|null} Description (string or Node array), or null if nothing to describe
 */
ve.dm.Model.static.describeChange = function ( key, change ) {
	Iif ( ( typeof change.from === 'object' && change.from !== null ) || ( typeof change.to === 'object' && change.to !== null ) ) {
		return ve.htmlMsg( 'visualeditor-changedesc-unknown', key );
	} else if ( change.from === undefined || change.from === null ) {
		return ve.htmlMsg( 'visualeditor-changedesc-set', key, this.wrapText( 'ins', change.to ) );
	} else if ( change.to === undefined || change.to === null ) {
		return ve.htmlMsg( 'visualeditor-changedesc-unset', key, this.wrapText( 'del', change.from ) );
	} else if ( key === 'listItemDepth' ) {
		// listItemDepth is a special key used on nodes which have isDiffedAsList set
		if ( change.to > change.from ) {
			return ve.msg( 'visualeditor-changedesc-list-indent' );
		} else Eif ( change.to < change.from ) {
			return ve.msg( 'visualeditor-changedesc-list-outdent' );
		}
	} else {
		// Use String() for string casting as values could be null
		const diff = this.getAttributeDiff( String( change.from ), String( change.to ) );
		Iif ( diff ) {
			return ve.htmlMsg( 'visualeditor-changedesc-changed-diff', key, diff );
		} else {
			return ve.htmlMsg( 'visualeditor-changedesc-changed', key, this.wrapText( 'del', change.from ), this.wrapText( 'ins', change.to ) );
		}
	}
	return null;
};
 
/**
 * Compare two attribute strings and return an HTML diff
 *
 * @param {string} oldText Old attribute text
 * @param {string} newText New attribute text
 * @param {boolean} [allowRemoveInsert] Allow the diff to be a full remove insert
 * @return {HTMLElement|null} An HTML diff in a span element, or null if the diff
 * was a simple remove-insert, and allowRemoveInsert wasn't set.
 */
ve.dm.Model.static.getAttributeDiff = function ( oldText, newText, allowRemoveInsert ) {
	const span = document.createElement( 'span' ),
		/* global diff_match_patch */
		// eslint-disable-next-line new-cap
		differ = new diff_match_patch();
	let isRemoveInsert = true;
 
	const diff = differ.diff_main( oldText, newText );
	differ.diff_cleanupEfficiency( diff );
 
	diff.forEach( ( part ) => {
		switch ( part[ 0 ] ) {
			case -1:
				span.appendChild( this.wrapText( 'del', part[ 1 ] ) );
				break;
			case 1:
				span.appendChild( this.wrapText( 'ins', part[ 1 ] ) );
				break;
			case 0:
				isRemoveInsert = false;
				span.appendChild( document.createTextNode( part[ 1 ] ) );
				break;
		}
	} );
 
	return !isRemoveInsert || allowRemoveInsert ? span : null;
};
 
/**
 * Utility function for wrapping text in a tag, equivalent to `$( '<tag>' ).text( text )`
 *
 * @param {string} tag Wrapping element's tag
 * @param {string} text
 * @return {HTMLElement} Element wrapping text
 */
ve.dm.Model.static.wrapText = function ( tag, text ) {
	const wrapper = document.createElement( tag );
	wrapper.appendChild( document.createTextNode( text ) );
	return wrapper;
};
 
/**
 * Check if this element is of the same type as another element for the purposes of diffing.
 *
 * @static
 * @param {Object} element This element
 * @param {Object} other Another element
 * @param {ve.dm.HashValueStore} elementStore Store used by this element
 * @param {ve.dm.HashValueStore} otherStore Store used by other elements
 * @return {boolean} Elements are of a comparable type
 */
ve.dm.Model.static.isDiffComparable = function ( element, other ) {
	return element.type === other.type;
};
 
/* Methods */
 
/**
 * Check whether this node can be inspected by a context item.
 *
 * The default implementation always returns true. If your node type is uninspectable in certain
 * cases, you should override this function.
 *
 * @return {boolean} Whether this node is inspectable
 */
ve.dm.Model.prototype.isInspectable = function () {
	return true;
};
 
/**
 * Check whether this node can be edited by a context item
 *
 * The default implementation always returns true. If your node type is uneditable in certain
 * cases, you should override this function.
 *
 * @return {boolean} Whether this node is editable
 */
ve.dm.Model.prototype.isEditable = function () {
	return true;
};
 
/**
 * Get a reference to the linear model element.
 *
 * @return {Object} Linear model element passed to the constructor, by reference
 */
ve.dm.Model.prototype.getElement = function () {
	return this.element;
};
 
/**
 * Get a reference to the hash-value store used by the element.
 *
 * @return {ve.dm.HashValueStore} Hash-value store
 */
ve.dm.Model.prototype.getStore = function () {
	return this.store;
};
 
/**
 * Get the symbolic name of this model's type.
 *
 * @return {string} Type name
 */
ve.dm.Model.prototype.getType = function () {
	return this.constructor.static.name;
};
 
/**
 * Get the value of an attribute.
 *
 * Return value is by reference if array or object.
 *
 * @param {string} key Name of attribute to get
 * @return {any} Value of attribute, or undefined if no such attribute exists
 */
ve.dm.Model.prototype.getAttribute = function ( key ) {
	return this.element && this.element.attributes ? this.element.attributes[ key ] : undefined;
};
 
/**
 * Get a copy of all attributes.
 *
 * Values are by reference if array or object, similar to using the getAttribute method.
 *
 * @param {string} [prefix] Only return attributes with this prefix, and remove the prefix from them
 * @return {Object} Attributes
 */
ve.dm.Model.prototype.getAttributes = function ( prefix ) {
	const attributes = this.element && this.element.attributes ? this.element.attributes : {};
	Iif ( prefix ) {
		const filtered = {};
		for ( const key in attributes ) {
			if ( key.indexOf( prefix ) === 0 ) {
				filtered[ key.slice( prefix.length ) ] = attributes[ key ];
			}
		}
		return filtered;
	}
	return ve.extendObject( {}, attributes );
};
 
/**
 * Get the DOM element(s) this model was originally converted from, if any.
 *
 * @return {string|undefined} Store hash of DOM elements this model was converted from
 */
ve.dm.Model.prototype.getOriginalDomElementsHash = function () {
	return this.element ? this.element.originalDomElementsHash : undefined;
};
 
/**
 * Get the DOM element(s) this model was originally converted from, if any.
 *
 * @param {ve.dm.HashValueStore} store Hash value store where the DOM elements are stored
 * @return {HTMLElement[]} DOM elements this model was converted from, empty if not applicable
 */
ve.dm.Model.prototype.getOriginalDomElements = function ( store ) {
	return store.value( this.getOriginalDomElementsHash() ) || [];
};
 
/**
 * Get a clone of the model's linear model element.
 *
 * The attributes object will be deep-copied.
 *
 * @return {Object} Cloned element object
 */
ve.dm.Model.prototype.getClonedElement = function () {
	return ve.copy( this.element );
};
 
/**
 * Get the hash object of the linear model element.
 *
 * The actual logic is in a static function as this needs
 * to be accessible from ve.dm.Converter
 *
 * This is a custom hash function for OO#getHash.
 *
 * @return {Object} Hash object
 */
ve.dm.Model.prototype.getHashObject = function () {
	return this.constructor.static.getHashObject( this.element );
};
 
/**
 * Check if this element is of the same type as another element for the purposes of diffing.
 *
 * Elements which aren't of the same type will always be shown as removal and an insertion,
 * whereas comarable elements will be shown as an attribute change.
 *
 * @param {Object} other Another element
 * @return {boolean} Elements are of a comparable type
 */
ve.dm.Model.prototype.isDiffComparable = function ( other ) {
	return this.constructor.static.isDiffComparable( this.element, other.element, this.getStore(), other.getStore() );
};