/** * DataModel document. * * @class * @extends {ve.dm.DocumentFragment} * @constructor * @param {Array} data Linear model data to start with */ ve.dm.Document = function( data ) { // Inheritance ve.dm.DocumentFragment.call( this, data ); }; /* Static methods */ /** * Checks 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. * * @example Content offsets: * a b c * . ^ ^ . ^ ^ ^ . ^ . * * @example Content offsets: * * . . . . . * * @static * @method * @param {Array} data Document data * @param {Integer} offset Document offset * @returns {Boolean} Content can be inserted at offset */ ve.dm.Document.isContentOffset = function( data, offset ) { // Edges are never content if ( offset === 0 || offset === data.length ) { return false; } var left = data[offset - 1], right = data[offset], factory = ve.dm.factory; 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 // |a| ( 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 // | ( // Is an element typeof left.type === 'string' && // Is a closing left.type.charAt( 0 ) === '/' && // Is a leaf !factory.canNodeHaveChildren( left.type.substr( 1 ) ) ) || // Left of a leaf // | ( // Is an element typeof right.type === 'string' && // Is not a closing right.type.charAt( 0 ) !== '/' && // Is a leaf !factory.canNodeHaveChildren( right.type ) ) || // Inside empty content branch // | ( // Inside empty element '/' + left.type === right.type && // Both are content branches (right is the same type) factory.canNodeHaveChildren( left.type ) && !factory.canNodeHaveGrandchildren( left.type ) ) ) ) ); }; /** * Checks 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. * * @example Structural offsets: * a b c * ^ . . ^ . . . . . ^ * * @static * @method * @param {Array} data Document data * @param {Integer} offset Document offset * @returns {Boolean} Structure can be inserted at offset */ ve.dm.Document.isStructuralOffset = function( data, offset ) { // Edges are always structural if ( offset === 0 || offset === data.length ) { return true; } // Offsets must be within range and both sides must be elements var left = data[offset - 1], right = data[offset], factory = ve.dm.factory; return ( ( left !== undefined && right !== undefined && typeof left.type === 'string' && typeof right.type === 'string' ) && ( // Right of a branch // a||| ( // Is a closing left.type.charAt( 0 ) === '/' && // Is a branch factory.canNodeHaveChildren( left.type.substr( 1 ) ) ) || // Left of branch // |||a ( // Is not a closing right.type.charAt( 0 ) !== '/' && // Is a branch factory.canNodeHaveChildren( right.type ) ) || // Inside empty non-content branch // | or | ( // Inside empty element '/' + left.type === right.type && // Both are non-content branches (right is the same type) factory.canNodeHaveGrandchildren( left.type ) ) ) ); }; /** * Checks if a data at a given offset is an element. * * This method assumes that any value that has a type property that's a string is an element object. * * @example Element data: * a b c * ^ . ^ ^ . . ^ ^ ^ . * * @static * @method * @param {Array} data Document data * @param {Integer} offset Document offset * @returns {Boolean} Data at offset is an element */ ve.dm.Document.isElementData = function( data, offset ) { // Data exists at offset and appears to be an element return data[offset] !== undefined && typeof data[offset].type === 'string'; }; /** * Checks 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). * * @static * @method * @param {Array} data Document data * @returns {Boolean} At least one elements exists in data */ ve.dm.Document.containsElementData = function( data ) { var i = data.length; while ( i-- ) { if ( data[i].type !== undefined ) { return true; } } return false; }; /** * Get the parent node that would be affected by inserting given data into its child. * * This is used when inserting data that closes and reopens one or more parent nodes into a * child node, which requires rebuilding at a higher level. * * @param {ve.Node} node Child node to start from * @param {Array} data Data to inspect for closings * @returns {ve.Node} Lowest level parent node being affected */ ve.dm.Document.getScope = function( node, data ) { // TODO improve this to return a set of siblings, rather than a common ancestor, that'll // make for much more efficient rebuilds // TODO also make it track offsets var i, length, level = 0, max = 0; for ( i = 0, length = data.length; i < length; i++ ) { if ( data[i].type ) { level += data[i].type.charAt( 0 ) === '/' ? 1 : -1; max = level > max ? level : max; } } for ( i = 0; i < max; i++ ) { // Skip over unwrapped parents, they're not counted in the level count while ( !node.isWrapped() && node.getParent() ) { node = node.getParent(); } // Go up one level node = node.getParent() || node; } return node; }; /* Methods */ /** * Rebuild one or more nodes from a linear model fragment. * * The data provided to this method may contain either one node or multiple sibling nodes, but it * must be balanced and valid. Data provided to this method also may not contain any content at the * top level. The tree and offset map are updated during this operation. * * Process: * 1. Nodes between {index} and {index} + {numNodes} in {parent} will be removed * 2. Data will be retrieved from this.data using {offset} and {newLength} * 3. A document fragment will be generated from the retrieved data * 4. The document fragment's offset map will be inserted into this document at {offset} * 5. The document fragment's nodes will be inserted into {parent} at {index} * * Use cases: * 1. Rebuild old nodes and offset data after a change to the linear model. * 2. Insert new nodes and offset data after a insertion in the linear model. * * @param {ve.dm.Node} parent Parent of the node(s) being rebuilt * @param {Integer} index Index within parent to rebuild or insert nodes * - If {numNodes} == 0: Index to insert nodes at * - If {numNodes} >= 1: Index of first node to rebuild * @param {Integer} numNodes Total number of nodes to rebuild * - If {numNodes} == 0: Nothing will be rebuilt, but the node(s) built from data will be * inserted before {index}. To insert nodes at the end, use number of children in {parent} * - If {numNodes} == 1: Only the node at {index} will be rebuilt * - If {numNodes} > 1: The node at {index} and the next {numNodes-1} nodes will be rebuilt * @param {Integer} offset Linear model offset to rebuild or insert offset map data * - If {numNodes} == 0: Offset to insert offset map data at * - If {numNodes} >= 1: Offset to remove old and insert new offset map data at * @param {Integer} newLength Length of data in linear model to rebuild or insert nodes for * @returns {ve.dm.Node[]} Array containing the rebuilt/inserted nodes */ ve.dm.Document.prototype.rebuildNodes = function( parent, index, numNodes, offset, newLength ) { // Compute the length of the old nodes (so we can splice their offsets out of the offset map) var oldLength = 0; for ( var i = index; i < index + numNodes; i++ ) { oldLength += parent.children[i].getOuterLength(); } // Get a slice of the document where it's been changed var data = this.data.slice( offset, offset + newLength ); // Build document fragment from data var fragment = new ve.dm.DocumentFragment( data, this ); // Get generated child nodes from the document fragment var nodes = fragment.getDocumentNode().getChildren(); // Replace nodes in the model tree ve.batchSplice( parent, index, numNodes, nodes ); // Update offset map ve.batchSplice( this.offsetMap, offset, oldLength, fragment.getOffsetMap() ); // Return inserted nodes return nodes; }; /** * Gets a content offset a given distance forwards or backwards from another. * * @method * @param {Integer} offset Offset to start from * @param {Integer} distance Number of content offsets to move * @returns {Integer} Relative content offset */ ve.dm.Document.prototype.getRelativeContentOffset = function( offset, distance ) { if ( distance === 0 ) { return offset; } var direction = distance > 0 ? 1 : -1, i = offset + direction, steps = 0; distance = Math.abs( distance ); while ( i > 0 && i < this.data.length ) { if ( !ve.dm.Document.isStructuralOffset( this.data, i ) ) { steps++; offset = i; if ( distance === steps ) { return offset; } } i += direction; } return offset; }; /* Inheritance */ ve.extendClass( ve.dm.Document, ve.dm.DocumentFragment );