blib/lib/Dancer/Template/TemplateFlute.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 7 | 9 | 77.7 |
branch | n/a | ||
condition | n/a | ||
subroutine | 3 | 3 | 100.0 |
pod | n/a | ||
total | 10 | 12 | 83.3 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package Dancer::Template::TemplateFlute; | ||||||
2 | |||||||
3 | 1 | 1 | 13246 | use strict; | |||
1 | 2 | ||||||
1 | 28 | ||||||
4 | 1 | 1 | 3 | use warnings; | |||
1 | 1 | ||||||
1 | 20 | ||||||
5 | |||||||
6 | 1 | 1 | 167 | use Template::Flute; | |||
0 | |||||||
0 | |||||||
7 | use Template::Flute::Iterator; | ||||||
8 | use Template::Flute::Utils; | ||||||
9 | use Template::Flute::I18N; | ||||||
10 | use Module::Load; | ||||||
11 | use Scalar::Util qw/blessed/; | ||||||
12 | |||||||
13 | use Dancer::Config; | ||||||
14 | |||||||
15 | use base 'Dancer::Template::Abstract'; | ||||||
16 | |||||||
17 | our $VERSION = '0.0142'; | ||||||
18 | |||||||
19 | =head1 NAME | ||||||
20 | |||||||
21 | Dancer::Template::TemplateFlute - Template::Flute wrapper for Dancer | ||||||
22 | |||||||
23 | =head1 VERSION | ||||||
24 | |||||||
25 | Version 0.0142 | ||||||
26 | |||||||
27 | =head1 DESCRIPTION | ||||||
28 | |||||||
29 | This class is an interface between Dancer's template engine abstraction layer | ||||||
30 | and the L |
||||||
31 | |||||||
32 | In order to use this engine, use the template setting: | ||||||
33 | |||||||
34 | template: template_flute | ||||||
35 | |||||||
36 | The default template extension is ".html". | ||||||
37 | |||||||
38 | =head2 LAYOUT | ||||||
39 | |||||||
40 | Each layout needs a specification file and a template file. To embed | ||||||
41 | the content of your current view into the layout, put the following | ||||||
42 | into your specification file, e.g. F |
||||||
43 | |||||||
44 | |
||||||
45 | |
||||||
46 | |||||||
47 | |||||||
48 | This replaces the contents of the following block in your HTML | ||||||
49 | template, e.g. F |
||||||
50 | |||||||
51 | |
||||||
52 | Your content | ||||||
53 | |||||||
54 | |||||||
55 | =head2 ITERATORS | ||||||
56 | |||||||
57 | Iterators can be specified explicitly in the configuration file as below. | ||||||
58 | |||||||
59 | engines: | ||||||
60 | template_flute: | ||||||
61 | iterators: | ||||||
62 | fruits: | ||||||
63 | class: JSON | ||||||
64 | file: fruits.json | ||||||
65 | |||||||
66 | =head2 FILTER OPTIONS | ||||||
67 | |||||||
68 | Filter options and classes can be specified in the configuration file as below. | ||||||
69 | |||||||
70 | engines: | ||||||
71 | template_flute: | ||||||
72 | filters: | ||||||
73 | currency: | ||||||
74 | options: | ||||||
75 | int_curr_symbol: "$" | ||||||
76 | image: | ||||||
77 | class: "Flowers::Filters::Image" | ||||||
78 | |||||||
79 | =head2 ADJUSTING URIS | ||||||
80 | |||||||
81 | We automatically adjust links in the templates if the value of | ||||||
82 | C |
||||||
83 | |||||||
84 | =head2 EMBEDDING IMAGES IN EMAILS | ||||||
85 | |||||||
86 | If you pass a value named C |
||||||
87 | reference, all the images C |
||||||
88 | the CIDs, and the reference will be populated with an hashref, as | ||||||
89 | documented in L |
||||||
90 | |||||||
91 | Further options for the CIDs should be passed in an optional value | ||||||
92 | named C |
||||||
93 | |||||||
94 | |||||||
95 | =head2 DISABLE OBJECT AUTODETECTION | ||||||
96 | |||||||
97 | Sometimes you want to pass values to a template which are objects, but | ||||||
98 | don't have an accessor, so they should be treated like hashrefs instead. | ||||||
99 | |||||||
100 | By default, the class C |
||||||
101 | can specify additional classes with the following syntax: | ||||||
102 | |||||||
103 | engines: | ||||||
104 | template_flute: | ||||||
105 | autodetect: | ||||||
106 | disable: | ||||||
107 | - My::Class1 | ||||||
108 | - My::Class2 | ||||||
109 | |||||||
110 | |||||||
111 | The class matching is checked by L |
||||||
112 | any parent class would do. | ||||||
113 | |||||||
114 | =head2 LOCALIZATION | ||||||
115 | |||||||
116 | Templates can be localized using the Template::Flute::I18N module. You | ||||||
117 | can define a class that provides a method which takes as first (and | ||||||
118 | only argument) the string to translate, and returns the translated | ||||||
119 | one. You have to provide the class and the method. If the class is not | ||||||
120 | provided, no localization is done. If no method is specified, | ||||||
121 | 'localize' will be used. The app will crash if the class doesn't | ||||||
122 | provide such method. | ||||||
123 | |||||||
124 | B | ||||||
125 | translate the string>. | ||||||
126 | |||||||
127 | Example configuration, assuming the class C |
||||||
128 | C |
||||||
129 | |||||||
130 | engines: | ||||||
131 | template_flute: | ||||||
132 | i18n: | ||||||
133 | class: MyApp::Lexicon | ||||||
134 | method: try_to_translate | ||||||
135 | |||||||
136 | |||||||
137 | A class could be something like this: | ||||||
138 | |||||||
139 | package MyTestApp::Lexicon; | ||||||
140 | use Dancer ':syntax'; | ||||||
141 | |||||||
142 | sub new { | ||||||
143 | my $class = shift; | ||||||
144 | debug "Loading up $class"; | ||||||
145 | my $self = { | ||||||
146 | dictionary => { | ||||||
147 | en => { | ||||||
148 | 'TRY' => 'Try', | ||||||
149 | }, | ||||||
150 | it => { | ||||||
151 | 'TRY' => 'Prova', | ||||||
152 | }, | ||||||
153 | } | ||||||
154 | }; | ||||||
155 | bless $self, $class; | ||||||
156 | } | ||||||
157 | |||||||
158 | sub dictionary { | ||||||
159 | return shift->{dictionary}; | ||||||
160 | } | ||||||
161 | |||||||
162 | sub try_to_translate { | ||||||
163 | my ($self, $string) = @_; | ||||||
164 | my $lang = session('lang') || var('lang'); | ||||||
165 | return $string unless $lang; | ||||||
166 | return $string unless $self->dictionary->{$lang}; | ||||||
167 | my $tr = $self->dictionary->{$lang}->{$string}; | ||||||
168 | defined $tr ? return $tr : return $string; | ||||||
169 | } | ||||||
170 | |||||||
171 | 1; | ||||||
172 | |||||||
173 | Optionally, you can pass the options to instantiate the class in the | ||||||
174 | configuration. Like this: | ||||||
175 | |||||||
176 | engines: | ||||||
177 | template_flute: | ||||||
178 | i18n: | ||||||
179 | class: MyApp::Lexicon | ||||||
180 | method: localize | ||||||
181 | options: | ||||||
182 | append: 'X' | ||||||
183 | prepend: 'Y' | ||||||
184 | lexicon: 'path/to/po/files' | ||||||
185 | |||||||
186 | This will call | ||||||
187 | |||||||
188 | MyApp::Lexicon->new(append => 'X', prepend => 'Y', lexicon => 'path/to/po/files'); | ||||||
189 | |||||||
190 | when the engine is initialized, and will call the C |
||||||
191 | on it to get the translations. | ||||||
192 | |||||||
193 | =head2 DEBUG TOOLS | ||||||
194 | |||||||
195 | If you set C |
||||||
196 | will run a check (using the L |
||||||
197 | C |
||||||
198 | of the specifications which are not bound to any HTML elements. | ||||||
199 | |||||||
200 | In this case a debug message is issued (so keep in mind that with | ||||||
201 | higher logging level you are not going to see it). | ||||||
202 | |||||||
203 | Example configuration: | ||||||
204 | |||||||
205 | engines: | ||||||
206 | template_flute: | ||||||
207 | check_dangling: 1 | ||||||
208 | |||||||
209 | When the environment is set to C |
||||||
210 | on by default. You can silence the logs by setting: | ||||||
211 | |||||||
212 | engines: | ||||||
213 | template_flute: | ||||||
214 | disable_check_dangling: 1 | ||||||
215 | |||||||
216 | =head2 FORMS | ||||||
217 | |||||||
218 | Dancer::Template::TemplateFlute includes a form plugin L |
||||||
219 | which supports L |
||||||
220 | |||||||
221 | The token C | ||||||
222 | L |
||||||
223 | L |
||||||
224 | |||||||
225 | =head3 Typical usage for a single form. | ||||||
226 | |||||||
227 | =head4 XML Specification | ||||||
228 | |||||||
229 | |
||||||
230 | |||||||
231 | |
||||||
232 | |
||||||
233 | |
||||||
234 | |||||||
235 | |||||||
236 | |||||||
237 | =head4 HTML | ||||||
238 | |||||||
239 | |||||||
240 | |||||||
241 | Info |
||||||
242 | |
||||||
243 | |
||||||
244 | |||||||
245 | |||||||
246 | |||||||
247 | |
||||||
248 | |||||||
249 | |||||||
250 | |||||||
251 | |
||||||
252 | |||||||
253 | |||||||
254 | |||||||
255 | |
||||||
256 | |||||||
257 | |||||||
258 | |||||||
259 | |||||||
260 | |||||||
261 | |||||||
262 | =head4 Code | ||||||
263 | |||||||
264 | any [qw/get post/] => '/register' => sub { | ||||||
265 | my $form = form('registration'); | ||||||
266 | my %values = %{$form->values}; | ||||||
267 | # VALIDATE, filter, etc. the values | ||||||
268 | $form->fill(\%values); | ||||||
269 | template register => {form => $form }; | ||||||
270 | }; | ||||||
271 | |||||||
272 | =head3 Usage example for multiple forms | ||||||
273 | |||||||
274 | =head4 Specification | ||||||
275 | |||||||
276 | |
||||||
277 | |||||||
278 | |
||||||
279 | |
||||||
280 | |
||||||
281 | |||||||
282 | |||||||
283 | |
||||||
284 | |
||||||
285 | |||||||
286 | |||||||
287 | |||||||
288 | =head4 HTML | ||||||
289 | |||||||
290 | Register |
||||||
291 | |||||||
292 | |||||||
293 | Info |
||||||
294 | |
||||||
295 | |
||||||
296 | |||||||
297 | |||||||
298 | |||||||
299 | |
||||||
300 | |||||||
301 | |||||||
302 | |||||||
303 | |
||||||
304 | |||||||
305 | |||||||
306 | |||||||
307 | |
||||||
308 | |||||||
309 | |||||||
310 | |||||||
311 | |||||||
312 | |||||||
313 | Login |
||||||
314 | |||||||
315 | |||||||
316 | Info |
||||||
317 | |
||||||
318 | |
||||||
319 | |||||||
320 | |||||||
321 | |||||||
322 | |
||||||
323 | |||||||
324 | |||||||
325 | |||||||
326 | |
||||||
327 | |||||||
328 | |||||||
329 | |||||||
330 | |||||||
331 | |||||||
332 | |||||||
333 | |||||||
334 | =head4 Code | ||||||
335 | |||||||
336 | any [qw/get post/] => '/multiple' => sub { | ||||||
337 | my $login = form('logintest'); | ||||||
338 | debug to_dumper({params}); | ||||||
339 | if (params->{login}) { | ||||||
340 | my %vals = %{$login->values}; | ||||||
341 | # VALIDATE %vals here | ||||||
342 | $login->fill(\%vals); | ||||||
343 | } | ||||||
344 | else { | ||||||
345 | # pick from session | ||||||
346 | $login->fill; | ||||||
347 | } | ||||||
348 | my $registration = form('registrationtest'); | ||||||
349 | if (params->{register}) { | ||||||
350 | my %vals = %{$registration->values}; | ||||||
351 | # VALIDATE %vals here | ||||||
352 | $registration->fill(\%vals); | ||||||
353 | } | ||||||
354 | else { | ||||||
355 | # pick from session | ||||||
356 | $registration->fill; | ||||||
357 | } | ||||||
358 | template multiple => { form => [ $login, $registration ] }; | ||||||
359 | }; | ||||||
360 | |||||||
361 | =head1 METHODS | ||||||
362 | |||||||
363 | =head2 default_tmpl_ext | ||||||
364 | |||||||
365 | Returns default template extension. | ||||||
366 | |||||||
367 | =head2 render TEMPLATE TOKENS | ||||||
368 | |||||||
369 | Renders template TEMPLATE with values from TOKENS. | ||||||
370 | |||||||
371 | =cut | ||||||
372 | |||||||
373 | sub default_tmpl_ext { | ||||||
374 | return 'html'; | ||||||
375 | } | ||||||
376 | |||||||
377 | sub _i18n_obj { | ||||||
378 | my $self = shift; | ||||||
379 | unless (exists $self->{_i18n_obj}) { | ||||||
380 | my $conf = $self->config; | ||||||
381 | my $localize; | ||||||
382 | if ($conf and exists $conf->{i18n} and exists $conf->{i18n}->{class}) { | ||||||
383 | my $class = $conf->{i18n}->{class}; | ||||||
384 | load $class; | ||||||
385 | my %args; | ||||||
386 | if ($conf->{i18n}->{options}) { | ||||||
387 | # do a shallow copy and pass that | ||||||
388 | %args = %{ $conf->{i18n}->{options} }; | ||||||
389 | } | ||||||
390 | my $obj = $class->new(%args); | ||||||
391 | my $method = $conf->{i18n}->{method} || 'localize'; | ||||||
392 | # store the closure in the object to avoid loading it up each time | ||||||
393 | $localize = sub { | ||||||
394 | my $to_translate = shift; | ||||||
395 | return $obj->$method($to_translate); | ||||||
396 | }; | ||||||
397 | } | ||||||
398 | # provide a common interface with Template::Flute::I18N | ||||||
399 | $self->{_i18n_obj} = Template::Flute::I18N->new($localize); | ||||||
400 | } | ||||||
401 | return $self->{_i18n_obj}; | ||||||
402 | } | ||||||
403 | |||||||
404 | |||||||
405 | sub render ($$$) { | ||||||
406 | my ($self, $template, $tokens) = @_; | ||||||
407 | my (%args, $flute, $html, $name, $value, %parms, %template_iterators, %iterators, $class); | ||||||
408 | |||||||
409 | %args = (template_file => $template, | ||||||
410 | scopes => 1, | ||||||
411 | auto_iterators => 1, | ||||||
412 | values => $tokens, | ||||||
413 | filters => $self->config->{filters}, | ||||||
414 | autodetect => { disable => [qw/Dancer::Session::Abstract/] }, | ||||||
415 | ); | ||||||
416 | |||||||
417 | # determine whether we need to pass an adjust URI to Template::Flute | ||||||
418 | if (my $request = $tokens->{request}) { | ||||||
419 | my $pos = index($request->path, $request->path_info); | ||||||
420 | if ($pos > 0) { | ||||||
421 | $args{uri} = substr($request->path, 0, $pos); | ||||||
422 | } | ||||||
423 | } | ||||||
424 | |||||||
425 | if (my $i18n = $self->_i18n_obj) { | ||||||
426 | $args{i18n} = $i18n; | ||||||
427 | } | ||||||
428 | |||||||
429 | if (my $email_cids = $tokens->{email_cids}) { | ||||||
430 | $args{email_cids} = $email_cids; | ||||||
431 | # use the 'cids' tokens only if email_cids is defined | ||||||
432 | if (my $cid_options = $tokens->{cids}) { | ||||||
433 | $args{cids} = { %$cid_options }; | ||||||
434 | } | ||||||
435 | } | ||||||
436 | |||||||
437 | if ($self->config->{autodetect} && $self->config->{autodetect}->{disable}) { | ||||||
438 | push @{$args{autodetect}{disable}}, | ||||||
439 | @{$self->config->{autodetect}->{disable}}; | ||||||
440 | } | ||||||
441 | |||||||
442 | $flute = Template::Flute->new(%args); | ||||||
443 | |||||||
444 | # process HTML template to determine iterators used by template | ||||||
445 | $flute->process_template(); | ||||||
446 | |||||||
447 | # instantiate iterators where object isn't yet available | ||||||
448 | if (%template_iterators = $flute->template()->iterators) { | ||||||
449 | my $selector; | ||||||
450 | |||||||
451 | for my $name (keys %template_iterators) { | ||||||
452 | if ($value = $self->config->{iterators}->{$name}) { | ||||||
453 | %parms = %$value; | ||||||
454 | |||||||
455 | $class = "Template::Flute::Iterator::$parms{class}"; | ||||||
456 | |||||||
457 | if ($parms{file}) { | ||||||
458 | $parms{file} = Template::Flute::Utils::derive_filename($template, | ||||||
459 | $parms{file}, 1); | ||||||
460 | } | ||||||
461 | |||||||
462 | if ($selector = delete $parms{selector}) { | ||||||
463 | if ($selector eq '*') { | ||||||
464 | $parms{selector} = '*'; | ||||||
465 | } | ||||||
466 | elsif ($tokens->{$selector}) { | ||||||
467 | $parms{selector} = {$selector => $tokens->{$selector}}; | ||||||
468 | } | ||||||
469 | } | ||||||
470 | |||||||
471 | eval "require $class"; | ||||||
472 | if ($@) { | ||||||
473 | die "Failed to load class $class for iterator $name: $@\n"; | ||||||
474 | } | ||||||
475 | |||||||
476 | eval { | ||||||
477 | $iterators{$name} = $class->new(%parms); | ||||||
478 | }; | ||||||
479 | |||||||
480 | if ($@) { | ||||||
481 | die "Failed to instantiate class $class for iterator $name: $@\n"; | ||||||
482 | } | ||||||
483 | |||||||
484 | $flute->specification->set_iterator($name, $iterators{$name}); | ||||||
485 | } | ||||||
486 | } | ||||||
487 | } | ||||||
488 | |||||||
489 | # check for forms | ||||||
490 | if (my @forms = $flute->template->forms()) { | ||||||
491 | if ($tokens->{form}) { | ||||||
492 | $self->_tf_manage_forms($flute, $tokens, @forms); | ||||||
493 | } | ||||||
494 | else { | ||||||
495 | Dancer::Logger::debug('Missing form parameters for forms ' . | ||||||
496 | join(", ", sort map { $_->name } @forms)); | ||||||
497 | } | ||||||
498 | } | ||||||
499 | elsif ($tokens->{form}) { | ||||||
500 | my $form_name = blessed($tokens->{form}) ? $tokens->{form}->name : $tokens->{form}; | ||||||
501 | |||||||
502 | Dancer::Logger::debug("Form $form_name passed, but no forms found in the template $template."); | ||||||
503 | } | ||||||
504 | |||||||
505 | $html = $flute->process(); | ||||||
506 | |||||||
507 | if ($self->config->{check_dangling} or | ||||||
508 | ($tokens->{settings}->{environment} eq 'development' && | ||||||
509 | !$self->config->{disable_check_dangling})) { | ||||||
510 | |||||||
511 | if (my @warnings = $flute->specification->dangling) { | ||||||
512 | foreach my $warn (@warnings) { | ||||||
513 | Dancer::Logger::debug('Found dangling element ' | ||||||
514 | . $warn->{type} . ' ' . $warn->{name} | ||||||
515 | . ' (' , $warn->{dump} , ')'); | ||||||
516 | } | ||||||
517 | } | ||||||
518 | } | ||||||
519 | return $html; | ||||||
520 | } | ||||||
521 | |||||||
522 | sub _tf_manage_forms { | ||||||
523 | my ($self, $flute, $tokens, @forms) = @_; | ||||||
524 | |||||||
525 | # simple case: only one form passed and one in the flute | ||||||
526 | if (ref($tokens->{form}) ne 'ARRAY') { | ||||||
527 | my $form_name = $tokens->{form}->name; | ||||||
528 | if (@forms == 1) { | ||||||
529 | my $form = shift @forms; | ||||||
530 | if ($form_name eq 'main' or | ||||||
531 | $form_name eq $form->name) { | ||||||
532 | # Dancer::Logger::debug("Filling the template form with" . Dumper($tokens->{form}->values)); | ||||||
533 | $self->_tf_fill_forms($flute, $tokens->{form}, $form, $tokens); | ||||||
534 | } | ||||||
535 | } | ||||||
536 | else { | ||||||
537 | my $found = 0; | ||||||
538 | foreach my $form (@forms) { | ||||||
539 | # Dancer::Logger::debug("Filling the template form with" . Dumper($tokens->{form}->values)); | ||||||
540 | if ($form_name eq $form->name) { | ||||||
541 | $self->_tf_fill_forms($flute, $tokens->{form}, $form, $tokens); | ||||||
542 | $found++; | ||||||
543 | } | ||||||
544 | } | ||||||
545 | if ($found != 1) { | ||||||
546 | Dancer::Logger::error("Multiple form are not being managed correctly, found $found corresponding forms, but we expected just one!") | ||||||
547 | } | ||||||
548 | } | ||||||
549 | } | ||||||
550 | else { | ||||||
551 | foreach my $passed_form (@{$tokens->{form}}) { | ||||||
552 | foreach my $form (@forms) { | ||||||
553 | if ($passed_form->name eq $form->name) { | ||||||
554 | $self->_tf_fill_forms($flute, $passed_form, $form, $tokens); | ||||||
555 | } | ||||||
556 | } | ||||||
557 | } | ||||||
558 | } | ||||||
559 | } | ||||||
560 | |||||||
561 | |||||||
562 | sub _tf_fill_forms { | ||||||
563 | my ($self, $flute, $passed_form, $form, $tokens) = @_; | ||||||
564 | # arguments: | ||||||
565 | # $flute is the template object. | ||||||
566 | |||||||
567 | # $passed_form is the Dancer::Plugin::Form object we got from the | ||||||
568 | # tokens, which is $tokens->{form} when we have just a single one. | ||||||
569 | |||||||
570 | # $form is the form object we got from the template itself, with | ||||||
571 | # $flute->template->forms | ||||||
572 | |||||||
573 | # $tokens is the hashref passed to the template. We need it for the | ||||||
574 | # iterators. | ||||||
575 | |||||||
576 | my ($iter, $action); | ||||||
577 | for my $name ($form->iterators) { | ||||||
578 | if (ref($tokens->{$name}) eq 'ARRAY') { | ||||||
579 | $iter = Template::Flute::Iterator->new($tokens->{$name}); | ||||||
580 | $flute->specification->set_iterator($name, $iter); | ||||||
581 | } | ||||||
582 | } | ||||||
583 | if ($action = $passed_form->action()) { | ||||||
584 | $form->set_action($action); | ||||||
585 | } | ||||||
586 | $passed_form->fields([map {$_->{name}} @{$form->fields()}]); | ||||||
587 | $form->fill($passed_form->fill()); | ||||||
588 | |||||||
589 | if (Dancer::Config::settings->{session}) { | ||||||
590 | $passed_form->to_session; | ||||||
591 | } | ||||||
592 | } | ||||||
593 | |||||||
594 | |||||||
595 | =head1 SEE ALSO | ||||||
596 | |||||||
597 | L |
||||||
598 | |||||||
599 | =head1 AUTHOR | ||||||
600 | |||||||
601 | Stefan Hornburg (Racke), |
||||||
602 | |||||||
603 | =head1 BUGS | ||||||
604 | |||||||
605 | Please report any bugs or feature requests to C |
||||||
606 | the web interface at L |
||||||
607 | |||||||
608 | =head1 SUPPORT | ||||||
609 | |||||||
610 | You can find documentation for this module with the perldoc command. | ||||||
611 | |||||||
612 | perldoc Template::Flute | ||||||
613 | |||||||
614 | You can also look for information at: | ||||||
615 | |||||||
616 | =over 4 | ||||||
617 | |||||||
618 | =item * RT: CPAN's request tracker | ||||||
619 | |||||||
620 | L |
||||||
621 | |||||||
622 | =item * AnnoCPAN: Annotated CPAN documentation | ||||||
623 | |||||||
624 | L |
||||||
625 | |||||||
626 | =item * CPAN Ratings | ||||||
627 | |||||||
628 | L |
||||||
629 | |||||||
630 | =item * Search CPAN | ||||||
631 | |||||||
632 | L |
||||||
633 | |||||||
634 | =back | ||||||
635 | |||||||
636 | =head1 LICENSE AND COPYRIGHT | ||||||
637 | |||||||
638 | Copyright 2011-2015 Stefan Hornburg (Racke) |
||||||
639 | |||||||
640 | This program is free software; you can redistribute it and/or modify it | ||||||
641 | under the terms of either: the GNU General Public License as published | ||||||
642 | by the Free Software Foundation; or the Artistic License. | ||||||
643 | |||||||
644 | See http://dev.perl.org/licenses/ for more information. | ||||||
645 | |||||||
646 | =cut | ||||||
647 | |||||||
648 | 1; |