/
Jemplate.pm
680 lines (505 loc) · 18.9 KB
/
Jemplate.pm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
##
# name: Jemplate
# abstract: JavaScript Templating with Template Toolkit
# author: Ingy döt Net <ingy@cpan.org>
# license: perl
# copyright: 2006-2008, 2011
# ToDo:
# - Module::Package
# - Stardoc
# - Use TT:Simple in Makefiles
package Jemplate;
use 5.006001;
use strict;
use warnings;
use Template 2.14;
use Getopt::Long;
our $VERSION = '0.27';
use Jemplate::Parser;
#-------------------------------------------------------------------------------
sub usage {
<<'...';
Usage:
jemplate --runtime [runtime-opt]
jemplate --compile [compile-opt] <template-list>
jemplate --runtime [runtime-opt] --compile [compile-opt] <template-list>
jemplate --list <template-list>
Where "--runtime" and "runtime-opt" can include:
--runtime Equivalent to --ajax=ilinsky --json=json2
--runtime=standard
--runtime=lite Same as --ajax=none --json=none
--runtime=jquery Same as --ajax=jquery --json=none
--runtime=yui Same as --ajax=yui --json=yui
--runtime=legacy Same as --ajax=gregory --json=json2
--json By itself, equivalent to --json=json2
--json=json2 Include http://www.json.org/json2.js for parsing/stringifying
--json=yui Use YUI: YAHOO.lang.JSON (requires external YUI)
--json=none Doesn't provide any JSON functionality except a warning
--ajax By itself, equivalent to --ajax=xhr
--ajax=jquery Use jQuery for Ajax get and post (requires external jQuery)
--ajax=yui Use YUI: yui/connection/connection.js (requires external YUI)
--ajax=xhr Use XMLHttpRequest (will automatically use --xhr=ilinsky if --xhr is not set)
--ajax=none Doesn't provide any Ajax functionality except a warning
--xhr By itself, equivalent to --xhr=ilinsky
--xhr=ilinsky Include http://code.google.com/p/xmlhttprequest/
--xhr=gregory Include http://www.scss.com.au/family/andrew/webdesign/xmlhttprequest/
--xxx Include XXX and JJJ helper functions
--compact Use the YUICompressor compacted version of the runtime
Where "compile-opt" can include:
--start-tag
--end-tag
--pre-chomp
--post-chomp
--trim
--any-case
--eval
--noeval
-s, --source
For more information use:
perldoc jemplate
...
}
sub main {
my $class = shift;
my @argv = @_;
my ($template_options, $jemplate_options) = get_options(@argv);
my ($runtime, $compile, $list) = @$jemplate_options{qw/runtime compile list/};
if ($runtime) {
print runtime_source_code(@$jemplate_options{qw/runtime ajax json xhr xxx compact/});
return unless $compile;
}
my $templates = make_file_list(@argv);
print_usage_and_exit() unless @$templates;
if ($list) {
foreach (@$templates) {
print STDOUT $_->{short} . "\n";
}
return;
}
if ($compile) {
my $jemplate = Jemplate->new(%$template_options);
print STDOUT $jemplate->_preamble;
foreach my $template (@$templates) {
my $content = slurp($template->{full});
if ($content) {
print STDOUT $jemplate->compile_template_content(
$content,
$template->{short},
);
}
}
return;
}
print_usage_and_exit();
}
sub get_options {
local @ARGV = @_;
my $runtime;
my $compile = 0;
my $list = 0;
my $start_tag = exists $ENV{JEMPLATE_START_TAG}
? $ENV{JEMPLATE_START_TAG}
: undef;
my $end_tag = exists $ENV{JEMPLATE_END_TAG}
? $ENV{JEMPLATE_END_TAG}
: undef;
my $pre_chomp = exists $ENV{JEMPLATE_PRE_CHOMP}
? $ENV{JEMPLATE_PRE_CHOMP}
: undef;
my $post_chomp = exists $ENV{JEMPLATE_POST_CHOMP}
? $ENV{JEMPLATE_POST_CHOMP}
: undef;
my $trim = exists $ENV{JEMPLATE_TRIM}
? $ENV{JEMPLATE_TRIM}
: undef;
my $anycase = exists $ENV{JEMPLATE_ANYCASE}
? $ENV{JEMPLATE_ANYCASE}
: undef;
my $eval_javascript = exists $ENV{JEMPLATE_EVAL_JAVASCRIPT}
? $ENV{JEMPLATE_EVAL_JAVASCRIPT}
: 1;
my $source = 0;
my ($ajax, $json, $xxx, $xhr, $compact, $minify);
my $help = 0;
GetOptions(
"compile|c" => \$compile,
"list|l" => \$list,
"runtime|r:s" => \$runtime,
"start-tag=s" => \$start_tag,
"end-tag=s" => \$end_tag,
"trim=s" => \$trim,
"pre-chomp" => \$pre_chomp,
"post-chomp" => \$post_chomp,
"any-case" => \$anycase,
"eval!" => \$eval_javascript,
"source|s" => \$source,
"ajax:s" => \$ajax,
"json:s" => \$json,
"xxx" => \$xxx,
"xhr:s" => \$xhr,
"compact" => \$compact,
"minify:s" => \$minify,
"help|?" => \$help,
) or print_usage_and_exit();
if ($help) {
print_usage_and_exit();
}
($runtime, $ajax, $json, $xxx, $xhr, $minify) = map { defined $_ && ! length $_ ? 1 : $_ } ($runtime, $ajax, $json, $xxx, $xhr, $minify);
$runtime = "standard" if $runtime && $runtime eq 1;
print_usage_and_exit("Don't understand '--runtime $runtime'") if defined $runtime && ! grep { $runtime =~ m/$_/ } qw/standard lite jquery yui legacy/;
print_usage_and_exit("Can't specify --list with a --runtime and/or the --compile option") if $list && ($runtime || $compile);
print_usage_and_exit() unless $list || $runtime || $compile;
my $command =
$runtime ? 'runtime' :
$compile ? 'compile' :
$list ? 'list' :
print_usage_and_exit();
my $options = {};
$options->{START_TAG} = $start_tag if defined $start_tag;
$options->{END_TAG} = $end_tag if defined $end_tag;
$options->{PRE_CHOMP} = $pre_chomp if defined $pre_chomp;
$options->{POST_CHOMP} = $post_chomp if defined $post_chomp;
$options->{TRIM} = $trim if defined $trim;
$options->{ANYCASE} = $anycase if defined $anycase;
$options->{EVAL_JAVASCRIPT} = $eval_javascript if defined $eval_javascript;
return (
$options,
{ compile => $compile, runtime => $runtime, list => $list,
source => $source,
ajax => $ajax, json => $json, xxx => $xxx, xhr => $xhr,
compact => $compact, minify => $minify },
);
}
sub slurp {
my $filepath = shift;
open(F, '<', $filepath) or die "Can't open '$filepath' for input:\n$!";
my $contents = do {local $/; <F>};
close(F);
return $contents;
}
sub recurse_dir {
require File::Find::Rule;
my $dir = shift;
my @files;
foreach ( File::Find::Rule->file->in( $dir ) ) {
if ( m{/\.[^\.]+} ) {} # Skip ".hidden" files or directories
else {
push @files, $_;
}
}
return @files;
}
sub make_file_list {
my @args = @_;
my @list;
foreach my $arg (@args) {
unless (-e $arg) { next; } # file exists
unless (-s $arg or -d $arg) { next; } # file size > 0 or directory (for Win platform)
if (-d $arg) {
foreach my $full ( recurse_dir($arg) ) {
$full =~ /$arg(\/|)(.*)/;
my $short = $2;
push(@list, {full=>$full, short=>$short} );
}
}
else {
my $full = $arg;
my $short = $full;
$short =~ s/.*[\/\\]//;
push(@list, {full=>$arg, short=>$short} );
}
}
return [ sort { $a->{short} cmp $b->{short} } @list ];
}
sub print_usage_and_exit {
print STDOUT join "\n", "", @_, "Aborting!", "\n" if @_;
print STDOUT usage();
exit;
}
sub runtime_source_code {
require Jemplate::Runtime;
require Jemplate::Runtime::Compact;
unshift @_, "standard" unless @_;
my ($runtime, $ajax, $json, $xhr, $xxx, $compact) = map { defined $_ ? lc $_ : "" } @_[0 .. 5];
my $Jemplate_Runtime = $compact ? "Jemplate::Runtime::Compact" : "Jemplate::Runtime";
if ($runtime eq "standard") {
$ajax ||= "xhr";
$json ||= "json2";
$xhr ||= "ilinsky";
}
elsif ($runtime eq "jquery") {
$ajax ||= "jquery";
}
elsif ($runtime eq "yui") {
$ajax ||= "yui";
$json ||= "yui";
}
elsif ($runtime eq "legacy") {
$ajax ||= "xhr";
$json ||= "json2";
$xhr ||= "gregory";
$xxx = 1;
}
elsif ($runtime eq "lite") {
}
$ajax = "xhr" if $ajax eq 1;
$xhr ||= 1 if $ajax eq "xhr";
$json = "json2" if $json eq 1;
$xhr = "ilinsky" if $xhr eq 1;
my @runtime;
push @runtime, $Jemplate_Runtime->kernel if $runtime;
push @runtime, $Jemplate_Runtime->json2 if $json =~ m/^json2?$/i;
push @runtime, $Jemplate_Runtime->ajax_xhr if $ajax eq "xhr";
push @runtime, $Jemplate_Runtime->ajax_jquery if $ajax eq "jquery";
push @runtime, $Jemplate_Runtime->ajax_yui if $ajax eq "yui";
push @runtime, $Jemplate_Runtime->json_json2 if $json =~ m/^json2?$/i;
push @runtime, $Jemplate_Runtime->json_json2_internal if $json =~ m/^json2?[_-]?internal$/i;
push @runtime, $Jemplate_Runtime->json_yui if $json eq "yui";
push @runtime, $Jemplate_Runtime->xhr_ilinsky if $xhr eq "ilinsky";
push @runtime, $Jemplate_Runtime->xhr_gregory if $xhr eq "gregory";
push @runtime, $Jemplate_Runtime->xxx if $xxx;
return join ";", @runtime;
}
#-------------------------------------------------------------------------------
sub new {
my $class = shift;
return bless { @_ }, $class;
}
sub compile_module {
my ($self, $module_path, $template_file_paths) = @_;
my $result = $self->compile_template_files(@$template_file_paths)
or return;
open MODULE, "> $module_path"
or die "Can't open '$module_path' for output:\n$!";
print MODULE $result;
close MODULE;
return 1;
}
sub compile_module_cached {
my ($self, $module_path, $template_file_paths) = @_;
my $m = -M $module_path;
return 0 unless grep { -M($_) < $m } @$template_file_paths;
return $self->compile_module($module_path, $template_file_paths);
}
sub compile_template_files {
my $self = shift;
my $output = $self->_preamble;
for my $filepath (@_) {
my $filename = $filepath;
$filename =~ s/.*[\/\\]//;
open FILE, $filepath
or die "Can't open '$filepath' for input:\n$!";
my $template_input = do {local $/; <FILE>};
close FILE;
$output .=
$self->compile_template_content($template_input, $filename);
}
return $output;
}
sub compile_template_content {
die "Invalid arguments in call to Jemplate->compile_template_content"
unless @_ == 3;
my ($self, $template_content, $template_name) = @_;
my $parser = Jemplate::Parser->new( ref($self) ? %$self : () );
my $parse_tree = $parser->parse(
$template_content, {name => $template_name}
) or die $parser->error;
my $output =
"Jemplate.templateMap['$template_name'] = " .
$parse_tree->{BLOCK} .
"\n";
for my $function_name (sort keys %{$parse_tree->{DEFBLOCKS}}) {
$output .=
"Jemplate.templateMap['$function_name'] = " .
$parse_tree->{DEFBLOCKS}{$function_name} .
"\n";
}
return $output;
}
sub _preamble {
return <<'...';
/*
This JavaScript code was generated by Jemplate, the JavaScript
Template Toolkit. Any changes made to this file will be lost the next
time the templates are compiled.
Copyright 2006-2008 - Ingy döt Net - All rights reserved.
*/
var Jemplate;
if (typeof(exports) == 'object') {
Jemplate = require("jemplate").Jemplate;
}
if (typeof(Jemplate) == 'undefined')
throw('Jemplate.js must be loaded before any Jemplate template files');
...
}
1;
=head1 SYNOPSIS
var data = Ajax.get('url/data.json');
var elem = document.getElementById('some-div');
elem.innerHTML = Jemplate.process('my-template.html', data);
or:
var data = Ajax.get('url/data.json');
var elem = document.getElementById('some-div');
Jemplate.process('my-template.html', data, elem);
or simply:
Jemplate.process('my-template.html', 'url/data.json', '#some-div');
or, with jQuery.js:
jQuery.getJSON("url/data.json", function(data) {
Jemplate.process('my-template.html', data, '#some-div');
});
From the commandline:
jemplate --runtime --compile path/to/jemplate/directory/ > jemplate.js
=head1 DESCRIPTION
Jemplate is a templating framework for JavaScript that is built over
Perl's Template Toolkit (TT2).
Jemplate parses TT2 templates using the TT2 Perl framework, but with a
twist. Instead of compiling the templates into Perl code, it compiles
them into JavaScript.
Jemplate then provides a JavaScript runtime module for processing
the template code. Presto, we have full featured JavaScript
templating language!
Combined with JSON and xmlHttpRequest, Jemplate provides a really simple
and powerful way to do Ajax stuff.
=head1 HOWTO
Jemplate comes with a command line tool call C<jemplate> that you use to
precompile your templates into a JavaScript file. For example if you have
a template directory called C<templates> that contains:
> ls templates/
body.html
footer.html
header.html
You might run this command:
> jemplate --compile template/* > js/jemplates.js
This will compile all the templates into one JavaScript file.
You also need to generate the Jemplate runtime.
> jemplate --runtime > js/Jemplate.js
Now all you need to do is include these two files in your HTML:
<script src="js/Jemplate.js" type="text/javascript"></script>
<script src="js/jemplates.js" type="text/javascript"></script>
Now you have Jemplate support for these templates in your HTML document.
=head1 PUBLIC API
The Jemplate.js JavaScript runtime module has the following API method:
=over
=item Jemplate.process(template-name, data, target);
The C<template-name> is a string like C<'body.html'> that is the name of
the top level template that you wish to process.
The optional C<data> specififies the data object to be used by the
templates. It can be an object, a function or a url. If it is an object,
it is used directly. If it is a function, the function is called and the
returned object is used. If it is a url, an asynchronous <Ajax.get> is
performed. The result is expected to be a JSON string, which gets turned
into an object.
The optional C<target> can be an HTMLElement reference, a function or a
string beginning with a C<#> char. If the target is omitted, the
template result is returned. If it is a function, the function is called
with the result. If it is a string, the string is used as an id to find
an HTMLElement.
If an HTMLElement is used (by id or directly) then the innerHTML
property is set to the template processing result.
=back
The Jemplate.pm Perl module has the following public class methods,
although you won't likely need to use them directly. Normally, you just
use the C<jemplate> command line tool.
=over
=item Jemplate->compile_template_files(@template_file_paths);
Take a list of template file paths and compile them into a module of
functions. Returns the text of the module.
=item Jemplate->compile_template_content($content, $template_name);
Compile one template whose content is in memory. You must provide a
unique template name. Returns the JavaScript text result of the
compilation.
=item Jemplate->compile_module($module_path, \@template_file_paths);
Similar to `compile_template_files`, but prints to result to the
$module_path. Returns 1 if successful, undef if error.
=item Jemplate->compile_module_cached($module_path, \@template_file_paths);
Similar to `compile_module`, but only compiles if one of the templates
is newer than the module. Returns 1 if sucessful compile, 0 if no
compile due to cache, undef if error.
=back
=head1 AJAX AND JSON METHODS
Jemplate comes with builtin Ajax and JSON support.
=over
=item Ajax.get(url, [callback]);
Does a GET operation to the url.
If a callback is provided, the operation is asynchronous, and the data
is passed to the callback. Otherwise, the operation is synchronous and
the data is returned.
=item Ajax.post(url, data, [callback]);
Does a POST operation to the url.
Same callback rules as C<get> apply.
=item JSON.stringify(object);
Return the JSON serialization of an object.
=item JSON.parse(jsonString);
Turns a JSON string into an object and returns the object.
=back
=head1 CURRENT SUPPORT
The goal of Jemplate is to support all of the Template Toolkit features
that can possibly be supported.
Jemplate now supports almost all the TT directives, including:
* Plain text
* [% [GET] variable %]
* [% CALL variable %]
* [% [SET] variable = value %]
* [% DEFAULT variable = value ... %]
* [% INCLUDE [arguments] %]
* [% PROCESS [arguments] %]
* [% BLOCK name %]
* [% FILTER filter %] text... [% END %]
* [% JAVASCRIPT %] code... [% END %]
* [% WRAPPER template [variable = value ...] %]
* [% IF condition %]
* [% ELSIF condition %]
* [% ELSE %]
* [% SWITCH variable %]
* [% CASE [{value|DEFAULT}] %]
* [% FOR x = y %]
* [% WHILE expression %]
* [% RETURN %]
* [% THROW type message %]
* [% STOP %]
* [% NEXT %]
* [% LAST %]
* [% CLEAR %]
* [%# this is a comment %]
* [% MACRO name(param1, param2) BLOCK %] ... [% END %]
ALL of the string virtual functions are supported.
ALL of the array virtual functions are supported:
ALL of the hash virtual functions are supported:
MANY of the standard filters are implemented.
The remaining features will be added very soon. See the DESIGN document
in the distro for a list of all features and their progress.
=head1 BROWSER SUPPORT
Tested successfully in:
* Firefox Mac/Win32/Linux
* IE 6.0
* Safari
* Opera
* Konqueror
All tests run 100% successful in the above browsers.
=head1 DEVELOPMENT
The bleeding edge code is available via Git at
git://github.com/ingydotnet/jemplate.git
You can run the runtime tests directly from
http://svn.jemplate.net/repo/trunk/tests/run/index.html or from the
corresponding CPAN or JSAN directories.
Jemplate development is being discussed at irc://irc.freenode.net/#jemplate
If you want a committer bit, just ask ingy on the irc channel.
=head1 CREDIT
This module is only possible because of Andy Wardley's mighty Template
Toolkit. Thanks Andy. I will gladly give you half of any beers I
receive for this work. (As long as you are in the same room when I'm
drinking them ;)
=head1 AUTHORS
Ingy döt Net <ingy@cpan.org>
(Note: I had to list myself first so that this line would go into META.yml)
Jemplate is truly a community authored project:
Ingy döt Net <ingy@cpan.org>
Tatsuhiko Miyagawa <miyagawa@bulknews.net>
Yann Kerherve <yannk@cpan.org>
David Davis <xantus@xantus.org>
Cory Bennett <coryb@corybennett.org>
Cees Hek <ceeshek@gmail.com>
Christian Hansen
David A. Coffey <dacoffey@cogsmith.com>
Robert Krimen <robertkrimen@gmail.com>
Nickolay Platonov <nickolay8@gmail.com>