File Coverage

blib/lib/XMLNews/HTMLTemplate.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package XMLNews::HTMLTemplate;
2              
3 1     1   823 use strict;
  1         2  
  1         40  
4 1     1   5 use vars qw($VERSION);
  1         2  
  1         50  
5 1     1   2169 use IO::Handle;
  1         10253  
  1         61  
6 1     1   2937 use XML::Parser;
  0            
  0            
7             use XMLNews::Meta;
8              
9             $VERSION = '0.01';
10              
11              
12            
13             ########################################################################
14             # Compiled information from the XML/NITF document.
15             #
16             # This class is intended for internal use only.
17             #
18             # Information extracted from the NITF document is stored as a series
19             # of hash/value pairs, where the hash values are buckets containing
20             # zero or more literal values (in document order).
21             ########################################################################
22              
23             package XMLNews::HTMLTemplate::NITF;
24             # Use static variables -- closures would
25             # be better, but currently they leak
26             # memory very badly.
27             use vars qw($SELF $DATA @DATA_STACK);
28             use Carp;
29              
30              
31             #
32             # Constructor.
33             #
34             sub new {
35             my ($class) = (@_);
36             my $self = {};
37             return bless $self, $class;
38             }
39              
40              
41             #
42             # Return the values of a pseudo-property as an array.
43             #
44             sub getValues {
45             my ($self, $propname) = (@_);
46             if ($self->{$propname}) {
47             return @{$self->{$propname}};
48             } else {
49             return ();
50             }
51             }
52              
53              
54             #
55             # Add a value for a pseudo-property.
56             #
57             sub addValue {
58             my ($self, $propname, $value) = (@_);
59             unless ($self->{$propname}) {
60             $self->{$propname} = [];
61             }
62             push @{$self->{$propname}}, $value;
63             }
64              
65              
66             #
67             # Return true if there are any values available for a pseudo-property.
68             #
69             sub hasValue {
70             my ($self, $propname) = (@_);
71             if ($self->{$propname}) {
72             my @values = @{$self->{$propname}};
73             } else {
74             return undef;
75             }
76             }
77              
78              
79             #
80             # Direct mappings between NITF and HTML
81             # TODO: handle tables, media objects, and postaddr.
82             #
83             my %html_mappings = ('h1' => 'h1',
84             'h2' => 'h2',
85             'h3' => 'h3',
86             'h4' => 'h4',
87             'p' => 'p',
88             'ol' => 'ol',
89             'ul' => 'ul',
90             'li' => 'li',
91             'dl' => 'dl',
92             'dt' => 'dt',
93             'dd' => 'dd',
94             'em' => 'em',
95             'pre' => 'pre',
96             'table' => 'table',
97             'tr' => 'tr',
98             'th' => 'th',
99             'td' => 'td');
100              
101              
102             #
103             # XML event handler for the start of an element.
104             # This should be a closure, but closures leak memory, so we're using
105             # static variables instead.
106             #
107             sub _start {
108             my ($expat, $name, %atts) = (@_);
109             my $self = $SELF;
110             push @DATA_STACK, $DATA;
111             $DATA = '';
112              
113             if ($html_mappings{$name}) {
114             $DATA .= "<" . $html_mappings{$name} . ">";
115             }
116             }
117              
118              
119             #
120             # XML event handler for the end of an element.
121             # This should be a closure, but closures leak memory, so we're using
122             # static variables instead.
123             #
124             sub _end {
125             my ($expat, $name) = (@_);
126             my $self = $SELF;
127              
128             # If the element ending has a direct
129             # mapping to an HTML element,
130             # include the appropriate end tag.
131             if ($html_mappings{$name}) {
132             $DATA .= "";
133             }
134              
135             # If the element ending is one of the
136             # fields we need, save it; otherwise,
137             # append its data to its parent's.
138             if ($name eq 'hl1') {
139             $self->addValue('headline', $DATA);
140             $DATA = pop @DATA_STACK;
141             } elsif ($name eq 'hl2') {
142             $self->addValue('subheadline', $DATA);
143             $DATA = pop @DATA_STACK;
144             } elsif ($name eq 'bytag') {
145             $self->addValue('byline', $DATA);
146             $DATA = pop @DATA_STACK;
147             } elsif ($name eq 'distributor') {
148             $self->addValue('distributor', $DATA);
149             $DATA = pop @DATA_STACK;
150             } elsif ($name eq 'dateline') {
151             $self->addValue('dateline', $DATA);
152             $DATA = pop @DATA_STACK;
153             } elsif ($name eq 'body.content') {
154             $self->addValue('body', $DATA);
155             $DATA = pop @DATA_STACK;
156             } else {
157             $DATA = (pop @DATA_STACK) . $DATA;
158             }
159             }
160              
161              
162             #
163             # XML event handler for character data.
164             # This should be a closure, but closures leak memory, so we're using
165             # static variables instead.
166             #
167             sub _char {
168             my ($expat, $data) = (@_);
169              
170             $DATA .= $data;
171             }
172              
173              
174             #
175             # Read an XML/NITF document and fill in the object's fields.
176             # TODO: add parse error handling.
177             #
178             sub readNITF {
179             my ($self, $input) = (@_);
180              
181             unless (ref($input)) {
182             $input = new IO::File("<$input") || croak "Cannot read NITF file $input";
183             }
184              
185             # Create a new parser object.
186             my $parser = new XML::Parser(Handlers => {Start => \&_start,
187             End => \&_end,
188             Char => \&_char});
189            
190             # Parse the XML/NITF file using the
191             # handlers (closures) defined above.
192             $SELF = $self;
193             $DATA = '';
194             @DATA_STACK = ();
195             $parser->parse($input);
196             $SELF = $DATA = @DATA_STACK = undef;
197             }
198              
199              
200            
201             ########################################################################
202             # Compiled HTML template file.
203             #
204             # This is stored as a tree of array references: the first element of
205             # each array is a tag giving the node type. The nodes take the
206             # following format:
207             #
208             # template node (root): ['template', CHILDREN...]
209             # text node (leaf): ['text', STRING]
210             # insert node (leaf): ['insert', PROPNAME]
211             # if node: ['if', [TRUE-CHILDREN], [ELSE-CHILDREN]]
212             # foreach node: ['foreach', CHILDREN...]
213             ########################################################################
214              
215             package XMLNews::HTMLTemplate;
216             use strict;
217             use vars qw($START_TEXT
218             $START_PATTERN
219             $END_TEXT
220             $END_PATTERN
221             $MODE_TOP
222             $MODE_IF
223             $MODE_ELSE
224             $MODE_FOREACH
225             $OUT
226             $META
227             $NITF);
228             use Carp;
229              
230             #
231             # Constants
232             #
233             # PATTERNS:
234              
235             $START_TEXT = "
236             $START_PATTERN = "\\<\\?XNews";
237             $END_TEXT = ">"; # end-of-command pattern
238             $END_PATTERN = "\\??\\>";
239              
240             # PARSING STATES:
241              
242             $MODE_TOP = 0; # top-level
243             $MODE_IF = 1; # in main part of 'if' block
244             $MODE_ELSE = 2; # in 'else' part of 'if' block
245             $MODE_FOREACH = 3; # in 'foreach' block
246              
247              
248             #
249             # Constructor.
250             #
251             sub new {
252             my ($class) = (@_);
253             my $self = [['template']];
254             return bless $self, $class;
255             }
256              
257              
258             #
259             # Read a template file and parse it into a tree structure.
260             #
261             # This uses simple regular-expression matching for the parse; it does
262             # not attempt to read the template as a proper SGML/HTML or XML/HTML
263             # document, because it is unlikely that the template was written that
264             # way (even though it could be).
265             #
266             sub readTemplate {
267             my ($self, $input) = (@_);
268              
269             my $oldRS = $/;
270              
271             # Erase any old compiled tree, in case
272             # this object is being reused.
273             $self->[0] = ['template'];
274              
275             # If the input argument is a file name,
276             # attempt to open the file; otherwise,
277             # treat it as a handle.
278             unless (ref($input)) {
279             $input = new IO::File("<$input")
280             || croak "Cannot read HTML template file $input";
281             $self->readTemplate($input);
282             $input->close();
283             return;
284             }
285              
286             #
287             # Variables to hold parse state.
288             #
289             my @node = (); # node stack
290             my $node = $self->[0]; # current node (start at top level)
291              
292             my @container = (); # container stack
293             my $container = $node; # current container (not always the
294             # same as current node, since 'if'
295             # nodes have two containers)
296              
297             my @mode = (); # mode stack
298             my $mode = $MODE_TOP; # current mode (see constants above)
299              
300             my $data = '';
301             my %namespaces; # declared namespaces
302              
303             #
304             # Main parsing loop
305             #
306             # Keep looping until we do not find the start of a command.
307             #
308             $/ = $START_TEXT; # set the delimiter to "
309             LOOP: while (defined($data = <$input>)) {
310              
311             # Did we find "
312             if ($data =~ /^(.*)($START_PATTERN)$/s) {
313             # ...yes
314             if ($1) {
315             push @{$container}, ['text', $1];
316             }
317             } else {
318             # ...no, end the document
319             if ($data) {
320             push @{$container}, ['text', $data];
321             }
322             last LOOP;
323             }
324              
325            
326             # Now, try to read to the end of the
327             # command, and report an error if
328             # there is something wrong (there's no
329             # graceful way to recover from an
330             # unterminated command)
331             $/ = $END_TEXT;
332             unless (defined($data = <$input>) &&
333             ($data =~ /^([^\>\?]*)($END_PATTERN)$/m)) {
334             croak "Template: unterminated command: $data (line $.)";
335             }
336            
337             # Split up the command into the keyword
338             # and the optional parameter
339             my $command = $1;
340             unless ($command =~ m/^\s*(\S+)\s*(\S+|\S+\s*=\s*\S+)?\s*$/) {
341             croak "Malformed template command: $command (line $.)";
342             }
343             my ($key, $param) = ($1, $2);
344              
345             # Deal with the known command types;
346             # print a warning for an unknown command,
347             # but don't actually stop processing,
348             # since we can probably recover
349             SWITCH: {
350              
351             # 'namespace' declares a namespace
352             # prefix for later use; it does not
353             # generate a node in the final tree
354             ($key eq 'namespace') && do {
355             unless (defined($param) && $param =~ /^(\S+)\s*=\s*(\S+)$/) {
356             croak "Template: malformed namespace assignment: $param (line $.)";
357             }
358             my ($prefix, $uri) = ($1, $2);
359             if ($uri =~ /^([\'\"])/) {
360             my $delim = $1;
361             $uri = $';
362             if ($uri =~ /$delim$/) {
363             $uri = $`;
364             } else {
365             croak "Template: unterminated namespace URI starting with $delim (line $.)";
366             }
367             }
368             if ($uri =~ /^[\'\"](.*)[\'\"]$/) {
369             $uri = $1;
370             }
371             $namespaces{$prefix} = $uri;
372             last SWITCH;
373             };
374              
375             # At this point, everything else
376             # will need namespace processing.
377             my $prop;
378             if (defined($param) && $param =~ /^([^:]+):([^:]+)$/) {
379             $prop = [$namespaces{$1}, $2];
380             unless ($prop->[0]) {
381             carp "Unrecognised namespace prefix: $1";
382             }
383             } else {
384             $prop = [undef, $param];
385             }
386              
387             # 'insert' is a leaf node, so we don't
388             # have to mess with the state or the
389             # stacks
390             ($key eq 'insert') && do {
391             push @{$container}, ['insert', $prop];
392             last SWITCH;
393             };
394             # 'if' is a branch node, so we have to
395             # push a new state and a new container
396             # ('if' branches have two containers;
397             # we always start with the first one)
398             ($key eq 'if') && do {
399             push @mode, $mode;
400             $mode = $MODE_IF;
401             push @node, $node;
402             push @container, $container;
403             $node = ['if', $prop, [], []];
404             $container = $node->[2];
405             last SWITCH;
406             };
407              
408             # 'else' is just a continuation of
409             # an 'if' node, so simply switch from
410             # the first 'if' container to the
411             # second and set a new mode
412             ($key eq 'else') && do {
413             if ($mode eq $MODE_IF) {
414             $mode = $MODE_ELSE;
415             $container = $node->[3];
416             } else {
417             carp "'else' outside of 'if' block";
418             }
419             last SWITCH;
420             };
421              
422             # 'end' means finish up the current
423             # container and pop up a level
424             ($key eq 'end') && do {
425             if ($mode eq $MODE_TOP) {
426             carp "'end' outside of 'if' or 'foreach' block";
427             } else {
428             $mode = pop @mode;
429             $container = pop @container;
430             push @{$container}, $node;
431             $node = pop @node;
432             }
433             last SWITCH;
434             };
435              
436             # 'foreach' is a branch node, so we
437             # have to change the state and push
438             # a new container
439             ($key eq 'foreach') && do {
440             push @mode, $mode;
441             $mode = $MODE_FOREACH;
442             push @node, $node;
443             push @container, $container;
444             $node = ['foreach', $prop];
445             $container = $node;
446             last SWITCH;
447             };
448            
449             # Unrecognised command: whine a bit
450             carp "Unrecognised XNews template command: $key $param";
451             }
452              
453             $/ = $START_TEXT;
454             }
455              
456             # end of loop.
457              
458             # OK, now the parse loop is finished,
459             # and it's time to clean up.
460              
461             # Check that any 'if' or 'foreach'
462             # blocks have been ended correctly
463             if ($mode eq $MODE_IF || $mode eq $MODE_ELSE) {
464             croak("Template finished before end of 'if' block");
465             } elsif ($mode eq $MODE_FOREACH) {
466             croak("Template finished before end of 'foreach' block");
467             }
468              
469             # Restore the former record separator.
470             $/ = $oldRS;
471             }
472              
473              
474             #
475             # Apply a compiled template to an NITF file and an RDF file.
476             #
477             sub applyTemplate {
478             my ($self, $out, $nitf, $meta) = (@_);
479             my @openHandles = ();
480              
481             #
482             # If the 'out' argument is a string, try to open it as a file.
483             #
484             unless (ref($out)) {
485             $out = new IO::File(">$out") || croak "Cannot write to file $out";
486             push @openHandles, $out;
487             }
488            
489             #
490             # Ensure that we have an NITF object to work with
491             #
492             if ($nitf && (ref($nitf) ne 'XMLNews::HTMLTemplate::NITF')) {
493             my $input = $nitf;
494             $nitf = new XMLNews::HTMLTemplate::NITF();
495             $nitf->readNITF($input);
496             }
497            
498             #
499             # Ensure that we have a meta object to work with.
500             #
501             if ($meta && (ref($meta) ne 'XMLNews::Meta')) {
502             my $input = $meta;
503             $meta = new XMLNews::Meta();
504             $meta->importRDF($input);
505             }
506             # Recursively walk the template tree
507             $META = $meta;
508             $NITF = $nitf;
509             $OUT = $out;
510             _writeNode($self->[0]);
511             $META = $NITF = $OUT = undef;
512              
513             # Close any handles that we opened
514             # ourselves.
515             my $handle;
516             foreach $handle (@openHandles) {
517             $handle->close();
518             }
519             }
520              
521              
522             #
523             # Closure: write out a node in the HTML file.
524             #
525             # This is an internal subroutine that has access to all of
526             # the local variables in the context where it was defined.
527             #
528             # Scheme programmers love this sort of thing; it's still pretty
529             # new to Perl, though.
530             #
531             sub _writeNode {
532             my ($node, $foreachProp, $foreachValue) = (@_);
533              
534             # Copy the node...
535             my $type = $node->[0];
536              
537             SWITCH: {
538              
539             # top-level template: process
540             # the children
541             ($type eq 'template') && do {
542             my ($type, @children) = (@{$node});
543             my $child;
544             foreach $child (@children) {
545             _writeNode($child);
546             }
547             last SWITCH;
548             };
549              
550             # literal text: just dump it out
551             ($type eq 'text') && do {
552             my $text = $node->[1];
553             $OUT->print($text);
554             last SWITCH;
555             };
556              
557             # insertion: look up the value
558             # or values for the property (and
559             # check whether we're in a foreach)
560             ($type eq 'insert') && do {
561             my $prop = $node->[1];
562             if (defined($foreachProp) &&
563             $prop->[0] eq $foreachProp->[0] &&
564             $prop->[1] eq $foreachProp->[1]) {
565             $OUT->print($foreachValue);
566             } else {
567             my @values = _getValues($prop);
568             $OUT->print("@values");
569             }
570             last SWITCH;
571             };
572              
573             # if: execute the appropriate block,
574             # depending on whether the property
575             # has any values
576             ($type eq 'if') && do {
577             my ($type, $prop, $mainblock, $altblock) = (@{$node});
578             if (_hasValue($prop)) {
579             foreach $node (@{$mainblock}) {
580             _writeNode($node);
581             }
582             } else {
583             foreach $node (@{$altblock}) {
584             _writeNode($node);
585             }
586             }
587             last SWITCH;
588             };
589              
590             # foreach: iterate through all of the
591             # values for the property, processing
592             # the block with a different value
593             # bound each time
594             ($type eq 'foreach') && do {
595             my ($type, $prop, @block) = (@{$node});
596             my @values = _getValues($prop);
597             my $value;
598             foreach $value (@values) {
599             foreach $node (@block) {
600             _writeNode($node, $prop, $value);
601             }
602             }
603             last SWITCH;
604             };
605              
606             carp "Unknown template node type: $type";
607             }
608             }
609              
610              
611             #
612             # Closure: return all NITF and/or RDF values for a property.
613             #
614             sub _getValues {
615             my ($prop) = shift;
616             my ($uripart, $localpart) = (@{$prop});
617             my @values = ();
618              
619             # If there's a URI part, look in the
620             # RDF; otherwise, look in the NITF.
621             if ($uripart) {
622             @values = $META->getValues($uripart, $localpart) if $META;
623             } else {
624             @values = $NITF->getValues($localpart) if $NITF;
625             }
626              
627             return @values;
628             }
629              
630              
631             #
632             # Closure: return true if there are values available for a property.
633             #
634             sub _hasValue {
635             my ($prop) = (@_);
636             my ($uripart, $localpart) = (@{$prop});
637              
638             if ($uripart) {
639             return $META && $META->hasValue($uripart, $localpart);
640             } else {
641             return $NITF && $NITF->hasValue($localpart);
642             }
643             }
644              
645              
646             1;
647             # Autoload methods go after =cut, and are processed by the autosplit program.
648              
649             1;
650             __END__