Skip to content

Commit

Permalink
fix(traversing): Make filter work on all collections (#1870)
Browse files Browse the repository at this point in the history
Fixes #1867
  • Loading branch information
fb55 committed May 17, 2021
1 parent c370f4e commit fb8d31e
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 54 deletions.
10 changes: 3 additions & 7 deletions src/api/attributes.ts
Expand Up @@ -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-';
Expand Down Expand Up @@ -989,14 +990,9 @@ export function toggleClass<T extends Node, R extends ArrayLike<T>>(
* @returns Whether or not the selector matches an element of the instance.
* @see {@link https://api.jquery.com/is/}
*/
export function is<T extends Node>(
export function is<T>(
this: Cheerio<T>,
selector?:
| string
| ((this: Element, i: number, el: Element) => boolean)
| Cheerio<T>
| T
| null
selector?: AcceptedFilters<T>
): boolean {
if (selector) {
return this.filter(selector).length > 0;
Expand Down
11 changes: 10 additions & 1 deletion 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,
Expand Down Expand Up @@ -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.'
);
Expand Down Expand Up @@ -856,6 +857,14 @@ describe('$(...)', () => {

expect(orange).toBe('Orange');
});

it('should also iterate over text nodes (#1867)', () => {
const text = $('<a>a</a>b<c></c>').filter((_, el): el is Text =>
isText(el)
);

expect(text[0].data).toBe('b');
});
});

describe('.not', () => {
Expand Down
130 changes: 90 additions & 40 deletions src/api/traversing.ts
Expand Up @@ -44,7 +44,7 @@ export function find<T extends Node>(

if (typeof selectorOrHaystack !== 'string') {
const haystack = isCheerio(selectorOrHaystack)
? selectorOrHaystack.get()
? selectorOrHaystack.toArray()
: [selectorOrHaystack];

return this._make(
Expand Down Expand Up @@ -85,7 +85,7 @@ export function find<T extends Node>(
*/
export function parent<T extends Node>(
this: Cheerio<T>,
selector?: AcceptedFilters
selector?: AcceptedFilters<T>
): Cheerio<Element> {
const set: Element[] = [];

Expand Down Expand Up @@ -123,7 +123,7 @@ export function parent<T extends Node>(
*/
export function parents<T extends Node>(
this: Cheerio<T>,
selector?: AcceptedFilters
selector?: AcceptedFilters<T>
): Cheerio<Element> {
const parentNodes: Element[] = [];

Expand Down Expand Up @@ -166,7 +166,7 @@ export function parents<T extends Node>(
export function parentsUntil<T extends Node>(
this: Cheerio<T>,
selector?: string | Node | Cheerio<Node>,
filterBy?: AcceptedFilters
filterBy?: AcceptedFilters<T>
): Cheerio<Element> {
const parentNodes: Element[] = [];
let untilNode: Node | undefined;
Expand Down Expand Up @@ -237,7 +237,7 @@ export function parentsUntil<T extends Node>(
*/
export function closest<T extends Node>(
this: Cheerio<T>,
selector?: AcceptedFilters
selector?: AcceptedFilters<T>
): Cheerio<Node> {
const set: Node[] = [];

Expand Down Expand Up @@ -274,7 +274,7 @@ export function closest<T extends Node>(
*/
export function next<T extends Node>(
this: Cheerio<T>,
selector?: AcceptedFilters
selector?: AcceptedFilters<T>
): Cheerio<Element> {
const elems: Element[] = [];

Expand Down Expand Up @@ -311,7 +311,7 @@ export function next<T extends Node>(
*/
export function nextAll<T extends Node>(
this: Cheerio<T>,
selector?: AcceptedFilters
selector?: AcceptedFilters<T>
): Cheerio<Element> {
const elems: Element[] = [];

Expand Down Expand Up @@ -347,7 +347,7 @@ export function nextAll<T extends Node>(
export function nextUntil<T extends Node>(
this: Cheerio<T>,
selector?: string | Cheerio<Node> | Node | null,
filterSelector?: AcceptedFilters
filterSelector?: AcceptedFilters<T>
): Cheerio<Element> {
const elems: Element[] = [];
let untilNode: Node | undefined;
Expand Down Expand Up @@ -401,7 +401,7 @@ export function nextUntil<T extends Node>(
*/
export function prev<T extends Node>(
this: Cheerio<T>,
selector?: AcceptedFilters
selector?: AcceptedFilters<T>
): Cheerio<Element> {
const elems: Element[] = [];

Expand Down Expand Up @@ -439,7 +439,7 @@ export function prev<T extends Node>(
*/
export function prevAll<T extends Node>(
this: Cheerio<T>,
selector?: AcceptedFilters
selector?: AcceptedFilters<T>
): Cheerio<Element> {
const elems: Element[] = [];

Expand Down Expand Up @@ -475,7 +475,7 @@ export function prevAll<T extends Node>(
export function prevUntil<T extends Node>(
this: Cheerio<T>,
selector?: string | Cheerio<Node> | Node | null,
filterSelector?: AcceptedFilters
filterSelector?: AcceptedFilters<T>
): Cheerio<Element> {
const elems: Element[] = [];
let untilNode: Node | undefined;
Expand Down Expand Up @@ -532,7 +532,7 @@ export function prevUntil<T extends Node>(
*/
export function siblings<T extends Node>(
this: Cheerio<T>,
selector?: AcceptedFilters
selector?: AcceptedFilters<T>
): Cheerio<Element> {
// TODO Still get siblings if `parent` is null; see DomUtils' `getSiblings`.
const parent = this.parent();
Expand Down Expand Up @@ -566,7 +566,7 @@ export function siblings<T extends Node>(
*/
export function children<T extends Node>(
this: Cheerio<T>,
selector?: AcceptedFilters
selector?: AcceptedFilters<T>
): Cheerio<Element> {
const elems = this.toArray().reduce<Element[]>(
(newElems, elem) =>
Expand Down Expand Up @@ -679,16 +679,16 @@ export function map<T, M>(
return this._make(elems);
}

function getFilterFn<T extends Node, S extends T>(
match: ((this: S, i: number, el: S) => boolean) | Cheerio<T> | T
): (el: S, i: number) => boolean {
function getFilterFn<T>(
match: FilterFunction<T> | Cheerio<T> | T
): (el: T, i: number) => boolean {
if (typeof match === 'function') {
return function (el, i) {
return match.call(el, i, el);
return (match as FilterFunction<T>).call(el, i, el);
};
}
if (isCheerio(match)) {
return match.is.bind(match);
if (isCheerio<T>(match)) {
return (el) => match.is(el);
}
return function (el) {
return match === el;
Expand All @@ -697,12 +697,42 @@ function getFilterFn<T extends Node, S extends T>(

/**
* 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 <caption>Function</caption>
*
* ```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<T, S extends T>(
this: Cheerio<T>,
match: (this: T, index: number, value: T) => value is S
): Cheerio<S>;
/**
* 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 <caption>Selector</caption>
Expand All @@ -723,29 +753,50 @@ function getFilterFn<T extends Node, S extends T>(
* .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<T, S extends AcceptedFilters<T>>(
this: Cheerio<T>,
match: S
): Cheerio<S extends string ? Element : T>;
/**
* 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> | Node[],
match: AcceptedFilters,
export function filter<T>(
this: T[],
match: AcceptedFilters<T>,
container: Cheerio<Node>
): Cheerio<Element>;
export function filter<T>(
this: Cheerio<T> | T[],
match: AcceptedFilters<T>,
container = this
): Cheerio<Element> {
): Cheerio<unknown> {
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<unknown>(result);
}

/**
Expand Down Expand Up @@ -783,7 +834,7 @@ export function filter(
*/
export function not<T extends Node>(
this: Cheerio<T> | T[],
match: Node | Cheerio<Node> | string | FilterFunction<T>,
match: AcceptedFilters<T>,
container = this
): Cheerio<T> {
if (!isCheerio(container)) {
Expand Down Expand Up @@ -834,8 +885,7 @@ export function has(
this: Cheerio<Node | Element>,
selectorOrHaystack: string | Cheerio<Element> | Element
): Cheerio<Node | Element> {
return filter.call(
this,
return this.filter(
typeof selectorOrHaystack === 'string'
? // Using the `:has` selector here short-circuits searches.
`:has(${selectorOrHaystack})`
Expand Down Expand Up @@ -1036,7 +1086,7 @@ export function slice<T>(
function traverseParents<T extends Node>(
self: Cheerio<T>,
elem: Node | null,
selector: AcceptedFilters | undefined,
selector: AcceptedFilters<T> | undefined,
limit: number
): Node[] {
const elems: Node[] = [];
Expand Down
8 changes: 2 additions & 6 deletions src/types.ts
Expand Up @@ -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<T extends Node> = Cheerio<T> | T[] | T | string;
Expand All @@ -53,8 +53,4 @@ export type AcceptedElems<T extends Node> =
/** Function signature, for traversal methods. */
export type FilterFunction<T> = (this: T, i: number, el: T) => boolean;
/** Supported filter types, for traversal methods. */
export type AcceptedFilters =
| string
| FilterFunction<Element>
| Node
| Cheerio<Node>;
export type AcceptedFilters<T> = string | FilterFunction<T> | T | Cheerio<T>;

0 comments on commit fb8d31e

Please sign in to comment.