introduce ZConfig system, first cut at HTML::Zoom itself
[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
9 sub new {
10   my ($class, $args) = @_;
11   my $new = {};
12   $new->{zconfig} = HTML::Zoom::ZConfig->new($args->{zconfig}||{});
13   bless($new, $class);
14 }
15
16 sub zconfig { shift->_self_or_new->{zconfig} }
17
18 sub _self_or_new {
19   ref($_[0]) ? $_[0] : $_[0]->new
20 }
21
22 sub _with {
23   bless({ %{$_[0]}, %{$_[1]} }, ref($_[0]));
24 }
25
26 sub from_html {
27   my $self = shift->_self_or_new;
28   $self->_with({
29     initial_events => $self->zconfig->parser->html_to_events($_[0])
30   });
31 }
32
33 sub to_stream {
34   my $self = shift;
35   die "No events to build from - forgot to call from_html?"
36     unless $self->{initial_events};
37   my $sutils = $self->zconfig->stream_utils;
38   my $stream = $sutils->stream_from_array(@{$self->{initial_events}});
39   foreach my $filter_spec (@{$self->{filters}||[]}) {
40     $stream = $sutils->wrap_with_filter($stream, @{$filter_spec});
41   }
42   $stream
43 }
44
45 sub to_html {
46   my $self = shift;
47   $self->zconfig->producer->html_from_stream($self->to_stream);
48 }
49
50 sub memoize {
51   my $self = shift;
52   ref($self)->new($self)->from_html($self->to_html);
53 }
54
55 sub with_filter {
56   my ($self, $selector, $filter) = @_;
57   my $match = $self->parse_selector($selector);
58   $self->_with({
59     filters => [ @{$self->{filters}||[]}, [ $match, $filter ] ]
60   });
61 }
62
63 sub select {
64   my ($self, $selector) = @_;
65   my $match = $self->parse_selector($selector);
66   return HTML::Zoom::MatchWithoutFilter->construct(
67     $self, $match, $self->zconfig->filter_builder,
68   );
69 }
70
71 # There's a bug waiting to happen here: if you do something like
72 #
73 # $zoom->select('.foo')
74 #      ->remove_attribute({ class => 'foo' })
75 #      ->then
76 #      ->well_anything_really
77 #
78 # the second action won't execute because it doesn't match anymore.
79 # Ideally instead we'd merge the match subs but that's more complex to
80 # implement so I'm deferring it for the moment.
81
82 sub then {
83   my $self = shift;
84   die "Can't call ->then without a previous filter"
85     unless $self->{filters};
86   $self->select($self->{filters}->[-1][0]);
87 }
88
89 sub parse_selector {
90   my ($self, $selector) = @_;
91   return $selector if ref($selector); # already a match sub
92   $self->zconfig->selector_parser->parse_selector($selector);
93 }
94
95 1;
96
97 =head1 NAME
98
99 HTML::Zoom - selector based streaming template engine
100
101 =head1 SYNOPSIS
102
103   use HTML::Zoom;
104
105   my $template = <<HTML;
106   <html>
107     <head>
108       <title>Hello people</title>
109     </head>
110     <body>
111       <h1 id="greeting">Placeholder</h1>
112       <div id="list">
113         <span>
114           <p>Name: <span class="name">Bob</span></p>
115           <p>Age: <span class="age">23</span></p>
116         </span>
117         <hr class="between" />
118       </div>
119     </body>
120   </html>
121   HTML
122
123   my $output = HTML::Zoom
124     ->from_html($template)
125     ->select('title, #greeting')->replace_content('Hello world & dog!')
126     ->select('#list')->repeat_content(
127         [
128           sub {
129             $_->select('.name')->replace_content('Matt')
130               ->select('.age')->replace_content('26')
131           },
132           sub {
133             $_->select('.name')->replace_content('Mark')
134               ->select('.age')->replace_content('0x29')
135           },
136           sub {
137             $_->select('.name')->replace_content('Epitaph')
138               ->select('.age')->replace_content('<redacted>')
139           },
140         ],
141         { repeat_between => '.between' }
142       )
143     ->to_html;
144
145 will produce:
146
147 =begin testinfo
148
149   my $expect = <<HTML;
150
151 =end testinfo
152
153   <html>
154     <head>
155       <title>Hello world &amp; dog!</title>
156     </head>
157     <body>
158       <h1 id="greeting">Hello world &amp; dog!</h1>
159       <div id="list">
160         <span>
161           <p>Name: <span class="name">Matt</span></p>
162           <p>Age: <span class="age">26</span></p>
163         </span>
164         <hr class="between" />
165         <span>
166           <p>Name: <span class="name">Mark</span></p>
167           <p>Age: <span class="age">0x29</span></p>
168         </span>
169         <hr class="between" />
170         <span>
171           <p>Name: <span class="name">Epitaph</span></p>
172           <p>Age: <span class="age">&lt;redacted&gt;</span></p>
173         </span>
174         
175       </div>
176     </body>
177   </html>
178
179 =begin testinfo
180
181   HTML
182   is($output, $expect, 'Synopsis code works ok');
183
184 =end testinfo
185
186 =head1 SOMETHING ELSE
187
188 =cut