%PDF- %PDF-
Direktori : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/ |
Current File : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/ve.utils.js |
/*! * VisualEditor utilities. * * @copyright 2011-2016 VisualEditor Team and others; see http://ve.mit-license.org */ /** * @class ve */ /** * Checks if an object is an instance of one or more classes. * * @param {Object} subject Object to check * @param {Function[]} classes Classes to compare with * @return {boolean} Object inherits from one or more of the classes */ ve.isInstanceOfAny = function ( subject, classes ) { var i = classes.length; while ( classes[ --i ] ) { if ( subject instanceof classes[ i ] ) { return true; } } return false; }; /** * @method * @inheritdoc OO#getProp */ ve.getProp = OO.getProp; /** * @method * @inheritdoc OO#setProp */ ve.setProp = OO.setProp; /** * @method * @inheritdoc OO#cloneObject */ ve.cloneObject = OO.cloneObject; /** * @method * @inheritdoc OO#getObjectValues */ ve.getObjectValues = OO.getObjectValues; /** * @method * @inheritdoc OO#binarySearch */ ve.binarySearch = OO.binarySearch; /** * @method * @inheritdoc OO#compare */ ve.compare = OO.compare; /** * @method * @inheritdoc OO#copy */ ve.copy = OO.copy; /** * @method * @inheritdoc OO.ui#debounce */ ve.debounce = OO.ui.debounce; /** * @method * @inheritdoc OO.ui#throttle */ ve.throttle = OO.ui.throttle; /** * @method * @inheritdoc OO.ui.Element#scrollIntoView */ ve.scrollIntoView = OO.ui.Element.static.scrollIntoView.bind( OO.ui.Element.static ); /** * Copy an array of DOM elements, optionally into a different document. * * @param {HTMLElement[]} domElements DOM elements to copy * @param {HTMLDocument} [doc] Document to create the copies in; if unset, simply clone each element * @return {HTMLElement[]} Copy of domElements with copies of each element */ ve.copyDomElements = function ( domElements, doc ) { return domElements.map( function ( domElement ) { return doc ? doc.importNode( domElement, true ) : domElement.cloneNode( true ); } ); }; /** * Check if two arrays of DOM elements are equal (according to .isEqualNode()) * * @param {HTMLElement[]} domElements1 First array of DOM elements * @param {HTMLElement[]} domElements2 Second array of DOM elements * @return {boolean} All elements are pairwise equal */ ve.isEqualDomElements = function ( domElements1, domElements2 ) { var i = 0, len = domElements1.length; if ( len !== domElements2.length ) { return false; } for ( ; i < len; i++ ) { if ( !domElements1[ i ].isEqualNode( domElements2[ i ] ) ) { return false; } } return true; }; /** * Compare two class lists, either whitespace separated strings or arrays * * Class lists are equivalent if they contain the same members, * excluding duplicates and ignoring order. * * @param {string[]|string} classList1 First class list * @param {string[]|string} classList2 Second class list * @return {boolean} Class lists are equivalent */ ve.compareClassLists = function ( classList1, classList2 ) { var removeEmpty = function ( c ) { return c !== ''; }; classList1 = Array.isArray( classList1 ) ? classList1 : classList1.trim().split( /\s+/ ); classList2 = Array.isArray( classList2 ) ? classList2 : classList2.trim().split( /\s+/ ); classList1 = classList1.filter( removeEmpty ); classList2 = classList2.filter( removeEmpty ); return ve.compare( OO.unique( classList1 ).sort(), OO.unique( classList2 ).sort() ); }; /** * Check to see if an object is a plain object (created using "{}" or "new Object"). * * @method * @source <http://api.jquery.com/jQuery.isPlainObject/> * @param {Object} obj The object that will be checked to see if it's a plain object * @return {boolean} */ ve.isPlainObject = $.isPlainObject; /** * Check to see if an object is empty (contains no properties). * * @method * @source <http://api.jquery.com/jQuery.isEmptyObject/> * @param {Object} obj The object that will be checked to see if it's empty * @return {boolean} */ ve.isEmptyObject = $.isEmptyObject; /** * Merge properties of one or more objects into another. * Preserves original object's inheritance (e.g. Array, Object, whatever). * In case of array or array-like objects only the indexed properties * are copied over. * Beware: If called with only one argument, it will consider * 'target' as 'source' and 'this' as 'target'. Which means * ve.extendObject( { a: 1 } ); sets ve.a = 1; * * @method * @source <http://api.jquery.com/jQuery.extend/> * @param {boolean} [recursive=false] * @param {Mixed} [target] Object that will receive the new properties * @param {...Mixed} [sources] Variadic list of objects containing properties * to be merged into the target. * @return {Mixed} Modified version of first or second argument */ ve.extendObject = $.extend; /** * @private * @property {boolean} */ ve.supportsSplice = ( function () { var a, n; // This returns false in Safari 8 a = new Array( 100000 ); a.splice( 30, 0, 'x' ); a.splice( 20, 1 ); if ( a.indexOf( 'x' ) !== 29 ) { return false; } // This returns false in Opera 12.15 a = []; n = 256; a[ n ] = 'a'; a.splice( n + 1, 0, 'b' ); if ( a[ n ] !== 'a' ) { return false; } // Splice is supported return true; } )(); /** * Splice one array into another. * * This is the equivalent of arr.splice( offset, remove, d1, d2, d3, ... ) except that arguments are * specified as an array rather than separate parameters. * * This method has been proven to be faster than using slice and concat to create a new array, but * performance tests should be conducted on each use of this method to verify this is true for the * particular use. Also, browsers change fast, never assume anything, always test everything. * * Includes a replacement for broken implementations of Array.prototype.splice(). * * @param {Array|ve.dm.BranchNode} arr Target object (must have `splice` method, object will be modified) * @param {number} offset Offset in arr to splice at. This may NOT be negative, unlike the * 'index' parameter in Array#splice. * @param {number} remove Number of elements to remove at the offset. May be zero * @param {Array} data Array of items to insert at the offset. Must be non-empty if remove=0 * @return {Array} Array of items removed */ ve.batchSplice = function ( arr, offset, remove, data ) { // We need to splice insertion in in batches, because of parameter list length limits which vary // cross-browser - 1024 seems to be a safe batch size on all browsers var splice, spliced, index = 0, batchSize = 1024, toRemove = remove, removed = []; if ( !Array.isArray( arr ) ) { splice = arr.splice; } else { if ( ve.supportsSplice ) { splice = Array.prototype.splice; } else { // Standard Array.prototype.splice() function implemented using .slice() and .push(). splice = function ( offset, remove/*, data... */ ) { var data, begin, removed, end; data = Array.prototype.slice.call( arguments, 2 ); begin = this.slice( 0, offset ); removed = this.slice( offset, offset + remove ); end = this.slice( offset + remove ); this.length = 0; ve.batchPush( this, begin ); ve.batchPush( this, data ); ve.batchPush( this, end ); return removed; }; } } if ( data.length === 0 ) { // Special case: data is empty, so we're just doing a removal // The code below won't handle that properly, so we do it here return splice.call( arr, offset, remove ); } while ( index < data.length ) { // Call arr.splice( offset, remove, i0, i1, i2, ..., i1023 ); // Only set remove on the first call, and set it to zero on subsequent calls spliced = splice.apply( arr, [ index + offset, toRemove ].concat( data.slice( index, index + batchSize ) ) ); if ( toRemove > 0 ) { removed = spliced; } index += batchSize; toRemove = 0; } return removed; }; /** * Insert one array into another. * * Shortcut for `ve.batchSplice( arr, offset, 0, src )`. * * @see #batchSplice * @param {Array|ve.dm.BranchNode} arr Target object (must have `splice` method) * @param {number} offset Offset in arr where items will be inserted * @param {Array} src Items to insert at offset */ ve.insertIntoArray = function ( arr, offset, src ) { ve.batchSplice( arr, offset, 0, src ); }; /** * Push one array into another. * * This is the equivalent of arr.push( d1, d2, d3, ... ) except that arguments are * specified as an array rather than separate parameters. * * @param {Array|ve.dm.BranchNode} arr Object supporting .push() to insert at the end of the array. Will be modified * @param {Array} data Array of items to insert. * @return {number} length of the new array */ ve.batchPush = function ( arr, data ) { // We need to push insertion in batches, because of parameter list length limits which vary // cross-browser - 1024 seems to be a safe batch size on all browsers var length, index = 0, batchSize = 1024; while ( index < data.length ) { // Call arr.push( i0, i1, i2, ..., i1023 ); length = arr.push.apply( arr, data.slice( index, index + batchSize ) ); index += batchSize; } return length; }; /** * Log data to the console. * * This implementation does nothing, to add a real implementation ve.debug needs to be loaded. * * @param {...Mixed} [args] Data to log */ ve.log = ve.log || function () { // don't do anything, this is just a stub }; /** * Log error to the console. * * This implementation does nothing, to add a real implementation ve.debug needs to be loaded. * * @param {...Mixed} [args] Data to log */ ve.error = ve.error || function () { // don't do anything, this is just a stub }; /** * Log an object to the console. * * This implementation does nothing, to add a real implementation ve.debug needs to be loaded. * * @param {Object} obj */ ve.dir = ve.dir || function () { // don't do anything, this is just a stub }; /** * Select the contents of an element * * @param {HTMLElement} element Element */ ve.selectElement = function ( element ) { var win = OO.ui.Element.static.getWindow( element ), nativeRange = win.document.createRange(), nativeSelection = win.getSelection(); nativeRange.setStart( element, 0 ); nativeRange.setEnd( element, element.childNodes.length ); try { nativeSelection.removeAllRanges(); } catch ( e ) { // Support: IE9 // IE9 can throw an exception if the range is invisible } nativeSelection.addRange( nativeRange ); }; /** * Get a localized message. * * @param {string} key Message key * @param {...Mixed} [params] Message parameters * @return {string} Localized message */ ve.msg = function () { // Avoid using bind because ve.init.platform doesn't exist yet. // TODO: Fix dependency issues between ve.js and ve.init.platform return ve.init.platform.getMessage.apply( ve.init.platform, arguments ); }; /** * Get platform config value(s) * * @param {string|string[]} key Config key, or list of keys * @return {Mixed|Object} Config value, or keyed object of config values if list of keys provided */ ve.config = function () { return ve.init.platform.getConfig.apply( ve.init.platform, arguments ); }; /** * Get or set a user config value. * * @param {string|string[]|Object} key Config key, list of keys or object mapping keys to values * @param {Mixed} [value] Value to set, if setting and key is a string * @return {Mixed|Object|boolean} Config value, keyed object of config values if list of keys provided, * or success boolean if setting. */ ve.userConfig = function ( key ) { if ( arguments.length <= 1 && ( typeof key === 'string' || Array.isArray( key ) ) ) { // get( string key ) // get( Array keys ) return ve.init.platform.getUserConfig.apply( ve.init.platform, arguments ); } else { // set( Object values ) // set( key, value ) return ve.init.platform.setUserConfig.apply( ve.init.platform, arguments ); } }; /** * Determine if the text consists of only unattached combining marks. * * @param {string} text Text to test * @return {boolean} The text is unattached combining marks */ ve.isUnattachedCombiningMark = function ( text ) { return ( /^[\u0300-\u036F]+$/ ).test( text ); }; /** * Convert a grapheme cluster offset to a byte offset. * * @param {string} text Text in which to calculate offset * @param {number} clusterOffset Grapheme cluster offset * @return {number} Byte offset */ ve.getByteOffset = function ( text, clusterOffset ) { return unicodeJS.graphemebreak.splitClusters( text ) .slice( 0, clusterOffset ) .join( '' ) .length; }; /** * Convert a byte offset to a grapheme cluster offset. * * @param {string} text Text in which to calculate offset * @param {number} byteOffset Byte offset * @return {number} Grapheme cluster offset */ ve.getClusterOffset = function ( text, byteOffset ) { return unicodeJS.graphemebreak.splitClusters( text.slice( 0, byteOffset ) ).length; }; /** * Get a text substring, taking care not to split grapheme clusters. * * @param {string} text Text to take the substring from * @param {number} start Start offset * @param {number} end End offset * @param {boolean} [outer=false] Include graphemes if the offset splits them * @return {string} Substring of text */ ve.graphemeSafeSubstring = function ( text, start, end, outer ) { // TODO: improve performance by incrementally inspecting characters around the offsets var unicodeStart = ve.getByteOffset( text, ve.getClusterOffset( text, start ) ), unicodeEnd = ve.getByteOffset( text, ve.getClusterOffset( text, end ) ); // If the selection collapses and we want an inner, then just return empty // otherwise we'll end up crossing over start and end if ( unicodeStart === unicodeEnd && !outer ) { return ''; } // The above calculations always move to the right of a multibyte grapheme. // Depending on the outer flag, we may want to move to the left: if ( unicodeStart > start && outer ) { unicodeStart = ve.getByteOffset( text, ve.getClusterOffset( text, start ) - 1 ); } if ( unicodeEnd > end && !outer ) { unicodeEnd = ve.getByteOffset( text, ve.getClusterOffset( text, end ) - 1 ); } return text.slice( unicodeStart, unicodeEnd ); }; /** * Escape non-word characters so they can be safely used as HTML attribute values. * * @param {string} value Attribute value to escape * @return {string} Escaped attribute value */ ve.escapeHtml = ( function () { function escape( value ) { switch ( value ) { case '\'': return '''; case '"': return '"'; case '<': return '<'; case '>': return '>'; case '&': return '&'; } } return function ( value ) { return value.replace( /['"<>&]/g, escape ); }; }() ); /** * Generate HTML attributes. * * NOTE: While the values of attributes are escaped, the names of attributes (i.e. the keys in * the attributes objects) are NOT ESCAPED. The caller is responsible for making sure these are * sane tag/attribute names and do not contain unsanitized content from an external source * (e.g. from the user or from the web). * * @param {Object} [attributes] Key-value map of attributes for the tag * @return {string} HTML attributes */ ve.getHtmlAttributes = function ( attributes ) { var attrName, attrValue, parts = []; if ( !ve.isPlainObject( attributes ) || ve.isEmptyObject( attributes ) ) { return ''; } for ( attrName in attributes ) { attrValue = attributes[ attrName ]; if ( attrValue === true ) { // Convert name=true to name=name attrValue = attrName; } else if ( attrValue === false ) { // Skip name=false continue; } parts.push( attrName + '="' + ve.escapeHtml( String( attrValue ) ) + '"' ); } return parts.join( ' ' ); }; /** * Generate an opening HTML tag. * * NOTE: While the values of attributes are escaped, the tag name and the names of * attributes (i.e. the keys in the attributes objects) are NOT ESCAPED. The caller is * responsible for making sure these are sane tag/attribute names and do not contain * unsanitized content from an external source (e.g. from the user or from the web). * * @param {string} tagName HTML tag name * @param {Object} [attributes] Key-value map of attributes for the tag * @return {string} Opening HTML tag */ ve.getOpeningHtmlTag = function ( tagName, attributes ) { var attr = ve.getHtmlAttributes( attributes ); return '<' + tagName + ( attr ? ' ' + attr : '' ) + '>'; }; /** * Get the attributes of a DOM element as an object with key/value pairs. * * @param {HTMLElement} element * @return {Object} */ ve.getDomAttributes = function ( element ) { var i, result = {}; for ( i = 0; i < element.attributes.length; i++ ) { result[ element.attributes[ i ].name ] = element.attributes[ i ].value; } return result; }; /** * Set the attributes of a DOM element as an object with key/value pairs. * * Use the `null` or `undefined` value to ensure an attribute's absence. * * @param {HTMLElement} element DOM element to apply attributes to * @param {Object} attributes Attributes to apply * @param {string[]} [whitelist] List of attributes to exclusively allow (all lowercase names) */ ve.setDomAttributes = function ( element, attributes, whitelist ) { var key; // Duck-typing for attribute setting if ( !element.setAttribute || !element.removeAttribute ) { return; } for ( key in attributes ) { if ( whitelist && whitelist.indexOf( key.toLowerCase() ) === -1 ) { continue; } if ( attributes[ key ] === undefined || attributes[ key ] === null ) { element.removeAttribute( key ); } else { element.setAttribute( key, attributes[ key ] ); } } }; /** * Get an HTML representation of a DOM element node, text node or comment node * * @param {Node} node The DOM node * @return {string} HTML representation of the node */ ve.getNodeHtml = function ( node ) { var div; if ( node.nodeType === Node.ELEMENT_NODE ) { return node.outerHTML; } else { div = document.createElement( 'div' ); div.appendChild( node.cloneNode( true ) ); return div.innerHTML; } }; /** * Build a summary of an HTML element. * * Summaries include node name, text, attributes and recursive summaries of children. * Used for serializing or comparing HTML elements. * * @private * @param {HTMLElement} element Element to summarize * @param {boolean} [includeHtml=false] Include an HTML summary for element nodes * @param {Function} [getAttributeSummary] Callback to modify the summary of an attribute * @param {string} [getAttributeSummary.name] Name of the attribute to modify. * @param {string} [getAttributeSummary.value] Value to return for the given attribute. * @return {Object} Summary of element. */ ve.getDomElementSummary = function ( element, includeHtml, getAttributeSummary ) { var i, name, value, summary = { type: element.nodeName.toLowerCase(), text: element.textContent, attributes: {}, children: [] }; if ( includeHtml && element.nodeType === Node.ELEMENT_NODE ) { summary.html = element.outerHTML; } // Gather attributes if ( element.attributes ) { for ( i = 0; i < element.attributes.length; i++ ) { name = element.attributes[ i ].name; value = element.attributes[ i ].value; summary.attributes[ name ] = getAttributeSummary ? getAttributeSummary( name, value ) : value; } } // Summarize children if ( element.childNodes ) { for ( i = 0; i < element.childNodes.length; i++ ) { summary.children.push( ve.getDomElementSummary( element.childNodes[ i ], includeHtml ) ); } } return summary; }; /** * Callback for #copy to convert nodes to a comparable summary. * * @private * @param {Object} value Value in the object/array * @return {Object} DOM element summary if value is a node, otherwise just the value */ ve.convertDomElements = function ( value ) { // Use duck typing rather than instanceof Node; the latter doesn't always work correctly if ( value && value.nodeType ) { return ve.getDomElementSummary( value ); } return value; }; /** * Check whether a given DOM element has a block element type. * * @param {HTMLElement|string} element Element or element name * @return {boolean} Element is a block element */ ve.isBlockElement = function ( element ) { var elementName = typeof element === 'string' ? element : element.nodeName; return ve.elementTypes.block.indexOf( elementName.toLowerCase() ) !== -1; }; /** * Check whether a given DOM element is a void element (can't have children). * * @param {HTMLElement|string} element Element or element name * @return {boolean} Element is a void element */ ve.isVoidElement = function ( element ) { var elementName = typeof element === 'string' ? element : element.nodeName; return ve.elementTypes.void.indexOf( elementName.toLowerCase() ) !== -1; }; ve.elementTypes = { block: [ 'div', 'p', // tables 'table', 'tbody', 'thead', 'tfoot', 'caption', 'th', 'tr', 'td', // lists 'ul', 'ol', 'li', 'dl', 'dt', 'dd', // HTML5 heading content 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hgroup', // HTML5 sectioning content 'article', 'aside', 'body', 'nav', 'section', 'footer', 'header', 'figure', 'figcaption', 'fieldset', 'details', 'blockquote', // other 'hr', 'button', 'canvas', 'center', 'col', 'colgroup', 'embed', 'map', 'object', 'pre', 'progress', 'video' ], void: [ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' ] }; /** * Check whether a given node is contentEditable * * Handles 'inherit', via checking isContentEditable. Knows to check for text * nodes, and will return whether the text node's parent is contentEditable. * * @param {HTMLElement|Text} node Node to check contenteditable status of * @return {boolean} Node is contenteditable */ ve.isContentEditable = function ( node ) { return ( node.nodeType === Node.TEXT_NODE ? node.parentNode : node ).isContentEditable; }; /** * Create an HTMLDocument from an HTML string. * * The html parameter is supposed to be a full HTML document with a doctype and an `<html>` tag. * If you pass a document fragment, it may or may not work, this is at the mercy of the browser. * * To create an empty document, pass the empty string. * * If your input is both valid HTML and valid XML, and you need to work around style * normalization bugs in Internet Explorer, use #parseXhtml and #serializeXhtml. * * @param {string} html HTML string * @return {HTMLDocument} Document constructed from the HTML string */ ve.createDocumentFromHtml = function ( html ) { var newDocument; newDocument = ve.createDocumentFromHtmlUsingDomParser( html ); if ( newDocument ) { return newDocument; } newDocument = ve.createDocumentFromHtmlUsingIframe( html ); if ( newDocument ) { return newDocument; } return ve.createDocumentFromHtmlUsingInnerHtml( html ); }; /** * Private method for creating an HTMLDocument using the DOMParser * * @private * @param {string} html HTML string * @return {HTMLDocument|undefined} Document constructed from the HTML string or undefined if it failed */ ve.createDocumentFromHtmlUsingDomParser = function ( html ) { var newDocument; // Support: IE // IE doesn't like empty strings html = html || '<body></body>'; try { newDocument = new DOMParser().parseFromString( html, 'text/html' ); if ( newDocument ) { return newDocument; } } catch ( e ) { } }; /** * Private fallback for browsers which don't support DOMParser * * @private * @param {string} html HTML string * @return {HTMLDocument|undefined} Document constructed from the HTML string or undefined if it failed */ ve.createDocumentFromHtmlUsingIframe = function ( html ) { var newDocument, $iframe, iframe; // Here's what this fallback code should look like: // // var newDocument = document.implementation.createHtmlDocument( '' ); // newDocument.open(); // newDocument.write( html ); // newDocument.close(); // return newDocument; // // Sadly, it's impossible: // * On IE 9, calling open()/write() on such a document throws an "Unspecified error" (sic). // * On Firefox 20, calling open()/write() doesn't actually do anything, including writing. // This is reported as Firefox bug 867102. // * On Opera 12, calling open()/write() behaves as if called on window.document, replacing the // entire contents of the page with new HTML. This is reported as Opera bug DSK-384486. // // Funnily, in all of those browsers it's apparently perfectly legal and possible to access the // newly created document's DOM itself, including modifying documentElement's innerHTML, which // would achieve our goal. But that requires some nasty magic to strip off the <html></html> tag // itself, so we're not doing that. (We can't use .outerHTML, either, as the spec disallows // assigning to it for the root element.) // // There is one more way - create an <iframe>, append it to current document, and access its // contentDocument. The only browser having issues with that is Opera (sometimes the accessible // value is not actually a Document, but something which behaves just like an empty regular // object...), so we're detecting that and using the innerHTML hack described above. html = html || '<body></body>'; // Create an invisible iframe $iframe = $( '<iframe frameborder="0" width="0" height="0" />' ); iframe = $iframe.get( 0 ); // Attach it to the document. We have to do this to get a new document out of it document.documentElement.appendChild( iframe ); // Write the HTML to it newDocument = ( iframe.contentWindow && iframe.contentWindow.document ) || iframe.contentDocument; newDocument.open(); newDocument.write( html ); // Party like it's 1995! newDocument.close(); // Detach the iframe iframe.parentNode.removeChild( iframe ); // Support: IE9 // Prevent garbage collection of iframe as long as newDocument exists, as destroying // the original iframe makes access to the document impossible in IE9 newDocument.originalIframe = iframe; if ( !newDocument.documentElement || newDocument.documentElement.cloneNode( false ) === undefined ) { // Surprise! The document is not a document! Only happens on Opera. // (Or its nodes are not actually nodes, while the document // *is* a document. This only happens when debugging with Dragonfly.) return; } return newDocument; }; /** * Private fallback for browsers which don't support iframe technique * * @private * @param {string} html HTML string * @return {HTMLDocument} Document constructed from the HTML string */ ve.createDocumentFromHtmlUsingInnerHtml = function ( html ) { var i, htmlAttributes, wrapper, attributes, newDocument = document.implementation.createHTMLDocument( '' ); html = html || '<body></body>'; // Carefully unwrap the HTML out of the root node (and doctype, if any). newDocument.documentElement.innerHTML = html .replace( /^\s*(?:<!doctype[^>]*>)?\s*<html[^>]*>/i, '' ) .replace( /<\/html>\s*$/i, '' ); // Preserve <html> attributes, if any htmlAttributes = html.match( /<html([^>]*>)/i ); if ( htmlAttributes && htmlAttributes[ 1 ] ) { wrapper = document.createElement( 'div' ); wrapper.innerHTML = '<div ' + htmlAttributes[ 1 ] + '></div>'; attributes = wrapper.firstChild.attributes; for ( i = 0; i < attributes.length; i++ ) { newDocument.documentElement.setAttribute( attributes[ i ].name, attributes[ i ].value ); } } return newDocument; }; /** * Resolve a URL relative to a given base. * * @param {string} url URL to resolve * @param {HTMLDocument} base Document whose base URL to use * @return {string} Resolved URL */ ve.resolveUrl = function ( url, base ) { var node = base.createElement( 'a' ); node.setAttribute( 'href', url ); // If doc.baseURI isn't set, node.href will be an empty string // This is crazy, returning the original URL is better return node.href || url; }; /** * Modify a set of DOM elements to resolve attributes in the context of another document. * * This performs node.setAttribute( 'attr', nodeInDoc[attr] ); for every node. * * @param {jQuery} $elements Set of DOM elements to modify * @param {HTMLDocument} doc Document to resolve against (different from $elements' .ownerDocument) * @param {string[]} attrs Attributes to resolve */ ve.resolveAttributes = function ( $elements, doc, attrs ) { var i, len, attr; /** * Callback for jQuery.fn.each that resolves the value of attr to the computed * property value. Called in the context of an HTMLElement. * * @private */ function resolveAttribute() { var nodeInDoc = doc.createElement( this.nodeName ); nodeInDoc.setAttribute( attr, this.getAttribute( attr ) ); if ( nodeInDoc[ attr ] ) { this.setAttribute( attr, nodeInDoc[ attr ] ); } } for ( i = 0, len = attrs.length; i < len; i++ ) { attr = attrs[ i ]; $elements.find( '[' + attr + ']' ).addBack( '[' + attr + ']' ).each( resolveAttribute ); } }; /** * Take a target document with a possibly relative base URL, and modify it to be absolute. * The base URL of the target document is resolved using the base URL of the source document. * * Note that the the fallbackBase parameter will be used if there is no <base> tag, even if * the document does have a valid base URL: this is to work around Firefox's behavior of having * documents created by DOMParser inherit the base URL of the main document. * * @param {HTMLDocument} targetDoc Document whose base URL should be resolved * @param {HTMLDocument} sourceDoc Document whose base URL should be used for resolution * @param {string} [fallbackBase] Base URL to use if resolving the base URL fails or there is no <base> tag */ ve.fixBase = function ( targetDoc, sourceDoc, fallbackBase ) { var baseNode = targetDoc.getElementsByTagName( 'base' )[ 0 ]; if ( baseNode ) { if ( !targetDoc.baseURI ) { // <base> tag present but not valid, try resolving its URL baseNode.setAttribute( 'href', ve.resolveUrl( baseNode.getAttribute( 'href' ), sourceDoc ) ); if ( !targetDoc.baseURI && fallbackBase ) { // That didn't work, use the fallback baseNode.setAttribute( 'href', fallbackBase ); } } // else: <base> tag present and valid, do nothing } else if ( fallbackBase ) { // No <base> tag, add one baseNode = targetDoc.createElement( 'base' ); baseNode.setAttribute( 'href', fallbackBase ); targetDoc.head.appendChild( baseNode ); } }; /** * Check if a string is a valid URI component. * * A URI component is considered invalid if decodeURIComponent() throws an exception. * * @param {string} s String to test * @return {boolean} decodeURIComponent( s ) did not throw an exception * @see #safeDecodeURIComponent */ ve.isUriComponentValid = function ( s ) { try { decodeURIComponent( s ); } catch ( e ) { return false; } return true; }; /** * Safe version of decodeURIComponent() that doesn't throw exceptions. * * If the native decodeURIComponent() call threw an exception, the original string * will be returned. * * @param {string} s String to decode * @return {string} Decoded string, or same string if decoding failed * @see #isUriComponentValid */ ve.safeDecodeURIComponent = function ( s ) { try { s = decodeURIComponent( s ); } catch ( e ) {} return s; }; /** * Get the actual inner HTML of a DOM node. * * In most browsers, .innerHTML is broken and eats newlines in `<pre>` elements, see * https://bugzilla.mozilla.org/show_bug.cgi?id=838954 . This function detects this behavior * and works around it, to the extent possible. `<pre>\nFoo</pre>` will become `<pre>Foo</pre>` * if the browser is broken, but newlines are preserved in all other cases. * * @param {HTMLElement} element HTML element to get inner HTML of * @return {string} Inner HTML */ ve.properInnerHtml = function ( element ) { return ve.fixupPreBug( element ).innerHTML; }; /** * Get the actual outer HTML of a DOM node. * * @see ve#properInnerHtml * @param {HTMLElement} element HTML element to get outer HTML of * @return {string} Outer HTML */ ve.properOuterHtml = function ( element ) { return ve.fixupPreBug( element ).outerHTML; }; /** * Helper function for #properInnerHtml, #properOuterHtml and #serializeXhtml. * * Detect whether the browser has broken `<pre>` serialization, and if so return a clone * of the node with extra newlines added to make it serialize properly. If the browser is not * broken, just return the original node. * * @param {HTMLElement} element HTML element to fix up * @return {HTMLElement} Either element, or a fixed-up clone of it */ ve.fixupPreBug = function ( element ) { var div, $element; if ( ve.isPreInnerHtmlBroken === undefined ) { // Test whether newlines in `<pre>` are serialized back correctly div = document.createElement( 'div' ); div.innerHTML = '<pre>\n\n</pre>'; ve.isPreInnerHtmlBroken = div.innerHTML === '<pre>\n</pre>'; } if ( !ve.isPreInnerHtmlBroken ) { return element; } // Workaround for bug 42469: if a `<pre>` starts with a newline, that means .innerHTML will // screw up and stringify it with one fewer newline. Work around this by adding a newline. // If we don't see a leading newline, we still don't know if the original HTML was // `<pre>Foo</pre>` or `<pre>\nFoo</pre>`, but that's a syntactic difference, not a // semantic one, and handling that is the integration target's job. $element = $( element ).clone(); $element.find( 'pre, textarea, listing' ).each( function () { var matches; if ( this.firstChild && this.firstChild.nodeType === Node.TEXT_NODE ) { matches = this.firstChild.data.match( /^(\r\n|\r|\n)/ ); if ( matches && matches[ 1 ] ) { // Prepend a newline exactly like the one we saw this.firstChild.insertData( 0, matches[ 1 ] ); } } } ); return $element.get( 0 ); }; /** * Helper function for #transformStyleAttributes. * * Normalize an attribute value. In compliant browsers, this should be * a no-op, but in IE style attributes are normalized on all elements, * color and bgcolor attributes are normalized on some elements (like `<tr>`), * and width and height attributes are normalized on some elements( like `<table>`). * * @param {string} name Attribute name * @param {string} value Attribute value * @param {string} [nodeName='div'] Element name * @return {string} Normalized attribute value */ ve.normalizeAttributeValue = function ( name, value, nodeName ) { var node = document.createElement( nodeName || 'div' ); node.setAttribute( name, value ); // Support: IE // IE normalizes invalid CSS to empty string, then if you normalize // an empty string again it becomes null. Return an empty string // instead of null to make this function idempotent. return node.getAttribute( name ) || ''; }; /** * Helper function for #parseXhtml and #serializeXhtml. * * Map attributes that are broken in IE to attributes prefixed with data-ve- * or vice versa. * * @param {string} html HTML string. Must also be valid XML * @param {boolean} unmask Map the masked attributes back to their originals * @return {string} HTML string modified to mask/unmask broken attributes */ ve.transformStyleAttributes = function ( html, unmask ) { var xmlDoc, fromAttr, toAttr, i, len, maskAttrs = [ // Support: IE 'style', // IE normalizes 'color:#ffd' to 'color: rgb(255, 255, 221);' 'bgcolor', // IE normalizes '#FFDEAD' to '#ffdead' 'color', // IE normalizes 'Red' to 'red' 'width', // IE normalizes '240px' to '240' 'height', // Same as width // Support: Firefox 'rowspan', // IE and Firefox normalize rowspan="02" to rowspan="2" 'colspan' // Same as rowspan ]; // Parse the HTML into an XML DOM xmlDoc = new DOMParser().parseFromString( html, 'text/xml' ); // Go through and mask/unmask each attribute on all elements that have it for ( i = 0, len = maskAttrs.length; i < len; i++ ) { fromAttr = unmask ? 'data-ve-' + maskAttrs[ i ] : maskAttrs[ i ]; toAttr = unmask ? maskAttrs[ i ] : 'data-ve-' + maskAttrs[ i ]; /*jshint loopfunc:true */ $( xmlDoc ).find( '[' + fromAttr + ']' ).each( function () { var toAttrValue, fromAttrNormalized, fromAttrValue = this.getAttribute( fromAttr ); if ( unmask ) { this.removeAttribute( fromAttr ); // If the data-ve- version doesn't normalize to the same value, // the attribute must have changed, so don't overwrite it fromAttrNormalized = ve.normalizeAttributeValue( toAttr, fromAttrValue, this.nodeName ); // toAttr can't not be set, but IE returns null if the value was '' toAttrValue = this.getAttribute( toAttr ) || ''; if ( toAttrValue !== fromAttrNormalized ) { return; } } this.setAttribute( toAttr, fromAttrValue ); } ); } // FIXME T126032: Inject empty text nodes into empty non-void tags to prevent // things like <a></a> from being serialized as <a /> and wreaking havoc $( xmlDoc ).find( ':empty:not(' + ve.elementTypes.void.join( ',' ) + ')' ).each( function () { this.appendChild( xmlDoc.createTextNode( '' ) ); } ); // Serialize back to a string return new XMLSerializer().serializeToString( xmlDoc ); }; /** * Parse an HTML string into an HTML DOM, while masking attributes affected by * normalization bugs if a broken browser is detected. * Since this process uses an XML parser, the input must be valid XML as well as HTML. * * @param {string} html HTML string. Must also be valid XML * @return {HTMLDocument} HTML DOM */ ve.parseXhtml = function ( html ) { // Support: IE // Feature-detect style attribute breakage in IE if ( ve.isStyleAttributeBroken === undefined ) { ve.isStyleAttributeBroken = ve.normalizeAttributeValue( 'style', 'color:#ffd' ) !== 'color:#ffd'; } if ( ve.isStyleAttributeBroken ) { html = ve.transformStyleAttributes( html, false ); } return ve.createDocumentFromHtml( html ); }; /** * Serialize an HTML DOM created with #parseXhtml back to an HTML string, unmasking any * attributes that were masked. * * @param {HTMLDocument} doc HTML DOM * @return {string} Serialized HTML string */ ve.serializeXhtml = function ( doc ) { return ve.serializeXhtmlElement( doc.documentElement ); }; /** * Serialize an HTML element created with #parseXhtml back to an HTML string, unmasking any * attributes that were masked. * * @param {HTMLElement} element HTML element * @return {string} Serialized HTML string */ ve.serializeXhtmlElement = function ( element ) { var xml; // Support: IE // Feature-detect style attribute breakage in IE if ( ve.isStyleAttributeBroken === undefined ) { ve.isStyleAttributeBroken = ve.normalizeAttributeValue( 'style', 'color:#ffd' ) !== 'color:#ffd'; } if ( !ve.isStyleAttributeBroken ) { // Use outerHTML if possible because in Firefox, XMLSerializer URL-encodes // hrefs but outerHTML doesn't return ve.properOuterHtml( element ); } xml = new XMLSerializer().serializeToString( ve.fixupPreBug( element ) ); // FIXME T126035: This strips out xmlns as a quick hack xml = xml.replace( '<html xmlns="http://www.w3.org/1999/xhtml"', '<html' ); return ve.transformStyleAttributes( xml, true ); }; /** * Wrapper for node.normalize(). The native implementation is broken in IE, * so we use our own implementation in that case. * * @param {Node} node Node to normalize */ ve.normalizeNode = function ( node ) { var p, nodeIterator, textNode; if ( ve.isNormalizeBroken === undefined ) { // Support: IE11 // Feature-detect IE11's broken .normalize() implementation. // We know that it fails to remove the empty text node at the end // in this example, but for mysterious reasons it also fails to merge // text nodes in other cases and we don't quite know why. So if we detect // that .normalize() is broken, fall back to a completely manual version. p = document.createElement( 'p' ); p.appendChild( document.createTextNode( 'Foo' ) ); p.appendChild( document.createTextNode( 'Bar' ) ); p.appendChild( document.createTextNode( '' ) ); p.normalize(); ve.isNormalizeBroken = p.childNodes.length !== 1; } if ( ve.isNormalizeBroken ) { // Perform normalization manually nodeIterator = node.ownerDocument.createNodeIterator( node, NodeFilter.SHOW_TEXT, function () { return NodeFilter.FILTER_ACCEPT; }, false ); while ( ( textNode = nodeIterator.nextNode() ) ) { // Remove if empty if ( textNode.data === '' ) { textNode.parentNode.removeChild( textNode ); continue; } // Merge in any adjacent text nodes while ( textNode.nextSibling && textNode.nextSibling.nodeType === Node.TEXT_NODE ) { textNode.appendData( textNode.nextSibling.data ); textNode.parentNode.removeChild( textNode.nextSibling ); } } } else { // Use native implementation node.normalize(); } }; /** * Translate rect by some fixed vector and return a new offset object * * @param {Object} rect Offset object containing all or any of top, left, bottom, right, width & height * @param {number} x Horizontal translation * @param {number} y Vertical translation * @return {Object} Translated rect */ ve.translateRect = function ( rect, x, y ) { var translatedRect = {}; if ( rect.top !== undefined ) { translatedRect.top = rect.top + y; } if ( rect.bottom !== undefined ) { translatedRect.bottom = rect.bottom + y; } if ( rect.left !== undefined ) { translatedRect.left = rect.left + x; } if ( rect.right !== undefined ) { translatedRect.right = rect.right + x; } if ( rect.width !== undefined ) { translatedRect.width = rect.width; } if ( rect.height !== undefined ) { translatedRect.height = rect.height; } return translatedRect; }; /** * Get the start and end rectangles (in a text flow sense) from a list of rectangles * * @param {Array} rects Full list of rectangles * @return {Object|null} Object containing two rectangles: start and end, or null if there are no rectangles */ ve.getStartAndEndRects = function ( rects ) { var i, l, startRect, endRect; if ( !rects || !rects.length ) { return null; } for ( i = 0, l = rects.length; i < l; i++ ) { if ( !startRect || rects[ i ].top < startRect.top ) { // Use ve.extendObject as ve.copy copies non-plain objects by reference startRect = ve.extendObject( {}, rects[ i ] ); } else if ( rects[ i ].top === startRect.top ) { // Merge rects with the same top coordinate startRect.left = Math.min( startRect.left, rects[ i ].left ); startRect.right = Math.max( startRect.right, rects[ i ].right ); startRect.width = startRect.right - startRect.left; } if ( !endRect || rects[ i ].bottom > endRect.bottom ) { // Use ve.extendObject as ve.copy copies non-plain objects by reference endRect = ve.extendObject( {}, rects[ i ] ); } else if ( rects[ i ].bottom === endRect.bottom ) { // Merge rects with the same bottom coordinate endRect.left = Math.min( endRect.left, rects[ i ].left ); endRect.right = Math.max( endRect.right, rects[ i ].right ); endRect.width = startRect.right - startRect.left; } } return { start: startRect, end: endRect }; }; /** * Find the length of the common start sequence of one or more sequences * * * @param {Array} sequences Array of sequences (arrays, strings etc) * @return {number} Common start sequence length */ ve.getCommonStartSequenceLength = function ( sequences ) { var i, len, val, commonLength = 0; if ( sequences.length === 0 ) { throw new Error( 'Need at least one sequence' ); } commonLengthLoop: while ( true ) { if ( commonLength >= sequences[ 0 ].length ) { break; } val = sequences[ 0 ][ commonLength ]; for ( i = 1, len = sequences.length; i < len; i++ ) { if ( sequences[ i ].length < commonLength || sequences[ i ][ commonLength ] !== val ) { break commonLengthLoop; } } commonLength++; } return commonLength; }; /** * Find the nearest common ancestor of DOM nodes * * @param {...Node|null} DOM nodes * @return {Node|null} Nearest common ancestor; or null if there is none / an argument is null */ ve.getCommonAncestor = function () { var i, j, nodeCount, chain, node, minHeight = null, chains = [], args = Array.prototype.slice.call( arguments ); nodeCount = args.length; if ( nodeCount === 0 ) { throw new Error( 'Need at least one node' ); } // Build every chain for ( i = 0; i < nodeCount; i++ ) { chain = []; node = args[ i ]; while ( node !== null ) { chain.unshift( node ); node = node.parentNode; } if ( chain.length === 0 ) { // args[ i ] was null (so no common ancestor) return null; } if ( i > 0 && chain[ 0 ] !== chains[ chains.length - 1 ][ 0 ] ) { // no common ancestor (different documents or unattached branches) return null; } if ( minHeight === null || minHeight > chain.length ) { minHeight = chain.length; } chains.push( chain ); } // Step through chains in parallel, until they differ. // All chains are guaranteed to start with the common document element (or the common root // of an unattached branch) for ( i = 1; i < minHeight; i++ ) { node = chains[ 0 ][ i ]; for ( j = 1; j < nodeCount; j++ ) { if ( node !== chains[ j ][ i ] ) { return chains[ 0 ][ i - 1 ]; } } } return chains[ 0 ][ minHeight - 1 ]; }; /** * Get the index of a node in its parentNode's childNode list * * @param {Node} node The node * @return {number} Index in parentNode's childNode list */ ve.parentIndex = function ( node ) { return Array.prototype.indexOf.call( node.parentNode.childNodes, node ); }; /** * Get the offset path from ancestor to offset in descendant * * @param {Node} ancestor The ancestor node * @param {Node} node The descendant node * @param {number} nodeOffset The offset in the descendant node * @return {number[]} The offset path */ ve.getOffsetPath = function ( ancestor, node, nodeOffset ) { var path = [ nodeOffset ]; while ( node !== ancestor ) { if ( node.parentNode === null ) { ve.log( node, 'is not a descendant of', ancestor ); throw new Error( 'Not a descendant' ); } path.unshift( ve.parentIndex( node ) ); node = node.parentNode; } return path; }; /** * Compare two tuples in lexicographical order. * * This function first compares `a[0]` with `b[0]`, then `a[1]` with `b[1]`, etc. * until it encounters a pair where `a[k] != b[k]`; then returns `a[k] - b[k]`. * * If `a[k] == b[k]` for every `k`, this function returns 0. * * If a and b are of unequal length, but `a[k] == b[k]` for all `k` that exist in both a and b, then * this function returns `Infinity` (if a is longer) or `-Infinity` (if b is longer). * * @param {number[]} a First tuple * @param {number[]} b Second tuple * @return {number} `a[k] - b[k]` where k is the lowest k such that `a[k] != b[k]` */ ve.compareTuples = function ( a, b ) { var i, len; for ( i = 0, len = Math.min( a.length, b.length ); i < len; i++ ) { if ( a[ i ] !== b[ i ] ) { return a[ i ] - b[ i ]; } } if ( a.length > b.length ) { return Infinity; } if ( a.length < b.length ) { return -Infinity; } return 0; }; /** * Compare two nodes for position in document * * Return null if either position is either null or incomparable (e.g. where one of the nodes * is detached or the nodes are from different documents). * * @param {Node|null} node1 First node * @param {number|null} offset1 First offset * @param {Node|null} node2 Second node * @param {number|null} offset2 Second offset * @return {number|null} negative, zero or positive number, or null if nodes null or incomparable */ ve.compareDocumentOrder = function ( node1, offset1, node2, offset2 ) { var commonAncestor = ve.getCommonAncestor( node1, node2 ); if ( commonAncestor === null ) { // Signal no common ancestor. In theory we could disallow this case, and check // the nodes for detachedness and same-documentness before each call, but such // guard checks would duplicate (either explicitly or implicitly) much of the // branch traversal performed in this method. return null; } return ve.compareTuples( ve.getOffsetPath( commonAncestor, node1, offset1 ), ve.getOffsetPath( commonAncestor, node2, offset2 ) ); }; /** * Get the client platform string from the browser. * * FIXME T126036: This is a wrapper for calling getSystemPlatform() on the current * platform except that if the platform hasn't been constructed yet, it falls back * to using the base class implementation in {ve.init.Platform}. A proper solution * would be not to need this information before the platform is constructed. * * @see ve.init.Platform#getSystemPlatform * @return {string} Client platform string */ ve.getSystemPlatform = function () { return ( ve.init.platform && ve.init.platform.constructor || ve.init.Platform ).static.getSystemPlatform(); }; /** * Get the closest matching DOM position in document order (forward or reverse) * * A DOM position is represented as an object with "node" and "offset" properties. * * The noDescend option can be used to exclude the positions inside certain element nodes; it is * a jQuery selector/function ( used as a test by $node.is() - see http://api.jquery.com/is/ ); * it defaults to ve.rejectsCursor . Void elements (those matching ve.isVoidElement) are always * excluded. * * As well as the end position, an array of ve.PositionSteps (node traversals) is returned. * The "stop" option is a boolean-valued function used to test each ve.PositionStep in turn. If * If it returns true, the position arrived at is returned; else the stepping continues to the * next matching DOM position. It defaults to ve.isHardCursorStep . * * Limitation: some DOM positions cannot actually hold the cursor; e.g. the start of the interior * of a table node. Browser cursoring jumps over text node/annotation node boundaries as if they * were invisible, and skips over most grapheme clusters e.g. 'x\u0301' (though not all e.g. * '\u062D\u0627'). Also, Chromium normalizes cursor focus/offset, when they are set, to the * start-most such cursor position (Firefox does not). * * @param {Object} position Start position * @param {Node} position.node Start node * @param {Node} position.offset Start offset * @param {number} direction +1 for forward, -1 for reverse * @param {Object} options * @param {Function|string} [options.noDescend] Selector or function: nodes to skip over * @param {Function} [options.stop] Boolean-valued ve.PositionStep test function * @return {Object} The adjacent DOM position encountered * @return {Node|null} return.node The node, or null if we stepped past the root node * @return {number|null} return.offset The offset, or null if we stepped past the root node * @return {Object[]} return.steps Steps taken {node: x, type: leave|cross|enter|internal, offset: n} * @see ve#isHardCursorStep */ ve.adjacentDomPosition = function ( position, direction, options ) { var forward, childNode, noDescend, stop, step, node = position.node, offset = position.offset, steps = []; noDescend = options.noDescend || ve.rejectsCursor; stop = options.stop || ve.isHardCursorStep; direction = direction < 0 ? -1 : 1; forward = ( direction === 1 ); while ( true ) { // If we're at the node's leading edge, move to the adjacent position in the parent node if ( offset === ( forward ? node.length || node.childNodes.length : 0 ) ) { step = new ve.PositionStep( node, 'leave' ); steps.push( step ); if ( node.parentNode === null ) { return { node: null, offset: null, steps: steps }; } offset = ve.parentIndex( node ) + ( forward ? 1 : 0 ); node = node.parentNode; if ( stop( step ) ) { return { node: node, offset: offset, steps: steps }; } // Else take another step continue; } // Else we're in the interior of a node // If we're in a text node, move to the position in this node at the next offset if ( node.nodeType === Node.TEXT_NODE ) { step = new ve.PositionStep( node, 'internal', offset - ( forward ? 0 : 1 ) ); steps.push( step ); offset += direction; if ( stop( step ) ) { return { node: node, offset: offset, steps: steps }; } continue; } // Else we're in the interior of an element node childNode = node.childNodes[ forward ? offset : offset - 1 ]; // If the child is uncursorable, or is an element matching noDescend, do not // descend into it: instead, return the position just beyond it in the current node if ( childNode.nodeType === Node.ELEMENT_NODE && ( ve.isVoidElement( childNode ) || $( childNode ).is( noDescend ) ) ) { step = new ve.PositionStep( childNode, 'cross' ); steps.push( step ); offset += forward ? 1 : -1; if ( stop( step ) ) { return { node: node, offset: offset, steps: steps }; } // Else take another step continue; } // Go to the closest offset inside the child node node = childNode; offset = forward ? 0 : node.length || node.childNodes.length; step = new ve.PositionStep( node, 'enter' ); steps.push( step ); if ( stop( step ) ) { return { node: node, offset: offset, steps: steps }; } } }; /** * Test whether a cursor movement step uses up a cursor press * * Essentially, this is true unless entering/exiting a contentEditable text/annotation node. * For instance in <#text>X</#text><b><#text>y</#text></b> * a single cursor press will jump from just after X to just after Y. * * @param {ve.PositionStep} step The cursor movement step to test * @return {boolean} Whether the cursor movement step uses up a cursor press * @see ve#adjacentDomPosition */ ve.isHardCursorStep = function ( step ) { if ( step.node.nodeType === Node.TEXT_NODE ) { return step.type === 'internal'; } return ve.isBlockElement( step.node ) || ve.rejectsCursor( step.node ); }; /** * Tests whether an adjacent cursor would be prevented from entering the node * * @param {Node} [node] Element node or text node; defaults to "this" if a Node * @return {boolean} Whether an adjacent cursor would be prevented from entering */ ve.rejectsCursor = function ( node ) { if ( !node && this instanceof Node ) { node = this; } if ( node.nodeType === node.TEXT_NODE ) { return false; } if ( ve.isVoidElement( node ) ) { return true; } // We don't need to check whether the ancestor-nearest contenteditable tag is // false, because if so then there can be no adjacent cursor. return node.contentEditable === 'false'; }; /** * Count the common elements at the start and end of two sequences * * @param {Array|string} before The original sequence * @param {Array|string} after The modified sequence * @param {Function} [equals] Two-argument comparison returning boolean (defaults to ===) * @return {Object|null} Change offsets (valid in both sequences), or null if unchanged * @return {number} return.start Offset from start of first changed element * @return {number} return.end Offset from end of last changed element (nonoverlapping with start) */ ve.countEdgeMatches = function ( before, after, equals ) { var len, start, end; if ( !equals ) { equals = function ( x, y ) { return x === y; }; } len = Math.min( before.length, after.length ); // Find maximal matching left slice for ( start = 0; start < len; start++ ) { if ( !equals( before[ start ], after[ start ] ) ) { break; } } if ( start === len && before.length === after.length ) { return null; } // Find maximal matching right slice that doesn't overlap the left slice for ( end = 0; end < len - start; end++ ) { if ( !equals( before[ before.length - 1 - end ], after[ after.length - 1 - end ] ) ) { break; } } return { start: start, end: end }; }; /** * Repeat a string n times * * @param {string} str The string to repeat * @param {number} n The number of times to repeat * @param {string} The string, repeated n times */ ve.repeatString = function ( str, n ) { return new Array( n + 1 ).join( str ); }; /** * Check whether a jQuery event represents a plain left click, without any modifiers * * @param {jQuery.Event} e The jQuery event object * @return {boolean} Whether it was an unmodified left click */ ve.isUnmodifiedLeftClick = function ( e ) { return e && e.which && e.which === OO.ui.MouseButtons.LEFT && !( e.shiftKey || e.altKey || e.ctrlKey || e.metaKey ); };