4f1c599a668caac102f8b5df67a49fb5181c77d7
[catagits/HTML-Zoom.git] / lib / HTML / Zoom.pm
1 package HTML::Zoom;
2
3 use strictures 1;
4
5 use HTML::Zoom::ZConfig;
6 use HTML::Zoom::ReadFH;
7 use HTML::Zoom::Transform;
8 use HTML::Zoom::TransformBuilder;
9
10 our $VERSION = '0.009004';
11
12 $VERSION = eval $VERSION;
13
14 sub new {
15   my ($class, $args) = @_;
16   my $new = {};
17   $new->{zconfig} = HTML::Zoom::ZConfig->new($args->{zconfig}||{});
18   bless($new, $class);
19 }
20
21 sub zconfig { shift->_self_or_new->{zconfig} }
22
23 sub _self_or_new {
24   ref($_[0]) ? $_[0] : $_[0]->new
25 }
26
27 sub _with {
28   bless({ %{$_[0]}, %{$_[1]} }, ref($_[0]));
29 }
30
31 sub from_events {
32   my $self = shift->_self_or_new;
33   $self->_with({
34     initial_events => shift,
35   });
36 }
37
38 sub from_html {
39   my $self = shift->_self_or_new;
40   $self->from_events($self->zconfig->parser->html_to_events($_[0]))
41 }
42
43 sub from_file {
44   my $self = shift->_self_or_new;
45   my $filename = shift;
46   $self->from_html(do { local (@ARGV, $/) = ($filename); <> });
47 }
48
49 sub to_stream {
50   my $self = shift;
51   die "No events to build from - forgot to call from_html?"
52     unless $self->{initial_events};
53   my $sutils = $self->zconfig->stream_utils;
54   my $stream = $sutils->stream_from_array(@{$self->{initial_events}});
55   $stream = $_->apply_to_stream($stream) for @{$self->{transforms}||[]};
56   $stream
57 }
58
59 sub to_fh {
60   HTML::Zoom::ReadFH->from_zoom(shift);
61 }
62
63 sub to_events {
64   my $self = shift;
65   [ $self->zconfig->stream_utils->stream_to_array($self->to_stream) ];
66 }
67
68 sub run {
69   my $self = shift;
70   $self->to_events;
71   return
72 }
73
74 sub apply {
75   my ($self, $code) = @_;
76   local $_ = $self;
77   $self->$code;
78 }
79
80 sub apply_if {
81   my ($self, $predicate, $code) = @_;
82   if($predicate) {
83     local $_ = $self;
84     $self->$code;
85   }
86   else {
87     $self;
88   }
89 }
90
91 sub to_html {
92   my $self = shift;
93   $self->zconfig->producer->html_from_stream($self->to_stream);
94 }
95
96 sub memoize {
97   my $self = shift;
98   ref($self)->new($self)->from_html($self->to_html);
99 }
100
101 sub with_transform {
102   my $self = shift->_self_or_new;
103   my ($transform) = @_;
104   $self->_with({
105     transforms => [
106       @{$self->{transforms}||[]},
107       $transform
108     ]
109   });
110 }
111   
112 sub with_filter {
113   my $self = shift->_self_or_new;
114   my ($selector, $filter) = @_;
115   $self->with_transform(
116     HTML::Zoom::Transform->new({
117       zconfig => $self->zconfig,
118       selector => $selector,
119       filters => [ $filter ]
120     })
121   );
122 }
123
124 sub select {
125   my $self = shift->_self_or_new;
126   my ($selector) = @_;
127   return HTML::Zoom::TransformBuilder->new({
128     zconfig => $self->zconfig,
129     selector => $selector,
130     proto => $self
131   });
132 }
133
134 # There's a bug waiting to happen here: if you do something like
135 #
136 # $zoom->select('.foo')
137 #      ->remove_attribute(class => 'foo')
138 #      ->then
139 #      ->well_anything_really
140 #
141 # the second action won't execute because it doesn't match anymore.
142 # Ideally instead we'd merge the match subs but that's more complex to
143 # implement so I'm deferring it for the moment.
144
145 sub then {
146   my $self = shift;
147   die "Can't call ->then without a previous transform"
148     unless $self->{transforms};
149   $self->select($self->{transforms}->[-1]->selector);
150 }
151
152 1;
153
154 =head1 NAME
155
156 HTML::Zoom - selector based streaming template engine
157
158 =head1 SYNOPSIS
159
160   use HTML::Zoom;
161
162   my $template = <<HTML;
163   <html>
164     <head>
165       <title>Hello people</title>
166     </head>
167     <body>
168       <h1 id="greeting">Placeholder</h1>
169       <div id="list">
170         <span>
171           <p>Name: <span class="name">Bob</span></p>
172           <p>Age: <span class="age">23</span></p>
173         </span>
174         <hr class="between" />
175       </div>
176     </body>
177   </html>
178   HTML
179
180   my $output = HTML::Zoom
181     ->from_html($template)
182     ->select('title, #greeting')->replace_content('Hello world & dog!')
183     ->select('#list')->repeat_content(
184         [
185           sub {
186             $_->select('.name')->replace_content('Matt')
187               ->select('.age')->replace_content('26')
188           },
189           sub {
190             $_->select('.name')->replace_content('Mark')
191               ->select('.age')->replace_content('0x29')
192           },
193           sub {
194             $_->select('.name')->replace_content('Epitaph')
195               ->select('.age')->replace_content('<redacted>')
196           },
197         ],
198         { repeat_between => '.between' }
199       )
200     ->to_html;
201
202 will produce:
203
204 =begin testinfo
205
206   my $expect = <<HTML;
207
208 =end testinfo
209
210   <html>
211     <head>
212       <title>Hello world &amp; dog!</title>
213     </head>
214     <body>
215       <h1 id="greeting">Hello world &amp; dog!</h1>
216       <div id="list">
217         <span>
218           <p>Name: <span class="name">Matt</span></p>
219           <p>Age: <span class="age">26</span></p>
220         </span>
221         <hr class="between" />
222         <span>
223           <p>Name: <span class="name">Mark</span></p>
224           <p>Age: <span class="age">0x29</span></p>
225         </span>
226         <hr class="between" />
227         <span>
228           <p>Name: <span class="name">Epitaph</span></p>
229           <p>Age: <span class="age">&lt;redacted&gt;</span></p>
230         </span>
231         
232       </div>
233     </body>
234   </html>
235
236 =begin testinfo
237
238   HTML
239   is($output, $expect, 'Synopsis code works ok');
240
241 =end testinfo
242
243 =head1 DANGER WILL ROBINSON
244
245 This is a 0.9 release. That means that I'm fairly happy the API isn't going
246 to change in surprising and upsetting ways before 1.0 and a real compatibility
247 freeze. But it also means that if it turns out there's a mistake the size of
248 a politician's ego in the API design that I haven't spotted yet there may be
249 a bit of breakage between here and 1.0. Hopefully not though. Appendages
250 crossed and all that.
251
252 Worse still, the rest of the distribution isn't documented yet. I'm sorry.
253 I suck. But lots of people have been asking me to ship this, docs or no, so
254 having got this class itself at least somewhat documented I figured now was
255 a good time to cut a first real release.
256
257 =head1 DESCRIPTION
258
259 HTML::Zoom is a lazy, stream oriented, streaming capable, mostly functional,
260 CSS selector based semantic templating engine for HTML and HTML-like
261 document formats.
262
263 Which is, on the whole, a bit of a mouthful. So let me step back a moment
264 and explain why you care enough to understand what I mean:
265
266 =head2 JQUERY ENVY
267
268 HTML::Zoom is the cure for JQuery envy. When your javascript guy pushes a
269 piece of data into a document by doing:
270
271   $('.username').replaceAll(username);
272
273 In HTML::Zoom one can write
274
275   $zoom->select('.username')->replace_content($username);
276
277 which is, I hope, almost as clear, hampered only by the fact that Zoom can't
278 assume a global document and therefore has nothing quite so simple as the
279 $() function to get the initial selection.
280
281 L<HTML::Zoom::SelectorParser> implements a subset of the JQuery selector
282 specification, and will continue to track that rather than the W3C standards
283 for the forseeable future on grounds of pragmatism. Also on grounds of their
284 spec is written in EN_US rather than EN_W3C, and I read the former much better.
285
286 I am happy to admit that it's very, very much a subset at the moment - see the
287 L<HTML::Zoom::SelectorParser> POD for what's currently there, and expect more
288 and more to be supported over time as we need it and patch it in.
289
290 =head2 CLEAN TEMPLATES
291
292 HTML::Zoom is the cure for messy templates. How many times have you looked at
293 templates like this:
294
295   <form action="/somewhere">
296   [% FOREACH field IN fields %]
297     <label for="[% field.id %]">[% field.label %]</label>
298     <input name="[% field.name %]" type="[% field.type %]" value="[% field.value %]" />
299   [% END %]
300   </form>
301
302 and despaired of the fact that neither the HTML structure nor the logic are
303 remotely easy to read? Fortunately, with HTML::Zoom we can separate the two
304 cleanly:
305
306   <form class="myform" action="/somewhere">
307     <label />
308     <input />
309   </form>
310
311   $zoom->select('.myform')->repeat_content([
312     map { my $field = $_; sub {
313
314      $_->select('label')
315        ->add_to_attribute( for => $field->{id} )
316        ->then
317        ->replace_content( $field->{label} )
318
319        ->select('input')
320        ->add_to_attribute( name => $field->{name} )
321        ->then
322        ->add_to_attribute( type => $field->{type} )
323        ->then
324        ->add_to_attribute( value => $field->{value} )
325
326     } } @fields
327   ]);
328
329 This is, admittedly, very much not shorter. However, it makes it extremely
330 clear what's happening and therefore less hassle to maintain. Especially
331 because it allows the designer to fiddle with the HTML without cutting
332 himself on sharp ELSE clauses, and the developer to add available data to
333 the template without getting angle bracket cuts on sensitive parts.
334
335 Better still, HTML::Zoom knows that it's inserting content into HTML and
336 can escape it for you - the example template should really have been:
337
338   <form action="/somewhere">
339   [% FOREACH field IN fields %]
340     <label for="[% field.id | html %]">[% field.label | html %]</label>
341     <input name="[% field.name | html %]" type="[% field.type | html %]" value="[% field.value | html %]" />
342   [% END %]
343   </form>
344
345 and frankly I'll take slightly more code any day over *that* crawling horror.
346
347 (addendum: I pick on L<Template Toolkit|Template> here specifically because
348 it's the template system I hate the least - for text templating, I don't
349 honestly think I'll ever like anything except the next version of Template
350 Toolkit better - but HTML isn't text. Zoom knows that. Do you?)
351
352 =head2 PUTTING THE FUN INTO FUNCTIONAL
353
354 The principle of HTML::Zoom is to provide a reusable, functional container
355 object that lets you build up a set of transforms to be applied; every method
356 call you make on a zoom object returns a new object, so it's safe to do so
357 on one somebody else gave you without worrying about altering state (with
358 the notable exception of ->next for stream objects, which I'll come to later).
359
360 So:
361
362   my $z2 = $z1->select('.name')->replace_content($name);
363
364   my $z3 = $z2->select('.title')->replace_content('Ms.');
365
366 each time produces a new Zoom object. If you want to package up a set of
367 transforms to re-use, HTML::Zoom provides an 'apply' method:
368
369   my $add_name = sub { $_->select('.name')->replace_content($name) };
370  
371   my $same_as_z2 = $z1->apply($add_name);
372
373 =head2 LAZINESS IS A VIRTUE
374
375 HTML::Zoom does its best to defer doing anything until it's absolutely
376 required. The only point at which it descends into state is when you force
377 it to create a stream, directly by:
378
379   my $stream = $zoom->to_stream;
380
381   while (my $evt = $stream->next) {
382     # handle zoom event here
383   }
384
385 or indirectly via:
386
387   my $final_html = $zoom->to_html;
388
389   my $fh = $zoom->to_fh;
390
391   while (my $chunk = $fh->getline) {
392     ...
393   }
394
395 Better still, the $fh returned doesn't create its stream until the first
396 call to getline, which means that until you call that and force it to be
397 stateful you can get back to the original stateless Zoom object via:
398
399   my $zoom = $fh->to_zoom;
400
401 which is exceedingly handy for filtering L<Plack> PSGI responses, among other
402 things.
403
404 Because HTML::Zoom doesn't try and evaluate everything up front, you can
405 generally put things together in whatever order is most appropriate. This
406 means that:
407
408   my $start = HTML::Zoom->from_html($html);
409
410   my $zoom = $start->select('div')->replace_content('THIS IS A DIV!');
411
412 and:
413
414   my $start = HTML::Zoom->select('div')->replace_content('THIS IS A DIV!');
415
416   my $zoom = $start->from_html($html);
417
418 will produce equivalent final $zoom objects, thus proving that there can be
419 more than one way to do it without one of them being a
420 L<bait and switch|Switch>.
421
422 =head2 STOCKTON TO DARLINGTON UNDER STREAM POWER
423
424 HTML::Zoom's execution always happens in terms of streams under the hood
425 - that is, the basic pattern for doing anything is -
426
427   my $stream = get_stream_from_somewhere
428
429   while (my ($evt) = $stream->next) {
430     # do something with the event
431   }
432
433 More importantly, all selectors and filters are also built as stream
434 operations, so a selector and filter pair is effectively:
435
436   sub next {
437     my ($self) = @_;
438     my $next_evt = $self->parent_stream->next;
439     if ($self->selector_matches($next_evt)) {
440       return $self->apply_filter_to($next_evt);
441     } else {
442       return $next_evt;
443     }
444   }
445
446 Internally, things are marginally more complicated than that, but not enough
447 that you as a user should normally need to care.
448
449 In fact, an HTML::Zoom object is mostly just a container for the relevant
450 information from which to build the final stream that does the real work. A
451 stream built from a Zoom object is a stream of events from parsing the
452 initial HTML, wrapped in a filter stream per selector/filter pair provided
453 as described above.
454
455 The upshot of this is that the application of filters works just as well on
456 streams as on the original Zoom object - in fact, when you run a
457 L</repeat_content> operation your subroutines are applied to the stream for
458 that element of the repeat, rather than constructing a new zoom per repeat
459 element as well.
460
461 More concretely:
462
463   $_->select('div')->replace_content('I AM A DIV!');
464
465 works on both HTML::Zoom objects themselves and HTML::Zoom stream objects and
466 shares sufficient of the implementation that you can generally forget the
467 difference - barring the fact that a stream already has state attached so
468 things like to_fh are no longer available.
469
470 =head2 POP! GOES THE WEASEL
471
472 ... and by Weasel, I mean layout.
473
474 HTML::Zoom's filehandle object supports an additional event key, 'flush',
475 that is transparent to the rest of the system but indicates to the filehandle
476 object to end a getline operation at that point and return the HTML so far.
477
478 This means that in an environment where streaming output is available, such
479 as a number of the L<Plack> PSGI handlers, you can add the flush key to an
480 event in order to ensure that the HTML generated so far is flushed through
481 to the browser right now. This can be especially useful if you know you're
482 about to call a web service or a potentially slow database query or similar
483 to ensure that at least the header/layout of your page renders now, improving
484 perceived user responsiveness while your application waits around for the
485 data it needs.
486
487 This is currently exposed by the 'flush_before' option to the collect filter,
488 which incidentally also underlies the replace and repeat filters, so to
489 indicate we want this behaviour to happen before a query is executed we can
490 write something like:
491
492   $zoom->select('.item')->repeat(sub {
493     if (my $row = $db_thing->next) {
494       return sub { $_->select('.item-name')->replace_content($row->name) }
495     } else {
496       return
497     }
498   }, { flush_before => 1 });
499
500 which should have the desired effect given a sufficiently lazy $db_thing (for
501 example a L<DBIx::Class::ResultSet> object).
502
503 =head2 A FISTFUL OF OBJECTS
504
505 At the core of an HTML::Zoom system lurks an L<HTML::Zoom::ZConfig> object,
506 whose purpose is to hang on to the various bits and pieces that things need
507 so that there's a common way of accessing shared functionality.
508
509 Were I a computer scientist I would probably call this an "Inversion of
510 Control" object - which you'd be welcome to google to learn more about, or
511 you can just imagine a computer scientist being suspended upside down over
512 a pit. Either way works for me, I'm a pure maths grad.
513
514 The ZConfig object hangs on to one each of the following for you:
515
516 =over 4
517
518 =item * An HTML parser, normally L<HTML::Zoom::Parser::BuiltIn>
519
520 =item * An HTML producer (emitter), normally L<HTML::Zoom::Producer::BuiltIn>
521
522 =item * An object to build event filters, normally L<HTML::Zoom::FilterBuilder>
523
524 =item * An object to parse CSS selectors, normally L<HTML::Zoom::SelectorParser>
525
526 =item * An object to build streams, normally L<HTML::Zoom::StreamUtils>
527
528 =back
529
530 In theory you could replace any of these with anything you like, but in
531 practice you're probably best restricting yourself to subclasses, or at
532 least things that manage to look like the original if you squint a bit.
533
534 If you do something more clever than that, or find yourself overriding things
535 in your ZConfig a lot, please please tell us about it via one of the means
536 mentioned under L</SUPPORT>.
537
538 =head2 SEMANTIC DIDACTIC
539
540 Some will argue that overloading CSS selectors to do data stuff is a terrible
541 idea, and possibly even a step towards the "Concrete Javascript" pattern
542 (which I abhor) or Smalltalk's Morphic (which I ignore, except for the part
543 where it keeps reminding me of the late, great Tony Hart's plasticine friend).
544
545 To which I say, "eh", "meh", and possibly also "feh". If it really upsets
546 you, either use extra classes for this (and remove them afterwards) or
547 use special fake elements or, well, honestly, just use something different.
548 L<Template::Semantic> provides a similar idea to zoom except using XPath
549 and XML::LibXML transforms rather than a lightweight streaming approach -
550 maybe you'd like that better. Or maybe you really did want
551 L<Template Toolkit|Template> after all. It is still damn good at what it does,
552 after all.
553
554 So far, however, I've found that for new sites the designers I'm working with
555 generally want to produce nice semantic HTML with classes that represent the
556 nature of the data rather than the structure of the layout, so sharing them
557 as a common interface works really well for us.
558
559 In the absence of any evidence that overloading CSS selectors has killed
560 children or unexpectedly set fire to grandmothers - and given microformats
561 have been around for a while there's been plenty of opportunity for
562 octagenarian combustion - I'd suggest you give it a try and see if you like it.
563
564 =head2 GET THEE TO A SUMMARY!
565
566 Erm. Well.
567
568 HTML::Zoom is a lazy, stream oriented, streaming capable, mostly functional,
569 CSS selector based semantic templating engine for HTML and HTML-like
570 document formats.
571
572 But I said that already. Although hopefully by now you have some idea what I
573 meant when I said it. If you didn't have any idea the first time. I mean, I'm
574 not trying to call you stupid or anything. Just saying that maybe it wasn't
575 totally obvious without the explanation. Or something.
576
577 Er.
578
579 Maybe we should just move on to the method docs.
580
581 =head1 METHODS
582
583 =head2 new
584
585   my $zoom = HTML::Zoom->new;
586
587   my $zoom = HTML::Zoom->new({ zconfig => $zconfig });
588
589 Create a new empty Zoom object. You can optionally pass an
590 L<HTML::Zoom::ZConfig> instance if you're trying to override one or more of
591 the default components.
592
593 This method isn't often used directly since several other methods can also
594 act as constructors, notable L</select> and L</from_html>
595
596 =head2 zconfig
597
598   my $zconfig = $zoom->zconfig;
599
600 Retrieve the L<HTML::Zoom::ZConfig> instance used by this Zoom object. You
601 shouldn't usually need to call this yourself.
602
603 =head2 from_html
604
605   my $zoom = HTML::Zoom->from_html($html);
606
607   my $z2 = $z1->from_html($html);
608
609 Parses the HTML using the current zconfig's parser object and returns a new
610 zoom instance with that as the source HTML to be transformed.
611
612 =head2 from_file
613
614   my $zoom = HTML::Zoom->from_file($file);
615
616   my $z2 = $z1->from_file($file);
617
618 Convenience method - slurps the contents of $file and calls from_html with it.
619
620 =head2 to_stream
621
622   my $stream = $zoom->to_stream;
623
624   while (my ($evt) = $stream->next) {
625     ...
626
627 Creates a stream, starting with a stream of the events from the HTML supplied
628 via L</from_html> and then wrapping it in turn with each selector+filter pair
629 that have been applied to the zoom object.
630
631 =head2 to_fh
632
633   my $fh = $zoom->to_fh;
634
635   call_something_expecting_a_filehandle($fh);
636
637 Returns an L<HTML::Zoom::ReadFH> instance that will create a stream the first
638 time its getline method is called and then return all HTML up to the next
639 event with 'flush' set.
640
641 You can pass this filehandle to compliant PSGI handlers (and probably most
642 web frameworks).
643
644 =head2 run
645
646   $zoom->run;
647
648 Runs the zoom object's transforms without doing anything with the results.
649
650 Normally used to get side effects of a zoom run - for example when using
651 L<HTML::Zoom::FilterBuilder/collect> to slurp events for scraping or layout.
652
653 =head2 apply
654
655   my $z2 = $z1->apply(sub {
656     $_->select('div')->replace_content('I AM A DIV!') })
657   });
658
659 Sets $_ to the zoom object and then runs the provided code. Basically syntax
660 sugar, the following is entirely equivalent:
661
662   my $sub = sub {
663     shift->select('div')->replace_content('I AM A DIV!') })
664   };
665
666   my $z2 = $sub->($z1);
667
668 =head2 to_html
669
670   my $html = $zoom->to_html;
671
672 Runs the zoom processing and returns the resulting HTML.
673
674 =head2 memoize
675
676   my $z2 = $z1->memoize;
677
678 Creates a new zoom whose source HTML is the results of the original zoom's
679 processing. Effectively syntax sugar for:
680
681   my $z2 = HTML::Zoom->from_html($z1->to_html);
682
683 but preserves your L<HTML::Zoom::ZConfig> object.
684
685 =head2 with_filter
686
687   my $zoom = HTML::Zoom->with_filter(
688     'div', $filter_builder->replace_content('I AM A DIV!')
689   );
690
691   my $z2 = $z1->with_filter(
692     'div', $filter_builder->replace_content('I AM A DIV!')
693   );
694
695 Lower level interface than L</select> to adding filters to your zoom object.
696
697 In normal usage, you probably don't need to call this yourself.
698
699 =head2 select
700
701   my $zoom = HTML::Zoom->select('div')->replace_content('I AM A DIV!');
702
703   my $z2 = $z1->select('div')->replace_content('I AM A DIV!');
704
705 Returns an intermediary object of the class L<HTML::Zoom::TransformBuilder>
706 on which methods of your L<HTML::Zoom::FilterBuilder> object can be called.
707
708 In normal usage you should generally always put the pair of method calls
709 together; the intermediary object isn't designed or expected to stick around.
710
711 =head2 then
712
713   my $z2 = $z1->select('div')->add_to_attribute(class => 'spoon')
714                              ->then
715                              ->replace_content('I AM A DIV!');
716
717 Re-runs the previous select to allow you to chain actions together on the
718 same selector.
719
720 =head1 AUTHOR
721
722 mst - Matt S. Trout (cpan:MSTROUT) <mst@shadowcat.co.uk>
723
724 =head1 CONTRIBUTORS
725
726 Oliver Charles
727
728 Jakub Nareski
729
730 Simon Elliot
731
732 Joe Highton
733
734 John Napiorkowski
735
736 =head1 COPYRIGHT
737
738 Copyright (c) 2010-2011 the HTML::Zoom L</AUTHOR> and L</CONTRIBUTORS>
739 as listed above.
740
741 =head1 LICENSE
742
743 This library is free software, you can redistribute it and/or modify
744 it under the same terms as Perl itself.
745
746 =cut
747