mediawiki-extensions-Visual.../modules/ve/dm/lineardata/ve.dm.ElementLinearData.js
Ed Sanders 6a52fba643 Only strip style attributes on rich paste from VE
Stripping all HTML atributes (to avoid CE-added styles such as
'font-size: 1em;') also strips data-parsoid which can cause
round trip errors. As an improvement only strip the style
attribute.

Bug: 58136
Change-Id: I34386bd847d1cf0583317a8b07916e43ff7af029
2013-12-09 22:25:43 +00:00

940 lines
31 KiB
JavaScript

/*!
* VisualEditor ElementLinearData classes.
*
* Class containing element linear data and an index-value store.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Element linear data storage
*
* @class
* @extends ve.dm.FlatLinearData
* @constructor
* @param {ve.dm.IndexValueStore} store Index-value store
* @param {Array} [data] Linear data
*/
ve.dm.ElementLinearData = function VeDmElementLinearData( store, data ) {
ve.dm.FlatLinearData.call( this, store, data );
};
/* Inheritance */
OO.inheritClass( ve.dm.ElementLinearData, ve.dm.FlatLinearData );
/* Static Methods */
/**
* Compare two elements ignoring any annotations
*
* @param {Object|Array|string} a First element
* @param {Object|Array|string} b Second element
* @returns {boolean} Elements are comparable
*/
ve.dm.ElementLinearData.static.compareUnannotated = function ( a, b ) {
if ( a === undefined || b === undefined ) {
return false;
}
var aPlain = a, bPlain = b;
if ( ve.isArray( a ) ) {
aPlain = a[0];
}
if ( ve.isArray( b ) ) {
bPlain = b[0];
}
if ( a && a.type ) {
aPlain = ve.copy( a );
delete aPlain.annotations;
delete aPlain.internal;
}
if ( b && b.type ) {
bPlain = ve.copy( b );
delete bPlain.annotations;
delete bPlain.internal;
}
return ve.compare( aPlain, bPlain );
};
/* Methods */
/**
* Check if content can be inserted at an offset in document data.
*
* This method assumes that any value that has a type property that's a string is an element object.
*
* Content offsets:
*
* <heading> a </heading> <paragraph> b c <img> </img> </paragraph>
* . ^ ^ . ^ ^ ^ . ^ .
*
* Content offsets:
*
* <list> <listItem> </listItem> <list>
* . . . . .
*
* @method
* @param {number} offset Document offset
* @returns {boolean} Content can be inserted at offset
*/
ve.dm.ElementLinearData.prototype.isContentOffset = function ( offset ) {
// Edges are never content
if ( offset === 0 || offset === this.getLength() ) {
return false;
}
var left = this.getData( offset - 1 ),
right = this.getData( offset ),
factory = ve.dm.nodeFactory;
return (
// Data exists at offsets
( left !== undefined && right !== undefined ) &&
(
// If there's content on the left or the right of the offset than we are good
// <paragraph>|a|</paragraph>
( typeof left === 'string' || typeof right === 'string' ) ||
// Same checks but for annotated characters - isArray is slower, try it next
( ve.isArray( left ) || ve.isArray( right ) ) ||
// The most expensive test are last, these deal with elements
(
// Right of a leaf
// <paragraph><image></image>|</paragraph>
(
// Is an element
typeof left.type === 'string' &&
// Is a closing
left.type.charAt( 0 ) === '/' &&
// Is a leaf
factory.isNodeContent( left.type.substr( 1 ) )
) ||
// Left of a leaf
// <paragraph>|<image></image></paragraph>
(
// Is an element
typeof right.type === 'string' &&
// Is not a closing
right.type.charAt( 0 ) !== '/' &&
// Is a leaf
factory.isNodeContent( right.type )
) ||
// Inside empty content branch
// <paragraph>|</paragraph>
(
// Inside empty element
'/' + left.type === right.type &&
// Both are content branches (right is the same type)
factory.canNodeContainContent( left.type )
)
)
)
);
};
/**
* Check if structure can be inserted at an offset in document data.
*
* If the {unrestricted} param is true than only offsets where any kind of element can be inserted
* will return true. This can be used to detect the difference between a location that a paragraph
* can be inserted, such as between two tables but not direclty inside a table.
*
* This method assumes that any value that has a type property that's a string is an element object.
*
* Structural offsets (unrestricted = false):
*
* <heading> a </heading> <paragraph> b c <img> </img> </paragraph>
* ^ . . ^ . . . . . ^
*
* Structural offsets (unrestricted = true):
*
* <heading> a </heading> <paragraph> b c <img> </img> </paragraph>
* ^ . . ^ . . . . . ^
*
* Structural offsets (unrestricted = false):
*
* <list> <listItem> </listItem> <list>
* ^ ^ ^ ^ ^
*
* Content branch offsets (unrestricted = true):
*
* <list> <listItem> </listItem> <list>
* ^ . ^ . ^
*
* @method
* @param {number} offset Document offset
* @param {boolean} [unrestricted] Only return true if any kind of element can be inserted at offset
* @returns {boolean} Structure can be inserted at offset
*/
ve.dm.ElementLinearData.prototype.isStructuralOffset = function ( offset, unrestricted ) {
// Edges are always structural
if ( offset === 0 || offset === this.getLength() ) {
return true;
}
// Offsets must be within range and both sides must be elements
var left = this.getData( offset - 1 ),
right = this.getData( offset ),
factory = ve.dm.nodeFactory;
return (
(
left !== undefined &&
right !== undefined &&
typeof left.type === 'string' &&
typeof right.type === 'string'
) &&
(
// Right of a branch
// <list><listItem><paragraph>a</paragraph>|</listItem>|</list>|
(
// Is a closing
left.type.charAt( 0 ) === '/' &&
// Is a branch or non-content leaf
(
factory.canNodeHaveChildren( left.type.substr( 1 ) ) ||
!factory.isNodeContent( left.type.substr( 1 ) )
) &&
(
// Only apply this rule in unrestricted mode
!unrestricted ||
// Right of an unrestricted branch
// <list><listItem><paragraph>a</paragraph>|</listItem></list>|
// Both are non-content branches that can have any kind of child
factory.getParentNodeTypes( left.type.substr( 1 ) ) === null
)
) ||
// Left of a branch
// |<list>|<listItem>|<paragraph>a</paragraph></listItem></list>
(
// Is not a closing
right.type.charAt( 0 ) !== '/' &&
// Is a branch or non-content leaf
(
factory.canNodeHaveChildren( right.type ) ||
!factory.isNodeContent( right.type )
) &&
(
// Only apply this rule in unrestricted mode
!unrestricted ||
// Left of an unrestricted branch
// |<list><listItem>|<paragraph>a</paragraph></listItem></list>
// Both are non-content branches that can have any kind of child
factory.getParentNodeTypes( right.type ) === null
)
) ||
// Inside empty non-content branch
// <list>|</list> or <list><listItem>|</listItem></list>
(
// Inside empty element
'/' + left.type === right.type &&
// Both are non-content branches (right is the same type)
factory.canNodeHaveChildrenNotContent( left.type ) &&
(
// Only apply this rule in unrestricted mode
!unrestricted ||
// Both are non-content branches that can have any kind of child
factory.getChildNodeTypes( left.type ) === null
)
)
)
);
};
/**
* Check for elements in document data.
*
* This method assumes that any value that has a type property that's a string is an element object.
* Elements are discovered by iterating through the entire data array (backwards).
*
* @method
* @returns {boolean} At least one elements exists in data
*/
ve.dm.ElementLinearData.prototype.containsElementData = function () {
var i = this.getLength();
while ( i-- ) {
if ( this.getData( i ).type !== undefined ) {
return true;
}
}
return false;
};
/**
* Check for non-content elements in document data.
*
* This method assumes that any value that has a type property that's a string is an element object.
* Elements are discovered by iterating through the entire data array.
*
* @method
* @returns {boolean} True if all elements in data are content elements
*/
ve.dm.ElementLinearData.prototype.isContentData = function () {
var item, i = this.getLength();
while ( i-- ) {
item = this.getData( i );
if ( item.type !== undefined &&
item.type.charAt( 0 ) !== '/' &&
!ve.dm.nodeFactory.isNodeContent( item.type )
) {
return false;
}
}
return true;
};
/**
* Get annotations' store indexes covered by an offset.
*
* @method
* @param {number} offset Offset to get annotations for
* @returns {number[]} An array of annotation store indexes the offset is covered by
* @throws {Error} offset out of bounds
*/
ve.dm.ElementLinearData.prototype.getAnnotationIndexesFromOffset = function ( offset ) {
if ( offset < 0 || offset > this.getLength() ) {
throw new Error( 'offset ' + offset + ' out of bounds' );
}
var element = this.getData( offset );
// Since annotations are not stored on a closing leaf node,
// rewind offset by 1 to return annotations for that structure
if (
ve.isPlainObject( element ) && // structural offset
element.hasOwnProperty( 'type' ) && // just in case
element.type.charAt( 0 ) === '/' && // closing offset
ve.dm.nodeFactory.canNodeHaveChildren(
element.type.substr( 1 )
) === false // leaf node
) {
offset = this.getRelativeContentOffset( offset, -1 );
element = this.getData( offset );
}
if ( element === undefined || typeof element === 'string' ) {
return [];
} else {
return element.annotations || element[1] || [];
}
};
/**
* Get annotations covered by an offset.
*
* The returned AnnotationSet is a clone of the one in the document data.
*
* @method
* @param {number} offset Offset to get annotations for
* @returns {ve.dm.AnnotationSet} A set of all annotation objects offset is covered by
* @throws {Error} offset out of bounds
*/
ve.dm.ElementLinearData.prototype.getAnnotationsFromOffset = function ( offset ) {
return new ve.dm.AnnotationSet( this.getStore(), this.getAnnotationIndexesFromOffset( offset ) );
};
/**
* Set annotations of data at a specified offset.
*
* Cleans up data structure if annotation set is empty.
*
* @method
* @param {number} offset Offset to set annotations at
* @param {ve.dm.AnnotationSet} annotations Annotations to set
*/
ve.dm.ElementLinearData.prototype.setAnnotationsAtOffset = function ( offset, annotations ) {
var character, item = this.getData( offset ), isElement = this.isElementData( offset );
if ( !annotations.isEmpty() ) {
if ( isElement ) {
// New element annotation
item.annotations = this.getStore().indexes( annotations.get() );
} else {
// New character annotation
character = this.getCharacterData( offset );
this.setData( offset, [character, this.getStore().indexes( annotations.get() )] );
}
} else {
if ( isElement ) {
// Cleanup empty element annotation
delete item.annotations;
} else {
// Cleanup empty character annotation
character = this.getCharacterData( offset );
this.setData( offset, character );
}
}
};
/** */
ve.dm.ElementLinearData.prototype.getCharacterData = function ( offset ) {
var item = this.getData( offset );
return ve.isArray( item ) ? item[0] : item;
};
/**
* Gets the range of content surrounding a given offset that's covered by a given annotation.
*
* @method
* @param {number} offset Offset to begin looking forward and backward from
* @param {Object} annotation Annotation to test for coverage with
* @returns {ve.Range|null} Range of content covered by annotation, or null if offset is not covered
*/
ve.dm.ElementLinearData.prototype.getAnnotatedRangeFromOffset = function ( offset, annotation ) {
var start = offset,
end = offset;
if ( this.getAnnotationsFromOffset( offset ).contains( annotation ) === false ) {
return null;
}
while ( start > 0 ) {
start--;
if ( this.getAnnotationsFromOffset( start ).contains( annotation ) === false ) {
start++;
break;
}
}
while ( end < this.getLength() ) {
if ( this.getAnnotationsFromOffset( end ).contains( annotation ) === false ) {
break;
}
end++;
}
return new ve.Range( start, end );
};
/**
* Get the range of an annotation found within a range.
*
* @method
* @param {number} offset Offset to begin looking forward and backward from
* @param {ve.dm.Annotation} annotation Annotation to test for coverage with
* @returns {ve.Range|null} Range of content covered by annotation, or a copy of the range
*/
ve.dm.ElementLinearData.prototype.getAnnotatedRangeFromSelection = function ( range, annotation ) {
var start = range.start,
end = range.end;
while ( start > 0 ) {
start--;
if ( this.getAnnotationsFromOffset( start ).contains( annotation ) === false ) {
start++;
break;
}
}
while ( end < this.getLength() ) {
if ( this.getAnnotationsFromOffset( end ).contains( annotation ) === false ) {
break;
}
end++;
}
return new ve.Range( start, end );
};
/**
* Get annotations common to all content in a range.
*
* @method
* @param {ve.Range} range Range to get annotations for
* @param {boolean} [all=false] Get all annotations found within the range, not just those that cover it
* @returns {ve.dm.AnnotationSet} All annotation objects range is covered by
*/
ve.dm.ElementLinearData.prototype.getAnnotationsFromRange = function ( range, all ) {
var i, left, right;
// Look at left side of range for annotations
left = this.getAnnotationsFromOffset( range.start );
// Shortcut for single character and zero-length ranges
if ( range.getLength() === 0 || range.getLength() === 1 ) {
return left;
}
// Iterator over the range, looking for annotations, starting at the 2nd character
for ( i = range.start + 1; i < range.end; i++ ) {
// Skip non-content data
if ( this.isElementData( i ) && !ve.dm.nodeFactory.isNodeContent( this.getType( i ) ) ) {
continue;
}
// Current character annotations
right = this.getAnnotationsFromOffset( i );
if ( all && !right.isEmpty() ) {
left.addSet( right );
} else if ( !all ) {
// A non annotated character indicates there's no full coverage
if ( right.isEmpty() ) {
return new ve.dm.AnnotationSet( this.getStore() );
}
// Exclude annotations that are in left but not right
left.removeNotInSet( right );
// If we've reduced left down to nothing, just stop looking
if ( left.isEmpty() ) {
break;
}
}
}
return left;
};
/**
* Get a range without any whitespace content at the beginning and end.
*
* @method
* @param {ve.Range} [range] Range of data to get, all data will be given by default
* @returns {Object} A new range if modified, otherwise returns passed range
*/
ve.dm.ElementLinearData.prototype.trimOuterSpaceFromRange = function ( range ) {
var start = range.start,
end = range.end;
while ( this.getCharacterData( end - 1 ) === ' ' ) {
end--;
}
while ( start < end && this.getCharacterData( start ) === ' ' ) {
start++;
}
return range.to < range.end ? new ve.Range( end, start ) : new ve.Range( start, end );
};
/**
* Get an offset at a distance to an offset that passes a validity test.
*
* - If {offset} is not already valid, one step will be used to move it to a valid one.
* - If {offset} is already valid and cannot be moved in the direction of {distance} and still be
* valid, it will be left where it is
* - If {distance} is zero the result will either be {offset} if it's already valid or the
* nearest valid offset to the right if possible and to the left otherwise.
* - If {offset} is after the last valid offset and {distance} is >= 1, or if {offset} if
* before the first valid offset and {distance} <= 1 than the result will be the nearest
* valid offset in the opposite direction.
* - If the document does not contain a single valid offset the result will be -1
*
* @method
* @param {number} offset Offset to start from
* @param {number} distance Number of valid offsets to move
* @param {Function} callback Function to call to check if an offset is valid which will be
* given initial argument of offset
* @param {Mixed...} [args] Additional arguments to pass to the callback
* @returns {number} Relative valid offset or -1 if there are no valid offsets in document
*/
ve.dm.ElementLinearData.prototype.getRelativeOffset = function ( offset, distance, callback ) {
var i, direction,
dataOffset,
args = Array.prototype.slice.call( arguments, 3 ),
start = offset,
steps = 0,
turnedAround = false,
inHandlesOwnChildren = false;
// If offset is already a structural offset and distance is zero than no further work is needed,
// otherwise distance should be 1 so that we can get out of the invalid starting offset
if ( distance === 0 ) {
if ( callback.apply( this, [offset].concat( args ) ) ) {
return offset;
} else {
distance = 1;
}
}
// Initial values
direction = (
offset <= 0 ? 1 : (
offset >= this.getLength() ? -1 : (
distance > 0 ? 1 : -1
)
)
);
distance = Math.abs( distance );
i = start + direction;
offset = -1;
// Iteration
while ( i >= 0 && i <= this.getLength() ) {
// Detect when the search for a valid offset enters a node which handles its own
// children, and don't return an offset inside such a node. This clearly won't work
// if you start inside such a node, but you shouldn't be doing that to being with
dataOffset = i + ( direction > 0 ? -1 : 0 );
if (
this.isElementData( dataOffset ) &&
ve.dm.nodeFactory.doesNodeHandleOwnChildren( this.getType( dataOffset ) )
) {
// We have entered a node if we step right over an open, or left over a close
inHandlesOwnChildren =
( direction > 0 && this.isOpenElementData( dataOffset ) ) ||
( direction < 0 && this.isCloseElementData( dataOffset ) );
}
if ( callback.apply( this, [i].concat( args ) ) ) {
if ( !inHandlesOwnChildren ) {
steps++;
offset = i;
if ( distance === steps ) {
return offset;
}
}
} else if (
// Don't keep turning around over and over
!turnedAround &&
// Only turn around if not a single step could be taken
steps === 0 &&
// Only turn around if we're about to reach the edge
( ( direction < 0 && i === 0 ) || ( direction > 0 && i === this.getLength() ) )
) {
// Before we turn around, let's see if we are at a valid position
if ( callback.apply( this, [start].concat( args ) ) ) {
// Stay where we are
return start;
}
// Start over going in the opposite direction
direction *= -1;
i = start;
distance = 1;
turnedAround = true;
inHandlesOwnChildren = false;
}
i += direction;
}
return offset;
};
/**
* Get a content offset at a distance from an offset.
*
* This method is a wrapper around {getRelativeOffset}, using {ve.dm.Document.isContentOffset} as
* the offset validation callback.
*
* @method
* @param {number} offset Offset to start from
* @param {number} distance Number of content offsets to move
* @returns {number} Relative content offset or -1 if there are no valid offsets in document
*/
ve.dm.ElementLinearData.prototype.getRelativeContentOffset = function ( offset, distance ) {
return this.getRelativeOffset( offset, distance, this.constructor.prototype.isContentOffset );
};
/**
* Get the nearest content offset to an offset.
*
* If the offset is already a valid offset, it will be returned unchanged. This method differs from
* calling {getRelativeContentOffset} with a zero length difference because the direction can be
* controlled without necessarily moving the offset if it's already valid. Also, if the direction
* is 0 or undefined than nearest offsets will be found to the left and right and the one with the
* shortest distance will be used.
*
* This method is a wrapper around {getRelativeOffset}, using {this.isContentOffset} as
* the offset validation callback.
*
* @method
* @param {number} offset Offset to start from
* @param {number} [direction] Direction to prefer matching offset in, -1 for left and 1 for right
* @returns {number} Nearest content offset or -1 if there are no valid offsets in document
*/
ve.dm.ElementLinearData.prototype.getNearestContentOffset = function ( offset, direction ) {
if ( this.isContentOffset( offset ) ) {
return offset;
}
if ( direction === undefined ) {
var left = this.getRelativeOffset( offset, -1, this.constructor.prototype.isContentOffset ),
right = this.getRelativeOffset( offset, 1, this.constructor.prototype.isContentOffset );
return offset - left < right - offset ? left : right;
} else {
return this.getRelativeOffset(
offset, direction > 0 ? 1 : -1, this.constructor.prototype.isContentOffset
);
}
};
/**
* Get a structural offset at a distance from an offset.
*
* This method is a wrapper around {getRelativeOffset}, using {this.isStructuralOffset} as
* the offset validation callback.
*
* @method
* @param {number} offset Offset to start from
* @param {number} distance Number of structural offsets to move
* @param {boolean} [unrestricted] Only consider offsets where any kind of element can be inserted
* @returns {number} Relative structural offset
*/
ve.dm.ElementLinearData.prototype.getRelativeStructuralOffset = function ( offset, distance, unrestricted ) {
// Optimization: start and end are always unrestricted structural offsets
if ( distance === 0 && ( offset === 0 || offset === this.getLength() ) ) {
return offset;
}
return this.getRelativeOffset(
offset, distance, this.constructor.prototype.isStructuralOffset, unrestricted
);
};
/**
* Get the nearest structural offset to an offset.
*
* If the offset is already a valid offset, it will be returned unchanged. This method differs from
* calling {getRelativeStructuralOffset} with a zero length difference because the direction can be
* controlled without necessarily moving the offset if it's already valid. Also, if the direction
* is 0 or undefined than nearest offsets will be found to the left and right and the one with the
* shortest distance will be used.
*
* This method is a wrapper around {getRelativeOffset}, using {this.isStructuralOffset} as
* the offset validation callback.
*
* @method
* @param {number} offset Offset to start from
* @param {number} [direction] Direction to prefer matching offset in, -1 for left and 1 for right
* @param {boolean} [unrestricted] Only consider offsets where any kind of element can be inserted
* @returns {number} Nearest structural offset
*/
ve.dm.ElementLinearData.prototype.getNearestStructuralOffset = function ( offset, direction, unrestricted ) {
if ( this.isStructuralOffset( offset, unrestricted ) ) {
return offset;
}
if ( !direction ) {
var left = this.getRelativeOffset(
offset, -1, this.constructor.prototype.isStructuralOffset, unrestricted
),
right = this.getRelativeOffset(
offset, 1, this.constructor.prototype.isStructuralOffset, unrestricted
);
return offset - left < right - offset ? left : right;
} else {
return this.getRelativeOffset(
offset, direction > 0 ? 1 : -1, this.constructor.prototype.isStructuralOffset, unrestricted
);
}
};
/**
* Get the nearest word boundaries as a range.
*
* The offset will first be moved to the nearest content offset if it's not at one already.
* Elements are always word boundaries.
*
* @method
* @param {number} offset Offset to start from
* @returns {ve.Range} Range around nearest word boundaries
*/
ve.dm.ElementLinearData.prototype.getNearestWordRange = function ( offset ) {
var offsetLeft, offsetRight,
dataString = new ve.dm.DataString( this.getData() );
offset = this.getNearestContentOffset( offset );
// If the cursor offset is a break (i.e. the start/end of word) we should
// check one position either side to see if there is a non-break
// and if so, move the offset accordingly
if ( unicodeJS.wordbreak.isBreak( dataString, offset ) ) {
if ( !unicodeJS.wordbreak.isBreak( dataString, offset + 1 ) ) {
offset++;
} else if ( !unicodeJS.wordbreak.isBreak( dataString, offset - 1 ) ) {
offset--;
} else {
return new ve.Range( offset );
}
}
offsetRight = unicodeJS.wordbreak.nextBreakOffset( dataString, offset );
offsetLeft = unicodeJS.wordbreak.prevBreakOffset( dataString, offset );
return new ve.Range( offsetLeft, offsetRight );
};
/**
* Finds all instances of items being stored in the index-value store for this data store
*
* Currently this is just all annotations still in use.
*
* @method
* @returns {Object} Object containing all store values, indexed by store index
*/
ve.dm.ElementLinearData.prototype.getUsedStoreValues = function () {
var i, indexes, j, valueStore = {};
i = this.getLength();
while ( i-- ) {
// Annotations
indexes = this.getAnnotationIndexesFromOffset( i );
j = indexes.length;
while ( j-- ) {
// Just flag item as in use for now - we will add its value
// in a separate loop to avoid multiple store lookups
valueStore[indexes[j]] = true;
}
}
for ( i in valueStore ) {
// Fill in actual store values
valueStore[i] = this.getStore().value( i );
}
return valueStore;
};
/**
* Remap the store indexes used in this linear data.
*
* Remaps annotations and calls remapStoreIndexes() on each node.
*
* @method
* @param {Object} mapping Mapping from store indexes to store indexes
*/
ve.dm.ElementLinearData.prototype.remapStoreIndexes = function ( mapping ) {
var i, ilen, j, jlen, indexes, nodeClass;
for ( i = 0, ilen = this.data.length; i < ilen; i++ ) {
indexes = this.getAnnotationIndexesFromOffset( i ); // returns by reference
for ( j = 0, jlen = indexes.length; j < jlen; j++ ) {
indexes[j] = mapping[indexes[j]];
}
if ( this.isOpenElementData( i ) ) {
nodeClass = ve.dm.nodeFactory.lookup( this.getType( i ) );
nodeClass.static.remapStoreIndexes( this.data[i], mapping );
}
}
};
/**
* Remap the internal list indexes used in this linear data.
*
* Calls remapInternalListIndexes() for each node.
*
* @method
* @param {Object} mapping Mapping from internal list indexes to 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.ElementLinearData.prototype.remapInternalListIndexes = function ( mapping, internalList ) {
var i, ilen, nodeClass;
for ( i = 0, ilen = this.data.length; i < ilen; i++ ) {
if ( this.isOpenElementData( i ) ) {
nodeClass = ve.dm.nodeFactory.lookup( this.getType( i ) );
nodeClass.static.remapInternalListIndexes( this.data[i], mapping, internalList );
}
}
};
/**
* Remap the internal list keys used in this linear data.
*
* Calls remapInternalListKeys() for each node.
*
* @method
* @param {ve.dm.InternalList} internalList Internal list the keys are being mapped into.
*/
ve.dm.ElementLinearData.prototype.remapInternalListKeys = function ( internalList ) {
var i, ilen, nodeClass;
for ( i = 0, ilen = this.data.length; i < ilen; i++ ) {
if ( this.isOpenElementData( i ) ) {
nodeClass = ve.dm.nodeFactory.lookup( this.getType( i ) );
nodeClass.static.remapInternalListKeys( this.data[i], internalList );
}
}
};
/**
* Sanitize data according to a set of rules.
*
* @param {Object} rules Sanitization rules
* @param {string[]} [rules.blacklist] Blacklist of model types which aren't allowed
* @param {boolean} [rules.removeHtmlAttributes] Remove all left over HTML attributes
* @param {boolean} [rules.removeStyles] Remove HTML style attributes
* @param {boolean} [plainText=false] Remove all formatting for plain text paste
* @param {boolean} [keepEmptyContentBranches=false] Preserve empty content branch nodes
*/
ve.dm.ElementLinearData.prototype.sanitize = function ( rules, plainText, keepEmptyContentBranches ) {
var i, len, annotations, emptySet, setToRemove, type,
allAnnotations = this.getAnnotationsFromRange( new ve.Range( 0, this.getLength() ), true );
function removeHtmlAttribute( element, attribute ) {
var i;
if ( element.htmlAttributes ) {
for ( i = 0; i < element.htmlAttributes.length; i++ ) {
delete element.htmlAttributes[i].values[attribute];
if ( ve.isEmptyObject( element.htmlAttributes[i].values ) ) {
delete element.htmlAttributes[i].values;
}
if ( ve.isEmptyObject( element.htmlAttributes[i] ) ) {
element.htmlAttributes.splice( i, 1 );
i--;
}
}
if ( !element.htmlAttributes.length ) {
delete element.htmlAttributes;
}
}
}
if ( plainText ) {
emptySet = new ve.dm.AnnotationSet( this.getStore() );
} else {
if ( rules.removeHtmlAttributes ) {
// Remove HTML attributes from annotations
for ( i = 0, len = allAnnotations.getLength(); i < len; i++ ) {
delete allAnnotations.get( i ).element.htmlAttributes;
}
}
if ( rules.removeStyles ) {
for ( i = 0, len = allAnnotations.getLength(); i < len; i++ ) {
// Remove inline style attributes from annotations
removeHtmlAttribute( allAnnotations.get( i ).element, 'style' );
}
}
// Create annotation set to remove from blacklist
setToRemove = allAnnotations.filter( function ( annotation ) {
return ve.indexOf( annotation.name, rules.blacklist ) !== -1 ||
// If HTML attributes are stripped and you are left with an empty span, remove it
( rules.removeHtmlAttributes && annotation.name === 'textStyle/span' && !annotation.element.htmlAttributes );
} );
}
for ( i = 0, len = this.getLength(); i < len; i++ ) {
if ( this.isElementData( i ) ) {
type = this.getType( i );
// Remove blacklisted nodes
if (
ve.indexOf( type, rules.blacklist ) !== -1 ||
( plainText && type !== 'paragraph' && type !== 'internalList' )
) {
this.splice( i, 1 );
// Make sure you haven't just unwrapped a wrapper paragraph
if ( ve.getProp( this.getData( i ), 'internal', 'generated' ) ) {
delete this.getData( i ).internal.generated;
if ( ve.isEmptyObject( this.getData( i ).internal ) ) {
delete this.getData( i ).internal;
}
}
i--;
len--;
continue;
}
// If a node is empty but can contain content, then just remove it
if (
!keepEmptyContentBranches &&
i > 0 && this.isCloseElementData( i ) && this.isOpenElementData( i - 1 ) &&
ve.dm.nodeFactory.canNodeContainContent( type )
) {
this.splice( i - 1, 2 );
i -= 2;
len -= 2;
continue;
}
}
annotations = this.getAnnotationsFromOffset( i );
if ( !annotations.isEmpty() ) {
if ( plainText ) {
this.setAnnotationsAtOffset( i, emptySet );
} else if ( setToRemove.getLength() ) {
// Remove blacklisted annotations
annotations.removeSet( setToRemove );
this.setAnnotationsAtOffset( i, annotations );
}
}
if ( this.isOpenElementData( i ) ) {
if ( rules.removeHtmlAttributes ) {
// Remove HTML attributes from nodes
delete this.getData( i ).htmlAttributes;
}
if ( rules.removeStyles ) {
// Remove inline style attributes from nodes
removeHtmlAttribute( this.getData( i ), 'style' );
}
}
}
};
/**
* Run all elements through getClonedElement(). This should be done if
* you intend to insert the sliced data back into the document as a copy
* of the original data (e.g. for copy and paste).
*/
ve.dm.ElementLinearData.prototype.cloneElements = function () {
var i, len, node;
for ( i = 0, len = this.getLength(); i < len; i++ ) {
if ( this.isOpenElementData( i ) ) {
node = ve.dm.nodeFactory.create( this.getType( i ), [], this.getData( i ) );
this.data[i] = node.getClonedElement();
}
}
};