Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
add support for <form> elements to Mojo::DOM::val and support for :ma…
…tches pseudo-class to Mojo::DOM::CSS
  • Loading branch information
kraih committed Jun 5, 2017
1 parent 4a2966a commit 542af30
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 18 deletions.
5 changes: 4 additions & 1 deletion Changes
@@ -1,5 +1,8 @@

7.33 2017-06-03
7.33 2017-06-05
- Added EXPERIMENTAL support for :matches pseudo-class and :not pseudo-class
with compount selectors to Mojo::DOM::CSS.
- Improved val method in Mojo::DOM with support for <form> elements.
- Fixed version command to use the new MetaCPAN API, since the old one got
shut down.

Expand Down
58 changes: 52 additions & 6 deletions lib/Mojo/DOM.pm
Expand Up @@ -161,8 +161,11 @@ sub type { shift->tree->[0] }
sub val {
my $self = shift;

# "form"
return $self->_form(@_) if (my $tag = $self->tag) eq 'form';

# "option"
return $self->{value} // $self->text if (my $tag = $self->tag) eq 'option';
return $self->{value} // $self->text if $tag eq 'option';

# "input" ("type=checkbox" and "type=radio")
my $type = $self->{type} // '';
Expand All @@ -173,7 +176,8 @@ sub val {
return $tag eq 'textarea' ? $self->text : $self->{value} if $tag ne 'select';

# "select"
my $v = $self->find('option:checked')->map('val');
my $v = $self->find('option:checked:not([disabled])')
->grep(sub { !$_->ancestors('optgroup[disabled]')->size })->map('val');
return exists $self->{multiple} ? $v->size ? $v->to_array : undef : $v->last;
}

Expand Down Expand Up @@ -241,6 +245,40 @@ sub _delegate {
return $self;
}

sub _form {
my ($self, $selector) = @_;
$selector //= 'button:not([disabled]), input:matches([type=button],'
. ' [type=submit], [type=image]):not([disabled])';

# The submit button
my $form = {};
if (my $e = $self->at($selector)) { _input($form, $e->{name}, $e->val) }

# "select" and "textarea"
_input($form, $_->{name}, $_->val)
for $self->find('select:not([disabled]), textarea:not([disabled])')->each;

# "input"
my $input = $self->find(
'input:not([type=button], [type=image], [type=submit], [disabled])');
for my $e ($input->each) {
my $type = $e->{type} // '';
_input($form, $e->{name}, $e->val)
if ($type ne 'radio' && $type ne 'checkbox') || exists $e->{checked};
}

return $form;
}

sub _input {
my ($form, $name, $value) = @_;
$form->{$name}
= exists $form->{$name}
? [ref $form->{$name} ? @{$form->{$name}} : $form->{$name}, $value]
: $value
if defined $name && defined $value;
}

sub _link {
my ($parent, @children) = @_;

Expand Down Expand Up @@ -919,12 +957,16 @@ C<root>, C<tag> or C<text>.
=head2 val
my $value = $dom->val;
my $value = $dom->val('input[type=submit]');
Extract value from form element (such as C<button>, C<input>, C<option>,
C<select> and C<textarea>), or return C<undef> if this element has no value. In
the case of C<select> with C<multiple> attribute, find C<option> elements with
Extract values from C<button>, C<form>, C<input>, C<option>, C<select> and
C<textarea> elements, or return C<undef> if this element has no value. In the
case of C<select> with C<multiple> attribute, find C<option> elements with
C<selected> attribute and return an array reference with all values, or C<undef>
if none could be found.
if none could be found. In the case of C<form>, find all elements mentioned
before that would be submitted by pressing the first button or the button
matching the CSS selector, and return a hash reference with their names and
values. All selectors from L<Mojo::DOM::CSS/"SELECTORS"> are supported.
# "a"
$dom->parse('<input name=test value=a>')->at('input')->val;
Expand All @@ -943,6 +985,10 @@ if none could be found.
$dom->parse('<select multiple><option selected>e</option></select>')
->at('select')->val->[0];
# "b"
$dom->parse('<form action="/"><input name="a" value="f"></form>')
->at('form')->val->{a};
# "on"
$dom->parse('<input name=test type=checkbox>')->at('input')->val;
Expand Down
30 changes: 25 additions & 5 deletions lib/Mojo/DOM/CSS.pm
Expand Up @@ -102,8 +102,8 @@ sub _compile {
elsif ($css =~ /\G:([\w\-]+)(?:\(((?:\([^)]+\)|[^)])+)\))?/gcs) {
my ($name, $args) = (lc $1, $2);

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

# ":nth-*" (with An+B notation)
$args = _equation($args) if $name =~ /^nth-/;
Expand Down Expand Up @@ -166,6 +166,9 @@ sub _pc {
# ":not"
return !_match($args, $current, $current) if $class eq 'not';

# ":matches"
return !!_match($args, $current, $current) if $class eq 'matches';

# ":empty"
return !grep { !_empty($_) } @$current[4 .. $#$current] if $class eq 'empty';

Expand Down Expand Up @@ -493,11 +496,28 @@ An C<E> element with C<ID> equal to "myid".
my $foo = $css->select('div#foo');
=head2 E:not(s)
=head2 E:not(s1, s2)
An C<E> element that does not match either compound selector C<s1> or compound
selector C<s2>. Note that support for compound selectors is EXPERIMENTAL and
might change without warning!
my $others = $css->select('div p:not(:first-child, :last-child)');
Support for compound selectors was added as part of
L<Selectors Level 4|http://dev.w3.org/csswg/selectors-4>, which is still a work
in progress.
An C<E> element that does not match simple selector C<s>.
=head2 E:matches(s1, s2)
my $others = $css->select('div p:not(:first-child)');
An C<E> element that matches compound selector C<s1> and/or compound selector
C<s2>. Note that this selector is EXPERIMENTAL and might change without warning!
my $headers = $css->select(':matches(section, article, aside, nav) h1');
This selector is part of
L<Selectors Level 4|http://dev.w3.org/csswg/selectors-4>, which is still a work
in progress.
=head2 E F
Expand Down
70 changes: 64 additions & 6 deletions t/mojo/dom.t
Expand Up @@ -1112,6 +1112,14 @@ is_deeply \@e, [qw(A C E G I)], 'found all odd elements';
$dom->find('li:nth-of-type(odd)')->each(sub { push @e, shift->text });
is_deeply \@e, [qw(A E H)], 'found all odd li elements';
@e = ();
$dom->find('ul li:not(:first-child, :last-child)')
->each(sub { push @e, shift->text });
is_deeply \@e, [qw(C E F H)], 'found all odd li elements';
@e = ();
$dom->find('ul li:matches(:first-child, :last-child)')
->each(sub { push @e, shift->text });
is_deeply \@e, [qw(A I)], 'found all odd li elements';
@e = ();
$dom->find('li:nth-last-of-type( odd )')->each(sub { push @e, shift->text });
is_deeply \@e, [qw(C F I)], 'found all odd li elements';
@e = ();
Expand Down Expand Up @@ -2209,8 +2217,10 @@ $dom = Mojo::DOM->new(<<EOF);
<form action="/foo">
<p>Test</p>
<input type="text" name="a" value="A" />
<input type=text name="a" value="A2">
<input type="checkbox" name="q">
<input type="checkbox" checked name="b" value="B">
<input type="checkbox" checked name="b">
<input type="radio" name="r">
<input type="radio" checked name="c" value="C">
<input name="s">
Expand All @@ -2221,19 +2231,40 @@ $dom = Mojo::DOM->new(<<EOF);
<optgroup>
<option>H</option>
<option selected>I</option>
<option selected disabled>V</option>
</optgroup>
<option value="J" selected>K</option>
<optgroup disabled>
<option selected>I2</option>
</optgroup>
</select>
<select name="n"><option>N</option></select>
<select multiple name="q"><option>Q</option></select>
<select name="y" disabled>
<option selected>Y</option>
</select>
<select name="d">
<option selected>R</option>
<option selected>D</option>
</select>
<textarea name="m">M</textarea>
<textarea name="m">M3</textarea>
<textarea name="m2" disabled>M2</textarea>
<button name="o" value="O">No!</button>
<input type=text name="x" value="X" disabled>
<input type="submit" name="p" value="P" />
</form>
<form><input type="submit" name="a" value="A"></form>
<form>
<input type="button" name="b" value="B" disabled>
<input type="button" name="c" value="C">
</form>
<form><input type="image" name="c" value="C"></form>
<form>
<button name="e" value="E" disabled>
<button name="d" value="D">
</form>
<form></form>
EOF
is $dom->at('p')->val, undef, 'no value';
is $dom->at('input')->val, 'A', 'right value';
Expand All @@ -2243,19 +2274,46 @@ is_deeply $dom->at('select')->val, ['I', 'J'], 'right values';
is $dom->at('select option')->val, 'F', 'right value';
is $dom->at('select optgroup option:not([selected])')->val, 'H', 'right value';
is $dom->find('select')->[1]->at('option')->val, 'N', 'right value';
is $dom->find('select')->[1]->val, undef, 'no value';
is_deeply $dom->find('select')->[2]->val, undef, 'no value';
is $dom->find('select')->[1]->val, undef, 'no value';
is $dom->find('select')->[2]->val, undef, 'no value';
is $dom->find('select')->[2]->at('option')->val, 'Q', 'right value';
is_deeply $dom->find('select')->last->val, 'D', 'right value';
is_deeply $dom->find('select')->last->at('option')->val, 'R', 'right value';
is $dom->at('select[disabled]')->val, 'Y', 'right value';
is $dom->find('select')->last->val, 'D', 'right value';
is $dom->find('select')->last->at('option')->val, 'R', 'right value';
is $dom->at('textarea')->val, 'M', 'right value';
is $dom->at('button')->val, 'O', 'right value';
is $dom->find('form input')->last->val, 'P', 'right value';
is $dom->find('textarea')->last->val, 'M2', 'right value';
is $dom->at('button')->val, 'O', 'right value';
is $dom->at('form')->find('input')->last->val, 'P', 'right value';
is $dom->at('input[name=q]')->val, 'on', 'right value';
is $dom->at('input[name=r]')->val, 'on', 'right value';
is $dom->at('input[name=s]')->val, undef, 'no value';
is $dom->at('input[name=t]')->val, '', 'right value';
is $dom->at('input[name=u]')->val, undef, 'no value';
my $form = {
a => ['A', 'A2'],
b => ['B', 'on'],
c => 'C',
d => 'D',
f => ['I', 'J'],
m => ['M', 'M3'],
o => 'O'
};
is_deeply $dom->at('form')->val, $form, 'right structure';
$form = {
a => ['A', 'A2'],
b => ['B', 'on'],
c => 'C',
d => 'D',
f => ['I', 'J'],
m => ['M', 'M3'],
p => 'P'
};
is_deeply $dom->at('form')->val('input[type=submit]'), $form, 'right structure';
is_deeply $dom->find('form')->[1]->val, {a => 'A'}, 'right structure';
is_deeply $dom->find('form')->[2]->val, {c => 'C'}, 'right structure';
is_deeply $dom->find('form')->[3]->val, {c => 'C'}, 'right structure';
is_deeply $dom->find('form')->[4]->val, {d => 'D'}, 'right structure';
is_deeply $dom->find('form')->[5]->val, {}, 'right structure';

# PoCo example with whitespace-sensitive text
$dom = Mojo::DOM->new(<<EOF);
Expand Down

0 comments on commit 542af30

Please sign in to comment.