'use strict'; /* global $:off */ /** * @external ThreadItem */ /** * Get the index of a node in its parentNode's childNode list * * @param {Node} child * @return {number} Index in parentNode's childNode list */ function childIndexOf( child ) { var i = 0; while ( ( child = child.previousSibling ) ) { i++; } return i; } /** * Check whether a Node contains (is an ancestor of) another Node (or is the same node) * * @param {Node} ancestor * @param {Node} descendant * @return {boolean} */ function contains( ancestor, descendant ) { // Support: IE 11 // Node#contains is only supported on HTMLElement nodes. Otherwise we could just use // `ancestor.contains( descendant )`. return ancestor === descendant || // eslint-disable-next-line no-bitwise ancestor.compareDocumentPosition( descendant ) & Node.DOCUMENT_POSITION_CONTAINED_BY; } /** * Find closest ancestor element using one of the given tag names. * * @param {Node} node * @param {string[]} tagNames * @return {HTMLElement|null} */ function closestElement( node, tagNames ) { do { if ( node.nodeType === Node.ELEMENT_NODE && tagNames.indexOf( node.tagName.toLowerCase() ) !== -1 ) { return node; } node = node.parentNode; } while ( node ); return null; } /** * Find the transclusion node which rendered the current node, if it exists. * * 1. Find the closest ancestor with an 'about' attribute * 2. Find the main node of the about-group (first sibling with the same 'about' attribute) * 3. If this is an mw:Transclusion node, return it; otherwise, go to step 1 * * @param {Node} node * @return {HTMLElement|null} Translcusion node, null if not found */ function getTranscludedFromElement( node ) { var about; while ( node ) { // 1. if ( node.nodeType === Node.ELEMENT_NODE && node.getAttribute( 'about' ) && /^#mwt\d+$/.test( node.getAttribute( 'about' ) ) ) { about = node.getAttribute( 'about' ); // 2. while ( node.previousSibling && node.previousSibling.nodeType === Node.ELEMENT_NODE && node.previousSibling.getAttribute( 'about' ) === about ) { node = node.previousSibling; } // 3. if ( node.getAttribute( 'typeof' ) && node.getAttribute( 'typeof' ).split( ' ' ).indexOf( 'mw:Transclusion' ) !== -1 ) { break; } } node = node.parentNode; } return node; } /** * Trim ASCII whitespace, as defined in the HTML spec. * * @param {string} str * @return {string} */ function htmlTrim( str ) { // https://infra.spec.whatwg.org/#ascii-whitespace return str.replace( /^[\t\n\f\r ]+/, '' ).replace( /[\t\n\f\r ]+$/, '' ); } /** * Get the indent level of the node, relative to rootNode. * * The indent level is the number of lists inside of which it is nested. * * @private * @param {Node} node * @param {Node} rootNode * @return {number} */ function getIndentLevel( node, rootNode ) { var indent = 0, tagName; while ( node ) { if ( node === rootNode ) { break; } tagName = node.tagName && node.tagName.toLowerCase(); if ( tagName === 'li' || tagName === 'dd' ) { indent++; } node = node.parentNode; } return indent; } /** * Get an array of sibling nodes that contain parts of the given thread item. * * @param {ThreadItem} item Thread item * @return {HTMLElement[]} */ function getCoveredSiblings( item ) { var range, ancestor, siblings, start, end; range = item.getNativeRange(); ancestor = range.commonAncestorContainer; if ( ancestor === range.startContainer || ancestor === range.endContainer ) { return [ ancestor ]; } siblings = ancestor.childNodes; start = 0; end = siblings.length - 1; // Find first of the siblings that contains the item while ( !contains( siblings[ start ], range.startContainer ) ) { start++; } // Find last of the siblings that contains the item while ( !contains( siblings[ end ], range.endContainer ) ) { end--; } return Array.prototype.slice.call( siblings, start, end + 1 ); } /** * Get the nodes (if any) that contain the given thread item, and nothing else. * * @param {ThreadItem} item Thread item * @return {HTMLElement[]|null} */ function getFullyCoveredSiblings( item ) { var siblings, node, startMatches, endMatches, length, parent; siblings = getCoveredSiblings( item ); function isIgnored( n ) { // Ignore empty text nodes, and our own reply buttons return ( n.nodeType === Node.TEXT_NODE && htmlTrim( n.textContent ) === '' ) || ( n.className && n.className.indexOf( 'dt-init-replylink-buttons' ) !== -1 ); } function firstNonemptyChild( n ) { n = n.firstChild; while ( n && isIgnored( n ) ) { n = n.nextSibling; } return n; } function lastNonemptyChild( n ) { n = n.lastChild; while ( n && isIgnored( n ) ) { n = n.previousSibling; } return n; } startMatches = false; node = siblings[ 0 ]; while ( node ) { if ( item.range.startContainer === node && item.range.startOffset === 0 ) { startMatches = true; break; } node = firstNonemptyChild( node ); } endMatches = false; node = siblings[ siblings.length - 1 ]; while ( node ) { length = node.nodeType === Node.TEXT_NODE ? node.textContent.replace( /[\t\n\f\r ]+$/, '' ).length : node.childNodes.length; if ( item.range.endContainer === node && item.range.endOffset === length ) { endMatches = true; break; } node = lastNonemptyChild( node ); } if ( startMatches && endMatches ) { // If these are all of the children (or the only child), go up one more level while ( ( parent = siblings[ 0 ].parentNode ) && firstNonemptyChild( parent ) === siblings[ 0 ] && lastNonemptyChild( parent ) === siblings[ siblings.length - 1 ] ) { siblings = [ parent ]; } return siblings; } return null; } module.exports = { childIndexOf: childIndexOf, closestElement: closestElement, getIndentLevel: getIndentLevel, getFullyCoveredSiblings: getFullyCoveredSiblings, getTranscludedFromElement: getTranscludedFromElement, htmlTrim: htmlTrim };