Commit | Line | Data |
d80786d0 |
1 | package HTML::Zoom; |
2 | |
3 | use strict; |
4 | use warnings FATAL => 'all'; |
5 | |
6 | use HTML::Zoom::ZConfig; |
7 | use HTML::Zoom::MatchWithoutFilter; |
bf5a23d0 |
8 | use HTML::Zoom::ReadFH; |
d80786d0 |
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 | |
bf5a23d0 |
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 | |
d80786d0 |
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 | |
bf5a23d0 |
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 | |
d80786d0 |
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 & dog!</title> |
179 | </head> |
180 | <body> |
181 | <h1 id="greeting">Hello world & 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"><redacted></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 |