Merge remote branch 'kc/master' into new/enh/bug_5917
[koha.git] / installer / html-template-to-template-toolkit.pl
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5 use Carp;
6 use Data::Dumper;
7
8 use Getopt::Long;
9 use File::Basename;
10 use File::Copy;
11
12 my $help_msg = <<EOH;
13 This script does a first-cut conversion of koha HTML::Template template files (.tmpl).
14 It creates a mirror of koha-tmpl called koha-tt where converted files will be placed.
15 By default all files will be converted: use the --file (-f) argument to specify
16   individual files to process.
17
18 Options:
19     --koharoot (-r): Root directory of koha installation.
20     --type (-t): template file extenstions to match
21         (defaults to tmpl|inc|xsl).
22     --copyall (-c): Also copy across all files in template directory
23     --file (-f): specify individual files to process
24     --debug (-d): output more information.
25 EOH
26
27 my $tmpl_in_dir      = 'koha-tmpl';
28 my $tmpl_out_dir     = 'koha-tt';
29
30 # template toolkit variables NOT to scope, in other words, variables that need to remain global (case sensitive)
31 my @globals = ("themelang");
32
33 # Arguments:
34 my $KOHA_ROOT;
35 my $tmpl_extn_match  = "tmpl|inc|xsl"; # Type match defaults to *.tmpl plus *.inc if not specified
36 my $copy_other_files = 0;
37 my @template_files;
38 my @files_w_tmpl_loops;
39 my $verbose          = 0;
40 GetOptions (
41     "koharoot=s"        => \$KOHA_ROOT,
42     "type|t=s"          => \$tmpl_extn_match,
43     "copyall|c"         => \$copy_other_files,
44     "file|f=s"          => \@template_files,         # array of filenames
45     "verbose+"          => \$verbose,                # incremental flag
46 ) or die $help_msg;
47
48 if ( ! $KOHA_ROOT || ! -d $KOHA_ROOT ) {
49     croak "Koha root not passed or is not correct.";
50 }
51 if ( ! -d "$KOHA_ROOT/$tmpl_in_dir" ) {
52     croak "Cannot find template dir ($tmpl_in_dir)";
53 }
54
55 # Attempt to create koha-tt dir..
56 if ( ! -d "$KOHA_ROOT/$tmpl_out_dir" ) {
57     mkdir("$KOHA_ROOT/$tmpl_out_dir") #, '0755'
58        or croak "Cannot create $tmpl_out_dir directory in $KOHA_ROOT: $!";
59 }
60
61 # Obtain list of files to process - go recursively through tmpl_in_dir and subdirectories..
62 unless ( scalar(@template_files) ) {
63     @template_files = mirror_template_dir_structure_return_files("$KOHA_ROOT/$tmpl_in_dir", "$tmpl_extn_match");
64 }
65 foreach my $file (@template_files) {
66     (my $new_path = $file) =~ s/$tmpl_in_dir/$tmpl_out_dir/;
67     $new_path =~ s/\.tmpl/.tt/;
68     $new_path = "$KOHA_ROOT/$new_path" unless ( $new_path =~ m/^$KOHA_ROOT/ );
69
70     open my $ITMPL, '<', $file or croak "Can't open $file for input: $!";
71     open my $OTT, '>', $new_path or croak "Can't open $new_path for output: $!";
72
73     # allows 'proper' handling of for loop scope
74     # cur_scope is a stack of scopes, the last being the current
75     #   when opening a for loop push scope onto end, when closing for loop pop
76     my @cur_scope = ("");
77     # flag representing if we've found a for loop this iteration
78     my $for_loop_found = 0;
79
80     for my $input_tmpl(<$ITMPL>){
81         my @parts = split "<", $input_tmpl;
82         for( my $i=0; $i<=$#parts; ++$i ){
83             my $input_tmpl = $i ? "<" . $parts[$i] : $parts[$i]; # add < sign back in to every part except the first
84         $for_loop_found = 0;
85
86         # handle poorly names variable such as f1!, f1+, f1-, f1| and mod
87         $input_tmpl =~ s/"(\w+)\|"/"$1pipe"/ig;
88         $input_tmpl =~ s/"(\w+)\+"/"$1plus"/ig;
89         $input_tmpl =~ s/"(\w+)\-"/"$1minus"/ig;
90         $input_tmpl =~ s/"(\w+)!"/"$1exclamation"/ig;
91 #       $input_tmpl =~ s/"(\w+),(\w+)"/"$1comma$2"/ig; #caused a problem in patron search
92         $input_tmpl =~ s/NAME="mod"/NAME="modname"/ig;
93         # handle 'naked' TMPL_VAR "parameter" by turning them into what they should be, TMPL_VAR NAME="parameter"
94         $input_tmpl =~ s/TMPL_VAR\s+"(\w+)"/TMPL_VAR NAME="$1"/ig;
95         # make an end (ESCAPE NAME DEFAULT) into a ned (NAME ESCAPE DEFAULT)
96         $input_tmpl =~ s/ESCAPE="(\w+?)"\s+NAME=['"](\w+?)['"]\s+DEFAULT=['"](.+?)['"]/NAME="$2" ESCAPE="$1" DEFAULT="$3"/ig;
97
98         # Process..
99         # NB: if you think you're seeing double, you probably are, *some* (read:most) patterns appear twice: once with quotations marks, once without.
100         #     trying to combine them into a single pattern proved troublesome as a regex like ['"]?(.*?)['"]? was causing problems and fixing the problem caused (alot) more complex regex
101
102         # variables
103         $input_tmpl =~ s/<[!-]*\s*TMPL_VAR\s+NAME\s?=\s?['"]?\s*(\w*?)\s*['"]?\s+ESCAPE=['"](\w*?)['"]\s+DEFAULT=['"]?(.*?)['"]?\s*-*>/[% DEFAULT $cur_scope[-1]$1="$3" |$2 %]/ig;
104         $input_tmpl =~ s/<[!-]*\s*TMPL_VAR\s+NAME\s?=\s?['"]\s*(\w*?)\s*['"]\s+ESCAPE=['"]?(\w*?)['"]?\s*-*>/[% $cur_scope[-1]$1 |$2 %]/ig;
105         $input_tmpl =~ s/<[!-]*\s*TMPL_VAR\s+NAME\s?=\s?(\w*?)\s+ESCAPE=['"]?(\w*?)['"]?\s*-*>/[% $cur_scope[-1]$1 |$2 %]/ig;
106         $input_tmpl =~ s/<[!-]*\s*TMPL_VAR\s+ESCAPE=['"]?(\w*?)['"]?\s+NAME\s?=\s?['"]?([\w-]*?)['"]?\s*-*>/[% $cur_scope[-1]$2 |$1 %]/ig;
107         $input_tmpl =~ s/<[!-]*\s*TMPL_VAR\s+NAME\s?=\s?['"]?(\w*?)['"]?\s+DEFAULT=['"](.*?)['"]\s*-*>/[% DEFAULT $cur_scope[-1]$1="$2" %]/ig;
108         $input_tmpl =~ s/<[!-]*\s*TMPL_VAR\s+NAME\s?=\s?['"]?\s*(\w*?)\s*['"]?\s+DEFAULT=(.*?)\s*-*>/[% DEFAULT $cur_scope[-1]$1=$2 %]/ig;
109         $input_tmpl =~ s/<[!-]*\s*TMPL[_\s]VAR\s+NAME\s?=\s?['"]?\s*(\w*?)\s*['"]?\s*-*>/[% $cur_scope[-1]$1 %]/ig;
110         $input_tmpl =~ s/<[!-]*\s*TMPL[_\s]VAR\s+EXPR\s?=\s?['"](.*?)['"]\s*-*>/[% $1 %]/ig;     # TMPL_VAR NAME and TMPL_VAR EXPR are logically equiv
111         $input_tmpl =~ s/<[!-]*\s*TMPL[_\s]VAR\s+EXPR\s?=\s?(.*?)\s*-*>/[% $1 %]/ig;
112
113         # if, elseif and unless blocks
114         $input_tmpl =~ s/<[!-]*\s*TMPL_IF\s+EXPR\s?=\s?['"](.*?)['"]\s*-*>/[% IF ( $1 ) %]/ig;
115         $input_tmpl =~ s/<[!-]*\s*TMPL_IF\s+EXPR\s?=\s?(.*?)\s*-*>/[% IF ( $1 ) %]/ig;
116         $input_tmpl =~ s/<[!-]*\s*TMPL_IF\s+NAME\s?=\s?['"]\s*(\w*?)\s*['"]\s*-*>/[% IF ( $cur_scope[-1]$1 ) %]/ig;
117         $input_tmpl =~ s/<[!-]*\s*TMPL_IF\s+NAME\s?=\s?(\w*?)\s*-*>/[% IF ( $cur_scope[-1]$1 ) %]/ig;
118         $input_tmpl =~ s/<[!-]*\s*TMPL_IF\s+['"](.*?)['"]\s*-*>/[% IF ( $cur_scope[-1]$1 ) %]/ig;
119         $input_tmpl =~ s/<[!-]*\s*TMPL_IF\s+([\w\s]*?)\s*-*>/[% IF ( $cur_scope[-1]$1 ) %]/ig;
120
121         $input_tmpl =~ s/<[!-]*\s*TMPL_ELSIF\s+EXPR\s?=\s?['"](.*?)['"]\s*-*>/[% ELSIF ( $1 ) %]/ig;
122         $input_tmpl =~ s/<[!-]*\s*TMPL_ELSIF\s+EXPR\s?=\s?(.*?)\s*-*>/[% ELSIF ( $1 ) %]/ig;
123         $input_tmpl =~ s/<[!-]*\s*TMPL_ELSIF\s+NAME\s?=\s?['"](\w*?)['"]\s*-*>/[% ELSIF ( $cur_scope[-1]$1 ) %]/ig;
124         $input_tmpl =~ s/<[!-]*\s*TMPL_ELSIF\s+NAME\s?=\s?(\w*?)\s*-*>/[% ELSIF ( $cur_scope[-1]$1 ) %]/ig;
125         $input_tmpl =~ s/<[!-]*\s*TMPL_ELSIF\s+['"](\w*?)['"]\s*-*>/[% ELSIF ( $cur_scope[-1]$1 ) %]/ig;
126         $input_tmpl =~ s/<[!-]*\s*TMPL_ELSIF\s+(\w*?)\s*-*>/[% ELSIF ( $cur_scope[-1]$1 ) %]/ig;
127
128         $input_tmpl =~ s/<[!-]*\s*TMPL_ELSE\s*-*>/[% ELSE %]/ig;
129         $input_tmpl =~ s/<[!-]*\s*\/TMPL_IF\s*-*>/[% END %]/ig;
130
131         $input_tmpl =~ s/<[!-]*\s*TMPL_UNLESS\s+NAME\s?=\s?['"]?(\w*?)['"]?\s*-*>/[% UNLESS ( $cur_scope[-1]$1 ) %]/ig;
132         $input_tmpl =~ s/<[!-]*\s*\/TMPL_UNLESS\s*-*>/[% END %]/ig;
133         # includes
134         $input_tmpl =~ s/<[!-]*\s*TMPL_INCLUDE\s+NAME\s?=\s?"(.*?\.inc)"\s*-*>/[% INCLUDE '$1' %]/ig;
135         $input_tmpl =~ s/<[!-]*\s*TMPL_INCLUDE\s+NAME\s?=\s?"(.*?)"\s*-*>/[% INCLUDE $1 %]/ig;
136
137         #reverse scoping bug fix
138         for my $tag (@globals){
139             next unless $cur_scope[-1];
140             $input_tmpl =~ s/$cur_scope[-1]$tag/$tag/g;
141         }
142
143         if ($input_tmpl =~ m/<[!-]*\s*TMPL_LOOP/i ){
144             $for_loop_found = 1;
145         }
146
147         $input_tmpl =~ s/<[!-]*\s*TMPL_LOOP\s+NAME\s?=\s?['"](?<SCOPE>.*?)['"]\s*-*>/"[% FOREACH " . substr($+{SCOPE}, 0, -1) . " IN $cur_scope[-1]$1 %]"/ieg;
148         $input_tmpl =~ s/<[!-]*\s*TMPL_LOOP\s+NAME\s?=\s?(?<SCOPE>.*?)\s*-*>/"[% FOREACH " . substr($+{SCOPE}, 0, -1) . " IN $cur_scope[-1]$1 %]"/ieg;
149
150         # handle new scope
151         if($for_loop_found){
152             my $scope = substr($+{SCOPE}, 0, -1) . ".";
153             push(@cur_scope, $scope);
154             $for_loop_found = 0;
155         }
156
157         # handle loops and old scope
158         if ( $input_tmpl =~ m/<!--[\s\/]*TMPL_LOOP\s*-->/i ) {
159             push(@files_w_tmpl_loops, $new_path);
160             pop(@cur_scope);
161         }
162
163         $input_tmpl =~ s/<[!-]*\s*\/TMPL_LOOP\s*-*>/[% END %]/ig;
164
165         # misc 'patches'
166         $input_tmpl =~ s/\seq\s/ == /ig;
167         $input_tmpl =~ s/HTML/html/g;
168         $input_tmpl =~ s/URL/url/g;
169         $input_tmpl =~ s/dhtmlcalendar_dateformat/DHTMLcalendar_dateformat/ig;
170         $input_tmpl =~ s/\w*\.__first__/loop.first/ig;
171         $input_tmpl =~ s/\w*\.__last__/loop.last/ig;
172         $input_tmpl =~ s/\w*\.__odd__/loop.odd/ig;
173         $input_tmpl =~ s/\w*\.__even__/loop.even/ig;
174         $input_tmpl =~ s/\w*\.__counter__/loop.count/ig; #loop.count gives the range (0..max) whereas loop.index gives the range (1..max+1), __counter__ is unknown
175
176         # hack to get around lack of javascript filter
177         $input_tmpl =~ s/\|\s*JS/|replace("'", "\\'") |replace('"', '\\"') |replace('\\n', '\\\\n') |replace('\\r', '\\\\r')/ig;
178     
179         # Write out..
180         print $OTT $input_tmpl;
181         }
182     }
183     close $ITMPL;
184     close $OTT;
185 }
186
187 if ( scalar(@files_w_tmpl_loops) && $verbose ) {
188     print "\nThese files contain TMPL_LOOPs that need double checking:\n";
189     foreach my $file (@files_w_tmpl_loops) {
190         print "$file\n";
191     }
192 }
193
194 ## SUB-ROUTINES ##
195
196 # Create new directory structure and return list of template files
197 sub mirror_template_dir_structure_return_files {
198     my($dir, $type) = @_;
199
200     my @files = ();
201     if ( opendir(DIR, $dir) ) {
202         my @dirent = readdir DIR;   # because DIR is shared when recursing
203         closedir DIR;
204         for my $dirent (@dirent) {
205             my $path = "$dir/$dirent";
206             if ( $dirent =~ /^\./ ) {
207               ;
208             }
209             elsif ( -f $path ) {
210                 (my $new_path = $path) =~ s/$tmpl_in_dir/$tmpl_out_dir/;
211                 $new_path = "$KOHA_ROOT/$new_path" unless ( $new_path =~ m/^$KOHA_ROOT/ );
212                 if ( !defined $type || $dirent =~ /\.(?:$type)$/) {
213                     push(@files, $path);
214                 }
215                 elsif ( $copy_other_files ) {
216                     copy($path, $new_path)
217                       or croak "Failed to copy $path to $new_path: $!";
218                 }
219             }
220             elsif ( -d $path ) {
221                 (my $new_path = $path) =~ s/$tmpl_in_dir/$tmpl_out_dir/;
222                 $new_path = "$KOHA_ROOT/$new_path" unless ( $new_path =~ m/^$KOHA_ROOT/ );
223                 if ( ! -d $new_path ) {
224                     mkdir($new_path) #, '0755'
225                       or croak "Failed to create " . $new_path ." directory: $!";
226                 }
227                 my @sub_files = mirror_template_dir_structure_return_files($path, $type);
228                 push(@files, @sub_files) if ( scalar(@sub_files) );
229             }
230         }
231     } else {
232         warn("Cannot open $dir: $! ... skipping");
233     }
234
235     return @files;
236 }