Skip to content

Commit

Permalink
feat: Add a limit argument to select (#307)
Browse files Browse the repository at this point in the history
  • Loading branch information
fb55 authored May 16, 2022
1 parent 9e949dd commit b7ffeeb
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 12 deletions.
13 changes: 13 additions & 0 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,17 @@ describe("index", () => {
select("ul:first li:lt(3)", dom, { context: [dom] })
).toHaveLength(2);
});

it("should support limit argument", () => {
const dom = parseDocument("<p>Paragraph".repeat(10));
expect(select("p:even", dom, {}, 1)).toHaveLength(1);
expect(select("p:even", dom, {}, 5)).toHaveLength(5);
expect(select("p:odd", dom, {}, 1)).toHaveLength(1);
expect(select("p:odd", dom, {}, 5)).toHaveLength(5);
expect(select("p:lt(5)", dom, {}, 2)).toHaveLength(2);
expect(select("p:lt(5)", dom, {}, 6)).toHaveLength(5);

// Should not use the limit for positionals before the end of the selector
expect(select("p:odd + p:eq(5)", dom, {}, 1)).toHaveLength(1);
});
});
35 changes: 26 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,16 +188,23 @@ function filterBySelector(
const root = options.root ?? getDocumentRoot(elements[0]);
const opts = { ...options, context: elements, relativeSelector: false };
selector.push(SCOPE_PSEUDO);
return findFilterElements(root, selector, opts, true);
return findFilterElements(root, selector, opts, true, elements.length);
}
// Performance optimization: If we don't have to traverse, just filter set.
return findFilterElements(elements, selector, options, false);
return findFilterElements(
elements,
selector,
options,
false,
elements.length
);
}

export function select(
selector: string | ((el: Element) => boolean),
root: AnyNode | AnyNode[],
options: Options = {}
options: Options = {},
limit = Infinity
): Element[] {
if (typeof selector === "function") {
return find(root, selector);
Expand All @@ -206,12 +213,12 @@ export function select(
const [plain, filtered] = groupSelectors(parse(selector));

const results: Element[][] = filtered.map((sel) =>
findFilterElements(root, sel, options, true)
findFilterElements(root, sel, options, true, limit)
);

// Plain selectors can be queried in a single go
if (plain.length) {
results.push(findElements(root, plain, options, Infinity));
results.push(findElements(root, plain, options, limit));
}

if (results.length === 0) {
Expand All @@ -238,17 +245,21 @@ function findFilterElements(
root: AnyNode | AnyNode[],
selector: Selector[],
options: Options,
queryForSelector: boolean
queryForSelector: boolean,
totalLimit: number
): Element[] {
const filterIndex = selector.findIndex(isFilter);
const sub = selector.slice(0, filterIndex);
const filter = selector[filterIndex] as CheerioSelector;
// If we are at the end of the selector, we can limit the number of elements to retrieve.
const partLimit =
selector.length - 1 === filterIndex ? totalLimit : Infinity;

/*
* Set the number of elements to retrieve.
* Eg. for :first, we only have to get a single element.
*/
const limit = getLimit(filter.name, filter.data);
const limit = getLimit(filter.name, filter.data, partLimit);

if (limit === 0) return [];

Expand Down Expand Up @@ -314,10 +325,16 @@ function findFilterElements(
* Otherwise,
*/
return remainingSelector.some(isFilter)
? findFilterElements(result, remainingSelector, options, false)
? findFilterElements(
result,
remainingSelector,
options,
false,
totalLimit
)
: remainingHasTraversal
? // Query existing elements to resolve traversal.
findElements(result, [remainingSelector], options, Infinity)
findElements(result, [remainingSelector], options, totalLimit)
: // If we don't have any more traversals, simply filter elements.
filterElements(result, [remainingSelector], options);
}
Expand Down
19 changes: 16 additions & 3 deletions src/positionals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ export function isFilter(s: Selector): s is CheerioSelector {
return false;
}

export function getLimit(filter: Filter, data: string | null): number {
export function getLimit(
filter: Filter,
data: string | null,
partLimit: number
): number {
const num = data != null ? parseInt(data, 10) : NaN;

switch (filter) {
Expand All @@ -47,10 +51,19 @@ export function getLimit(filter: Filter, data: string | null): number {
case "eq":
return isFinite(num) ? (num >= 0 ? num + 1 : Infinity) : 0;
case "lt":
return isFinite(num) ? (num >= 0 ? num : Infinity) : 0;
return isFinite(num)
? num >= 0
? Math.min(num, partLimit)
: Infinity
: 0;
case "gt":
return isFinite(num) ? Infinity : 0;
default:
case "odd":
return 2 * partLimit;
case "even":
return 2 * partLimit - 1;
case "last":
case "not":
return Infinity;
}
}

0 comments on commit b7ffeeb

Please sign in to comment.