Skip to content

Commit fb8d31e

Browse files
authored
fix(traversing): Make filter work on all collections (#1870)
Fixes #1867
1 parent c370f4e commit fb8d31e

File tree

4 files changed

+105
-54
lines changed

4 files changed

+105
-54
lines changed

src/api/attributes.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { text } from '../static';
88
import { isTag, domEach, camelCase, cssCase } from '../utils';
99
import type { Node, Element } from 'domhandler';
1010
import type { Cheerio } from '../cheerio';
11+
import { AcceptedFilters } from '../types';
1112
const hasOwn = Object.prototype.hasOwnProperty;
1213
const rspace = /\s+/;
1314
const dataAttrPrefix = 'data-';
@@ -989,14 +990,9 @@ export function toggleClass<T extends Node, R extends ArrayLike<T>>(
989990
* @returns Whether or not the selector matches an element of the instance.
990991
* @see {@link https://api.jquery.com/is/}
991992
*/
992-
export function is<T extends Node>(
993+
export function is<T>(
993994
this: Cheerio<T>,
994-
selector?:
995-
| string
996-
| ((this: Element, i: number, el: Element) => boolean)
997-
| Cheerio<T>
998-
| T
999-
| null
995+
selector?: AcceptedFilters<T>
1000996
): boolean {
1001997
if (selector) {
1002998
return this.filter(selector).length > 0;

src/api/traversing.spec.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import cheerio from '../../src';
22
import { Cheerio } from '../cheerio';
33
import type { CheerioAPI } from '../load';
4-
import { Node, Element, isText } from 'domhandler';
4+
import { Node, Element, Text, isText } from 'domhandler';
55
import {
66
food,
77
fruits,
@@ -818,6 +818,7 @@ describe('$(...)', () => {
818818

819819
describe('.filter', () => {
820820
it('should throw if it cannot construct an object', () => {
821+
// @ts-expect-error Calling `filter` without a cheerio instance.
821822
expect(() => $('').filter.call([], '')).toThrow(
822823
'Not able to create a Cheerio instance.'
823824
);
@@ -856,6 +857,14 @@ describe('$(...)', () => {
856857

857858
expect(orange).toBe('Orange');
858859
});
860+
861+
it('should also iterate over text nodes (#1867)', () => {
862+
const text = $('<a>a</a>b<c></c>').filter((_, el): el is Text =>
863+
isText(el)
864+
);
865+
866+
expect(text[0].data).toBe('b');
867+
});
859868
});
860869

861870
describe('.not', () => {

src/api/traversing.ts

+90-40
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function find<T extends Node>(
4444

4545
if (typeof selectorOrHaystack !== 'string') {
4646
const haystack = isCheerio(selectorOrHaystack)
47-
? selectorOrHaystack.get()
47+
? selectorOrHaystack.toArray()
4848
: [selectorOrHaystack];
4949

5050
return this._make(
@@ -85,7 +85,7 @@ export function find<T extends Node>(
8585
*/
8686
export function parent<T extends Node>(
8787
this: Cheerio<T>,
88-
selector?: AcceptedFilters
88+
selector?: AcceptedFilters<T>
8989
): Cheerio<Element> {
9090
const set: Element[] = [];
9191

@@ -123,7 +123,7 @@ export function parent<T extends Node>(
123123
*/
124124
export function parents<T extends Node>(
125125
this: Cheerio<T>,
126-
selector?: AcceptedFilters
126+
selector?: AcceptedFilters<T>
127127
): Cheerio<Element> {
128128
const parentNodes: Element[] = [];
129129

@@ -166,7 +166,7 @@ export function parents<T extends Node>(
166166
export function parentsUntil<T extends Node>(
167167
this: Cheerio<T>,
168168
selector?: string | Node | Cheerio<Node>,
169-
filterBy?: AcceptedFilters
169+
filterBy?: AcceptedFilters<T>
170170
): Cheerio<Element> {
171171
const parentNodes: Element[] = [];
172172
let untilNode: Node | undefined;
@@ -237,7 +237,7 @@ export function parentsUntil<T extends Node>(
237237
*/
238238
export function closest<T extends Node>(
239239
this: Cheerio<T>,
240-
selector?: AcceptedFilters
240+
selector?: AcceptedFilters<T>
241241
): Cheerio<Node> {
242242
const set: Node[] = [];
243243

@@ -274,7 +274,7 @@ export function closest<T extends Node>(
274274
*/
275275
export function next<T extends Node>(
276276
this: Cheerio<T>,
277-
selector?: AcceptedFilters
277+
selector?: AcceptedFilters<T>
278278
): Cheerio<Element> {
279279
const elems: Element[] = [];
280280

@@ -311,7 +311,7 @@ export function next<T extends Node>(
311311
*/
312312
export function nextAll<T extends Node>(
313313
this: Cheerio<T>,
314-
selector?: AcceptedFilters
314+
selector?: AcceptedFilters<T>
315315
): Cheerio<Element> {
316316
const elems: Element[] = [];
317317

@@ -347,7 +347,7 @@ export function nextAll<T extends Node>(
347347
export function nextUntil<T extends Node>(
348348
this: Cheerio<T>,
349349
selector?: string | Cheerio<Node> | Node | null,
350-
filterSelector?: AcceptedFilters
350+
filterSelector?: AcceptedFilters<T>
351351
): Cheerio<Element> {
352352
const elems: Element[] = [];
353353
let untilNode: Node | undefined;
@@ -401,7 +401,7 @@ export function nextUntil<T extends Node>(
401401
*/
402402
export function prev<T extends Node>(
403403
this: Cheerio<T>,
404-
selector?: AcceptedFilters
404+
selector?: AcceptedFilters<T>
405405
): Cheerio<Element> {
406406
const elems: Element[] = [];
407407

@@ -439,7 +439,7 @@ export function prev<T extends Node>(
439439
*/
440440
export function prevAll<T extends Node>(
441441
this: Cheerio<T>,
442-
selector?: AcceptedFilters
442+
selector?: AcceptedFilters<T>
443443
): Cheerio<Element> {
444444
const elems: Element[] = [];
445445

@@ -475,7 +475,7 @@ export function prevAll<T extends Node>(
475475
export function prevUntil<T extends Node>(
476476
this: Cheerio<T>,
477477
selector?: string | Cheerio<Node> | Node | null,
478-
filterSelector?: AcceptedFilters
478+
filterSelector?: AcceptedFilters<T>
479479
): Cheerio<Element> {
480480
const elems: Element[] = [];
481481
let untilNode: Node | undefined;
@@ -532,7 +532,7 @@ export function prevUntil<T extends Node>(
532532
*/
533533
export function siblings<T extends Node>(
534534
this: Cheerio<T>,
535-
selector?: AcceptedFilters
535+
selector?: AcceptedFilters<T>
536536
): Cheerio<Element> {
537537
// TODO Still get siblings if `parent` is null; see DomUtils' `getSiblings`.
538538
const parent = this.parent();
@@ -566,7 +566,7 @@ export function siblings<T extends Node>(
566566
*/
567567
export function children<T extends Node>(
568568
this: Cheerio<T>,
569-
selector?: AcceptedFilters
569+
selector?: AcceptedFilters<T>
570570
): Cheerio<Element> {
571571
const elems = this.toArray().reduce<Element[]>(
572572
(newElems, elem) =>
@@ -679,16 +679,16 @@ export function map<T, M>(
679679
return this._make(elems);
680680
}
681681

682-
function getFilterFn<T extends Node, S extends T>(
683-
match: ((this: S, i: number, el: S) => boolean) | Cheerio<T> | T
684-
): (el: S, i: number) => boolean {
682+
function getFilterFn<T>(
683+
match: FilterFunction<T> | Cheerio<T> | T
684+
): (el: T, i: number) => boolean {
685685
if (typeof match === 'function') {
686686
return function (el, i) {
687-
return match.call(el, i, el);
687+
return (match as FilterFunction<T>).call(el, i, el);
688688
};
689689
}
690-
if (isCheerio(match)) {
691-
return match.is.bind(match);
690+
if (isCheerio<T>(match)) {
691+
return (el) => match.is(el);
692692
}
693693
return function (el) {
694694
return match === el;
@@ -697,12 +697,42 @@ function getFilterFn<T extends Node, S extends T>(
697697

698698
/**
699699
* Iterates over a cheerio object, reducing the set of selector elements to
700-
* those that match the selector or pass the function's test. When a Cheerio
701-
* selection is specified, return only the elements contained in that selection.
702-
* When an element is specified, return only that element (if it is contained in
703-
* the original selection). If using the function method, the function is
704-
* executed in the context of the selected element, so `this` refers to the
705-
* current element.
700+
* those that match the selector or pass the function's test.
701+
*
702+
* This is the definition for using type guards; have a look below for other
703+
* ways to invoke this method. The function is executed in the context of the
704+
* selected element, so `this` refers to the current element.
705+
*
706+
* @category Traversing
707+
* @example <caption>Function</caption>
708+
*
709+
* ```js
710+
* $('li')
711+
* .filter(function (i, el) {
712+
* // this === el
713+
* return $(this).attr('class') === 'orange';
714+
* })
715+
* .attr('class'); //=> orange
716+
* ```
717+
*
718+
* @param match - Value to look for, following the rules above.
719+
* @returns The filtered collection.
720+
* @see {@link https://api.jquery.com/filter/}
721+
*/
722+
export function filter<T, S extends T>(
723+
this: Cheerio<T>,
724+
match: (this: T, index: number, value: T) => value is S
725+
): Cheerio<S>;
726+
/**
727+
* Iterates over a cheerio object, reducing the set of selector elements to
728+
* those that match the selector or pass the function's test.
729+
*
730+
* - When a Cheerio selection is specified, return only the elements contained in
731+
* that selection.
732+
* - When an element is specified, return only that element (if it is contained in
733+
* the original selection).
734+
* - If using the function method, the function is executed in the context of the
735+
* selected element, so `this` refers to the current element.
706736
*
707737
* @category Traversing
708738
* @example <caption>Selector</caption>
@@ -723,29 +753,50 @@ function getFilterFn<T extends Node, S extends T>(
723753
* .attr('class'); //=> orange
724754
* ```
725755
*
756+
* @param match - Value to look for, following the rules above. See
757+
* {@link AcceptedFilters}.
758+
* @returns The filtered collection.
759+
* @see {@link https://api.jquery.com/filter/}
760+
*/
761+
export function filter<T, S extends AcceptedFilters<T>>(
762+
this: Cheerio<T>,
763+
match: S
764+
): Cheerio<S extends string ? Element : T>;
765+
/**
766+
* Internal `filter` variant used by other functions to filter their elements.
767+
*
768+
* @private
726769
* @param match - Value to look for, following the rules above.
727-
* @param container - Optional node to filter instead.
770+
* @param container - The container that is used to create the resulting Cheerio instance.
728771
* @returns The filtered collection.
729772
* @see {@link https://api.jquery.com/filter/}
730773
*/
731-
export function filter(
732-
this: Cheerio<Node> | Node[],
733-
match: AcceptedFilters,
774+
export function filter<T>(
775+
this: T[],
776+
match: AcceptedFilters<T>,
777+
container: Cheerio<Node>
778+
): Cheerio<Element>;
779+
export function filter<T>(
780+
this: Cheerio<T> | T[],
781+
match: AcceptedFilters<T>,
734782
container = this
735-
): Cheerio<Element> {
783+
): Cheerio<unknown> {
736784
if (!isCheerio(container)) {
737785
throw new Error('Not able to create a Cheerio instance.');
738786
}
739787

740788
const nodes = isCheerio(this) ? this.toArray() : this;
741-
let elements: Element[] = nodes.filter(isTag);
742789

743-
elements =
790+
const result =
744791
typeof match === 'string'
745-
? select.filter(match, elements, container.options)
746-
: elements.filter(getFilterFn(match));
747-
748-
return container._make(elements);
792+
? select.filter(
793+
match,
794+
((nodes as unknown) as Node[]).filter(isTag),
795+
container.options
796+
)
797+
: nodes.filter(getFilterFn(match));
798+
799+
return container._make<unknown>(result);
749800
}
750801

751802
/**
@@ -783,7 +834,7 @@ export function filter(
783834
*/
784835
export function not<T extends Node>(
785836
this: Cheerio<T> | T[],
786-
match: Node | Cheerio<Node> | string | FilterFunction<T>,
837+
match: AcceptedFilters<T>,
787838
container = this
788839
): Cheerio<T> {
789840
if (!isCheerio(container)) {
@@ -834,8 +885,7 @@ export function has(
834885
this: Cheerio<Node | Element>,
835886
selectorOrHaystack: string | Cheerio<Element> | Element
836887
): Cheerio<Node | Element> {
837-
return filter.call(
838-
this,
888+
return this.filter(
839889
typeof selectorOrHaystack === 'string'
840890
? // Using the `:has` selector here short-circuits searches.
841891
`:has(${selectorOrHaystack})`
@@ -1036,7 +1086,7 @@ export function slice<T>(
10361086
function traverseParents<T extends Node>(
10371087
self: Cheerio<T>,
10381088
elem: Node | null,
1039-
selector: AcceptedFilters | undefined,
1089+
selector: AcceptedFilters<T> | undefined,
10401090
limit: number
10411091
): Node[] {
10421092
const elems: Node[] = [];

src/types.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export type SelectorType =
4141
| `${AlphaNumeric}${string}`;
4242

4343
import type { Cheerio } from './cheerio';
44-
import type { Node, Element } from 'domhandler';
44+
import type { Node } from 'domhandler';
4545

4646
/** Elements that can be passed to manipulation methods. */
4747
export type BasicAcceptedElems<T extends Node> = Cheerio<T> | T[] | T | string;
@@ -53,8 +53,4 @@ export type AcceptedElems<T extends Node> =
5353
/** Function signature, for traversal methods. */
5454
export type FilterFunction<T> = (this: T, i: number, el: T) => boolean;
5555
/** Supported filter types, for traversal methods. */
56-
export type AcceptedFilters =
57-
| string
58-
| FilterFunction<Element>
59-
| Node
60-
| Cheerio<Node>;
56+
export type AcceptedFilters<T> = string | FilterFunction<T> | T | Cheerio<T>;

0 commit comments

Comments
 (0)