Merge branch 'master' of git://git.shadowcat.co.uk/catagits/HTML-Zoom
Simon Elliott [Mon, 31 Jan 2011 16:34:44 +0000 (16:34 +0000)]
lib/HTML/Zoom.pm
lib/HTML/Zoom/FilterBuilder.pm
lib/HTML/Zoom/SelectorParser.pm
lib/HTML/Zoom/StreamBase.pm
t/apply.t [new file with mode: 0644]
t/flush.t [new file with mode: 0644]
t/selectors.t

index dba35d3..80ee8e9 100644 (file)
@@ -78,6 +78,17 @@ sub apply {
   $self->$code;
 }
 
+sub apply_if {
+  my ($self, $predicate, $code) = @_;
+  if($predicate) {
+    local $_ = $self;
+    $self->$code;
+  }
+  else {
+    $self;
+  }
+}
+
 sub to_html {
   my $self = shift;
   $self->zconfig->producer->html_from_stream($self->to_stream);
index 50398b3..a90a987 100644 (file)
@@ -381,7 +381,7 @@ alter the content of that stream.
 
 This class defines the following public API
 
-=head2 set_attribute ( $attr=>value | {name=>$attr,value=>$value} )
+=head2 set_attribute
 
 Sets an attribute of a given name to a given value for all matching selections.
 
@@ -396,7 +396,7 @@ Overrides existing values, if such exist.  When multiple L</set_attribute>
 calls are made against the same or overlapping selection sets, the final
 call wins.
 
-=head2 add_to_attribute ( $attr=>value | {name=>$attr,value=>$value} )
+=head2 add_to_attribute
 
 Adds a value to an existing attribute, or creates one if the attribute does not
 yet exist.
@@ -409,7 +409,7 @@ yet exist.
 
 Attributes with more than one value will have a dividing space.
 
-=head2 remove_attribute ( $attr | {name=>$attr} )
+=head2 remove_attribute
 
 Removes an attribute and all its values.
 
@@ -423,19 +423,131 @@ Removes attributes from the original stream or events already added.
 
 =head2 collect
 
-    TBD
+Collects and extracts results of L<HTML::Zoom/select>.  It takes the following
+optional common options as hash reference.
+
+=over
+
+=item into [ARRAY REFERENCE]
+
+Where to save collected events (selected elements).
+
+    $z1->select('#main-content')
+       ->collect({ into => \@body })
+       ->run;
+    $z2->select('#main-content')
+       ->replace(\@body)
+       ->memoize;
+
+=item filter [CODE]
+
+Run filter on collected elements (locally setting $_ to stream, and passing
+stream as an argument to given code reference).  Filtered stream would be
+returned.
+
+    $z->select('.outer')
+      ->collect({
+        filter => sub { $_->select('.inner')->replace_content('bar!') },
+        passthrough => 1,
+      })
+
+It can be used to further filter selection.  For example
+
+    $z->select('tr')
+      ->collect({
+        filter => sub { $_->select('td') },
+        passthrough => 1,
+      })
+
+is equivalent to (not implemented yet) descendant selector combination, i.e.
+
+    $z->select('tr td')
+
+=item passthrough [BOOLEAN]
+
+Extract copy of elements; the stream is unchanged (it does not remove collected
+elements).  For example without 'passthrough'
+
+    HTML::Zoom->from_html('<foo><bar /></foo>')
+      ->select('foo')
+      ->collect({ content => 1 })
+      ->to_html
+
+returns '<foo></foo>', while with C<passthrough> option
+
+    HTML::Zoom->from_html('<foo><bar /></foo>')
+      ->select('foo')
+      ->collect({ content => 1, passthough => 1 })
+      ->to_html
+
+returns '<foo><bar /></foo>'.
+
+=item content [BOOLEAN]
+
+Collect content of the element, and not the element itself.
+
+For example
+
+    HTML::Zoom->from_html('<h1>Title</h1><p>foo</p>')
+      ->select('h1')
+      ->collect
+      ->to_html
+
+would return '<p>foo</p>', while
+
+    HTML::Zoom->from_html('<h1>Title</h1><p>foo</p>')
+      ->select('h1')
+      ->collect({ content => 1 })
+      ->to_html
+
+would return '<h1></h1><p>foo</p>'.
+
+See also L</collect_content>.
+
+=item flush_before [BOOLEAN]
+
+Generate C<flush> event before collecting, to ensure that the HTML generated up
+to selected element being collected is flushed throught to the browser.  Usually
+used in L</repeat> or L</repeat_content>.
+
+=back
 
 =head2 collect_content
 
-    TBD
+Collects contents of L<HTML::Zoom/select> result.
+
+    HTML::Zoom->from_file($foo)
+              ->select('#main-content')
+              ->collect_content({ into => \@foo_body })
+              ->run;
+    $z->select('#foo')
+      ->replace_content(\@foo_body)
+      ->memoize;
+
+Equivalent to running L</collect> with C<content> option set.
 
 =head2 add_before
 
-    TBD
+Given a L<HTML::Zoom/select> result, add given content (which might be string,
+array or another L<HTML::Zoom> object) before it.
+
+    $html_zoom
+        ->select('input[name="foo"]')
+        ->add_before(\ '<span class="warning">required field</span>');
 
 =head2 add_after
 
-    TBD
+Like L</add_before>, only after L<HTML::Zoom/select> result.
+
+    $html_zoom
+        ->select('p')
+        ->add_after("\n\n");
+
+You can add zoom events directly
+
+    $html_zoom
+        ->select('p')
+        ->add_after([ { type => 'TEXT', raw => 'O HAI' } ]);
 
 =head2 prepend_content
 
@@ -447,20 +559,101 @@ Removes attributes from the original stream or events already added.
 
 =head2 replace
 
-    TBD
+Given a L<HTML::Zoom/select> result, replace it with a string, array or another
+L<HTML::Zoom> object.  It takes the same optional common options as L</collect>
+(via hash reference).
 
 =head2 replace_content
 
 Given a L<HTML::Zoom/select> result, replace the content with a string, array
 or another L<HTML::Zoom> object.
 
+    $html_zoom
+      ->select('title, #greeting')
+      ->replace_content('Hello world!');
+
 =head2 repeat
 
-    TBD
+    $zoom->select('.item')->repeat(sub {
+      if (my $row = $db_thing->next) {
+        return sub { $_->select('.item-name')->replace_content($row->name) }
+      } else {
+        return
+      }
+    }, { flush_before => 1 });
+
+Run I<$repeat_for>, which should be iterator (code reference) returning
+subroutines, reference to array of subroutines, or other zoom-able object
+consisting of transformations.  Those subroutines would be run with $_
+local-ized to result of L<HTML::Zoom/select> (of collected elements), and with
+said result passed as parameter to subroutine.
+
+You might want to use iterator when you don't have all elements upfront
+
+    $zoom = $zoom->select('.contents')->repeat(sub {
+      while (my $line = $fh->getline) {
+        return sub {
+          $_->select('.lno')->replace_content($fh->input_line_number)
+            ->select('.line')->replace_content($line)
+        }
+      }
+      return
+    });
+
+You might want to use array reference if it doesn't matter that all iterations
+are pre-generated
+
+    $zoom->select('table')->repeat([
+      map {
+        my $elem = $_;
+        sub {
+          $_->select('td')->replace_content($e);
+        }
+      } @list
+    ]);
+
+In addition to common options as in L</collect>, it also supports
+
+=over
+
+=item repeat_between [SELECTOR]
+
+Selects object to be repeated between items.  In the case of array this object
+is put between elements, in case of iterator it is put between results of
+subsequent iterations, in the case of streamable it is put between events
+(->to_stream->next).
+
+See documentation for L</repeat_content>
+
+=back
 
 =head2 repeat_content
 
-    TBD
+Given a L<HTML::Zoom/select> result, run provided iterator passing content of
+this result to this iterator.  Accepts the same options as L</repeat>.
+
+Equivalent to using C<contents> option with L</repeat>.
+
+    $html_zoom
+       ->select('#list')
+       ->repeat_content(
+          [
+             sub {
+                $_->select('.name')->replace_content('Matt')
+                  ->select('.age')->replace_content('26')
+             },
+             sub {
+                $_->select('.name')->replace_content('Mark')
+                  ->select('.age')->replace_content('0x29')
+             },
+             sub {
+                $_->select('.name')->replace_content('Epitaph')
+                  ->select('.age')->replace_content('<redacted>')
+             },
+          ],
+          { repeat_between => '.between' }
+       );
+
 
 =head1 ALSO SEE
 
index 9bc7bc3..48545ff 100644 (file)
@@ -81,6 +81,28 @@ sub _raw_parse_simple_selector {
         }
       };
 
+    # '[attr~=bar]' - match attribute contains word
+    /\G\[$sel_re~=$match_value_re\]/gc and
+      return do {
+        my $attribute = $1;
+        my $value = $2;
+        sub {
+          $_[0]->{attrs}{$attribute}
+          && $_[0]->{attrs}{$attribute} =~ qr/\b\Q$value\E\b/;
+        }
+      };
+
+    # '[attr!=bar]' - match attribute contains prefix (for language matches)
+    /\G\[$sel_re\|=$match_value_re\]/gc and
+      return do {
+        my $attribute = $1;
+        my $value = $2;
+        sub {
+          $_[0]->{attrs}{$attribute}
+          && $_[0]->{attrs}{$attribute} =~ qr/^\Q$value\E(?:-|$)/;
+        }
+      };
+
     # '[attr=bar]' - match attributes
     /\G\[$sel_re=$match_value_re\]/gc and
       return do {
@@ -92,7 +114,18 @@ sub _raw_parse_simple_selector {
         }
       };
 
-    # '[attr] - match attribute being present:
+    # '[attr!=bar]' - attributes doesn't match
+    /\G\[$sel_re!=$match_value_re\]/gc and
+      return do {
+        my $attribute = $1;
+        my $value = $2;
+        sub {
+          ! ($_[0]->{attrs}{$attribute}
+          && $_[0]->{attrs}{$attribute} eq $value);
+        }
+      };
+
+    # '[attr]' - match attribute being present:
     /\G\[$sel_re\]/gc and
       return do {
         my $attribute = $1;
index 6c0b416..e063c48 100644 (file)
@@ -83,6 +83,17 @@ sub apply {
   $self->$code;
 }
 
+sub apply_if {
+  my ($self, $predicate, $code) = @_;
+  if($predicate) {
+    local $_ = $self;
+    $self->$code;
+  }
+  else {
+    $self;
+  }
+}
+
 sub to_html {
   my ($self) = @_;
   $self->_zconfig->producer->html_from_stream($self);
diff --git a/t/apply.t b/t/apply.t
new file mode 100644 (file)
index 0000000..4071ff4
--- /dev/null
+++ b/t/apply.t
@@ -0,0 +1,31 @@
+use strict;
+use warnings FATAL => 'all';
+use Test::More 'no_plan';
+
+use HTML::Zoom;
+
+my $template = <<HTML;
+<html>
+  <body></body>
+</html>
+HTML
+
+my $expect = <<HTML;
+<html>
+  <body>Hello</body>
+</html>
+HTML
+
+my $output = HTML::Zoom
+  ->from_html($template)
+  ->apply_if(1, sub { $_->select('body')->replace_content('Hello') })
+  ->to_html;
+
+is( $output => $expect, 'apply_if with a true predicate' );
+
+$output = HTML::Zoom
+  ->from_html($template)
+  ->apply_if(0, sub { $_->select('body')->replace_content('Hello') })
+  ->to_html;
+
+is( $output => $template, 'apply_if with a false predicate' );
diff --git a/t/flush.t b/t/flush.t
new file mode 100644 (file)
index 0000000..bf60b64
--- /dev/null
+++ b/t/flush.t
@@ -0,0 +1,52 @@
+use strict;
+use warnings FATAL => 'all';
+
+use HTML::Zoom;
+use HTML::Zoom::CodeStream;
+
+use Test::More;
+
+
+# turns iterator into stream
+sub code_stream (&) {
+  my $code = shift;
+  return sub {
+    HTML::Zoom::CodeStream->new({
+      code => $code,
+    });
+  }
+}
+
+my $tmpl = <<'TMPL';
+<body>
+  <div class="item">
+    <div class="item-name"></div>
+  </div>
+</body>
+TMPL
+
+my $zoom = HTML::Zoom->from_html($tmpl);
+my @list = qw(foo bar baz);
+
+foreach my $flush (0..1) {
+
+  # from HTML::Zoom manpage, slightly modified
+  my $z2 = $zoom->select('.item')->repeat(code_stream {
+    if (my $name = shift @list) {
+      return sub { $_->select('.item-name')->replace_content($name) }
+    } else {
+      return
+    }
+  }, { flush_before => $flush });
+
+  my $fh = $z2->to_fh;
+  my $lineno = 0;
+  while (my $chunk = $fh->getline) {
+    $lineno++;
+    # debugging here
+  }
+
+  cmp_ok($lineno, '==', 1+$flush, "flush_before => $flush is $lineno chunks");
+}
+
+done_testing;
index 09875ad..8a800b1 100644 (file)
@@ -40,6 +40,14 @@ is( HTML::Zoom->from_html('<div frew="yo"></div>'.$stub)
    '<div frew="yo">grg</div>'.$stub,
    'E[attr] works' );
 
+# *[attr]
+is( HTML::Zoom->from_html('<div frew="yo"></div><span frew="ay"></span>'.$stub)
+   ->select('*[frew]')
+      ->replace_content('grg')
+   ->to_html,
+   '<div frew="yo">grg</div><span frew="ay">grg</span>'.$stub,
+   '*[attr] works' );
+
 # el[attr="foo"]
 is( HTML::Zoom->from_html('<div frew="yo"></div>'.$stub)
    ->select('div[frew="yo"]')
@@ -55,7 +63,14 @@ is( HTML::Zoom->from_html('<div frew="yo"></div>'.$stub)
     ->to_html,
     '<div frew="yo">grg</div>'.$stub,
     'E[attr=val] works' );
+
+# el[attr!="foo"]
+is( HTML::Zoom->from_html('<div f="f"></div><div class="quux"></div>'.$stub)
+    ->select('div[class!="waargh"]')
+       ->replace_content('grg')
+    ->to_html,
+    '<div f="f">grg</div><div class="quux">grg</div>'.$stub,
+    'E[attr!="val"] works' );
 
 # el[attr*="foo"]
 is( HTML::Zoom->from_html('<div f="frew goog"></div>'.$stub)
@@ -89,6 +104,24 @@ is( HTML::Zoom->from_html('<div f="foo bar"></div>'.$stub)
    '<div f="foo bar">grg</div>'.$stub,
    'E[attr*="val"] works' );
 
+# el[attr~="foo"]
+is( HTML::Zoom->from_html('<div frew="foo bar baz"></div>'.$stub)
+   ->select('div[frew~="bar"]')
+      ->replace_content('grg')
+   ->to_html,
+   '<div frew="foo bar baz">grg</div>'.$stub,
+   'E[attr~="val"] works' );
+
+# el[attr|="foo"]
+is( HTML::Zoom->from_html('<div lang="pl"></div><div lang="english"></div>'.
+                          '<div lang="en"></div><div lang="en-US"></div>'.$stub)
+   ->select('div[lang|="en"]')
+      ->replace_content('grg')
+   ->to_html,
+   '<div lang="pl"></div><div lang="english"></div>'.
+   '<div lang="en">grg</div><div lang="en-US">grg</div>'.$stub,
+   'E[attr|="val"] works' );
+
 # [attr=bar]
 ok( check_select( '[prop=moo]'), '[attr=bar]' );
 
@@ -107,33 +140,32 @@ eval{
 like( $@, qr/Error parsing dispatch specification/,
       'Malformed attribute selector ([att=bar) results in a helpful error' );
 
-=pod
 
+TODO: {
+local $TODO = "descendant selectors doesn't work yet";
 # sel1 sel2
-is( HTML::Zoom->from_html('<table><tr></tr><tr></tr></table>')
+is( eval { HTML::Zoom->from_html('<table><tr></tr><tr></tr></table>')
    ->select('table tr')
-      ->replace_content(\'<td></td>')
-   ->to_html,
+      ->replace_content('<td></td>')
+   ->to_html },
    '<table><tr><td></td></tr><tr><td></td></tr></table>',
    'sel1 sel2 works' );
-
+diag($@) if $@;
 
 # sel1 sel2 sel3
-is( HTML::Zoom->from_html('<table><tr><td></td></tr><tr><td></td></tr></table>')
+is( eval { HTML::Zoom->from_html('<table><tr><td></td></tr><tr><td></td></tr></table>')
    ->select('table tr td')
       ->replace_content('frew')
-   ->to_html,
+   ->to_html },
    '<table><tr><td>frew</td></tr><tr><td>frew</td></tr></table>',
    'sel1 sel2 sel3 works' );
-
-
-
-=cut
+diag($@) if $@;
+}
 
 done_testing;
 
 
-sub check_select{
+sub check_select {
     # less crude?:
     my $output = HTML::Zoom
     ->from_html($tmpl)