File Coverage

blib/lib/XML/SAX/RTF.pm
Criterion Covered Total %
statement 9 170 5.2
branch 0 60 0.0
condition 0 12 0.0
subroutine 3 27 11.1
pod 5 6 83.3
total 17 275 6.1


line stmt bran cond sub pod time code
1             package XML::SAX::RTF;
2             require 5.005_62;
3 1     1   986 use strict;
  1         2  
  1         42  
4 1     1   2064 use XML::SAX::Base;
  1         41145  
  1         36  
5 1     1   12 use vars qw( $VERSION @ISA @EXPORT @EXPORT_OK );
  1         7  
  1         2975  
6             require Exporter;
7             @ISA = qw( Exporter XML::SAX::Base );
8             @EXPORT = qw( Version );
9             $VERSION = '0.2';
10 0     0 1   sub Version { $VERSION; }
11             our %features =
12             (
13             DEBUG => 0,
14             );
15              
16              
17             #
18             # internal globals
19             #
20             my $file = ''; # name of file being parsed
21             my $inbuf; # input buffer with RTF to be processed
22             my $level; # element nesting level in result doc
23             my @elements; # open element stack for result doc
24              
25              
26             sub new {
27             #
28             # constructor
29             #
30 0     0 0   my $class = shift;
31 0           my $obj = {@_};
32 0           my $self = bless( $obj, $class );
33 0           return $self;
34             }
35              
36              
37             sub parse_file {
38             #
39             # parse a document, one line at a time
40             #
41 0     0 1   my $self = shift;
42 0           $file = shift;
43 0           my $buf = '';
44 0 0         if( open( F, $file )) {
45 0           while( ) {
46 0           $buf .= $_;
47             }
48 0           close F;
49             }
50 0           $self->parse_string( $buf );
51 0           $file = '';
52             }
53              
54              
55             sub parse_string {
56             #
57             # parse a string containing RTF
58             #
59 0     0 1   my $self = shift;
60 0           $inbuf = shift;
61 0           $level = 0;
62 0           @elements = ();
63 0           $self->_parse();
64 0           $self->_close_everything;
65             }
66              
67              
68             sub set_feature {
69             #
70             # set a parser feature
71             #
72 0     0 1   my( $self, $feature, $value ) = @_;
73 0 0         if( exists( $features{ $feature })) {
74 0           $features{ $feature } = $value;
75             } else {
76 0           $self->SUPER::set_feature( $feature, $value );
77             }
78             }
79              
80              
81             sub get_feature {
82             #
83             # query a parser feature
84             #
85 0     0 1   my( $self, $feature ) = @_;
86 0 0         if( exists( $features{ $feature })) {
87 0           return $features{ $feature };
88             } else {
89 0           return $self->SUPER::get_feature( $feature );
90             }
91             }
92              
93              
94             my %paramcmds =
95             #
96             # commands with parameters to wrap
97             #
98             (
99             b => 'bold',
100             deff => 'default-font',
101             deflang => 'language',
102             dy => 'day',
103             edmins => 'minutes-edited',
104             f => 'font',
105             fcharset => 'charset',
106             footery => 'footery',
107             fprq => 'pitch',
108             fs => 'font-size',
109             headery => 'headery',
110             hr => 'hour',
111             id => 'id',
112             keepn => 'keep-next',
113             li => 'indent-left',
114             margl => 'margin-left',
115             margr => 'margin-right',
116             min => 'min',
117             mo => 'month',
118             nofchars => 'number-chars',
119             nofcharsws => 'number-nonspace-chars',
120             nofpages => 'number-pages',
121             nofwords => 'numver-words',
122             nowidctlpar => 'nowidctlpar',
123             pard => 'style-default',
124             qc => 'align-center',
125             qj => 'align-justify',
126             ql => 'align-left',
127             qr => 'align-right',
128             ri => 'indent-right',
129             rtf => 'rtf-version',
130             sa => 'space-after',
131             sb => 'space-before',
132             sbasedon => 'style-base',
133             sec => 'sec',
134             sl => 'space-line',
135             snext => 'style-next',
136             vern => 'version',
137             yr => 'year',
138             );
139              
140              
141             my %params =
142             #
143             # commands that are parameters, to be wrapped
144             #
145             (
146             ascii => 'character-set',
147             mac => 'character-set',
148             pc => 'character-set',
149             pca => 'character-set',
150             fnil => 'family',
151             froman => 'family',
152             fswiss => 'family',
153             fmodern => 'family',
154             fscript => 'family',
155             fdecor => 'family',
156             ftech => 'family',
157             fbidi => 'family',
158             ftnil => 'type',
159             fttruetype => 'type',
160             );
161              
162              
163             my %groupnames =
164             #
165             # commands labelling groups
166             #
167             (
168             author => 'author',
169             b => 'bold',
170             buptim => 'time-backedup',
171             category => 'category',
172             colortbl => 'color-table',
173             comment => 'comment',
174             company => 'company',
175             creatim => 'time-created',
176             cs => 'char-style',
177             edmins => 'minutes-edited',
178             f => 'font',
179             field => 'field',
180             filetbl => 'file-table',
181             fldinst => 'field-inst',
182             fldrslt => 'field-result',
183             footer => 'footer',
184             footerf => 'footer-first',
185             footerl => 'footer-left',
186             footerr => 'footer-right',
187             footnote => 'footnote',
188             fonttbl => 'font-table',
189             header => 'header',
190             headerf => 'header-first',
191             headerl => 'header-left',
192             headerr => 'header-right',
193             i => 'italic',
194             info => 'info',
195             keywords => 'keywords',
196             listtables => 'list-tables',
197             manager => 'manager',
198             nofchars => 'number-chars',
199             nofcharsws => 'number-nonspace-chars',
200             nofpages => 'number-pages',
201             nofwords => 'numver-words',
202             operator => 'operator',
203             pn => 'para-number',
204             pnseclvl => 'pn-sec-level',
205             pntext => 'pn-text',
206             pntxta => 'pn-txta',
207             pntxtb => 'pn-txtb',
208             printim => 'time-printed',
209             revtbl => 'rev-table',
210             revtim => 'time-revised',
211             s => 'para-style',
212             title => 'title',
213             subject => 'subject',
214             stylesheet => 'stylesheet',
215             ul => 'ul',
216             vern => 'version',
217             version => 'version',
218             );
219              
220              
221             my %wraptext =
222             #
223             # situations where we want to wrap text in an element
224             #
225             (
226             font => 'name',
227             'para-style' => 'name',
228             );
229              
230              
231             sub _parse {
232             #
233             # parse contents of the input buffer
234             #
235 0     0     my $self = shift;
236 0           while( $inbuf ) {
237 0 0         if( $inbuf =~ /^\{/ ) {
    0          
    0          
238 0           $self->_handle_group();
239            
240             } elsif( $inbuf =~ /^\}/ ) {
241 0 0         $self->_parse_error() unless( $level > 0 );
242 0           return;
243            
244             } elsif( $inbuf =~ /^\\/ ) {
245 0           $self->_handle_ctlword();
246              
247             } else {
248 0           $self->_handle_content();
249             }
250             }
251             }
252              
253              
254             sub _handle_content {
255             #
256             # process character data
257             #
258 0     0     my $self = shift;
259 0           my $curr = $self->_current_element;
260 0 0         if( $inbuf =~ /([^\\\{\}]+)/ ) {
261 0           my $data = $1;
262 0           $inbuf = $';
263 0 0         if( exists( $wraptext{ $curr })) {
264 0           $data =~ s/;//;
265 0           $self->_indent_start_element( $wraptext{ $curr });
266 0           $self->_characters( $data );
267 0           $self->_end_element;
268             } else {
269 0           $self->_characters( $data );
270             }
271             } else {
272             }
273             }
274              
275              
276             sub _handle_ctlword {
277             #
278             # process a control word
279             #
280 0     0     my $self = shift;
281 0           $inbuf =~ s/^\\//;
282 0 0         if( $inbuf =~ /^([a-z]+)/ ) {
    0          
    0          
283 0           my $command = $1;
284 0           my $parameter;
285 0           $inbuf = $';
286 0 0         if( $inbuf =~ /^(-?[0-9]+)/ ) {
287 0           $parameter = $1;
288 0           $inbuf = $';
289             }
290 0 0         if( $inbuf =~ /^ / ) {
291 0           $inbuf = $';
292             }
293 0           $self->_command( $command, $parameter );
294              
295             } elsif( $inbuf =~/([\\\{\}])/ ) {
296 0           $self->_characters( $1 );
297              
298             } elsif( $inbuf =~/([^a-z])/ ) {
299 0           my $command = $1;
300 0           $inbuf = $';
301 0           $self->_start_element( 'command', {'param' => $command} );
302 0           $self->_end_element;
303              
304             } else {
305 0           parse_error();
306             }
307             }
308              
309              
310             sub _command {
311             #
312             # process a command
313             #
314 0     0     my( $self, $command, $param ) = @_;
315              
316 0 0         if( $command eq 'par' ) {
    0          
    0          
    0          
317 0 0         $self->_end_element
318             if( $self->_current_element eq 'para' );
319 0           $self->_indent_start_element( 'para' );
320              
321             } elsif( exists( $paramcmds{$command} )) {
322 0           $self->_indent_start_element( $paramcmds{ $command });
323 0           $self->_characters( $param );
324 0           $self->_end_element;
325              
326             } elsif( exists( $params{ $command })) {
327 0           $self->_indent_start_element( $params{ $command });
328 0           $self->_characters( $command );
329 0           $self->_end_element;
330              
331             } elsif( defined( $param )) {
332 0           $self->_start_element( $command, { param => $param });
333 0           $self->_end_element;
334              
335             } else {
336 0           $self->_start_element( $command );
337 0           $self->_end_element;
338             }
339             }
340              
341              
342             sub _handle_group {
343             #
344             # process a group
345             #
346 0     0     my $self = shift;
347 0           $inbuf =~ s/^\{//;
348 0 0 0       if( $level == 0 ) {
    0 0        
    0 0        
349 0           $self->_start_element( 'rtfdoc' );
350 0           $self->_indent_start_element( 'header' );
351              
352             } elsif(( $inbuf =~ /^\s*\\([a-z]+)/ and exists( $groupnames{$1} ))
353             or( $inbuf =~ /^\s*\\\*\\([a-z]+)/ and exists( $groupnames{$1} ))) {
354 0           $inbuf = $';
355 0           my $name = $groupnames{$1};
356 0 0 0       if( $name eq 'info' and $self->_current_element eq 'header' ) {
    0          
357 0           $self->_indent_end_element;
358 0           $self->_indent_start_element( 'document' );
359 0           $self->_indent_start_element( $name );
360              
361             } elsif( $inbuf =~ /^(-?[0-9]+)/ ) {
362 0           my $param = $1;
363 0           $inbuf = $';
364 0           $self->_indent_start_element( $name, { number => $param });
365              
366             } else {
367 0           $self->_indent_start_element( $name );
368             }
369 0 0         $inbuf = $' if( $inbuf =~ /^ / );
370              
371             } elsif( $self->_current_element eq 'stylesheet' ) {
372 0           $self->_indent_start_element( 'para-style' );
373              
374             } else {
375 0           $self->_indent_start_element( 'group', { level => $level });
376             }
377 0           $self->_parse();
378 0           $inbuf =~ s/^\}//;
379 0           $self->_indent_end_element;
380             }
381              
382              
383             sub _characters {
384             #
385             # clean up characters, call handler
386             #
387 0     0     my( $self, $data ) = @_;
388 0 0         return unless( defined( $data ));
389 0           $self->_debug( "CHARACTERS: [$data]", 3 );
390 0           $data = $self->_unprotect_chars( $data );
391 0           $data =~ s/&/&/g;
392 0           $data =~ s/
393 0           $data =~ s/>/>/g;
394 0           $data =~ s/\n//g;
395 0           $self->SUPER::characters({ Data => $data });
396             }
397              
398              
399             sub _newline {
400             #
401             # output a newline character
402             #
403 0     0     my $self = shift;
404 0           $self->SUPER::characters({ Data => "\n" });
405             }
406              
407              
408             sub _indent_start_element {
409             #
410             # start new element with indentation
411             #
412 0     0     my( $self, $name, $params ) = @_;
413 0           $self->_newline;
414 0           $self->_characters( ' ' x $level );
415 0           $self->_start_element( $name, $params );
416             }
417              
418              
419             sub _indent_end_element {
420             #
421             # end an indented element
422             #
423 0     0     my $self = shift;
424 0           $self->_newline;
425 0           $self->_characters( ' ' x ( $level-1 ));
426 0           $self->_end_element;
427             }
428              
429              
430             sub _start_element {
431             #
432             # generate element start event, push name onto stack
433             #
434 0     0     my( $self, $name, $atts ) = @_;
435 0           $self->_debug( "START ELEMENT: $name", 3 );
436 0           $level ++;
437 0           push( @elements, $name );
438 0 0         if( $atts ) {
439 0           $self->SUPER::start_element({ Name => $name, Attributes => $atts });
440             } else {
441 0           $self->SUPER::start_element({ Name => $name });
442             }
443             }
444              
445              
446             sub _end_element {
447             #
448             # generate element finished event, pop name from stack
449             #
450 0     0     my $self = shift;
451 0           my $name = pop( @elements );
452 0           $self->_debug( "END ELEMENT: $name", 3 );
453 0           $level --;
454 0           $self->SUPER::end_element({ Name => $name });
455 0           return $name;
456             }
457              
458              
459             sub _current_element {
460             #
461             # return name of current element on stack
462             #
463 0     0     my $self = shift;
464 0           return $elements[ $#elements ];
465             }
466              
467              
468             sub _inside {
469             #
470             # return true if current element or ancestor has given name
471             #
472 0     0     my $self = shift;
473 0           my $name = shift;
474 0           foreach( @elements ) {
475 0 0         return 1 if( $name eq $_ );
476             }
477 0           return 0;
478             }
479              
480              
481             sub _close_everything {
482             #
483             # close all open elements
484             #
485 0     0     my $self = shift;
486 0           $self->_debug( "ENTER _close_everything", 2 );
487 0           while( $level ) {
488 0           $self->_indent_end_element;
489             }
490 0           $self->_debug( "EXIT _close_everything", 2 );
491             }
492              
493              
494             sub _protect_chars {
495             #
496             # escape special characters from parsing
497             #
498 0     0     my $self = shift;
499 0           my $data = shift;
500 0           $data =~ s/&/\001RTF-AMPERSAND\001/g;
501             #$data =~ s/\\>/\001RTF-GREATER-THAN\001/g;
502             #$data =~ s/\\
503 0           return $data;
504             }
505              
506              
507             sub _unprotect_chars {
508             #
509             # resolve escaped characters
510             #
511 0     0     my $self = shift;
512 0           my $data = shift;
513 0           $data =~ s/\001RTF-AMPERSAND\001/&/g;
514 0           $data =~ s/\001RTF-COLON\001/:/g;
515 0           $data =~ s/\001RTF-EQUALS\001/=/g;
516 0           return $data;
517             }
518              
519              
520             sub _parse_error {
521             #
522             # handle parse exception
523             #
524 0     0     my $self = shift;
525 0           print STDERR "PARSE ERROR!\n";
526 0 0         print STDERR "HERE: $1\n" if( $inbuf =~ /(...........................)/ );
527 0           exit;
528             }
529              
530              
531             sub _debug {
532             #
533             # print a debug message
534             #
535 0     0     my( $self, $message, $level ) = @_;
536 0 0         if( $features{DEBUG} >= $level ) {
537 0           print STDERR "XML::SAX::RTF DEBUG-$level> $message\n";
538             }
539             }
540              
541              
542             1;
543             __END__