5 use HTML::Zoom::ZConfig;
6 use HTML::Zoom::ReadFH;
7 use HTML::Zoom::Transform;
8 use HTML::Zoom::TransformBuilder;
11 our $VERSION = '0.009006';
13 $VERSION = eval $VERSION;
16 my ($class, $args) = @_;
18 $new->{zconfig} = HTML::Zoom::ZConfig->new($args->{zconfig}||{});
22 sub zconfig { shift->_self_or_new->{zconfig} }
25 ref($_[0]) ? $_[0] : $_[0]->new
29 bless({ %{$_[0]}, %{$_[1]} }, ref($_[0]));
33 my $self = shift->_self_or_new;
35 initial_events => shift,
40 my $self = shift->_self_or_new;
41 $self->from_events($self->zconfig->parser->html_to_events($_[0]))
45 my $self = shift->_self_or_new;
47 $self->from_html(do { local (@ARGV, $/) = ($filename); <> });
52 die "No events to build from - forgot to call from_html?"
53 unless $self->{initial_events};
54 my $sutils = $self->zconfig->stream_utils;
55 my $stream = $sutils->stream_from_array(@{$self->{initial_events}});
56 $stream = $_->apply_to_stream($stream) for @{$self->{transforms}||[]};
61 HTML::Zoom::ReadFH->from_zoom(shift);
66 [ $self->zconfig->stream_utils->stream_to_array($self->to_stream) ];
76 my ($self, $code) = @_;
82 my ($self, $predicate, $code) = @_;
94 $self->zconfig->producer->html_from_stream($self->to_stream);
99 ref($self)->new($self)->from_html($self->to_html);
103 my $self = shift->_self_or_new;
104 my ($transform) = @_;
107 @{$self->{transforms}||[]},
114 my $self = shift->_self_or_new;
115 my ($selector, $filter) = @_;
116 $self->with_transform(
117 HTML::Zoom::Transform->new({
118 zconfig => $self->zconfig,
119 selector => $selector,
120 filters => [ $filter ]
126 my $self = shift->_self_or_new;
128 return HTML::Zoom::TransformBuilder->new({
129 zconfig => $self->zconfig,
130 selector => $selector,
135 # There's a bug waiting to happen here: if you do something like
137 # $zoom->select('.foo')
138 # ->remove_attribute(class => 'foo')
140 # ->well_anything_really
142 # the second action won't execute because it doesn't match anymore.
143 # Ideally instead we'd merge the match subs but that's more complex to
144 # implement so I'm deferring it for the moment.
148 die "Can't call ->then without a previous transform"
149 unless $self->{transforms};
150 $self->select($self->{transforms}->[-1]->selector);
154 my ($self, $selector, @args) = @_;
155 my $sel = $self->select($selector);
156 my $meth = our $AUTOLOAD;
158 if(my $cr = $sel->_zconfig->filter_builder->can($meth)) {
159 return $sel->$meth(@args);
161 die "We can't do $meth on ->select('$selector')";
171 HTML::Zoom - selector based streaming template engine
177 my $template = <<HTML;
180 <title>Hello people</title>
183 <h1 id="greeting">Placeholder</h1>
186 <p>Name: <span class="name">Bob</span></p>
187 <p>Age: <span class="age">23</span></p>
189 <hr class="between" />
195 my $output = HTML::Zoom
196 ->from_html($template)
197 ->select('title, #greeting')->replace_content('Hello world & dog!')
198 ->select('#list')->repeat_content(
201 $_->select('.name')->replace_content('Matt')
202 ->select('.age')->replace_content('26')
205 $_->select('.name')->replace_content('Mark')
206 ->select('.age')->replace_content('0x29')
209 $_->select('.name')->replace_content('Epitaph')
210 ->select('.age')->replace_content('<redacted>')
213 { repeat_between => '.between' }
227 <title>Hello world & dog!</title>
230 <h1 id="greeting">Hello world & dog!</h1>
233 <p>Name: <span class="name">Matt</span></p>
234 <p>Age: <span class="age">26</span></p>
236 <hr class="between" />
238 <p>Name: <span class="name">Mark</span></p>
239 <p>Age: <span class="age">0x29</span></p>
241 <hr class="between" />
243 <p>Name: <span class="name">Epitaph</span></p>
244 <p>Age: <span class="age"><redacted></span></p>
254 is($output, $expect, 'Synopsis code works ok');
258 =head1 DANGER WILL ROBINSON
260 This is a 0.9 release. That means that I'm fairly happy the API isn't going
261 to change in surprising and upsetting ways before 1.0 and a real compatibility
262 freeze. But it also means that if it turns out there's a mistake the size of
263 a politician's ego in the API design that I haven't spotted yet there may be
264 a bit of breakage between here and 1.0. Hopefully not though. Appendages
265 crossed and all that.
267 Worse still, the rest of the distribution isn't documented yet. I'm sorry.
268 I suck. But lots of people have been asking me to ship this, docs or no, so
269 having got this class itself at least somewhat documented I figured now was
270 a good time to cut a first real release.
274 HTML::Zoom is a lazy, stream oriented, streaming capable, mostly functional,
275 CSS selector based semantic templating engine for HTML and HTML-like
278 Which is, on the whole, a bit of a mouthful. So let me step back a moment
279 and explain why you care enough to understand what I mean:
283 HTML::Zoom is the cure for JQuery envy. When your javascript guy pushes a
284 piece of data into a document by doing:
286 $('.username').replaceAll(username);
288 In HTML::Zoom one can write
290 $zoom->select('.username')->replace_content($username);
292 which is, I hope, almost as clear, hampered only by the fact that Zoom can't
293 assume a global document and therefore has nothing quite so simple as the
294 $() function to get the initial selection.
296 L<HTML::Zoom::SelectorParser> implements a subset of the JQuery selector
297 specification, and will continue to track that rather than the W3C standards
298 for the forseeable future on grounds of pragmatism. Also on grounds of their
299 spec is written in EN_US rather than EN_W3C, and I read the former much better.
301 I am happy to admit that it's very, very much a subset at the moment - see the
302 L<HTML::Zoom::SelectorParser> POD for what's currently there, and expect more
303 and more to be supported over time as we need it and patch it in.
305 =head2 CLEAN TEMPLATES
307 HTML::Zoom is the cure for messy templates. How many times have you looked at
310 <form action="/somewhere">
311 [% FOREACH field IN fields %]
312 <label for="[% field.id %]">[% field.label %]</label>
313 <input name="[% field.name %]" type="[% field.type %]" value="[% field.value %]" />
317 and despaired of the fact that neither the HTML structure nor the logic are
318 remotely easy to read? Fortunately, with HTML::Zoom we can separate the two
321 <form class="myform" action="/somewhere">
326 $zoom->select('.myform')->repeat_content([
327 map { my $field = $_; sub {
330 ->add_to_attribute( for => $field->{id} )
332 ->replace_content( $field->{label} )
335 ->add_to_attribute( name => $field->{name} )
337 ->add_to_attribute( type => $field->{type} )
339 ->add_to_attribute( value => $field->{value} )
344 This is, admittedly, very much not shorter. However, it makes it extremely
345 clear what's happening and therefore less hassle to maintain. Especially
346 because it allows the designer to fiddle with the HTML without cutting
347 himself on sharp ELSE clauses, and the developer to add available data to
348 the template without getting angle bracket cuts on sensitive parts.
350 Better still, HTML::Zoom knows that it's inserting content into HTML and
351 can escape it for you - the example template should really have been:
353 <form action="/somewhere">
354 [% FOREACH field IN fields %]
355 <label for="[% field.id | html %]">[% field.label | html %]</label>
356 <input name="[% field.name | html %]" type="[% field.type | html %]" value="[% field.value | html %]" />
360 and frankly I'll take slightly more code any day over *that* crawling horror.
362 (addendum: I pick on L<Template Toolkit|Template> here specifically because
363 it's the template system I hate the least - for text templating, I don't
364 honestly think I'll ever like anything except the next version of Template
365 Toolkit better - but HTML isn't text. Zoom knows that. Do you?)
367 =head2 PUTTING THE FUN INTO FUNCTIONAL
369 The principle of HTML::Zoom is to provide a reusable, functional container
370 object that lets you build up a set of transforms to be applied; every method
371 call you make on a zoom object returns a new object, so it's safe to do so
372 on one somebody else gave you without worrying about altering state (with
373 the notable exception of ->next for stream objects, which I'll come to later).
377 my $z2 = $z1->select('.name')->replace_content($name);
379 my $z3 = $z2->select('.title')->replace_content('Ms.');
381 each time produces a new Zoom object. If you want to package up a set of
382 transforms to re-use, HTML::Zoom provides an 'apply' method:
384 my $add_name = sub { $_->select('.name')->replace_content($name) };
386 my $same_as_z2 = $z1->apply($add_name);
388 =head2 LAZINESS IS A VIRTUE
390 HTML::Zoom does its best to defer doing anything until it's absolutely
391 required. The only point at which it descends into state is when you force
392 it to create a stream, directly by:
394 my $stream = $zoom->to_stream;
396 while (my $evt = $stream->next) {
397 # handle zoom event here
402 my $final_html = $zoom->to_html;
404 my $fh = $zoom->to_fh;
406 while (my $chunk = $fh->getline) {
410 Better still, the $fh returned doesn't create its stream until the first
411 call to getline, which means that until you call that and force it to be
412 stateful you can get back to the original stateless Zoom object via:
414 my $zoom = $fh->to_zoom;
416 which is exceedingly handy for filtering L<Plack> PSGI responses, among other
419 Because HTML::Zoom doesn't try and evaluate everything up front, you can
420 generally put things together in whatever order is most appropriate. This
423 my $start = HTML::Zoom->from_html($html);
425 my $zoom = $start->select('div')->replace_content('THIS IS A DIV!');
429 my $start = HTML::Zoom->select('div')->replace_content('THIS IS A DIV!');
431 my $zoom = $start->from_html($html);
433 will produce equivalent final $zoom objects, thus proving that there can be
434 more than one way to do it without one of them being a
435 L<bait and switch|Switch>.
437 =head2 STOCKTON TO DARLINGTON UNDER STREAM POWER
439 HTML::Zoom's execution always happens in terms of streams under the hood
440 - that is, the basic pattern for doing anything is -
442 my $stream = get_stream_from_somewhere
444 while (my ($evt) = $stream->next) {
445 # do something with the event
448 More importantly, all selectors and filters are also built as stream
449 operations, so a selector and filter pair is effectively:
453 my $next_evt = $self->parent_stream->next;
454 if ($self->selector_matches($next_evt)) {
455 return $self->apply_filter_to($next_evt);
461 Internally, things are marginally more complicated than that, but not enough
462 that you as a user should normally need to care.
464 In fact, an HTML::Zoom object is mostly just a container for the relevant
465 information from which to build the final stream that does the real work. A
466 stream built from a Zoom object is a stream of events from parsing the
467 initial HTML, wrapped in a filter stream per selector/filter pair provided
470 The upshot of this is that the application of filters works just as well on
471 streams as on the original Zoom object - in fact, when you run a
472 L</repeat_content> operation your subroutines are applied to the stream for
473 that element of the repeat, rather than constructing a new zoom per repeat
478 $_->select('div')->replace_content('I AM A DIV!');
480 works on both HTML::Zoom objects themselves and HTML::Zoom stream objects and
481 shares sufficient of the implementation that you can generally forget the
482 difference - barring the fact that a stream already has state attached so
483 things like to_fh are no longer available.
485 =head2 POP! GOES THE WEASEL
487 ... and by Weasel, I mean layout.
489 HTML::Zoom's filehandle object supports an additional event key, 'flush',
490 that is transparent to the rest of the system but indicates to the filehandle
491 object to end a getline operation at that point and return the HTML so far.
493 This means that in an environment where streaming output is available, such
494 as a number of the L<Plack> PSGI handlers, you can add the flush key to an
495 event in order to ensure that the HTML generated so far is flushed through
496 to the browser right now. This can be especially useful if you know you're
497 about to call a web service or a potentially slow database query or similar
498 to ensure that at least the header/layout of your page renders now, improving
499 perceived user responsiveness while your application waits around for the
502 This is currently exposed by the 'flush_before' option to the collect filter,
503 which incidentally also underlies the replace and repeat filters, so to
504 indicate we want this behaviour to happen before a query is executed we can
505 write something like:
507 $zoom->select('.item')->repeat(sub {
508 if (my $row = $db_thing->next) {
509 return sub { $_->select('.item-name')->replace_content($row->name) }
513 }, { flush_before => 1 });
515 which should have the desired effect given a sufficiently lazy $db_thing (for
516 example a L<DBIx::Class::ResultSet> object).
518 =head2 A FISTFUL OF OBJECTS
520 At the core of an HTML::Zoom system lurks an L<HTML::Zoom::ZConfig> object,
521 whose purpose is to hang on to the various bits and pieces that things need
522 so that there's a common way of accessing shared functionality.
524 Were I a computer scientist I would probably call this an "Inversion of
525 Control" object - which you'd be welcome to google to learn more about, or
526 you can just imagine a computer scientist being suspended upside down over
527 a pit. Either way works for me, I'm a pure maths grad.
529 The ZConfig object hangs on to one each of the following for you:
533 =item * An HTML parser, normally L<HTML::Zoom::Parser::BuiltIn>
535 =item * An HTML producer (emitter), normally L<HTML::Zoom::Producer::BuiltIn>
537 =item * An object to build event filters, normally L<HTML::Zoom::FilterBuilder>
539 =item * An object to parse CSS selectors, normally L<HTML::Zoom::SelectorParser>
541 =item * An object to build streams, normally L<HTML::Zoom::StreamUtils>
545 In theory you could replace any of these with anything you like, but in
546 practice you're probably best restricting yourself to subclasses, or at
547 least things that manage to look like the original if you squint a bit.
549 If you do something more clever than that, or find yourself overriding things
550 in your ZConfig a lot, please please tell us about it via one of the means
551 mentioned under L</SUPPORT>.
553 =head2 SEMANTIC DIDACTIC
555 Some will argue that overloading CSS selectors to do data stuff is a terrible
556 idea, and possibly even a step towards the "Concrete Javascript" pattern
557 (which I abhor) or Smalltalk's Morphic (which I ignore, except for the part
558 where it keeps reminding me of the late, great Tony Hart's plasticine friend).
560 To which I say, "eh", "meh", and possibly also "feh". If it really upsets
561 you, either use extra classes for this (and remove them afterwards) or
562 use special fake elements or, well, honestly, just use something different.
563 L<Template::Semantic> provides a similar idea to zoom except using XPath
564 and XML::LibXML transforms rather than a lightweight streaming approach -
565 maybe you'd like that better. Or maybe you really did want
566 L<Template Toolkit|Template> after all. It is still damn good at what it does,
569 So far, however, I've found that for new sites the designers I'm working with
570 generally want to produce nice semantic HTML with classes that represent the
571 nature of the data rather than the structure of the layout, so sharing them
572 as a common interface works really well for us.
574 In the absence of any evidence that overloading CSS selectors has killed
575 children or unexpectedly set fire to grandmothers - and given microformats
576 have been around for a while there's been plenty of opportunity for
577 octagenarian combustion - I'd suggest you give it a try and see if you like it.
579 =head2 GET THEE TO A SUMMARY!
583 HTML::Zoom is a lazy, stream oriented, streaming capable, mostly functional,
584 CSS selector based semantic templating engine for HTML and HTML-like
587 But I said that already. Although hopefully by now you have some idea what I
588 meant when I said it. If you didn't have any idea the first time. I mean, I'm
589 not trying to call you stupid or anything. Just saying that maybe it wasn't
590 totally obvious without the explanation. Or something.
594 Maybe we should just move on to the method docs.
600 my $zoom = HTML::Zoom->new;
602 my $zoom = HTML::Zoom->new({ zconfig => $zconfig });
604 Create a new empty Zoom object. You can optionally pass an
605 L<HTML::Zoom::ZConfig> instance if you're trying to override one or more of
606 the default components.
608 This method isn't often used directly since several other methods can also
609 act as constructors, notable L</select> and L</from_html>
613 my $zconfig = $zoom->zconfig;
615 Retrieve the L<HTML::Zoom::ZConfig> instance used by this Zoom object. You
616 shouldn't usually need to call this yourself.
620 my $zoom = HTML::Zoom->from_html($html);
622 my $z2 = $z1->from_html($html);
624 Parses the HTML using the current zconfig's parser object and returns a new
625 zoom instance with that as the source HTML to be transformed.
629 my $zoom = HTML::Zoom->from_file($file);
631 my $z2 = $z1->from_file($file);
633 Convenience method - slurps the contents of $file and calls from_html with it.
637 my $zoom = HTML::Zoom->from_events($evt);
639 Create a new Zoom object from collected events
643 my $stream = $zoom->to_stream;
645 while (my ($evt) = $stream->next) {
648 Creates a stream, starting with a stream of the events from the HTML supplied
649 via L</from_html> and then wrapping it in turn with each selector+filter pair
650 that have been applied to the zoom object.
654 my $fh = $zoom->to_fh;
656 call_something_expecting_a_filehandle($fh);
658 Returns an L<HTML::Zoom::ReadFH> instance that will create a stream the first
659 time its getline method is called and then return all HTML up to the next
660 event with 'flush' set.
662 You can pass this filehandle to compliant PSGI handlers (and probably most
669 Runs the zoom object's transforms without doing anything with the results.
671 Normally used to get side effects of a zoom run - for example when using
672 L<HTML::Zoom::FilterBuilder/collect> to slurp events for scraping or layout.
676 my $z2 = $z1->apply(sub {
677 $_->select('div')->replace_content('I AM A DIV!') })
680 Sets $_ to the zoom object and then runs the provided code. Basically syntax
681 sugar, the following is entirely equivalent:
684 shift->select('div')->replace_content('I AM A DIV!') })
687 my $z2 = $sub->($z1);
691 my $z2 = $z1->apply_if($cond, sub {
692 $_->select('div')->replace_content('I AM A DIV!') })
695 ->apply but will only run the tranform if $cond is true
699 my $html = $zoom->to_html;
701 Runs the zoom processing and returns the resulting HTML.
705 my $z2 = $z1->memoize;
707 Creates a new zoom whose source HTML is the results of the original zoom's
708 processing. Effectively syntax sugar for:
710 my $z2 = HTML::Zoom->from_html($z1->to_html);
712 but preserves your L<HTML::Zoom::ZConfig> object.
716 my $zoom = HTML::Zoom->with_filter(
717 'div', $filter_builder->replace_content('I AM A DIV!')
720 my $z2 = $z1->with_filter(
721 'div', $filter_builder->replace_content('I AM A DIV!')
724 Lower level interface than L</select> to adding filters to your zoom object.
726 In normal usage, you probably don't need to call this yourself.
730 my $zoom = HTML::Zoom->select('div')->replace_content('I AM A DIV!');
732 my $z2 = $z1->select('div')->replace_content('I AM A DIV!');
734 Returns an intermediary object of the class L<HTML::Zoom::TransformBuilder>
735 on which methods of your L<HTML::Zoom::FilterBuilder> object can be called.
737 In normal usage you should generally always put the pair of method calls
738 together; the intermediary object isn't designed or expected to stick around.
742 my $z2 = $z1->select('div')->add_to_attribute(class => 'spoon')
744 ->replace_content('I AM A DIV!');
746 Re-runs the previous select to allow you to chain actions together on the
749 =head1 AUTOLOAD METHODS
751 L<HTML::Zoom> AUTOLOADS methods against L</select> so that you can reduce a
752 certain amount of boilerplate typing. This allows you to replace:
754 $z->select('div')->replace_content("Hello World");
758 $z->replace_content(div => "Hello World");
760 Besides saving a few keys per invocations, you may feel this looks neater
761 in your code and increases understanding.
765 mst - Matt S. Trout (cpan:MSTROUT) <mst@shadowcat.co.uk>
783 Copyright (c) 2010-2011 the HTML::Zoom L</AUTHOR> and L</CONTRIBUTORS>
788 This library is free software, you can redistribute it and/or modify
789 it under the same terms as Perl itself.