pagebreak-Mechanismus entfernen
[kivitendo-erp.git] / SL / Template / LaTeX.pm
1 package SL::Template::LaTeX;
2
3 use parent qw(SL::Template::Simple);
4
5 use strict;
6
7 use Cwd;
8 use Unicode::Normalize qw();
9
10 sub new {
11   my $type = shift;
12
13   my $self = $type->SUPER::new(@_);
14
15   return $self;
16 }
17
18 sub format_string {
19   my ($self, $variable) = @_;
20   my $form = $self->{"form"};
21
22   $variable = $main::locale->quote_special_chars('Template/LaTeX', $variable);
23
24   # Allow some HTML markup to be converted into the output format's
25   # corresponding markup code, e.g. bold or italic.
26   my %markup_replace = ('b' => 'textbf',
27                         'i' => 'textit',
28                         'u' => 'underline');
29
30   foreach my $key (keys(%markup_replace)) {
31     my $new = $markup_replace{$key};
32     $variable =~ s/\$\<\$${key}\$\>\$(.*?)\$<\$\/${key}\$>\$/\\${new}\{$1\}/gi;
33   }
34
35   $variable =~ s/[\x00-\x1f]//g;
36
37   return $variable;
38 }
39
40 sub parse_foreach {
41   my ($self, $var, $text, $start_tag, $end_tag, @indices) = @_;
42
43   my ($form, $new_contents) = ($self->{"form"}, "");
44
45   my $ary = $self->_get_loop_variable($var, 1, @indices);
46
47   my $sum                          = 0;
48   my $current_page                 = 1;
49   my ($current_line, $corrent_row) = (0, 1);
50   my $description_array            = $self->_get_loop_variable("description",     1);
51   my $longdescription_array        = $self->_get_loop_variable("longdescription", 1);
52   my $linetotal_array              = $self->_get_loop_variable("linetotal",       1);
53
54   $form->{TEMPLATE_ARRAYS}->{cumulatelinetotal} = [];
55
56   # forech block hasn't given us an array. ignore
57   return $new_contents unless ref $ary eq 'ARRAY';
58
59   for (my $i = 0; $i < scalar(@{$ary}); $i++) {
60     # do magic markers
61     $form->{"__first__"}   = $i == 0;
62     $form->{"__last__"}    = ($i + 1) == scalar(@{$ary});
63     $form->{"__odd__"}     = (($i + 1) % 2) == 1;
64     $form->{"__counter__"} = $i + 1;
65
66     if (   ref $linetotal_array eq 'ARRAY'
67         && $i < scalar(@{$linetotal_array})) {
68       $sum += $form->parse_amount($self->{"myconfig"}, $linetotal_array->[$i]);
69     }
70
71     $form->{TEMPLATE_ARRAYS}->{cumulatelinetotal}->[$i] = $form->format_amount($self->{"myconfig"}, $sum, 2);
72
73     my $new_text = $self->parse_block($text, (@indices, $i));
74     return undef unless (defined($new_text));
75     $new_contents .= $start_tag . $new_text . $end_tag;
76   }
77   map({ delete($form->{"__${_}__"}); } qw(first last odd counter));
78
79   return $new_contents;
80 }
81
82 sub find_end {
83   my ($self, $text, $pos, $var, $not) = @_;
84
85   my $tag_start_len = length $self->{tag_start};
86
87   my $depth = 1;
88   $pos = 0 unless ($pos);
89
90   while ($pos < length($text)) {
91     $pos++;
92
93     next if (substr($text, $pos - 1, length($self->{tag_start})) ne $self->{tag_start});
94
95     my $keyword_pos = $pos - 1 + $tag_start_len;
96
97     if ((substr($text, $keyword_pos, 2) eq 'if') || (substr($text, $keyword_pos, 7) eq 'foreach')) {
98       $depth++;
99
100     } elsif ((substr($text, $keyword_pos, 4) eq 'else') && (1 == $depth)) {
101       if (!$var) {
102         $self->{"error"} =
103             "$self->{tag_start}else$self->{tag_end} outside of "
104           . "$self->{tag_start}if$self->{tag_end} / "
105           . "$self->{tag_start}ifnot$self->{tag_end}.";
106         return undef;
107       }
108
109       my $block = substr($text, 0, $pos - 1);
110       substr($text, 0, $pos - 1) = "";
111       $text =~ s!^$self->{tag_start_qm}.+?$self->{tag_end_qm}!!;
112       $text =  $self->{tag_start} . 'if' . ($not ?  " " : "not ") . $var . $self->{tag_end} . $text;
113
114       return ($block, $text);
115
116     } elsif (substr($text, $keyword_pos, 3) eq 'end') {
117       $depth--;
118       if ($depth == 0) {
119         my $block = substr($text, 0, $pos - 1);
120         substr($text, 0, $pos - 1) = "";
121         $text =~ s!^$self->{tag_start_qm}.+?$self->{tag_end_qm}!!;
122
123         return ($block, $text);
124       }
125     }
126   }
127
128   return undef;
129 }
130
131 sub parse_block {
132   $main::lxdebug->enter_sub();
133
134   my ($self, $contents, @indices) = @_;
135
136   my $new_contents = "";
137
138   while ($contents ne "") {
139     my $pos_if      = index($contents, $self->{tag_start} . 'if');
140     my $pos_foreach = index($contents, $self->{tag_start} . 'foreach');
141
142     if ((-1 == $pos_if) && (-1 == $pos_foreach)) {
143       $new_contents .= $self->substitute_vars($contents, @indices);
144       last;
145     }
146
147     if ((-1 == $pos_if) || ((-1 != $pos_foreach) && ($pos_if > $pos_foreach))) {
148       $new_contents .= $self->substitute_vars(substr($contents, 0, $pos_foreach), @indices);
149       substr($contents, 0, $pos_foreach) = "";
150
151       if ($contents !~ m|^$self->{tag_start_qm}foreach (.+?)$self->{tag_end_qm}|) {
152         $self->{"error"} = "Malformed $self->{tag_start}foreach$self->{tag_end}.";
153         $main::lxdebug->leave_sub();
154         return undef;
155       }
156
157       my $var = $1;
158
159       substr($contents, 0, length($&)) = "";
160
161       my $block;
162       ($block, $contents) = $self->find_end($contents);
163       if (!$block) {
164         $self->{"error"} = "Unclosed $self->{tag_start}foreach$self->{tag_end}." unless ($self->{"error"});
165         $main::lxdebug->leave_sub();
166         return undef;
167       }
168
169       my $new_text = $self->parse_foreach($var, $block, "", "", @indices);
170       if (!defined($new_text)) {
171         $main::lxdebug->leave_sub();
172         return undef;
173       }
174       $new_contents .= $new_text;
175
176     } else {
177       if (!$self->_parse_block_if(\$contents, \$new_contents, $pos_if, @indices)) {
178         $main::lxdebug->leave_sub();
179         return undef;
180       }
181     }
182   }
183
184   $main::lxdebug->leave_sub();
185
186   return $new_contents;
187 }
188
189 sub parse_first_line {
190   my $self = shift;
191   my $line = shift || "";
192
193   if ($line =~ m/([^\s]+)set-tag-style([^\s]+)/) {
194     if ($1 eq $2) {
195       $self->{error} = "The tag start and end markers must not be equal.";
196       return 0;
197     }
198
199     $self->set_tag_style($1, $2);
200   }
201
202   return 1;
203 }
204
205 sub _parse_config_option {
206   my $self = shift;
207   my $line = shift;
208
209   $line =~ s/^\s*//;
210   $line =~ s/\s*$//;
211
212   my ($key, $value) = split m/\s*=\s*/, $line, 2;
213
214   if ($key eq 'tag-style') {
215     $self->set_tag_style(split(m/\s+/, $value, 2));
216   }
217 }
218
219 sub _parse_config_lines {
220   my $self  = shift;
221   my $lines = shift;
222
223   my ($comment_start, $comment_end) = ("", "");
224
225   if (ref $self eq 'SL::Template::LaTeX') {
226     $comment_start = '\s*%';
227   } elsif (ref $self eq 'SL::Template::HTML') {
228     $comment_start = '\s*<!--';
229     $comment_end   = '(?:--)?>\s*';
230   } else {
231     $comment_start = '\s*\#';
232   }
233
234   my $num_lines = scalar @{ $lines };
235   my $i         = 0;
236
237   while ($i < $num_lines) {
238     my $line = $lines->[$i];
239
240     if ($line !~ m/^${comment_start}\s*config\s*:(.*?)${comment_end}$/i) {
241       $i++;
242       next;
243     }
244
245     $self->_parse_config_option($1);
246     splice @{ $lines }, $i, 1;
247     $num_lines--;
248   }
249 }
250
251 sub _force_mandatory_packages {
252   my $self  = shift;
253   my $lines = shift;
254
255   my (%used_packages, $document_start_line, $last_usepackage_line);
256
257   foreach my $i (0 .. scalar @{ $lines } - 1) {
258     if ($lines->[$i] =~ m/\\usepackage[^\{]*{(.*?)}/) {
259       $used_packages{$1} = 1;
260       $last_usepackage_line = $i;
261
262     } elsif ($lines->[$i] =~ m/\\begin{document}/) {
263       $document_start_line = $i;
264       last;
265
266     }
267   }
268
269   my $insertion_point = defined($document_start_line)  ? $document_start_line
270                       : defined($last_usepackage_line) ? $last_usepackage_line
271                       :                                  scalar @{ $lines } - 1;
272
273   foreach my $package (qw(textcomp)) {
274     next if $used_packages{$package};
275     splice @{ $lines }, $insertion_point, 0, "\\usepackage{${package}}\n";
276     $insertion_point++;
277   }
278 }
279
280 sub parse {
281   my $self = $_[0];
282   local *OUT = $_[1];
283   my $form = $self->{"form"};
284
285   if (!open(IN, "$form->{templates}/$form->{IN}")) {
286     $self->{"error"} = "$!";
287     return 0;
288   }
289   binmode IN, ":utf8" if $::locale->is_utf8;
290   my @lines = <IN>;
291   close(IN);
292
293   $self->_parse_config_lines(\@lines);
294   $self->_force_mandatory_packages(\@lines) if (ref $self eq 'SL::Template::LaTeX');
295
296   my $contents = join("", @lines);
297
298   my $new_contents = $self->parse_block($contents);
299   if (!defined($new_contents)) {
300     $main::lxdebug->leave_sub();
301     return 0;
302   }
303
304   if ($::locale->is_utf8) {
305     binmode OUT, ":utf8";
306     print OUT Unicode::Normalize::normalize('C', $new_contents);
307
308   } else {
309     print OUT $new_contents;
310   }
311
312   if ($form->{"format"} =~ /postscript/i) {
313     return $self->convert_to_postscript();
314   } elsif ($form->{"format"} =~ /pdf/i) {
315     return $self->convert_to_pdf();
316   } else {
317     return 1;
318   }
319 }
320
321 sub convert_to_postscript {
322   my ($self) = @_;
323   my ($form, $userspath) = ($self->{"form"}, $self->{"userspath"});
324
325   # Convert the tex file to postscript
326
327   if (!chdir("$userspath")) {
328     $self->{"error"} = "chdir : $!";
329     $self->cleanup();
330     return 0;
331   }
332
333   $form->{tmpfile} =~ s/\Q$userspath\E\///g;
334
335   my $latex = $self->_get_latex_path();
336   my $old_home = $ENV{HOME};
337   my $old_openin_any = $ENV{openin_any};
338   $ENV{HOME}   = $userspath =~ m|^/| ? $userspath : getcwd();
339   $ENV{openin_any} = "p";
340
341   for (my $run = 1; $run <= 2; $run++) {
342     system("${latex} --interaction=nonstopmode $form->{tmpfile} " .
343            "> $form->{tmpfile}.err");
344     if ($?) {
345       $ENV{HOME} = $old_home;
346       $ENV{openin_any} = $old_openin_any;
347       $self->{"error"} = $form->cleanup($latex);
348       return 0;
349     }
350   }
351
352   $form->{tmpfile} =~ s/tex$/dvi/;
353
354   system("dvips $form->{tmpfile} -o -q > /dev/null");
355   $ENV{HOME} = $old_home;
356   $ENV{openin_any} = $old_openin_any;
357
358   if ($?) {
359     $self->{"error"} = "dvips : $!";
360     $self->cleanup('dvips');
361     return 0;
362   }
363   $form->{tmpfile} =~ s/dvi$/ps/;
364
365   $self->cleanup();
366
367   return 1;
368 }
369
370 sub convert_to_pdf {
371   my ($self) = @_;
372   my ($form, $userspath) = ($self->{"form"}, $self->{"userspath"});
373
374   # Convert the tex file to PDF
375
376   if (!chdir("$userspath")) {
377     $self->{"error"} = "chdir : $!";
378     $self->cleanup();
379     return 0;
380   }
381
382   $form->{tmpfile} =~ s/\Q$userspath\E\///g;
383
384   my $latex = $self->_get_latex_path();
385   my $old_home = $ENV{HOME};
386   my $old_openin_any = $ENV{openin_any};
387   $ENV{HOME}   = $userspath =~ m|^/| ? $userspath : getcwd();
388   $ENV{openin_any} = "p";
389
390   for (my $run = 1; $run <= 2; $run++) {
391     system("${latex} --interaction=nonstopmode $form->{tmpfile} " .
392            "> $form->{tmpfile}.err");
393     if ($?) {
394       $ENV{HOME}     = $old_home;
395       $ENV{openin_any} = $old_openin_any;
396       $self->{error} = $form->cleanup($latex);
397       return 0;
398     }
399   }
400
401   $ENV{HOME} = $old_home;
402   $ENV{openin_any} = $old_openin_any;
403   $form->{tmpfile} =~ s/tex$/pdf/;
404
405   $self->cleanup();
406 }
407
408 sub _get_latex_path {
409   return $::lx_office_conf{applications}->{latex} || 'pdflatex';
410 }
411
412 sub get_mime_type() {
413   my ($self) = @_;
414
415   if ($self->{"form"}->{"format"} =~ /postscript/i) {
416     return "application/postscript";
417   } else {
418     return "application/pdf";
419   }
420 }
421
422 sub uses_temp_file {
423   return 1;
424 }
425
426 1;