few extra utility routines
[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
10 sub new {
11   my ($class, $args) = @_;
12   my $new = {};
13   $new->{zconfig} = HTML::Zoom::ZConfig->new($args->{zconfig}||{});
14   bless($new, $class);
15 }
16
17 sub zconfig { shift->_self_or_new->{zconfig} }
18
19 sub _self_or_new {
20   ref($_[0]) ? $_[0] : $_[0]->new
21 }
22
23 sub _with {
24   bless({ %{$_[0]}, %{$_[1]} }, ref($_[0]));
25 }
26
27 sub from_html {
28   my $self = shift->_self_or_new;
29   $self->_with({
30     initial_events => $self->zconfig->parser->html_to_events($_[0])
31   });
32 }
33
34 sub from_file {
35   my $self = shift->_self_or_new;
36   my $filename = shift;
37   $self->from_html(do { local (@ARGV, $/) = ($filename); <> });
38 }
39
40 sub to_stream {
41   my $self = shift;
42   die "No events to build from - forgot to call from_html?"
43     unless $self->{initial_events};
44   my $sutils = $self->zconfig->stream_utils;
45   my $stream = $sutils->stream_from_array(@{$self->{initial_events}});
46   foreach my $filter_spec (@{$self->{filters}||[]}) {
47     $stream = $sutils->wrap_with_filter($stream, @{$filter_spec});
48   }
49   $stream
50 }
51
52 sub to_fh {
53   HTML::Zoom::ReadFH->from_zoom(shift);
54 }
55
56 sub run {
57   my $self = shift;
58   $self->zconfig->stream_utils->stream_to_array($self->to_stream);
59   return
60 }
61
62 sub apply {
63   my ($self, $code) = @_;
64   local $_ = $self;
65   $self->$code;
66 }
67
68 sub to_html {
69   my $self = shift;
70   $self->zconfig->producer->html_from_stream($self->to_stream);
71 }
72
73 sub memoize {
74   my $self = shift;
75   ref($self)->new($self)->from_html($self->to_html);
76 }
77
78 sub with_filter {
79   my ($self, $selector, $filter) = @_;
80   my $match = $self->parse_selector($selector);
81   $self->_with({
82     filters => [ @{$self->{filters}||[]}, [ $match, $filter ] ]
83   });
84 }
85
86 sub select {
87   my ($self, $selector) = @_;
88   my $match = $self->parse_selector($selector);
89   return HTML::Zoom::MatchWithoutFilter->construct(
90     $self, $match, $self->zconfig->filter_builder,
91   );
92 }
93
94 # There's a bug waiting to happen here: if you do something like
95 #
96 # $zoom->select('.foo')
97 #      ->remove_attribute({ class => 'foo' })
98 #      ->then
99 #      ->well_anything_really
100 #
101 # the second action won't execute because it doesn't match anymore.
102 # Ideally instead we'd merge the match subs but that's more complex to
103 # implement so I'm deferring it for the moment.
104
105 sub then {
106   my $self = shift;
107   die "Can't call ->then without a previous filter"
108     unless $self->{filters};
109   $self->select($self->{filters}->[-1][0]);
110 }
111
112 sub parse_selector {
113   my ($self, $selector) = @_;
114   return $selector if ref($selector); # already a match sub
115   $self->zconfig->selector_parser->parse_selector($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 SOMETHING ELSE
210
211 =cut