diff --git a/src/api/attributes.ts b/src/api/attributes.ts index 10ef082506..5b4c989dd1 100644 --- a/src/api/attributes.ts +++ b/src/api/attributes.ts @@ -8,6 +8,7 @@ import { text } from '../static'; import { isTag, domEach, camelCase, cssCase } from '../utils'; import type { Node, Element } from 'domhandler'; import type { Cheerio } from '../cheerio'; +import { AcceptedFilters } from '../types'; const hasOwn = Object.prototype.hasOwnProperty; const rspace = /\s+/; const dataAttrPrefix = 'data-'; @@ -989,14 +990,9 @@ export function toggleClass>( * @returns Whether or not the selector matches an element of the instance. * @see {@link https://api.jquery.com/is/} */ -export function is( +export function is( this: Cheerio, - selector?: - | string - | ((this: Element, i: number, el: Element) => boolean) - | Cheerio - | T - | null + selector?: AcceptedFilters ): boolean { if (selector) { return this.filter(selector).length > 0; diff --git a/src/api/traversing.spec.ts b/src/api/traversing.spec.ts index 63b2504777..b5597748d9 100644 --- a/src/api/traversing.spec.ts +++ b/src/api/traversing.spec.ts @@ -1,7 +1,7 @@ import cheerio from '../../src'; import { Cheerio } from '../cheerio'; import type { CheerioAPI } from '../load'; -import { Node, Element, isText } from 'domhandler'; +import { Node, Element, Text, isText } from 'domhandler'; import { food, fruits, @@ -818,6 +818,7 @@ describe('$(...)', () => { describe('.filter', () => { it('should throw if it cannot construct an object', () => { + // @ts-expect-error Calling `filter` without a cheerio instance. expect(() => $('').filter.call([], '')).toThrow( 'Not able to create a Cheerio instance.' ); @@ -856,6 +857,14 @@ describe('$(...)', () => { expect(orange).toBe('Orange'); }); + + it('should also iterate over text nodes (#1867)', () => { + const text = $('ab').filter((_, el): el is Text => + isText(el) + ); + + expect(text[0].data).toBe('b'); + }); }); describe('.not', () => { diff --git a/src/api/traversing.ts b/src/api/traversing.ts index aa10143b62..37d4980d28 100644 --- a/src/api/traversing.ts +++ b/src/api/traversing.ts @@ -44,7 +44,7 @@ export function find( if (typeof selectorOrHaystack !== 'string') { const haystack = isCheerio(selectorOrHaystack) - ? selectorOrHaystack.get() + ? selectorOrHaystack.toArray() : [selectorOrHaystack]; return this._make( @@ -85,7 +85,7 @@ export function find( */ export function parent( this: Cheerio, - selector?: AcceptedFilters + selector?: AcceptedFilters ): Cheerio { const set: Element[] = []; @@ -123,7 +123,7 @@ export function parent( */ export function parents( this: Cheerio, - selector?: AcceptedFilters + selector?: AcceptedFilters ): Cheerio { const parentNodes: Element[] = []; @@ -166,7 +166,7 @@ export function parents( export function parentsUntil( this: Cheerio, selector?: string | Node | Cheerio, - filterBy?: AcceptedFilters + filterBy?: AcceptedFilters ): Cheerio { const parentNodes: Element[] = []; let untilNode: Node | undefined; @@ -237,7 +237,7 @@ export function parentsUntil( */ export function closest( this: Cheerio, - selector?: AcceptedFilters + selector?: AcceptedFilters ): Cheerio { const set: Node[] = []; @@ -274,7 +274,7 @@ export function closest( */ export function next( this: Cheerio, - selector?: AcceptedFilters + selector?: AcceptedFilters ): Cheerio { const elems: Element[] = []; @@ -311,7 +311,7 @@ export function next( */ export function nextAll( this: Cheerio, - selector?: AcceptedFilters + selector?: AcceptedFilters ): Cheerio { const elems: Element[] = []; @@ -347,7 +347,7 @@ export function nextAll( export function nextUntil( this: Cheerio, selector?: string | Cheerio | Node | null, - filterSelector?: AcceptedFilters + filterSelector?: AcceptedFilters ): Cheerio { const elems: Element[] = []; let untilNode: Node | undefined; @@ -401,7 +401,7 @@ export function nextUntil( */ export function prev( this: Cheerio, - selector?: AcceptedFilters + selector?: AcceptedFilters ): Cheerio { const elems: Element[] = []; @@ -439,7 +439,7 @@ export function prev( */ export function prevAll( this: Cheerio, - selector?: AcceptedFilters + selector?: AcceptedFilters ): Cheerio { const elems: Element[] = []; @@ -475,7 +475,7 @@ export function prevAll( export function prevUntil( this: Cheerio, selector?: string | Cheerio | Node | null, - filterSelector?: AcceptedFilters + filterSelector?: AcceptedFilters ): Cheerio { const elems: Element[] = []; let untilNode: Node | undefined; @@ -532,7 +532,7 @@ export function prevUntil( */ export function siblings( this: Cheerio, - selector?: AcceptedFilters + selector?: AcceptedFilters ): Cheerio { // TODO Still get siblings if `parent` is null; see DomUtils' `getSiblings`. const parent = this.parent(); @@ -566,7 +566,7 @@ export function siblings( */ export function children( this: Cheerio, - selector?: AcceptedFilters + selector?: AcceptedFilters ): Cheerio { const elems = this.toArray().reduce( (newElems, elem) => @@ -679,16 +679,16 @@ export function map( return this._make(elems); } -function getFilterFn( - match: ((this: S, i: number, el: S) => boolean) | Cheerio | T -): (el: S, i: number) => boolean { +function getFilterFn( + match: FilterFunction | Cheerio | T +): (el: T, i: number) => boolean { if (typeof match === 'function') { return function (el, i) { - return match.call(el, i, el); + return (match as FilterFunction).call(el, i, el); }; } - if (isCheerio(match)) { - return match.is.bind(match); + if (isCheerio(match)) { + return (el) => match.is(el); } return function (el) { return match === el; @@ -697,12 +697,42 @@ function getFilterFn( /** * Iterates over a cheerio object, reducing the set of selector elements to - * those that match the selector or pass the function's test. When a Cheerio - * selection is specified, return only the elements contained in that selection. - * When an element is specified, return only that element (if it is contained in - * the original selection). If using the function method, the function is - * executed in the context of the selected element, so `this` refers to the - * current element. + * those that match the selector or pass the function's test. + * + * This is the definition for using type guards; have a look below for other + * ways to invoke this method. The function is executed in the context of the + * selected element, so `this` refers to the current element. + * + * @category Traversing + * @example Function + * + * ```js + * $('li') + * .filter(function (i, el) { + * // this === el + * return $(this).attr('class') === 'orange'; + * }) + * .attr('class'); //=> orange + * ``` + * + * @param match - Value to look for, following the rules above. + * @returns The filtered collection. + * @see {@link https://api.jquery.com/filter/} + */ +export function filter( + this: Cheerio, + match: (this: T, index: number, value: T) => value is S +): Cheerio; +/** + * Iterates over a cheerio object, reducing the set of selector elements to + * those that match the selector or pass the function's test. + * + * - When a Cheerio selection is specified, return only the elements contained in + * that selection. + * - When an element is specified, return only that element (if it is contained in + * the original selection). + * - If using the function method, the function is executed in the context of the + * selected element, so `this` refers to the current element. * * @category Traversing * @example Selector @@ -723,29 +753,50 @@ function getFilterFn( * .attr('class'); //=> orange * ``` * + * @param match - Value to look for, following the rules above. See + * {@link AcceptedFilters}. + * @returns The filtered collection. + * @see {@link https://api.jquery.com/filter/} + */ +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 - Optional node to filter instead. + * @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: Cheerio | Node[], - match: AcceptedFilters, +export function filter( + this: T[], + match: AcceptedFilters, + container: Cheerio +): Cheerio; +export function filter( + this: Cheerio | T[], + match: AcceptedFilters, container = this -): Cheerio { +): Cheerio { if (!isCheerio(container)) { throw new Error('Not able to create a Cheerio instance.'); } const nodes = isCheerio(this) ? this.toArray() : this; - let elements: Element[] = nodes.filter(isTag); - elements = + const result = typeof match === 'string' - ? select.filter(match, elements, container.options) - : elements.filter(getFilterFn(match)); - - return container._make(elements); + ? select.filter( + match, + ((nodes as unknown) as Node[]).filter(isTag), + container.options + ) + : nodes.filter(getFilterFn(match)); + + return container._make(result); } /** @@ -783,7 +834,7 @@ export function filter( */ export function not( this: Cheerio | T[], - match: Node | Cheerio | string | FilterFunction, + match: AcceptedFilters, container = this ): Cheerio { if (!isCheerio(container)) { @@ -834,8 +885,7 @@ export function has( this: Cheerio, selectorOrHaystack: string | Cheerio | Element ): Cheerio { - return filter.call( - this, + return this.filter( typeof selectorOrHaystack === 'string' ? // Using the `:has` selector here short-circuits searches. `:has(${selectorOrHaystack})` @@ -1036,7 +1086,7 @@ export function slice( function traverseParents( self: Cheerio, elem: Node | null, - selector: AcceptedFilters | undefined, + selector: AcceptedFilters | undefined, limit: number ): Node[] { const elems: Node[] = []; diff --git a/src/types.ts b/src/types.ts index 520679cee7..9c8250f23d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,7 +41,7 @@ export type SelectorType = | `${AlphaNumeric}${string}`; import type { Cheerio } from './cheerio'; -import type { Node, Element } from 'domhandler'; +import type { Node } from 'domhandler'; /** Elements that can be passed to manipulation methods. */ export type BasicAcceptedElems = Cheerio | T[] | T | string; @@ -53,8 +53,4 @@ export type AcceptedElems = /** Function signature, for traversal methods. */ export type FilterFunction = (this: T, i: number, el: T) => boolean; /** Supported filter types, for traversal methods. */ -export type AcceptedFilters = - | string - | FilterFunction - | Node - | Cheerio; +export type AcceptedFilters = string | FilterFunction | T | Cheerio;