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; |
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 & dog!</title> |
156 | </head> |
157 | <body> |
158 | <h1 id="greeting">Hello world & 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"><redacted></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 |