diff --git a/src/__fixtures__/fixtures.ts b/src/__fixtures__/fixtures.ts
index 749d15ae12..f1b7c147ad 100644
--- a/src/__fixtures__/fixtures.ts
+++ b/src/__fixtures__/fixtures.ts
@@ -47,6 +47,32 @@ export const drinks = [
export const food = ['
(
return this._make(select.select(selectorOrHaystack, elems, options));
}
+/**
+ * Creates a matcher, using a particular mapping function. Matchers provide a
+ * function that finds elements using a generating function, supporting filtering.
+ *
+ * @private
+ * @param matchMap - Mapping function.
+ * @returns - Function for wrapping generating functions.
+ */
+function _getMatcher(
+ matchMap: (fn: (elem: Node) => P, elems: Cheerio) => Element[]
+) {
+ return function (
+ fn: (elem: Node) => P,
+ ...postFns: ((elems: Element[]) => Element[])[]
+ ) {
+ return function (
+ this: Cheerio,
+ selector?: AcceptedFilters
+ ): Cheerio {
+ let matched: Element[] = matchMap(fn, this);
+
+ if (selector) {
+ matched = filterArray(matched, selector, this.options);
+ }
+
+ return this._make(
+ // Post processing is only necessary if there is more than one element.
+ this.length > 1 && matched.length > 1
+ ? postFns.reduce((elems, fn) => fn(elems), matched)
+ : matched
+ );
+ };
+ };
+}
+
+/** Matcher that adds multiple elements for each entry in the input. */
+const _matcher = _getMatcher((fn: (elem: Node) => Element[], elems) => {
+ const ret: Element[][] = [];
+
+ for (let i = 0; i < elems.length; i++) {
+ const value = fn(elems[i]);
+ ret.push(value);
+ }
+
+ return new Array().concat(...ret);
+});
+
+/** Matcher that adds at most one element for each entry in the input. */
+const _singleMatcher = _getMatcher(
+ (fn: (elem: Node) => Element | null, elems) => {
+ const ret: Element[] = [];
+
+ for (let i = 0; i < elems.length; i++) {
+ const value = fn(elems[i]);
+ if (value !== null) {
+ ret.push(value);
+ }
+ }
+ return ret;
+ }
+);
+
+/**
+ * Matcher that supports traversing until a condition is met.
+ *
+ * @returns A function usable for `*Until` methods.
+ */
+function _matchUntil(
+ nextElem: (elem: Node) => Element | null,
+ ...postFns: ((elems: Element[]) => Element[])[]
+) {
+ // We use a variable here that is used from within the matcher.
+ let matches: ((el: Element, i: number) => boolean) | null = null;
+
+ const innerMatcher = _getMatcher(
+ (nextElem: (elem: Node) => Element | null, elems) => {
+ const matched: Element[] = [];
+
+ domEach(elems, (elem) => {
+ for (let next; (next = nextElem(elem)); elem = next) {
+ // FIXME: `matched` might contain duplicates here and the index is too large.
+ if (matches?.(next, matched.length)) break;
+ matched.push(next);
+ }
+ });
+
+ return matched;
+ }
+ )(nextElem, ...postFns);
+
+ return function (
+ this: Cheerio,
+ selector?: AcceptedFilters | null,
+ filterSelector?: AcceptedFilters
+ ): Cheerio {
+ // Override `matches` variable with the new target.
+ matches =
+ typeof selector === 'string'
+ ? (elem: Element) => select.is(elem, selector, this.options)
+ : selector
+ ? getFilterFn(selector)
+ : null;
+
+ const ret = innerMatcher.call(this, filterSelector);
+
+ // Set `matches` to `null`, so we don't waste memory.
+ matches = null;
+
+ return ret;
+ };
+}
+
+function _removeDuplicates(elems: T[]): T[] {
+ return Array.from(new Set(elems));
+}
+
/**
* Get the parent of each element in the current set of matched elements,
* optionally filtered by a selector.
@@ -83,25 +200,10 @@ export function find(
* @returns The parents.
* @see {@link https://api.jquery.com/parent/}
*/
-export function parent(
- this: Cheerio,
- selector?: AcceptedFilters
-): Cheerio {
- const set: Element[] = [];
-
- domEach(this, (elem) => {
- const parentElem = elem.parent;
- if (
- parentElem &&
- parentElem.type !== 'root' &&
- !set.includes(parentElem as Element)
- ) {
- set.push(parentElem as Element);
- }
- });
-
- return selector ? filter.call(set, selector, this) : this._make(set);
-}
+export const parent = _singleMatcher(
+ ({ parent }) => (parent && !isDocument(parent) ? (parent as Element) : null),
+ _removeDuplicates
+);
/**
* Get a set of parents filtered by `selector` of each element in the current
@@ -121,30 +223,18 @@ export function parent(
* @returns The parents.
* @see {@link https://api.jquery.com/parents/}
*/
-export function parents(
- this: Cheerio,
- selector?: AcceptedFilters
-): Cheerio {
- const parentNodes: Element[] = [];
-
- /*
- * 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((elem) =>
- traverseParents(this, elem.parent, selector, Infinity).forEach((node) => {
- // We know these must be `Element`s, as we filter out root nodes.
- if (!parentNodes.includes(node as Element)) {
- parentNodes.push(node as Element);
- }
- })
- );
-
- return this._make(parentNodes);
-}
+export const parents = _matcher(
+ (elem) => {
+ const matched = [];
+ while (elem.parent && !isDocument(elem.parent)) {
+ matched.push(elem.parent as Element);
+ elem = elem.parent;
+ }
+ return matched;
+ },
+ uniqueSort,
+ (elems) => elems.reverse()
+);
/**
* Get the ancestors of each element in the current set of matched elements, up
@@ -159,56 +249,15 @@ export function parents(
* ```
*
* @param selector - Selector for element to stop at.
- * @param filterBy - Optional filter for parents.
+ * @param filterSelector - Optional filter for parents.
* @returns The parents.
* @see {@link https://api.jquery.com/parentsUntil/}
*/
-export function parentsUntil(
- this: Cheerio,
- selector?: string | Node | Cheerio,
- filterBy?: AcceptedFilters
-): Cheerio {
- const parentNodes: Element[] = [];
- let untilNode: Node | undefined;
- let untilNodes: Node[] | undefined;
-
- if (typeof selector === 'string') {
- untilNodes = this.parents(selector).toArray();
- } else if (selector && isCheerio(selector)) {
- 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((elem: Node) => {
- while (elem.parent) {
- elem = elem.parent;
- if (
- (untilNode && elem !== untilNode) ||
- (untilNodes && !untilNodes.includes(elem)) ||
- (!untilNode && !untilNodes)
- ) {
- if (isTag(elem) && !parentNodes.includes(elem)) {
- parentNodes.push(elem);
- }
- } else {
- break;
- }
- }
- }, this);
-
- return filterBy
- ? filter.call(parentNodes, filterBy, this)
- : this._make(parentNodes);
-}
+export const parentsUntil = _matchUntil(
+ ({ parent }) => (parent && !isDocument(parent) ? (parent as Element) : null),
+ uniqueSort,
+ (elems) => elems.reverse()
+);
/**
* For each element in the set, get the first element that matches the selector
@@ -237,7 +286,7 @@ export function parentsUntil(
*/
export function closest(
this: Cheerio,
- selector?: AcceptedFilters
+ selector?: AcceptedFilters
): Cheerio {
const set: Node[] = [];
@@ -245,12 +294,16 @@ export function closest(
return this._make(set);
}
- domEach(this, (elem) => {
- const closestElem = traverseParents(this, elem, selector, 1)[0];
-
- // Do not add duplicate elements to the set
- if (closestElem && !set.includes(closestElem)) {
- set.push(closestElem);
+ domEach(this, (elem: Node | null) => {
+ while (elem && elem.type !== 'root') {
+ if (!selector || filterArray([elem], selector, this.options).length) {
+ // Do not add duplicate elements to the set
+ if (elem && !set.includes(elem)) {
+ set.push(elem);
+ }
+ break;
+ }
+ elem = elem.parent;
}
});
@@ -272,24 +325,7 @@ export function closest(
* @returns The next nodes.
* @see {@link https://api.jquery.com/next/}
*/
-export function next(
- this: Cheerio,
- selector?: AcceptedFilters
-): Cheerio {
- const elems: Element[] = [];
-
- domEach(this, (elem) => {
- while (elem.next) {
- elem = elem.next;
- if (isTag(elem)) {
- elems.push(elem);
- return;
- }
- }
- });
-
- return selector ? filter.call(elems, selector, this) : this._make(elems);
-}
+export const next = _singleMatcher((elem) => DomUtils.nextElementSibling(elem));
/**
* Gets all the following siblings of the first selected element, optionally
@@ -309,23 +345,14 @@ export function next(
* @returns The next nodes.
* @see {@link https://api.jquery.com/nextAll/}
*/
-export function nextAll(
- this: Cheerio,
- selector?: AcceptedFilters
-): Cheerio {
- const elems: Element[] = [];
-
- domEach(this, (elem: Node) => {
- while (elem.next) {
- elem = elem.next;
- if (isTag(elem) && !elems.includes(elem)) {
- elems.push(elem);
- }
- }
- });
-
- return selector ? filter.call(elems, selector, this) : this._make(elems);
-}
+export const nextAll = _matcher((elem) => {
+ const matched = [];
+ while (elem.next) {
+ elem = elem.next;
+ if (isTag(elem)) matched.push(elem);
+ }
+ return matched;
+}, _removeDuplicates);
/**
* Gets all the following siblings up to but not including the element matched
@@ -344,44 +371,10 @@ export function nextAll(
* @returns The next nodes.
* @see {@link https://api.jquery.com/nextUntil/}
*/
-export function nextUntil(
- this: Cheerio,
- selector?: string | Cheerio | Node | null,
- filterSelector?: AcceptedFilters
-): Cheerio {
- const elems: Element[] = [];
- let untilNode: Node | undefined;
- let untilNodes: Node[] | undefined;
-
- if (typeof selector === 'string') {
- untilNodes = this.nextAll(selector).toArray();
- } else if (selector && isCheerio(selector)) {
- untilNodes = selector.get();
- } else if (selector) {
- untilNode = selector;
- }
-
- domEach(this, (elem) => {
- while (elem.next) {
- elem = elem.next;
- if (
- (untilNode && elem !== untilNode) ||
- (untilNodes && !untilNodes.includes(elem)) ||
- (!untilNode && !untilNodes)
- ) {
- if (isTag(elem) && !elems.includes(elem)) {
- elems.push(elem);
- }
- } else {
- break;
- }
- }
- });
-
- return filterSelector
- ? filter.call(elems, filterSelector, this)
- : this._make(elems);
-}
+export const nextUntil = _matchUntil(
+ (el) => DomUtils.nextElementSibling(el),
+ _removeDuplicates
+);
/**
* Gets the previous sibling of the first selected element optionally filtered
@@ -399,24 +392,7 @@ export function nextUntil(
* @returns The previous nodes.
* @see {@link https://api.jquery.com/prev/}
*/
-export function prev(
- this: Cheerio,
- selector?: AcceptedFilters
-): Cheerio {
- const elems: Element[] = [];
-
- domEach(this, (elem: Node) => {
- while (elem.prev) {
- elem = elem.prev;
- if (isTag(elem)) {
- elems.push(elem);
- return;
- }
- }
- });
-
- return selector ? filter.call(elems, selector, this) : this._make(elems);
-}
+export const prev = _singleMatcher((elem) => DomUtils.prevElementSibling(elem));
/**
* Gets all the preceding siblings of the first selected element, optionally
@@ -437,23 +413,14 @@ export function prev(
* @returns The previous nodes.
* @see {@link https://api.jquery.com/prevAll/}
*/
-export function prevAll(
- this: Cheerio,
- selector?: AcceptedFilters
-): Cheerio {
- const elems: Element[] = [];
-
- domEach(this, (elem) => {
- while (elem.prev) {
- elem = elem.prev;
- if (isTag(elem) && !elems.includes(elem)) {
- elems.push(elem);
- }
- }
- });
-
- return selector ? filter.call(elems, selector, this) : this._make(elems);
-}
+export const prevAll = _matcher((elem) => {
+ const matched = [];
+ while (elem.prev) {
+ elem = elem.prev;
+ if (isTag(elem)) matched.push(elem);
+ }
+ return matched;
+}, _removeDuplicates);
/**
* Gets all the preceding siblings up to but not including the element matched
@@ -472,44 +439,10 @@ export function prevAll(
* @returns The previous nodes.
* @see {@link https://api.jquery.com/prevUntil/}
*/
-export function prevUntil(
- this: Cheerio,
- selector?: string | Cheerio | Node | null,
- filterSelector?: AcceptedFilters
-): Cheerio {
- const elems: Element[] = [];
- let untilNode: Node | undefined;
- let untilNodes: Node[] | undefined;
-
- if (typeof selector === 'string') {
- untilNodes = this.prevAll(selector).toArray();
- } else if (selector && isCheerio(selector)) {
- untilNodes = selector.get();
- } else if (selector) {
- untilNode = selector;
- }
-
- domEach(this, (elem) => {
- while (elem.prev) {
- elem = elem.prev;
- if (
- (untilNode && elem !== untilNode) ||
- (untilNodes && !untilNodes.includes(elem)) ||
- (!untilNode && !untilNodes)
- ) {
- if (isTag(elem) && !elems.includes(elem)) {
- elems.push(elem);
- }
- } else {
- break;
- }
- }
- });
-
- return filterSelector
- ? filter.call(elems, filterSelector, this)
- : this._make(elems);
-}
+export const prevUntil = _matchUntil(
+ (el) => DomUtils.prevElementSibling(el),
+ _removeDuplicates
+);
/**
* Get the siblings of each element (excluding the element) in the set of
@@ -530,21 +463,13 @@ export function prevUntil(
* @returns The siblings.
* @see {@link https://api.jquery.com/siblings/}
*/
-export function siblings(
- this: Cheerio,
- selector?: AcceptedFilters
-): Cheerio {
- // TODO Still get siblings if `parent` is null; see DomUtils' `getSiblings`.
- const parent = this.parent();
-
- const elems = parent
- .children()
- .toArray()
- // TODO: This removes all elements in the selection. Note that they could be added here, if siblings are part of the selection.
- .filter((elem: Node) => !this.is(elem));
-
- return selector ? filter.call(elems, selector, this) : this._make(elems);
-}
+export const siblings = _matcher(
+ (elem) =>
+ DomUtils.getSiblings(elem).filter(
+ (el): el is Element => isTag(el) && el !== elem
+ ),
+ uniqueSort
+);
/**
* Gets the children of the first selected element.
@@ -564,20 +489,10 @@ export function siblings(
* @returns The children.
* @see {@link https://api.jquery.com/children/}
*/
-export function children(
- this: Cheerio,
- selector?: AcceptedFilters
-): Cheerio {
- const elems = this.toArray().reduce(
- (newElems, elem) =>
- hasChildren(elem)
- ? newElems.concat(elem.children.filter(isTag))
- : newElems,
- []
- );
-
- return selector ? filter.call(elems, selector, this) : this._make(elems);
-}
+export const children = _matcher(
+ (elem) => DomUtils.getChildren(elem).filter(isTag),
+ _removeDuplicates
+);
/**
* Gets the children of each element in the set of matched elements, including
@@ -679,6 +594,12 @@ export function map(
return this._make(elems);
}
+/**
+ * Creates a function to test if a filter is matched.
+ *
+ * @param match - A filter.
+ * @returns A function that determines if a filter has been matched.
+ */
function getFilterFn(
match: FilterFunction | Cheerio | T
): (el: T, i: number) => boolean {
@@ -686,7 +607,7 @@ function getFilterFn(
return (el, i) => (match as FilterFunction).call(el, i, el);
}
if (isCheerio(match)) {
- return (el) => match.is(el);
+ return (el) => Array.prototype.includes.call(match, el);
}
return function (el) {
return match === el;
@@ -760,41 +681,21 @@ export function filter>(
this: Cheerio,
match: S
): Cheerio;
-/**
- * Internal `filter` variant used by other functions to filter their elements.
- *
- * @private
- * @param match - Value to look for, following the rules above.
- * @param container - The container that is used to create the resulting Cheerio instance.
- * @returns The filtered collection.
- * @see {@link https://api.jquery.com/filter/}
- */
-export function filter(
- this: T[],
- match: AcceptedFilters,
- container: Cheerio
-): Cheerio;
export function filter(
- this: Cheerio | T[],
- match: AcceptedFilters,
- container = this
+ this: Cheerio,
+ match: AcceptedFilters
): Cheerio {
- if (!isCheerio(container)) {
- throw new Error('Not able to create a Cheerio instance.');
- }
-
- const nodes = isCheerio(this) ? this.toArray() : this;
-
- const result =
- typeof match === 'string'
- ? select.filter(
- match,
- (nodes as unknown as Node[]).filter(isTag),
- container.options
- )
- : nodes.filter(getFilterFn(match));
+ return this._make(filterArray(this.toArray(), match, this.options));
+}
- return container._make(result);
+export function filterArray(
+ nodes: T[],
+ match: AcceptedFilters,
+ options: InternalOptions
+): Element[] | T[] {
+ return typeof match === 'string'
+ ? select.filter(match, (nodes as unknown as Node[]).filter(isTag), options)
+ : nodes.filter(getFilterFn(match));
}
/**
@@ -859,28 +760,21 @@ export function is(
* @see {@link https://api.jquery.com/not/}
*/
export function not(
- this: Cheerio | T[],
- match: AcceptedFilters,
- container = this
+ this: Cheerio,
+ match: AcceptedFilters
): Cheerio {
- if (!isCheerio(container)) {
- throw new Error('Not able to create a Cheerio instance.');
- }
-
- let nodes = isCheerio(this) ? this.toArray() : this;
+ let nodes = this.toArray();
if (typeof match === 'string') {
const elements = (nodes as Node[]).filter(isTag);
- const matches = new Set(
- select.filter(match, elements, container.options)
- );
+ const matches = new Set(select.filter(match, elements, this.options));
nodes = nodes.filter((el) => !matches.has(el));
} else {
const filterFn = getFilterFn(match);
nodes = nodes.filter((el, i) => !filterFn(el, i));
}
- return container._make(nodes);
+ return this._make(nodes);
}
/**
@@ -1076,7 +970,7 @@ export function index(
: selectorOrNeedle;
}
- return $haystack.get().indexOf(needle);
+ return Array.prototype.indexOf.call($haystack, needle);
}
/**
@@ -1109,22 +1003,6 @@ export function slice(
return this._make(Array.prototype.slice.call(this, start, end));
}
-function traverseParents(
- self: Cheerio,
- elem: Node | null,
- selector: AcceptedFilters | undefined,
- limit: number
-): Node[] {
- const elems: Node[] = [];
- while (elem && elems.length < limit && elem.type !== 'root') {
- if (!selector || 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.