add stub synopsis.t to fail if you forgot to extract the real one
[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;
8
9sub new {
10 my ($class, $args) = @_;
11 my $new = {};
12 $new->{zconfig} = HTML::Zoom::ZConfig->new($args->{zconfig}||{});
13 bless($new, $class);
14}
15
16sub zconfig { shift->_self_or_new->{zconfig} }
17
18sub _self_or_new {
19 ref($_[0]) ? $_[0] : $_[0]->new
20}
21
22sub _with {
23 bless({ %{$_[0]}, %{$_[1]} }, ref($_[0]));
24}
25
26sub 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
33sub 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
45sub to_html {
46 my $self = shift;
47 $self->zconfig->producer->html_from_stream($self->to_stream);
48}
49
50sub memoize {
51 my $self = shift;
52 ref($self)->new($self)->from_html($self->to_html);
53}
54
55sub 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
63sub 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
82sub 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
89sub 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
951;
96
97=head1 NAME
98
99HTML::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
145will 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