comparison Lib/IMPL/Web/Handler/TTView.pm @ 285:546957c50a36

*IMPL::Web::Handler::TTView Reworked template selection mechanism *IMPL::Web::Application: refactoring -Removed obsolete IMPL::Text modules
author cin
date Mon, 18 Feb 2013 02:55:59 +0400
parents a8dbddf491dd
children 5d14baa35790
comparison
equal deleted inserted replaced
284:f2a6bc5f3184 285:546957c50a36
1 package IMPL::Web::Handler::TTView; 1 package IMPL::Web::Handler::TTView;
2 use strict; 2 use strict;
3 3
4 use List::Util qw(first); 4 use List::Util qw(first);
5 use IMPL::lang;
5 use IMPL::Const qw(:prop); 6 use IMPL::Const qw(:prop);
6 use IMPL::declare { 7 use IMPL::declare {
7 require => { 8 require => {
8 Factory => 'IMPL::Web::View::ObjectFactory', 9 Factory => 'IMPL::Web::View::ObjectFactory',
9 HttpResponse => 'IMPL::Web::HttpResponse', 10 HttpResponse => 'IMPL::Web::HttpResponse',
18 19
19 props => [ 20 props => [
20 contentType => PROP_RO, 21 contentType => PROP_RO,
21 contentCharset => PROP_RO, 22 contentCharset => PROP_RO,
22 loader => PROP_RO, 23 loader => PROP_RO,
23 selectors => PROP_RO | PROP_LIST, 24 selectors => PROP_RO,
24 defaultDocument => PROP_RW, 25 defaultDocument => PROP_RW,
25 indexResource => PROP_RW, 26 _selectorsCache => PROP_RW
26 _selectorsCache => PROP_RW,
27 _classTemplates => PROP_RW
28 ] 27 ]
29 }; 28 };
30 29
31 sub CTOR { 30 sub CTOR {
32 my ($this) = @_; 31 my ($this) = @_;
33 32
34 $this->indexResource('index') unless $this->indexResource; 33 $this->_selectorsCache([ map $this->ParseRule($_), @{$this->selectors || []} ]);
35 } 34 }
36 35
37 sub Invoke { 36 sub Invoke {
38 my ( $this, $action, $next ) = @_; 37 my ( $this, $action, $next ) = @_;
39 38
51 my $vars = { 50 my $vars = {
52 view => $view, 51 view => $view,
53 model => $model, 52 model => $model,
54 action => $action, 53 action => $action,
55 app => $action->application, 54 app => $action->application,
55 env => _cached($action->context->{environment}),
56 ImportClass => sub { 56 ImportClass => sub {
57 my $class = shift; 57 my $class = shift;
58 58
59 $class = Loader->safe->Require($class); 59 $class = Loader->safe->Require($class);
60 60
81 return HttpResponse->new( 81 return HttpResponse->new(
82 %responseParams 82 %responseParams
83 ); 83 );
84 } 84 }
85 85
86 sub _cached {
87 my $arg = shift;
88
89 return $arg unless ref $arg eq 'CODE';
90
91 return sub {
92 ref $arg eq 'CODE' ? $arg = &$arg() : $arg;
93 }
94 }
95
86 sub SelectView { 96 sub SelectView {
87 my ( $this, $action, $class ) = @_; 97 my ($this,$action) = @_;
88 98
89 my @path = split /\//, $action->query->path_info(), -1; 99 my @path;
90 100
91 shift @path; # remove always empty leading segment 101 for(my $r = $action->context->{resource}; $r ; $r = $r->parent ) {
92 102 unshift @path, {
93 $this->BuildCache unless $this->_selectorsCache; 103 name => $r->id,
94 my $cache = $this->_selectorsCache; 104 class => typeof($r->model)
95 105 };
96 @path = reverse @path; 106 }
97 107
98 foreach 108 @path = map { name => $_}, split /\/+/, $action->query->path_info()
99 my $subclass ( $class ? ( _GetHierarchy($class), '-default' ) : '-default' ) 109 unless (@path);
100 { 110
101 my @results; 111 return $this->MatchPath(\@path,$this->_selectorsCache) || $this->defaultDocument;
102 push @results, 112 }
103 { result => $this->_classTemplates->{$subclass}, level => 0 } 113
104 if $this->_classTemplates->{$subclass}; 114 sub ParseRule {
105 115 my ($this, $rule) = @_;
106 if ( $cache->{$subclass} ) { 116
107 my $alternatives = 117 my ($selector,$data) = split /\s+=>\s+/, $rule;
108 [ { selector => $cache->{$subclass}, immediate => 1 } ]; 118
109 $alternatives = 119 my @parts;
110 $this->MatchAlternatives( $_, $alternatives, \@results ) 120 my $first = 1;
111 foreach @path; 121 my $weight = 0;
112 } 122 foreach my $part ( split /\//, $selector ) {
113 123 # если первым символом является /
114 if (@results) { 124 # значит путь в селекторе абсолютный и не нужно
115 @results = sort { $b->{level} <=> $a->{level} } @results; 125 # добавлять "любой" элемент в начало
116 return ( shift @results )->{result}; 126
117 } 127 if($part) {
118 } 128 $weight ++;
119 129 push @parts,{ any => 1 } if $first;
120 return $this->defaultDocument; 130 } else {
121 } 131 push @parts,{ any => 1 } unless $first;
122 132 next;
123 sub _GetHierarchy { 133 }
124 my ($class) = @_; 134
125 return unless $class; 135 my ($name,$class) = split /@/, $part;
126 136
127 no strict 'refs'; 137 if ( my ( $varName, $rx ) = ( $name =~ m/^\{(?:(\w+)\:)?(.*)\}$/ ) ) {
128 138 #this is a regexp
129 return $class, map { _GetHierarchy($_) } @{"${class}::ISA"}; 139
130 } 140 push @parts, {
131 141 rx => $rx,
132 sub BuildCache { 142 var => $varName,
133 my ($this) = @_; 143 class => $class,
134 144 };
135 my @selectors; 145 } else {
136 146 push @parts, {
137 my $cache = $this->_selectorsCache( {} ); 147 name => length($name) ? $name : undef,
138 $this->_classTemplates( {} ); 148 class => $class,
139 149 };
140 foreach my $selector ( $this->selectors ) { 150 }
141 if ( not ref $selector ) { 151 } continue {
142 152 $first = 0;
143 my ( $path, $data ) = split( /\s*=>\s*/, $selector ); 153 }
144 154
145 my @path = split( /\s+/, $path ); 155 return { selector => \@parts, data => $data, weight => $weight };
146 156 }
147 my $class; 157
148 158 sub MatchPath {
149 # if this selector has a class part 159 my ($this,$path,$rules) = @_;
150 if ( $path[$#path] =~ m/^\@(.*)/ ) { 160
151 $class = $1; 161 $path ||= [];
152 pop @path; 162 $rules ||= [];
163
164 my @next;
165
166 foreach my $segment (@$path) {
167 foreach my $rule (@$rules) {
168 my @selector = @{$rule->{selector}};
169
170 my $part = shift @selector;
171
172 if ($part->{any}) {
173 #keep the rule for the next try
174 push @next, $rule;
175
176 $part = shift @selector while $part->{any};
153 } 177 }
154 else { 178
155 $class = '-default'; 179 # if this rule doesn't have a selector
180 next unless $part;
181
182 my $newRule = {
183 selector => \@selector,
184 data => $rule->{data},
185 weight => $rule->{weight},
186 vars => { %{$rule->{vars} || {}} }
187 };
188
189 my $success = 1;
190 if (my $class = $part->{class}) {
191 $success = isclass($segment->{class},$class);
156 } 192 }
157 193
158 #if this selector has a path 194 if($success && (my $name = $part->{name})) {
159 if (@path) { 195 $success = $segment->{name} eq $name;
160 @path = reverse @path; 196 } elsif ($success && (my $rx = $part->{rx})) {
161 my $last = pop @path; 197 if( my @captures = ($segment->{name} =~ m/($rx)/) ) {
162 my $t = ( $cache->{$class} ||= {} ); 198 $newRule->{vars}->{$part->{var}} = \@captures
163 my $level = 1; 199 if $part->{var};
164 foreach my $prim (@path) { 200 } else {
165 $t = ( $t->{$prim}->{next} ||= {} ); 201 $success = 0;
166 $level++;
167 }
168 $t->{$last}->{level} = $level;
169 $t->{$last}->{data} = $data;
170
171 }
172 else {
173
174 # we dont have a selector, only class
175
176 $this->_classTemplates->{$class} = $data;
177 }
178
179 }
180 }
181 }
182
183 sub MatchAlternatives {
184 my ( $this, $segment, $alternatives, $results ) = @_;
185
186 my @next;
187
188 foreach my $alt (@$alternatives) {
189 while ( my ( $selector, $match ) = each %{ $alt->{selector} } ) {
190
191 my $context = {
192 vars => \%{ $alt->{vars} || {} },
193 selector => $match->{next}
194 };
195
196 if ( $selector =~ s/^>// ) {
197 $context->{immediate} = 1;
198 }
199
200 if ( my ( $name, $rx ) =
201 ( $selector =~ m/^\{(?:(\w+)\:)?(.*)\}$/ ) )
202 {
203
204 #this is a regexp
205
206 if ( my @captures = ( $segment =~ m/($rx)/ ) ) {
207 $context->{success} = 1;
208
209 if ($name) {
210 $context->{vars}->{$name} = \@captures;
211 }
212 } 202 }
213 } 203 }
214 else { 204
215 205 push @next, $newRule if $success;
216 #this is a segment name 206
217 if ( $segment eq $selector ) { 207 }
218 $context->{success} = 1; 208 $rules = [@next];
219 } 209 undef @next;
210 }
211
212 my $result = (
213 sort {
214 $b->{weight} <=> $a->{weight}
215 }
216 grep {
217 scalar(@{$_->{selector}}) == 0
218 }
219 @$rules
220 )[0];
221
222 if($result) {
223 my $data = $result->{data};
224 $data =~ s/{(\w+)(?:\:(\d+))?}/
225 my ($name,$index) = ($1,$2 || 0);
226
227 if ($result->{vars}{$name}) {
228 $result->{vars}{$name}[$index];
229 } else {
230 "";
220 } 231 }
221 232 /gex;
222 # test if there were a match 233
223 if ( delete $context->{success} ) { 234 return $data;
224 if ( my $data = $match->{data} ) { 235 } else {
225 236 return;
226 # interpolate data 237 }
227 $data =~ s/{(\w+)(?:\:(\d+))?}/
228 my ($name,$index) = ($1,$2 || 0);
229
230 if ($context->{vars}{$name}) {
231 $context->{vars}{$name}[$index];
232 } else {
233 "";
234 }
235 /gex;
236
237 push @$results,
238 { level => $match->{level}, result => $data };
239 }
240 push @next, $context if $context->{selector};
241 }
242 else {
243
244 #repeat current alternative if it's not required to be immediate
245 push @next,
246 {
247 selector => { $selector, $match },
248 vars => $alt->{vars}
249 }
250 unless $alt->{immediate};
251 }
252 }
253 }
254
255 return \@next;
256 } 238 }
257 239
258 1; 240 1;
259 241
260 __END__ 242 __END__
299 281
300 Подбирает шаблон для представления результата, полученного при выполнении следующего обработчика. При 282 Подбирает шаблон для представления результата, полученного при выполнении следующего обработчика. При
301 выборе используется принцип похожий на селекторы C<CSS>, основывающийся на именах ресурсов и их типах 283 выборе используется принцип похожий на селекторы C<CSS>, основывающийся на именах ресурсов и их типах
302 данных. 284 данных.
303 285
286 Данный обработчик понимает определенные свойства контекста:
287
288 =over
289
290 =item * C<resourceLocation>
291
292 В данном свойстве может быть передана информация о текущем расположении ресурса,
293 для которого строится представление. Эта информация будет доступна в шаблоне
294 через свойство документа C<location>.
295
296 =item * C<environment>
297
298 В данном совойстве контекста передается дополнительная информация об окружении
299 ресурса, например, которую задали родительские ресурсы. Использование данного
300 свойства позволяет не загромождать ресурс реализацией функциональности по
301 поддержке окружения. Это свойство может быть ссылкой на функцию, что позволяет
302 формировать контекст только по необходимости, при этом указанная функция будет
303 выполнена только один раз, при первом обращении.
304
305 =back
306
304 =head1 SELECTORS 307 =head1 SELECTORS
305 308
306 =begin text 309 =begin text
307 310
308 [url-template] [class] => template 311 syntax::= selector => template
312
313 selector::= ([>]segment-template[@class-name])
314
315 segment-template::= {'{'name:regular-expr'}'|segment-name}
316
317 name::= \w+
318
319 segment-name::= \S+
320
321 class-name::= name[(::name)]
322
323 url-template@class => template
309 324
310 shoes => product/list 325 shoes => product/list
311 {action:*.} @My::Data::Product => product/{action} 326 /shop//{action:*.}@My::Data::Product => product/{action}
312 327
313 stuff >list => product/list 328 stuff >list => product/list
314 details => product/details 329 details => product/details
315 330
316 =end text 331 =end text