mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-30 00:55:00 +00:00
6a52fba643
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
940 lines
31 KiB
JavaScript
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();
|
|
}
|
|
}
|
|
};
|