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