Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(traversing): Make filter work on all collections #1870

Merged
merged 2 commits into from
May 17, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 3 additions & 7 deletions src/api/attributes.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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>;