File Coverage

blib/lib/HTML/Zoom.pm
Criterion Covered Total %
statement 74 91 81.3
branch 13 16 81.2
condition 1 2 50.0
subroutine 22 28 78.5
pod 15 17 88.2
total 125 154 81.1


line stmt bran cond sub pod time code
1             package HTML::Zoom;
2              
3 14     14   124268 use strictures 1;
  14         4270  
  14         333  
4              
5 14     14   5963 use HTML::Zoom::ZConfig;
  14         23  
  14         435  
6 14     14   5183 use HTML::Zoom::ReadFH;
  14         31  
  14         360  
7 14     14   5090 use HTML::Zoom::Transform;
  14         34  
  14         397  
8 14     14   87 use HTML::Zoom::TransformBuilder;
  14         22  
  14         249  
9 14     14   62 use Scalar::Util ();
  14         17  
  14         16037  
10              
11             our $VERSION = '0.009009';
12              
13             $VERSION = eval $VERSION;
14              
15             sub new {
16 62     62 1 109 my ($class, $args) = @_;
17 62         104 my $new = {};
18 62   50     498 $new->{zconfig} = HTML::Zoom::ZConfig->new($args->{zconfig}||{});
19 62         241 bless($new, $class);
20             }
21              
22 281     281 1 386 sub zconfig { shift->_self_or_new->{zconfig} }
23              
24             sub _self_or_new {
25 573 100   573   2024 ref($_[0]) ? $_[0] : $_[0]->new
26             }
27              
28             sub _with {
29 134     134   131 bless({ %{$_[0]}, %{$_[1]} }, ref($_[0]));
  134         307  
  134         976  
30             }
31              
32             sub from_events {
33 62     62 1 127 my $self = shift->_self_or_new;
34 62         196 $self->_with({
35             initial_events => shift,
36             });
37             }
38              
39             sub from_html {
40 60     60 1 11534 my $self = shift->_self_or_new;
41 60         164 $self->from_events($self->zconfig->parser->html_to_events($_[0]))
42             }
43              
44             sub from_file {
45 0     0 1 0 my $self = shift->_self_or_new;
46 0         0 my $filename = shift;
47 0         0 $self->from_html(do { local (@ARGV, $/) = ($filename); <> });
  0         0  
  0         0  
48             }
49              
50             sub to_stream {
51 62     62 1 72 my $self = shift;
52 62 50       154 die "No events to build from - forgot to call from_html?"
53             unless $self->{initial_events};
54 62         126 my $sutils = $self->zconfig->stream_utils;
55 62         74 my $stream = $sutils->stream_from_array(@{$self->{initial_events}});
  62         238  
56 62 100       126 $stream = $_->apply_to_stream($stream) for @{$self->{transforms}||[]};
  62         390  
57 59         210 $stream
58             }
59              
60             sub to_fh {
61 2     2 1 25 HTML::Zoom::ReadFH->from_zoom(shift);
62             }
63              
64             sub to_events {
65 1     1 0 1 my $self = shift;
66 1         2 [ $self->zconfig->stream_utils->stream_to_array($self->to_stream) ];
67             }
68              
69             sub run {
70 1     1 1 2 my $self = shift;
71 1         3 $self->to_events;
72             return
73 1         5 }
74              
75             sub apply {
76 0     0 1 0 my ($self, $code) = @_;
77 0         0 local $_ = $self;
78 0         0 $self->$code;
79             }
80              
81             sub apply_if {
82 2     2 1 3 my ($self, $predicate, $code) = @_;
83 2 100       7 if($predicate) {
84 1         2 local $_ = $self;
85 1         3 $self->$code;
86             }
87             else {
88 1         3 $self;
89             }
90             }
91              
92             sub to_html {
93 58     58 1 83 my $self = shift;
94 58         107 $self->zconfig->producer->html_from_stream($self->to_stream);
95             }
96              
97             sub memoize {
98 0     0 1 0 my $self = shift;
99 0         0 ref($self)->new($self)->from_html($self->to_html);
100             }
101              
102             sub with_transform {
103 72     72 0 132 my $self = shift->_self_or_new;
104 72         82 my ($transform) = @_;
105 72 100       415 $self->_with({
106             transforms => [
107 72         86 @{$self->{transforms}||[]},
108             $transform
109             ]
110             });
111             }
112            
113             sub with_filter {
114 0     0 1 0 my $self = shift->_self_or_new;
115 0         0 my ($selector, $filter) = @_;
116 0         0 $self->with_transform(
117             HTML::Zoom::Transform->new({
118             zconfig => $self->zconfig,
119             selector => $selector,
120             filters => [ $filter ]
121             })
122             );
123             }
124              
125             sub select {
126 98     98 1 958 my $self = shift->_self_or_new;
127 98         113 my ($selector) = @_;
128 98         200 return HTML::Zoom::TransformBuilder->new({
129             zconfig => $self->zconfig,
130             selector => $selector,
131             proto => $self
132             });
133             }
134              
135             # There's a bug waiting to happen here: if you do something like
136             #
137             # $zoom->select('.foo')
138             # ->remove_attribute(class => 'foo')
139             # ->then
140             # ->well_anything_really
141             #
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.
145              
146             sub then {
147 0     0 1 0 my $self = shift;
148 0 0       0 die "Can't call ->then without a previous transform"
149             unless $self->{transforms};
150 0         0 $self->select($self->{transforms}->[-1]->selector);
151             }
152              
153             sub AUTOLOAD {
154 25     25   89 my ($self, $selector, @args) = @_;
155 25         64 my $sel = $self->select($selector);
156 25         36 my $meth = our $AUTOLOAD;
157 25         142 $meth =~ s/.*:://;
158 25 100       58 if (ref($selector) eq 'HASH') {
159 2         4 my $ret = $self;
160 2         7 $ret = $ret->_do($_, $meth, @{$selector->{$_}}) for keys %$selector;
  4         10  
161 2         9 $ret;
162             } else {
163 23         55 $self->_do($selector, $meth, @args);
164             }
165             }
166              
167             sub _do {
168 27     27   48 my ($self, $selector, $meth, @args) = @_;
169 27         46 my $sel = $self->select($selector);
170 27 100       70 if( my $cr = $sel->_zconfig->filter_builder->can($meth)) {
171 26         132 return $sel->$meth(@args);
172             } else {
173 1         22 die "We can't do $meth on ->select('$selector')";
174             }
175             }
176              
177 0     0     sub DESTROY {}
178              
179             1;
180              
181             =head1 NAME
182              
183             HTML::Zoom - selector based streaming template engine
184              
185             =head1 SYNOPSIS
186              
187             use HTML::Zoom;
188              
189             my $template = <
190            
191            
192             Hello people
193            
194            
195            

Placeholder

196            
197            
198            

Name: Bob

199            

Age: 23

200            
201            
202            
203            
204            
205             HTML
206              
207             my $output = HTML::Zoom
208             ->from_html($template)
209             ->select('title, #greeting')->replace_content('Hello world & dog!')
210             ->select('#list')->repeat_content(
211             [
212             sub {
213             $_->select('.name')->replace_content('Matt')
214             ->select('.age')->replace_content('26')
215             },
216             # alternate form
217             sub {
218             $_->replace_content({'.name' => ['Mark'],'.age' => ['0x29'] })
219             },
220             #alternate alternate form
221             sub {
222             $_->replace_content('.name' => 'Epitaph')
223             ->replace_content('.age' => '')
224             },
225             ],
226             { repeat_between => '.between' }
227             )
228             ->to_html;
229              
230             will produce:
231              
232             =begin testinfo
233              
234             my $expect = <
235              
236             =end testinfo
237              
238            
239            
240             Hello world & dog!
241            
242            
243            

Hello world & dog!

244            
245            
246            

Name: Matt

247            

Age: 26

248            
249            
250            
251            

Name: Mark

252            

Age: 0x29

253            
254            
255            
256            

Name: Epitaph

257            

Age: <redacted>

258            
259            
260            
261            
262            
263              
264             =begin testinfo
265              
266             HTML
267             is($output, $expect, 'Synopsis code works ok');
268              
269             =end testinfo
270              
271             =head1 DANGER WILL ROBINSON
272              
273             This is a 0.9 release. That means that I'm fairly happy the API isn't going
274             to change in surprising and upsetting ways before 1.0 and a real compatibility
275             freeze. But it also means that if it turns out there's a mistake the size of
276             a politician's ego in the API design that I haven't spotted yet there may be
277             a bit of breakage between here and 1.0. Hopefully not though. Appendages
278             crossed and all that.
279              
280             Worse still, the rest of the distribution isn't documented yet. I'm sorry.
281             I suck. But lots of people have been asking me to ship this, docs or no, so
282             having got this class itself at least somewhat documented I figured now was
283             a good time to cut a first real release.
284              
285             =head1 DESCRIPTION
286              
287             HTML::Zoom is a lazy, stream oriented, streaming capable, mostly functional,
288             CSS selector based semantic templating engine for HTML and HTML-like
289             document formats.
290              
291             Which is, on the whole, a bit of a mouthful. So let me step back a moment
292             and explain why you care enough to understand what I mean:
293              
294             =head2 JQUERY ENVY
295              
296             HTML::Zoom is the cure for JQuery envy. When your javascript guy pushes a
297             piece of data into a document by doing:
298              
299             $('.username').replaceAll(username);
300              
301             In HTML::Zoom one can write
302              
303             $zoom->select('.username')->replace_content($username);
304              
305             which is, I hope, almost as clear, hampered only by the fact that Zoom can't
306             assume a global document and therefore has nothing quite so simple as the
307             $() function to get the initial selection.
308              
309             L implements a subset of the JQuery selector
310             specification, and will continue to track that rather than the W3C standards
311             for the forseeable future on grounds of pragmatism. Also on grounds of their
312             spec is written in EN_US rather than EN_W3C, and I read the former much better.
313              
314             I am happy to admit that it's very, very much a subset at the moment - see the
315             L POD for what's currently there, and expect more
316             and more to be supported over time as we need it and patch it in.
317              
318             =head2 CLEAN TEMPLATES
319              
320             HTML::Zoom is the cure for messy templates. How many times have you looked at
321             templates like this:
322              
323            
324             [% FOREACH field IN fields %]
325            
326            
327             [% END %]
328            
329              
330             and despaired of the fact that neither the HTML structure nor the logic are
331             remotely easy to read? Fortunately, with HTML::Zoom we can separate the two
332             cleanly:
333              
334            
335            
336            
337            
338              
339             $zoom->select('.myform')->repeat_content([
340             map { my $field = $_; sub {
341              
342             $_->select('label')
343             ->add_to_attribute( for => $field->{id} )
344             ->then
345             ->replace_content( $field->{label} )
346             ->add_to_attribute(
347             input => {
348             name => $field->{name},
349             type => $field->{type},
350             value => $field->{value}
351             })
352             } } @fields
353             ]);
354              
355             This is, admittedly, very much not shorter. However, it makes it extremely
356             clear what's happening and therefore less hassle to maintain. Especially
357             because it allows the designer to fiddle with the HTML without cutting
358             himself on sharp ELSE clauses, and the developer to add available data to
359             the template without getting angle bracket cuts on sensitive parts.
360              
361             Better still, HTML::Zoom knows that it's inserting content into HTML and
362             can escape it for you - the example template should really have been:
363              
364            
365             [% FOREACH field IN fields %]
366            
367            
368             [% END %]
369            
370              
371             and frankly I'll take slightly more code any day over *that* crawling horror.
372              
373             (addendum: I pick on L