Skip to content
Open
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
4 changes: 3 additions & 1 deletion src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,13 @@ function absolutize<Node, ElementNode extends Node>(
* @param token Selector token(s) to compile.
* @param options Options that control this operation.
* @param compilationContext Compilation context for relative selector handling.
* @param shouldAbsolutize Override relative handling for this call only.
*/
export function compileToken<Node, ElementNode extends Node>(
token: InternalSelector[][],
options: InternalOptions<Node, ElementNode>,
compilationContext?: Node[] | Node,
shouldAbsolutize: boolean | undefined = options.relativeSelector,
): CompiledQuery<ElementNode> {
for (const rules of token) {
sortRules(rules);
Expand All @@ -86,7 +88,7 @@ export function compileToken<Node, ElementNode extends Node>(
context && (Array.isArray(context) ? context : [context]);

// Check if the selector is relative
if (options.relativeSelector !== false) {
if (shouldAbsolutize !== false) {
absolutize(token, options, finalContext);
} else if (token.some((t) => t.length > 0 && isTraversal(t[0]))) {
throw new Error(
Expand Down
7 changes: 5 additions & 2 deletions src/pseudo-selectors/subselects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,18 @@ export const subselects: Record<string, Subselect> = {
const { adapter } = options;

const copiedOptions = copyOptions(options);
copiedOptions.relativeSelector = true;

const context = subselect.some((s) => s.some(isTraversal))
? // Used as a placeholder. Will be replaced with the actual element.
[PLACEHOLDER_ELEMENT as unknown as ElementNode]
: undefined;
const shouldSkipCache = isContextDependent(subselect);

const compiled = compileToken(subselect, copiedOptions, context);
/*
* Only the :has argument is relative. Nested selectors retain the
* caller's relativeSelector option.
*/
const compiled = compileToken(subselect, copiedOptions, context, true);

if (compiled === boolbase.falseFunc) {
return boolbase.falseFunc;
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,5 @@ export type CompileToken<Node, ElementNode extends Node> = (
token: InternalSelector[][],
options: InternalOptions<Node, ElementNode>,
context?: Node[] | Node,
shouldAbsolutize?: boolean,
) => CompiledQuery<ElementNode>;
110 changes: 110 additions & 0 deletions test/pseudo-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,116 @@ describe(":empty", () => {
});

describe(":has", () => {
const preceding = '<div class="p" id="target"></div>';
const nestedSelectors = [
{
selector: ".p:has(+ .a:not(.b))",
html: `${preceding}<div class="a b"></div>`,
expected: false,
relativeExpected: true,
},
{
selector: ".p:has(+ :is(.a))",
html: `${preceding}<div class="a"></div>`,
expected: true,
relativeExpected: false,
},
{
selector: ".p:has(~ :where(.a, .b))",
html: `${preceding}<i></i><div class="b"></div>`,
expected: true,
relativeExpected: false,
},
{
selector: ".p:has(+ input:checked)",
html: `${preceding}<input type="checkbox" checked>`,
expected: true,
relativeExpected: false,
},
{
selector: ".p:has(+ :nth-child(2 of .a))",
html: `<i class="a"></i>${preceding}<i class="a"></i>`,
expected: true,
relativeExpected: false,
},
{
selector: ".p:has(> .a:is(.x .a))",
html: '<section class="x"><div class="p" id="target"><b class="a"></b></div></section>',
expected: true,
relativeExpected: false,
},
{
selector: ".p:has(+ :matches(.a))",
html: `${preceding}<div class="a"></div>`,
expected: true,
relativeExpected: false,
},
{
selector: ".p:has(+ :is(:where(.a)))",
html: `${preceding}<div class="a"></div>`,
expected: true,
relativeExpected: false,
},
{
selector: ".p:has(> .a:not(.b))",
html: '<div class="p" id="target"><b class="a"></b></div>',
expected: true,
relativeExpected: true,
},
{
selector: ".p:has(> .a:is(.b))",
html: '<div class="p" id="target"><b class="a"></b></div>',
expected: false,
relativeExpected: false,
},
];

it.each(nestedSelectors)(
"should honor relativeSelector: false in $selector",
({ selector, html, expected }) => {
const document = parseDocument(html);
const [target] = CSSselect.selectAll<AnyNode, Element>(
"#target",
document,
);

for (const isCacheResults of [true, false]) {
const options = {
relativeSelector: false,
cacheResults: isCacheResults,
};
expect(CSSselect.compile(selector, options)(target)).toBe(
expected,
);
expect(CSSselect.is(target, selector, options)).toBe(expected);
expect(
CSSselect.selectAll<AnyNode, Element>(
selector,
document,
options,
).includes(target),
).toBe(expected);
}
},
);

it.each(nestedSelectors)(
"should retain enabled and default relative behavior in $selector",
({ selector, html, relativeExpected }) => {
const document = parseDocument(html);
const [target] = CSSselect.selectAll<AnyNode, Element>(
"#target",
document,
);

for (const options of [undefined, { relativeSelector: true }]) {
expect(CSSselect.compile(selector, options)(target)).toBe(
relativeExpected,
);
}
},
);

it("should cache :has if applicable", () => {
const dom = parseDocument(`
<div>
Expand Down