Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 121 additions & 134 deletions lib/api/traversing.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,63 @@ var uniqueSort = require('htmlparser2').DomUtils.uniqueSort;
var isTag = utils.isTag;
var reSiblingSelector = /^\s*[~+]/;

/**
* Matcher provides function, what finds elements based on function provided.
* It also houses filtering. What may provided later when function is called.
*
* @private
* @param {Function} fn - Function for collecting elements.
* @param {boolean} reverse - Is output in reverse order.
* @param {boolean} noSort - Function Result is always unique.
* @returns {Function} - Wrapped function.
*/
function _matcher(fn, reverse, noSort) {
// customized Map, discards null elements
function matchMap(elems) {
var len = elems.length;
var value;
var i = 0;
var ret = [];
for (; i < len; i++) {
value = fn(elems[i], i);
if (value !== null) {
ret.push(value);
}
}
return Array.prototype.concat.apply([], ret);
}

return function (selector) {
if (this[0]) {
var matched = matchMap(this);
var isSorted = false;
var needSort = this.length > 1;

// select.filter uses uniqueSort already internally
if (selector) {
if (typeof selector === 'string') {
matched = select.filter(selector, matched, this.options);
isSorted = true;
} else {
matched = matched.filter(getFilterFn(selector));
}
}

if (needSort && !isSorted && !noSort) {
matched = uniqueSort(matched);
}

// Reverse order for parents* and prev-derivatives
if (reverse && (needSort || isSorted)) {
matched.reverse();
}

return this._make(matched);
}
return this;
};
}

/**
* Get the descendants of each element in the current set of matched elements,
* filtered by a selector, jQuery object, or element.
Expand Down Expand Up @@ -70,31 +127,16 @@ exports.find = function (selectorOrHaystack) {
* $('.pear').parent().attr('id');
* //=> fruits
*
* @function
* @param {string} [selector] - If specified filter for parent.
* @see {@link https://api.jquery.com/parent/}
*
* @returns {Cheerio} The parents.
*/
exports.parent = function (selector) {
var set = [];

domEach(this, function (_, elem) {
var parentElem = elem.parent;
if (
parentElem &&
parentElem.type !== 'root' &&
set.indexOf(parentElem) < 0
) {
set.push(parentElem);
}
});

if (selector) {
set = exports.filter.call(set, selector, this);
}

return this._make(set);
};
exports.parent = _matcher(function (elem) {
var parent = elem.parent;
return parent && parent.type !== 'root' ? parent : null;
});

/**
* Get a set of parents filtered by `selector` of each element in the current
Expand All @@ -106,31 +148,19 @@ exports.parent = function (selector) {
* $('.orange').parents('#fruits').length;
* // => 1
*
* @function
* @param {string} [selector] - If specified filter for parents.
* @see {@link https://api.jquery.com/parents/}
*
* @returns {Cheerio} The parents.
*/
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.parents = _matcher(function (elem) {
var matched = [];
while ((elem = elem.parent) && elem.type !== 'root') {
matched.push(elem);
}
return matched;
}, true);

/**
* Get the ancestors of each element in the current set of matched elements, up
Expand Down Expand Up @@ -236,30 +266,20 @@ exports.closest = function (selector) {
* $('.apple').next().hasClass('orange');
* //=> true
*
* @function
* @param {string} [selector] - If specified filter for sibling.
* @see {@link https://api.jquery.com/next/}
*
* @returns {Cheerio} The next nodes.
*/
exports.next = function (selector) {
if (!this[0]) {
return this;
}
var elems = [];

domEach(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.next = _matcher(
function (elem) {
while ((elem = elem.next) && !isTag(elem));
return elem;
},
false,
true
);

/**
* Gets all the following siblings of the first selected element, optionally
Expand All @@ -271,29 +291,19 @@ exports.next = function (selector) {
* $('.apple').nextAll('.orange');
* //=> [<li class="orange">Orange</li>]
*
* @function
* @param {string} [selector] - If specified filter for siblings.
* @see {@link https://api.jquery.com/nextAll/}
*
* @returns {Cheerio} The next nodes.
*/
exports.nextAll = function (selector) {
if (!this[0]) {
return this;
exports.nextAll = _matcher(function (elem) {
var matched = [];
while ((elem = elem.next)) {
if (isTag(elem)) matched.push(elem);
}
var elems = [];

domEach(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);
};
return matched;
});

/**
* Gets all the following siblings up to but not including the element matched
Expand Down Expand Up @@ -354,30 +364,21 @@ exports.nextUntil = function (selector, filterSelector) {
* $('.orange').prev().hasClass('apple');
* //=> true
*
* @function
* @param {string} [selector] - If specified filter for siblings.
* @see {@link https://api.jquery.com/prev/}
*
* @returns {Cheerio} The previous nodes.
*/
exports.prev = function (selector) {
if (!this[0]) {
return this;
}
var elems = [];

domEach(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.prev = _matcher(
function (elem) {
// eslint-disable-next-line no-empty
while ((elem = elem.prev) && !isTag(elem)) {}
return elem;
},
true,
true
);

/**
* Gets all the preceding siblings of the first selected element, optionally
Expand All @@ -389,29 +390,19 @@ exports.prev = function (selector) {
* $('.pear').prevAll('.orange');
* //=> [<li class="orange">Orange</li>]
*
* @function
* @param {string} [selector] - If specified filter for siblings.
* @see {@link https://api.jquery.com/prevAll/}
*
* @returns {Cheerio} The previous nodes.
*/
exports.prevAll = function (selector) {
if (!this[0]) {
return this;
exports.prevAll = _matcher(function (elem) {
var matched = [];
while ((elem = elem.prev)) {
if (isTag(elem)) matched.push(elem);
}
var elems = [];

domEach(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);
};
return matched;
}, true);

/**
* Gets all the preceding siblings up to but not including the element matched
Expand Down Expand Up @@ -474,25 +465,22 @@ exports.prevUntil = function (selector, filterSelector) {
* $('.pear').siblings('.orange').length;
* //=> 1
*
* @function
* @param {string} [selector] - If specified filter for siblings.
* @see {@link https://api.jquery.com/siblings/}
*
* @returns {Cheerio} The siblings.
*/
exports.siblings = function (selector) {
var parent = this.parent();

var elems = (parent ? parent.children() : this.siblingsAndMe())
.toArray()
.filter(function (elem) {
return isTag(elem) && !this.is(elem);
}, this);

if (selector !== undefined) {
return exports.filter.call(elems, selector, this);
exports.siblings = _matcher(function (elem) {
var node = elem.parent && elem.parent.firstChild;
var matched = [];
for (; node; node = node.next) {
if (isTag(node) && node !== elem) {
matched.push(node);
}
}
return this._make(elems);
};
return matched;
});

/**
* Gets the children of the first selected element.
Expand All @@ -504,20 +492,19 @@ exports.siblings = function (selector) {
* $('#fruits').children('.pear').text();
* //=> Pear
*
* @function
* @param {string} [selector] - If specified filter for children.
* @see {@link https://api.jquery.com/children/}
*
* @returns {Cheerio} The children.
*/
exports.children = function (selector) {
var elems = this.toArray().reduce(function (newElems, elem) {
return newElems.concat(elem.children.filter(isTag));
}, []);

if (selector === undefined) return this._make(elems);

return exports.filter.call(elems, selector, this);
};
exports.children = _matcher(
function (elem) {
return elem.children.filter(isTag);
},
false,
true
);

/**
* Gets the children of each element in the set of matched elements, including
Expand Down
18 changes: 18 additions & 0 deletions test/__fixtures__/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,24 @@ exports.drinks = [
'</ul>',
].join('');

exports.eleven = [
'<html>\n<body>\n<ul>',
'<li>One</li>',
'<li>Two</li>',
'<li class="blue sel">Three</li>',
'<li class="red">Four</li>',
'</ul>\n\n<ul>',
'<li class="red">Five</li>',
'<li>Six</li>',
'<li class="blue">Seven</li>',
'</ul>\n\n<ul>',
'<li>Eight</li>',
'<li class="red sel">Nine</li>',
'<li>Ten</li>',
'<li class="sel">Eleven</li>',
'</ul>\n</body>\n</html>',
].join('\n');

exports.food = [
'<ul id="food">',
exports.fruits,
Expand Down
Loading