few extra utility routines
[catagits/HTML-Zoom.git] / lib / HTML / Zoom.pm
CommitLineData
d80786d0 1package HTML::Zoom;
2
3use strict;
4use warnings FATAL => 'all';
5
6use HTML::Zoom::ZConfig;
7use HTML::Zoom::MatchWithoutFilter;
bf5a23d0 8use HTML::Zoom::ReadFH;
d80786d0 9
10sub new {
11 my ($class, $args) = @_;
12 my $new = {};
13 $new->{zconfig} = HTML::Zoom::ZConfig->new($args->{zconfig}||{});
14 bless($new, $class);
15}
16
17sub zconfig { shift->_self_or_new->{zconfig} }
18
19sub _self_or_new {
20 ref($_[0]) ? $_[0] : $_[0]->new
21}
22
23sub _with {
24 bless({ %{$_[0]}, %{$_[1]} }, ref($_[0]));
25}
26
27sub 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
bf5a23d0 34sub from_file {
35 my $self = shift->_self_or_new;
36 my $filename = shift;
37 $self->from_html(do { local (@ARGV, $/) = ($filename); <> });
38}
39
d80786d0 40sub 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
bf5a23d0 52sub to_fh {
53 HTML::Zoom::ReadFH->from_zoom(shift);
54}
55
56sub run {
57 my $self = shift;
58 $self->zconfig->stream_utils->stream_to_array($self->to_stream);
59 return
60}
61
62sub apply {
63 my ($self, $code) = @_;
64 local $_ = $self;
65 $self->$code;
66}
67
d80786d0 68sub to_html {
69 my $self = shift;
70 $self->zconfig->producer->html_from_stream($self->to_stream);
71}
72
73sub memoize {
74 my $self = shift;
75 ref($self)->new($self)->from_html($self->to_html);
76}
77
78sub 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
86sub 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
105sub 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
112sub 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
1181;
119
120=head1 NAME
121
122HTML::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
168will 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