var select = require('css-select'), utils = require('../utils'), domEach = utils.domEach, uniqueSort = require('htmlparser2').DomUtils.uniqueSort, isTag = utils.isTag, _ = { bind: require('lodash.bind'), forEach: require('lodash.foreach'), reject: require('lodash.reject'), filter: require('lodash.filter'), reduce: require('lodash.reduce') }; exports.find = function(selectorOrHaystack) { var elems = _.reduce(this, function(memo, elem) { return memo.concat(_.filter(elem.children, isTag)); }, []); var contains = this.constructor.contains; var haystack; if (selectorOrHaystack && typeof selectorOrHaystack !== 'string') { if (selectorOrHaystack.cheerio) { haystack = selectorOrHaystack.get(); } else { haystack = [selectorOrHaystack]; } return this._make(haystack.filter(function(elem) { var idx, len; for (idx = 0, len = this.length; idx < len; ++idx) { if (contains(this[idx], elem)) { return true; } } }, this)); } var options = {__proto__: this.options, context: this.toArray()}; return this._make(select(selectorOrHaystack, elems, options)); }; // Get the parent of each element in the current set of matched elements, // optionally filtered by a selector. exports.parent = function(selector) { var set = []; domEach(this, function(idx, elem) { var parentElem = elem.parent; if (parentElem && set.indexOf(parentElem) < 0) { set.push(parentElem); } }); if (arguments.length) { set = exports.filter.call(set, selector, this); } return this._make(set); }; exports.parents = function(selector) { var parentNodes = []; // When multiple DOM elements are in the original set, the resulting set will // be in *reverse* order of the original elements as well, with duplicates // removed. this.get().reverse().forEach(function(elem) { traverseParents(this, elem.parent, selector, Infinity) .forEach(function(node) { if (parentNodes.indexOf(node) === -1) { parentNodes.push(node); } } ); }, this); return this._make(parentNodes); }; exports.parentsUntil = function(selector, filter) { var parentNodes = [], untilNode, untilNodes; if (typeof selector === 'string') { untilNode = select(selector, this.parents().toArray(), this.options)[0]; } else if (selector && selector.cheerio) { untilNodes = selector.toArray(); } else if (selector) { untilNode = selector; } // When multiple DOM elements are in the original set, the resulting set will // be in *reverse* order of the original elements as well, with duplicates // removed. this.toArray().reverse().forEach(function(elem) { while ((elem = elem.parent)) { if ((untilNode && elem !== untilNode) || (untilNodes && untilNodes.indexOf(elem) === -1) || (!untilNode && !untilNodes)) { if (isTag(elem) && parentNodes.indexOf(elem) === -1) { parentNodes.push(elem); } } else { break; } } }, this); return this._make(filter ? select(filter, parentNodes, this.options) : parentNodes); }; // For each element in the set, get the first element that matches the selector // by testing the element itself and traversing up through its ancestors in the // DOM tree. exports.closest = function(selector) { var set = []; if (!selector) { return this._make(set); } domEach(this, function(idx, elem) { var closestElem = traverseParents(this, elem, selector, 1)[0]; // Do not add duplicate elements to the set if (closestElem && set.indexOf(closestElem) < 0) { set.push(closestElem); } }.bind(this)); return this._make(set); }; exports.next = function(selector) { if (!this[0]) { return this; } var elems = []; _.forEach(this, function(elem) { while ((elem = elem.next)) { if (isTag(elem)) { elems.push(elem); return; } } }); return selector ? exports.filter.call(elems, selector, this) : this._make(elems); }; exports.nextAll = function(selector) { if (!this[0]) { return this; } var elems = []; _.forEach(this, function(elem) { while ((elem = elem.next)) { if (isTag(elem) && elems.indexOf(elem) === -1) { elems.push(elem); } } }); return selector ? exports.filter.call(elems, selector, this) : this._make(elems); }; exports.nextUntil = function(selector, filterSelector) { if (!this[0]) { return this; } var elems = [], untilNode, untilNodes; if (typeof selector === 'string') { untilNode = select(selector, this.nextAll().get(), this.options)[0]; } else if (selector && selector.cheerio) { untilNodes = selector.get(); } else if (selector) { untilNode = selector; } _.forEach(this, function(elem) { while ((elem = elem.next)) { if ((untilNode && elem !== untilNode) || (untilNodes && untilNodes.indexOf(elem) === -1) || (!untilNode && !untilNodes)) { if (isTag(elem) && elems.indexOf(elem) === -1) { elems.push(elem); } } else { break; } } }); return filterSelector ? exports.filter.call(elems, filterSelector, this) : this._make(elems); }; exports.prev = function(selector) { if (!this[0]) { return this; } var elems = []; _.forEach(this, function(elem) { while ((elem = elem.prev)) { if (isTag(elem)) { elems.push(elem); return; } } }); return selector ? exports.filter.call(elems, selector, this) : this._make(elems); }; exports.prevAll = function(selector) { if (!this[0]) { return this; } var elems = []; _.forEach(this, function(elem) { while ((elem = elem.prev)) { if (isTag(elem) && elems.indexOf(elem) === -1) { elems.push(elem); } } }); return selector ? exports.filter.call(elems, selector, this) : this._make(elems); }; exports.prevUntil = function(selector, filterSelector) { if (!this[0]) { return this; } var elems = [], untilNode, untilNodes; if (typeof selector === 'string') { untilNode = select(selector, this.prevAll().get(), this.options)[0]; } else if (selector && selector.cheerio) { untilNodes = selector.get(); } else if (selector) { untilNode = selector; } _.forEach(this, function(elem) { while ((elem = elem.prev)) { if ((untilNode && elem !== untilNode) || (untilNodes && untilNodes.indexOf(elem) === -1) || (!untilNode && !untilNodes)) { if (isTag(elem) && elems.indexOf(elem) === -1) { elems.push(elem); } } else { break; } } }); return filterSelector ? exports.filter.call(elems, filterSelector, this) : this._make(elems); }; exports.siblings = function(selector) { var parent = this.parent(); var elems = _.filter( parent ? parent.children() : this.siblingsAndMe(), _.bind(function(elem) { return isTag(elem) && !this.is(elem); }, this) ); if (selector !== undefined) { return exports.filter.call(elems, selector, this); } else { return this._make(elems); } }; exports.children = function(selector) { var elems = _.reduce(this, function(memo, elem) { return memo.concat(_.filter(elem.children, isTag)); }, []); if (selector === undefined) return this._make(elems); return exports.filter.call(elems, selector, this); }; exports.contents = function() { return this._make(_.reduce(this, function(all, elem) { all.push.apply(all, elem.children); return all; }, [])); }; exports.each = function(fn) { var i = 0, len = this.length; while (i < len && fn.call(this[i], i, this[i]) !== false) ++i; return this; }; exports.map = function(fn) { return this._make(_.reduce(this, function(memo, el, i) { var val = fn.call(el, i, el); return val == null ? memo : memo.concat(val); }, [])); }; var makeFilterMethod = function(filterFn) { return function(match, container) { var testFn; container = container || this; if (typeof match === 'string') { testFn = select.compile(match, container.options); } else if (typeof match === 'function') { testFn = function(el, i) { return match.call(el, i, el); }; } else if (match.cheerio) { testFn = match.is.bind(match); } else { testFn = function(el) { return match === el; }; } return container._make(filterFn(this, testFn)); }; }; exports.filter = makeFilterMethod(_.filter); exports.not = makeFilterMethod(_.reject); exports.has = function(selectorOrHaystack) { var that = this; return exports.filter.call(this, function() { return that._make(this).find(selectorOrHaystack).length > 0; }); }; exports.first = function() { return this.length > 1 ? this._make(this[0]) : this; }; exports.last = function() { return this.length > 1 ? this._make(this[this.length - 1]) : this; }; // Reduce the set of matched elements to the one at the specified index. exports.eq = function(i) { i = +i; // Use the first identity optimization if possible if (i === 0 && this.length <= 1) return this; if (i < 0) i = this.length + i; return this[i] ? this._make(this[i]) : this._make([]); }; // Retrieve the DOM elements matched by the jQuery object. exports.get = function(i) { if (i == null) { return Array.prototype.slice.call(this); } else { return this[i < 0 ? (this.length + i) : i]; } }; // Search for a given element from among the matched elements. exports.index = function(selectorOrNeedle) { var $haystack, needle; if (arguments.length === 0) { $haystack = this.parent().children(); needle = this[0]; } else if (typeof selectorOrNeedle === 'string') { $haystack = this._make(selectorOrNeedle); needle = this[0]; } else { $haystack = this; needle = selectorOrNeedle.cheerio ? selectorOrNeedle[0] : selectorOrNeedle; } return $haystack.get().indexOf(needle); }; exports.slice = function() { return this._make([].slice.apply(this, arguments)); }; function traverseParents(self, elem, selector, limit) { var elems = []; while (elem && elems.length < limit) { if (!selector || exports.filter.call([elem], selector, self).length) { elems.push(elem); } elem = elem.parent; } return elems; } // End the most recent filtering operation in the current chain and return the // set of matched elements to its previous state. exports.end = function() { return this.prevObject || this._make([]); }; exports.add = function(other, context) { var selection = this._make(other, context); var contents = uniqueSort(selection.get().concat(this.get())); for (var i = 0; i < contents.length; ++i) { selection[i] = contents[i]; } selection.length = contents.length; return selection; }; // Add the previous set of elements on the stack to the current set, optionally // filtered by a selector. exports.addBack = function(selector) { return this.add( arguments.length ? this.prevObject.filter(selector) : this.prevObject ); };