Skip to content

:has selector#236

Merged
g105b merged 4 commits intomasterfrom
235-has
Mar 16, 2026
Merged

:has selector#236
g105b merged 4 commits intomasterfrom
235-has

Conversation

@g105b
Copy link
Member

@g105b g105b commented Mar 11, 2026

Closes #235

This is a WIP while I'm working on the functionality.

The branch has a selection of tests for the upcoming :has selector.

@g105b
Copy link
Member Author

g105b commented Mar 11, 2026

I've implemented a working model for the :has selector now. There's a lot of refactoring required to get the code quality tests passing, but for now the unit tests I added the other day are passing.

Example code:

<!doctype html>
<main>
	<h2 id="pass">Pass</h2>
	<div>intermediate</div>
	<p class="warning">warning</p>
	<h2 id="fail">Fail</h2>
	<div>no warning sibling</div>
</main>
$translator = new Translator("h2:has(~ p.warning)");
$elements = $xpath->query($translator);

self::assertEquals(1, $elements->length);
self::assertEquals("pass", $elements->item(0)->getAttribute("id"));

@g105b
Copy link
Member Author

g105b commented Mar 11, 2026

The current regex tokenisation model cannot represent functional arguments reliably, as far as I can tell. I'm reminding myself how the :not builder path works so I can replace just the brittle parsing piece without disturbing the rest of the refactored architecture.

The plan so far:

  1. Replace regex-based selector tokenisation with a small scanner so functional pseudo arguments, attriobutes, whitespace combinators, and nested parentheses are parsed properly.
  2. Add a dedicated HasSelectorConditionBuilder that converts relative selectors to XPath, with explicit deferred-case checks for nested :has, pseudo-elements, and :nth-child(of S).
  3. Wire :has into PseudoSelectorConverter, keeping :not on the same token stream, then run the focussed :has tests and the full PHPUnit suite to catch any regressions.

It turns out that the latest release, refactoring this codebase into a set of readable classes, was a tremendous idea and perfectly timed to work on this!

Implementation notes:

I caught a recursive loop while wiring up the new builder. I need to switch the has builder to use lazy initialisation so nested selector conversion can reuse the same pipeline without recursion.


Due to the introduction of the new HasSelectorConditionBuilder I think I've implemented this functionality without breaking existing functionality! This wouldn't have been possible before the refactor, so effort was well spent.

@g105b
Copy link
Member Author

g105b commented Mar 11, 2026

Take a look at HasSelectorConditionBuilder and PseudoSelectorConverter.

I will tidy the implementation up in my next coding session on this. It's getting late now.

@reaganch, @Inverle and others, your feedback is appreciated, if you have any comment.

@g105b g105b mentioned this pull request Mar 12, 2026
@g105b g105b marked this pull request as ready for review March 14, 2026 13:22
@g105b
Copy link
Member Author

g105b commented Mar 14, 2026

Phew - all tests are green. This PR is good to go in my opinion, but I would really appreciate any feedback on the code, as this is the first feature implemented since refactoring the library into a more object oriented approach.

@g105b g105b merged commit 5777817 into master Mar 16, 2026
53 checks passed
@g105b g105b deleted the 235-has branch March 16, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

:has() selector support

1 participant