Navigation Menu

Skip to content

Commit

Permalink
simple implementation for :scope and :has selectors (without support …
Browse files Browse the repository at this point in the history
…for the descendant combinator in :has)
  • Loading branch information
kraih committed Jul 30, 2017
1 parent 0147516 commit 2e4f57b
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 27 deletions.
74 changes: 49 additions & 25 deletions lib/Mojo/DOM/CSS.pm
Expand Up @@ -22,16 +22,23 @@ sub matches {
return $tree->[0] ne 'tag' ? undef : _match(_compile(shift), $tree, $tree);
}

sub select { _select(0, shift->tree, _compile(@_)) }
sub select_one { _select(1, shift->tree, _compile(@_)) }
sub select {
my $tree = shift->tree;
return _select(0, $tree, $tree, _compile(@_));
}

sub select_one {
my $tree = shift->tree;
return _select(1, $tree, $tree, _compile(@_));
}

sub _ancestor {
my ($selectors, $current, $tree, $one, $pos) = @_;
my ($selectors, $current, $scope, $one, $pos) = @_;

while ($current = $current->[3]) {
return undef if $current->[0] eq 'root' || $current eq $tree;
return 1 if _combinator($selectors, $current, $tree, $pos);
last if $one;
return undef if $current->[0] eq 'root';
return 1 if _combinator($selectors, $current, $scope, $pos);
last if $one;
}

return undef;
Expand All @@ -51,38 +58,42 @@ sub _attr {
}

sub _combinator {
my ($selectors, $current, $tree, $pos) = @_;
my ($selectors, $current, $scope, $pos) = @_;

# Selector
return undef unless my $c = $selectors->[$pos];
if (ref $c) {
return undef unless _selector($c, $current);
return undef unless _selector($c, $current, $scope);
return 1 unless $c = $selectors->[++$pos];
}

# ">" (parent only)
return _ancestor($selectors, $current, $tree, 1, ++$pos) if $c eq '>';
return _ancestor($selectors, $current, $scope, 1, ++$pos) if $c eq '>';

# "~" (preceding siblings)
return _sibling($selectors, $current, $tree, 0, ++$pos) if $c eq '~';
return _sibling($selectors, $current, $scope, 0, ++$pos) if $c eq '~';

# "+" (immediately preceding siblings)
return _sibling($selectors, $current, $tree, 1, ++$pos) if $c eq '+';
return _sibling($selectors, $current, $scope, 1, ++$pos) if $c eq '+';

# " " (ancestor)
return _ancestor($selectors, $current, $tree, 0, ++$pos);
return _ancestor($selectors, $current, $scope, 0, ++$pos);
}

sub _compile {
my $css = trim "$_[0]";
$css = ":scope $css" if $css =~ /^\s*[>+~]/;

my $group = [[]];
while (my $selectors = $group->[-1]) {
push @$selectors, [] unless @$selectors && ref $selectors->[-1];
my $last = $selectors->[-1];

# Separator
if ($css =~ /\G\s*,\s*/gc) { push @$group, [] }
if ($css =~ /\G\s*,\s*/gc) {
push @$group, [];
$css = ":scope $css" if $css =~ /^\s*[>+~]/;

This comment has been minimized.

Copy link
@pipcet

pipcet Jul 31, 2017

Is this line correct? It seems to me to always insert ":scope" at the beginning of the string (and reset \G to the beginning of the string, as well). I think what would work is an extra

$css = substr $css, pos $css;

line. I confess I didn't think of that at first, so I gave up on making _compile do all the work.

}

# Combinator
elsif ($css =~ /\G\s*([ >+~])\s*/gc) { push @$selectors, $1 }
Expand All @@ -102,8 +113,9 @@ sub _compile {
elsif ($css =~ /\G:([\w\-]+)(?:\(((?:\([^)]+\)|[^)])+)\))?/gcs) {
my ($name, $args) = (lc $1, $2);

# ":matches" and ":not" (contains more selectors)
$args = _compile($args) if $name eq 'matches' || $name eq 'not';
# ":has", ":matches" and ":not" (contains more selectors)
$args = _compile($args)
if $name eq 'has' || $name eq 'matches' || $name eq 'not';

# ":nth-*" (with An+B notation)
$args = _equation($args) if $name =~ /^nth-/;
Expand Down Expand Up @@ -149,15 +161,25 @@ sub _equation {
}

sub _match {
my ($group, $current, $tree) = @_;
_combinator([reverse @$_], $current, $tree, 0) and return 1 for @$group;
my ($group, $current, $scope) = @_;
_combinator([reverse @$_], $current, $scope, 0) and return 1 for @$group;
return undef;
}

sub _name {qr/(?:^|:)\Q@{[_unescape(shift)]}\E$/}

sub _pc {
my ($class, $args, $current) = @_;
my ($class, $args, $current, $scope) = @_;

# ":scope"
return $current eq $scope if $class eq 'scope';

# ":has"
if ($class eq 'has') {
my $root = $scope;
$root = $root->[3] while $root->[0] ne 'root';
return !!_select(1, $root, $current, $args);
}

# ":checked"
return exists $current->[2]{checked} || exists $current->[2]{selected}
Expand Down Expand Up @@ -199,23 +221,23 @@ sub _pc {
}

sub _select {
my ($one, $tree, $group) = @_;
my ($one, $tree, $scope, $group) = @_;

my @results;
my @queue = @$tree[($tree->[0] eq 'root' ? 1 : 4) .. $#$tree];
while (my $current = shift @queue) {
next unless $current->[0] eq 'tag';

unshift @queue, @$current[4 .. $#$current];
next unless _match($group, $current, $tree);
next unless _match($group, $current, $scope);
$one ? return $current : push @results, $current;
}

return $one ? undef : \@results;
}

sub _selector {
my ($selector, $current) = @_;
my ($selector, $current, $scope) = @_;

for my $s (@$selector) {
my $type = $s->[0];
Expand All @@ -227,24 +249,26 @@ sub _selector {
elsif ($type eq 'attr') { return undef unless _attr(@$s[1, 2], $current) }

# Pseudo-class
elsif ($type eq 'pc') { return undef unless _pc(@$s[1, 2], $current) }
elsif ($type eq 'pc') {
return undef unless _pc(@$s[1, 2], $current, $scope);
}
}

return 1;
}

sub _sibling {
my ($selectors, $current, $tree, $immediate, $pos) = @_;
my ($selectors, $current, $scope, $immediate, $pos) = @_;

my $found;
for my $sibling (@{_siblings($current)}) {
return $found if $sibling eq $current;

# "+" (immediately preceding sibling)
if ($immediate) { $found = _combinator($selectors, $sibling, $tree, $pos) }
if ($immediate) { $found = _combinator($selectors, $sibling, $scope, $pos) }

# "~" (preceding sibling)
else { return 1 if _combinator($selectors, $sibling, $tree, $pos) }
else { return 1 if _combinator($selectors, $sibling, $scope, $pos) }
}

return undef;
Expand Down
29 changes: 27 additions & 2 deletions t/mojo/dom.t
Expand Up @@ -1214,6 +1214,31 @@ is_deeply \@e, ['J'], 'found only child';
$dom->find('div div:only-of-type')->each(sub { push @e, shift->text });
is_deeply \@e, [qw(J K)], 'found only child';

# Scoped selectors
$dom = Mojo::DOM->new(<<EOF);
<foo>
<foo>
<bar></bar>
</foo>
<bar></bar>
</foo>
EOF
is $dom->at('foo')->find('foo > bar')->size, 2, 'two elements found';
is $dom->at('foo')->find(':scope foo > bar')->size, 1, 'one element found';

# Relational pseudo-class
$dom = Mojo::DOM->new(<<EOF);
<foo>
<foo id=One>
<bar>A</bar>
</foo>
<foo id=Two></foo>
<baz>B</baz>
</foo>
EOF
ok $dom->find('foo:has(> bar)')->[0]->matches('#One'), 'element found';
ok $dom->find('foo:has(+ baz)')->first->matches('#Two'), 'element found';

# Sibling combinator
$dom = Mojo::DOM->new(<<EOF);
<ul>
Expand Down Expand Up @@ -2081,10 +2106,10 @@ $dom->find('b')->each(
$_->find('c a')->each(sub { push @results, $_->text });
}
);
is_deeply \@results, [qw(baz yada)], 'right results';
is_deeply \@results, [qw(bar baz yada)], 'right results';
is $dom->at('b')->at('a')->text, 'bar', 'right text';
is $dom->at('c > b > a')->text, 'bar', 'right text';
is $dom->at('b')->at('c > b > a'), undef, 'no result';
is $dom->at('b')->at('c > b > a')->text, 'bar', 'right text';

# Direct hash access to attributes in XML mode
$dom = Mojo::DOM->new->xml(1)->parse(<<EOF);
Expand Down

0 comments on commit 2e4f57b

Please sign in to comment.