File Coverage

blib/lib/MsOffice/Word/HTML/Writer.pm
Criterion Covered Total %
statement 140 172 81.4
branch 22 52 42.3
condition 10 20 50.0
subroutine 23 26 88.4
pod 10 10 100.0
total 205 280 73.2


line stmt bran cond sub pod time code
1             package MsOffice::Word::HTML::Writer;
2 3     3   208439 use utf8;
  3         56  
  3         15  
3 3     3   92 use warnings;
  3         6  
  3         68  
4 3     3   16 use strict;
  3         6  
  3         63  
5 3     3   1493 use MIME::Base64 qw/encode_base64/;
  3         2131  
  3         176  
6 3     3   1399 use MIME::Types;
  3         19931  
  3         134  
7 3     3   21 use Carp;
  3         6  
  3         154  
8 3     3   1732 use Params::Validate qw/validate SCALAR HASHREF/;
  3         28051  
  3         233  
9 3     3   22 use Scalar::Util qw/looks_like_number/;
  3         7  
  3         8651  
10            
11             our $VERSION = '1.08';
12            
13             sub new {
14 2     2 1 200 my $class = shift;
15            
16             # validate named parameters
17 2         34 my $param_spec = {
18             title => {type => SCALAR, optional => 1},
19             head => {type => SCALAR, optional => 1},
20             hf_head => {type => SCALAR, optional => 1},
21             WordDocument => {type => HASHREF, optional => 1},
22             charset => {type => SCALAR, optional => 1, default => 'utf-8'},
23             };
24 2         68 my %params = validate(@_, $param_spec);
25            
26             # create instance
27             my $self = {
28             MIME_parts => [],
29             sections => [{}],
30             title => $params{title}
31             || "Document generated by MsOffice::Word::HTML::Writer",
32             head => $params{head} || "",
33             hf_head => $params{hf_head} || "",
34             WordDocument => $params{WordDocument},
35             charset => $params{charset},
36 2   50     45 };
      50        
      50        
37            
38 2         15 bless $self, $class;
39             }
40            
41            
42             sub create_section {
43 4     4 1 27 my $self = shift;
44            
45             # validate named parameters
46 4         11 my $param_spec = {page => {type => HASHREF, optional => 1}};
47             $param_spec->{$_} = {type => SCALAR, optional => 1}
48 4         65 for qw/header footer first_header first_footer new_page/;
49 4         107 my %params = validate(@_, $param_spec);
50            
51             # if first automatic section is empty, delete it
52             $self->{sections} = []
53 4 50 66     19 if scalar(@{$self->{sections}}) == 1 && !$self->{sections}[0]{content};
  4         23  
54            
55             # add the new section
56 4         6 push @{$self->{sections}}, \%params;
  4         25  
57             }
58            
59            
60             sub write {
61 14     14 1 53 my $self = shift;
62            
63             # add html arguments to current section content
64 14         67 $self->{sections}[-1]{content} .= join ("", @_);
65             }
66            
67            
68            
69             sub save_as {
70 0     0 1 0 my ($self, $filename) = @_;
71            
72             # default extension is ".doc"
73 0 0       0 $filename .= ".doc" unless $filename =~ /\.\w{1,5}$/;
74            
75             # open the file
76 0 0       0 open my $fh, ">:crlf", $filename
77             or croak "could not open >$filename: $!";
78            
79             # write content and close
80 0         0 print $fh $self->content;
81 0         0 close $fh;
82            
83 0         0 return $filename;
84             }
85            
86            
87             sub attach {
88 0     0 1 0 my ($self, $name, $open1, $open2, @other) = @_;
89            
90             # open a handle to the attachment (need to dispatch according to number
91             # of args, because perlfunc/open() has complex prototyping behaviour)
92 0         0 my $fh;
93 0 0       0 if (@other) {
    0          
94 0 0       0 open $fh, $open1, $open2, @other
95             or croak "open $open1, $open2, @other : $!";
96             }
97             elsif ($open2) {
98 0 0       0 open $fh, $open1, $open2
99             or croak "open $open1, $open2 : $!";
100             }
101             else {
102 0 0       0 open $fh, $open1
103             or croak "open $open1 : $!";
104             }
105            
106             # slurp the content
107 0 0       0 binmode($fh) unless $name =~ /\.(html?|css|te?xt|rtf)$/i;
108 0         0 local $/;
109 0         0 my $attachment = <$fh>;
110            
111             # add the attachment (filename and content)
112 0         0 push @{$self->{MIME_parts}}, ["files/$name", $attachment];
  0         0  
113             }
114            
115            
116             sub page_break {
117 2     2 1 11 my ($self) = @_;
118 2         7 return qq{
\n};
119             }
120            
121            
122             sub tab {
123 0     0 1 0 my ($self, $n_tabs) = @_;
124 0   0     0 $n_tabs ||= 1;
125 0         0 return qq{};
126             }
127            
128             sub field {
129 2     2 1 7 my ($self, $fieldname, $args, $content, $prevent_html_entity_encoding) = @_;
130            
131 2         7 for ($args, $content) {
132 4   50     11 $_ ||= ""; # undef replaced by empty string
133 4 50       13 s/&/&/g, s//>/g # replace HTML entities
134             unless $prevent_html_entity_encoding;
135             }
136            
137 2         5 my $field;
138            
139             # when args : long form of field encoding
140 2 50       7 if ($args) {
141 2         5 my $space = qq{ };
142 2         14 $field = qq{}
143             . $space . $fieldname . $space . $args
144             . qq{}
145             . $content
146             . qq{};
147             }
148             # otherwise : short form of field encoding
149             else {
150 0         0 $field = qq{$content};
151             }
152            
153 2         8 return $field;
154             }
155            
156             sub quote {
157 2     2 1 16 my ($self, $text, $prevent_html_entity_encoding) = @_;
158 2         5 my $args = $text;
159 2         7 $args =~ s/"/\\"/g;
160 2         8 $args = qq{"$args"};
161 2         9 $args =~ s/"/"/g;
162 2         6 return $self->field('QUOTE', $args, $text, $prevent_html_entity_encoding);
163             }
164            
165            
166            
167             sub content {
168 2     2 1 6 my ($self) = @_;
169            
170             # separator for parts in MIME document
171 2         5 my $boundary = qw/__NEXT_PART__/;
172            
173             # MIME multipart header
174 2         19 my $mime = qq{MIME-Version: 1.0\n}
175             . qq{Content-Type: multipart/related; boundary="$boundary"\n\n}
176             . qq{MIME document generated by MsOffice::Word::HTML::Writer\n\n};
177            
178             # generate each part (main document must be first)
179 2         7 my @parts = $self->_MIME_parts;
180 2         15 my $filelist = $self->_filelist(@parts);
181 2         12 for my $pair ($self->_main, @parts, $filelist) {
182 4         18 my ($filename, $content) = @$pair;
183 4   50     30 my $mime_type = MIME::Types->new->mimeTypeOf($filename) || '';
184 4         130826 my ($encoding, $encoded);
185 4 50       14 if ($mime_type =~ /^text|xml$/) {
186             # no need for Windows-style end-of-lines of shape CRLF
187 4         64 $content =~ s/\r\n/\n/g;
188            
189             # if charset is not utf-8, wide chars are encoded as numerical HTML entities
190 4 100       54 $content =~ s/([^\x{0}-\x{FF}])/'&#'.ord($1).';'/eg unless $self->{charset} eq 'utf-8';
  8         87  
191            
192             # simple-minded MIME quoted-printable encoding
193 4         11 $encoding = 'quoted-printable';
194 4         53 ($encoded = $content) =~ s/=/=3D/g;
195 4         18 $mime_type .= "; charset=$self->{charset}";
196             }
197             else {
198 0         0 $encoding = 'base64';
199 0         0 $encoded = encode_base64($content);
200             }
201            
202 4         81 $mime .= qq{--$boundary\n}
203             . qq{Content-Location: file:///C:/foo/$filename\n}
204             . qq{Content-Transfer-Encoding: $encoding\n}
205             . qq{Content-Type: $mime_type\n\n}
206             . $encoded
207             . "\n";
208             }
209            
210             # close last MIME part
211 2         12 $mime .= "--$boundary--\n";
212            
213 2         26 return $mime;
214             }
215            
216            
217             #======================================================================
218             # PRIVATE METHODS
219             #======================================================================
220            
221             sub _main {
222 2     2   11 my ($self) = @_;
223            
224             # body : concatenate content from all sections
225 2         6 my $body = "";
226 2         4 my $i = 1;
227 2         5 foreach my $section (@{$self->{sections}}) {
  2         6  
228            
229             # section break
230 6 100       23 if ($i > 1) {
231             # type of break
232 4         8 my $break = $section->{new_page};
233 4 100 66     38 $break = 'always' if $break && looks_like_number($break); # if true but not a word
234 4   50     11 $break ||= 'auto'; # if false
235             # otherwise, type of break will just be the word given in {new_page}
236            
237             # insert into body
238 4         13 my $style = qq{page-break-before:$break;mso-break-type:section-break};
239 4         9 $body .= qq{
\n};
240             }
241            
242             # section content
243 6         34 $body .= qq{
\n$section->{content}\n
\n};
244            
245 6         13 $i += 1;
246             }
247            
248             # assemble head and body into a full document
249 2         8 my $html
250             = qq{
251             . qq{ xmlns:o="urn:schemas-microsoft-com:office:office"\n}
252             . qq{ xmlns:w="urn:schemas-microsoft-com:office:word"\n}
253             . qq{ xmlns:m="http://schemas.microsoft.com/office/2004/12/omml"\n}
254             . qq{ xmlns="http://www.w3.org/TR/REC-html40">\n}
255             . $self->_head
256             . qq{\n$body\n}
257             . qq{\n};
258 2         13 return ["main.htm", $html];
259             }
260            
261            
262             sub _head {
263 2     2   8 my ($self) = @_;
264            
265             # HTML head : link to filelist, title, view format and styles
266             my $head
267             = qq{\n}
268             . qq{\n}
269             . qq{$self->{title}\n}
270             . $self->_xml_WordDocument
271             . qq{\n}
272             . $self->{head}
273 2         10 . qq{\n};
274 2         30 return $head;
275             }
276            
277            
278            
279             sub _xml_WordDocument {
280 2     2   4 my ($self) = @_;
281 2 50       9 my $xml_root = $self->{WordDocument} or return "";
282 2         6 return "\n"
283             . _w_xml($xml_root)
284             . "\n";
285             }
286            
287            
288             sub _w_xml {
289 4     4   14 my $node = shift;
290 4         9 my $xml = "";
291 4         20 while (my ($k, $v) = each %$node) {
292 6 100       41 $xml .= $v ? ( # node with content
    100          
293             ""
294             . (ref $v ? _w_xml($v) : $v)
295             . "\n" )
296             : "\n"; # node without content
297             }
298 4         25 return $xml;
299             }
300            
301            
302             sub _section_styles {
303 2     2   7 my ($self) = @_;
304            
305 2         4 my $styles = "";
306 2         4 my $i = 1;
307 2         7 foreach my $section (@{$self->{sections}}) {
  2         8  
308            
309 6         12 my $properties = "";
310            
311             # page properties (size and margin)
312 6         9 foreach my $prop (qw/size margin/) {
313 12 50       42 my $val = $section->{page}{$prop} or next;
314 0         0 $properties .= qq{ $prop:$val;\n};
315             }
316            
317             # headers and footers
318 6         9 my $has_first_page;
319 6         14 foreach my $prop (qw/header_margin footer_margin
320             page_numbers paper_source/) {
321 24 50       60 my $val = $section->{page}{$prop} or next;
322 0         0 (my $property = $prop) =~ s/_/-/g;
323 0         0 $properties .= qq{ mso-$property:$val;\n};
324             }
325 6         20 foreach my $hf (qw/header footer first_header first_footer/) {
326 24 50       74 $section->{$hf} or next;
327 0 0       0 $has_first_page = 1 if $hf =~ /^first/;
328 0         0 (my $property = $hf) =~ s/_/-/;
329 0         0 $properties
330             .= qq{ mso-$property:url("files/header_footer.htm") $hf$i;\n};
331             }
332 6 50       15 $properties .= qq{ mso-title-page:yes;\n} if $has_first_page;
333            
334             # style definitions for this section
335 6         23 $styles .= qq[\@page Section$i {\n$properties}\n]
336             . qq[div.Section$i {page:Section$i}\n];
337 6         12 $i += 1;
338             }
339            
340 2         27 return $styles;
341             }
342            
343            
344             sub _MIME_parts {
345 2     2   4 my ($self) = @_;
346            
347             # attachments supplied by user
348 2         4 my @parts = @{$self->{MIME_parts}};
  2         6  
349            
350             # additional attachment : computed file with headers and footers
351 2         6 my $hf_content = $self->_header_footer;
352 2 50       6 unshift @parts, ["files/header_footer.htm", $hf_content] if $hf_content;
353            
354 2         6 return @parts;
355             }
356            
357            
358             sub _header_footer {
359 2     2   8 my ($self) = @_;
360            
361             # create a div for each header/footer in each section
362 2         6 my $hf_divs = "";
363 2         3 my $i = 1;
364 2         5 foreach my $section (@{$self->{sections}}) {
  2         7  
365            
366             # deal with headers/footers defined in that section
367 6         11 foreach my $hf (qw/header footer first_header first_footer/) {
368 24 50       50 $section->{$hf} or next;
369 0         0 (my $style = $hf) =~ s/^first_//;
370             $hf_divs .= qq{
\n}
371 0         0 . $section->{$hf} . "\n"
372             . qq{\n};
373             }
374            
375 6         13 $i += 1;
376             }
377            
378             # if at least one such div, need to create an attached file
379             my $header_footer = !$hf_divs ? "" :
380             qq{\n}
381             . qq{\n}
382             . qq{\n}
383             . $self->{hf_head}
384 2 50       18 . qq{\n}
385             . qq{\n} . $hf_divs . qq{\n}
386             . qq{\n};
387            
388 2         7 return $header_footer;
389             }
390            
391            
392            
393             sub _filelist {
394 2     2   8 my ($self, @parts) = @_;
395            
396             # xml header
397 2         5 my $xml = qq{\n}
398             . qq{ \n};
399            
400             # refer to each attached file
401 2         5 foreach my $part (@parts) {
402 0         0 $xml .= qq{ \n};
403             }
404            
405             # the filelist is itself an attached file
406 2         13 $xml .= qq{ \n};
407            
408             # closing tag;
409 2         7 $xml .= qq{\n};
410            
411 2         14 return ["files/filelist.xml", $xml];
412             }
413            
414            
415            
416             1;
417            
418             __END__