File Coverage

blib/lib/XHTML/Instrumented.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1 3     3   70707 use strict;
  3         7  
  3         138  
2 3     3   21 use warnings;
  3         4  
  3         126  
3              
4             package XHTML::Instrumented;
5              
6 3     3   1654 use XHTML::Instrumented::Entry;
  3         9  
  3         96  
7 3     3   2208 use XHTML::Instrumented::Context;
  3         9  
  3         99  
8              
9 3     3   19 use Carp qw (croak verbose);
  3         7  
  3         464  
10 3     3   2943 use XML::Parser;
  0            
  0            
11              
12             =head1 NAME
13              
14             XHTML::Instrumented - packages to control XHTML
15              
16             =head1 VERSION
17              
18             Version 0.092
19              
20             =cut
21              
22             our $VERSION = '0.092';
23              
24             our @CARP_NOT = ( 'XML::Parser::Expat' );
25              
26             use Params::Validate qw( validate SCALAR SCALARREF BOOLEAN HASHREF OBJECT UNDEF CODEREF );
27              
28             our $path = '.';
29             our $cachepath;
30              
31             sub path
32             {
33             my $self = shift;
34              
35             $self->{path} || $path;
36             }
37              
38             sub cachepath
39             {
40             my $self = shift;
41              
42             $self->{cachepath} || $cachepath || $self->path;
43             }
44              
45             sub cachefilename
46             {
47             my $self = shift;
48             my $file = $self->cachepath;
49              
50             if ($self->{type} || $self->{default_type}) {
51             $file .= '/' . $self->{type} || $self->{default_type} if $self->{type} || $self->{default_type};
52             $file .= '/' . $self->{name};
53             $file .= '.cxi';
54             } elsif ($self->{name}) {
55             $file .= '/' . $self->{name} . '.cxi';
56             } else {
57             $file = $self->{filename} . '.cxi';
58             }
59              
60             return $file;
61             }
62              
63             sub import
64             {
65             my $class = shift;
66             my %p = validate(@_, {
67             path => 0,
68             cachepath => 0,
69             });
70              
71             $path = $p{path};
72             $cachepath = $p{cachepath};
73             }
74              
75             sub new
76             {
77             my $class = shift;
78             my $self = bless { validate(@_, {
79             'name' => {
80             type => SCALAR | SCALARREF,
81             optional => 1,
82             },
83             'type' => {
84             type => SCALAR,
85             optional => 1,
86             },
87             'default_type' => {
88             type => SCALAR,
89             optional => 1,
90             },
91             'filename' => {
92             type => SCALAR,
93             optional => 1,
94             },
95             'filter' => {
96             optional => 1,
97             type => CODEREF,
98             },
99             'replace_name' => {
100             optional => 1,
101             type => SCALAR,
102             },
103             'cachepath' => {
104             optional => 1,
105             type => SCALAR,
106             },
107             'path' => {
108             optional => 1,
109             type => SCALAR,
110             },
111             })}, $class;
112              
113             my $path = $self->path();
114             my $type = $self->{type} || '';
115             my $name = $self->{name};
116             my $filename = $self->{filename};
117             my $alt_filename = $self->{filename};
118              
119             unless ($filename or ref($name) eq 'SCALAR') {
120             $filename = $self->{filename} = "$path/$type/$name";
121             my $type = $self->{default_type} || '';
122             unless (-f "$filename.html") {
123             $filename = $self->{filename} = "$path/$type/$name";
124             }
125             unless (-f "$filename.html") {
126             $filename = $self->{filename} = "$path/$name";
127             }
128             unless (-f "$filename.html") {
129             die "File not found: $filename";
130             }
131             }
132              
133             if ($filename) {
134             my $cachefile = $self->cachefilename;
135              
136             my @path = split('/', $cachefile);
137             pop @path;
138              
139             if (-r $cachefile and ( -M $cachefile < -M $filename . '.html')) {
140             require Storable;
141             $self->{parsed} = Storable::retrieve($cachefile);
142             } elsif ( -r $filename . '.html') {
143             $self->{parsed} = $self->parse(
144             $filename . '.html',
145             name => $name,
146             type => $self->{type},
147             default_type => $self->{default_type},
148             replace_name => $self->{replace_name} || 'home',
149             path => $self->path,
150             cachepath => $self->cachepath,
151             );
152             my $path = '';
153             while (@path) {
154             $path .= shift(@path) . '/';
155             unless ( -d $path ) {
156             mkdir $path or die 'Bad path ' . $path . " $cachefile @path";
157             }
158             }
159             require Storable;
160             Storable::nstore($self->{parsed}, $cachefile );
161             } else {
162             die "File not found: $filename";
163             }
164             } else {
165             unless (ref($name) eq 'SCALAR') {
166             croak "no template for $name [$path/$type/$name.tmpl]" unless (-f "$path/$type/$name.tmpl");
167             }
168             $self->{parsed} = $self->parse(
169             $name,
170             name => '_scalar_',
171             replace_name => $self->{replace_name} || 'home',
172             path => $self->path,
173             cachepath => $self->cachepath,
174             );
175             }
176              
177             $self;
178             }
179              
180             # helper functions
181              
182             sub loop
183             {
184             my $self = shift;
185             my %p = validate(@_, {
186             headers => 0,
187             data => 0,
188             inclusive => 0,
189             default => 0,
190             });
191             require XHTML::Instrumented::Loop;
192              
193             XHTML::Instrumented::Loop->new(%p);
194             }
195              
196             sub get_form
197             {
198             my $self = shift;
199              
200             require XHTML::Instrumented::Form;
201             XHTML::Instrumented::Form->new(@_);
202             }
203              
204             sub replace
205             {
206             my $self = shift;
207             my %p = validate(@_, {
208             args => 0,
209             text => 0,
210             src => 0,
211             replace => 0,
212             remove => 0,
213             remove_tag => 0,
214             });
215             require XHTML::Instrumented::Control;
216             XHTML::Instrumented::Control->new(%p);
217             }
218              
219             sub args
220             {
221             my $self = shift;
222              
223             $self->replace(args => { @_ });
224             }
225              
226             our @unused;
227              
228             # the main function
229             sub __filename
230             {
231             my $self = shift;
232             my ($path, $type, $name);
233             unless (-f "$path/$type/$name.tmpl") {
234             $type = $self->{default_type} || 'default';
235             }
236             die "no template for $name [$path/$type/$name.tmpl]" unless (-f "$path/$type/$name.tmpl");
237             my $file = "$path/$type/$name.tmpl";
238             }
239              
240             sub parse
241             {
242             my $self = shift;
243             my $data = shift;
244              
245             @unused = ();
246             my $parser = new XML::Parser::Expat(
247             NoExpand => 1,
248             ErrorContext => 1,
249             ProtocolEncoding => 'utf-8',
250             );
251             $parser->setHandlers('Start' => \&_sh,
252             'End' => \&_eh,
253             'Char' => \&_ch,
254             'Attlist' => \&_ah,
255             'Entity' => \&_ah,
256             'Element' => \&_ah,
257             'Default' => \&_ex,
258             'Unparsed' => \&_cm,
259             'CdataStart' => \&_cds,
260             'CdataEnd' => \&_cde,
261             );
262             $parser->{_OFF_} = 0;
263             $parser->{__filter__} = $self->{filter};
264             $parser->{__ids__} = {};
265             $parser->{__idr__} = {};
266             $parser->{__args__} = { @_ };
267              
268             $self->{_parser} = $parser;
269              
270             my $type = $self->{type};
271             my $name = $self->{name};
272             my %hash = (@_);
273              
274             $parser->{__data__} = {}; # FIXME this may need to be set
275             $parser->{__top__} = XHTML::Instrumented::Entry->new(
276             tag => '__global__',
277             flags => {},
278             args => {},
279             );
280             $parser->{__context__} = [ $parser->{__top__} ];
281              
282             if (ref($data) eq 'SCALAR') {
283             my $html = ${$data};
284             eval {
285             $parser->parse($html);
286             };
287             if ($@) {
288             die "$@";
289             }
290             } else {
291             my $filename = $data;
292             eval {
293             $parser->parsefile($filename);
294             };
295             if ($@) {
296             croak "$@ $filename";
297             }
298             }
299             bless({
300             idr => $parser->{__idr__},
301             data => $parser->{__top__}->{data}
302             }, 'XHTML::Intramented::Parsed');
303             }
304              
305             sub _get_tag
306             {
307             my $tag = shift;
308             my $start = shift;
309             my $data = $start;
310              
311             for my $element (@$data) {
312             next unless ref($element);
313              
314             return $element if $element->{tag} eq $tag;
315              
316             my $data = _get_tag($tag, $element->{data});
317             return $data if $data;
318             }
319             undef;
320             }
321              
322             sub get_tag
323             {
324             my $self = shift;
325             my $tag = shift;
326              
327             my $data = _get_tag($tag, $self->{parsed}{data});
328              
329             return $data;
330             }
331              
332             sub instrument
333             {
334             my $self = shift;
335             my %p = validate(@_, {
336             content_tag => 1,
337             control => {
338             },
339             });
340             my $data = {};
341             my $ret;
342              
343             $data->{data} = [ $self->{parsed}{data} ];
344              
345             if (my $tag = $p{content_tag}) {
346             $data = _get_tag($tag, $self->{parsed}{data});
347             $data->{data} = [ @{$self->{parsed}{data}} ] unless $data;
348             }
349             my $hash = $p{control} || {};
350              
351             for my $element ( @{$data->{data}} ) {
352             if (ref($element)) {
353             $ret .= $element->expand(
354             context => XHTML::Instrumented::Context->new(
355             hash => $hash,
356             ),
357             );
358             } else {
359             $ret .= $element;
360             }
361             }
362              
363             $ret;
364             }
365              
366             sub head
367             {
368             my $self = shift;
369             my %hash = (@_);
370              
371             return $self->instrument(
372             content_tag => 'head',
373             control => { %hash },
374             );
375             }
376              
377             sub output
378             {
379             my $self = shift;
380             my %hash = (@_);
381              
382             return $self->instrument(
383             content_tag => 'body',
384             control => { %hash },
385             );
386             }
387              
388             our $level = 0;
389              
390             use Encode;
391              
392             sub _fixup
393             {
394             my @ret;
395             for my $data (@_) {
396             $data =~ s/&/&/g;
397             my $x = $data;
398              
399             push @ret, $data;
400             }
401             @ret;
402             }
403              
404             sub _ex
405             {
406             my $self = shift;
407              
408             push(@{$self->{__context__}[-1]->{data}}, @_);
409             }
410              
411             sub _cm
412             {
413             die "Don't know how to handle Unparsed Data";
414             }
415              
416             sub _cds
417             {
418              
419             }
420              
421             sub _cde
422             {
423              
424             }
425              
426             sub _sh
427             {
428             my $self = shift;
429             my $tag = shift;
430             my %args = @_;
431              
432             my $top = $self->{__context__}->[-1];
433              
434             if (my $code = $self->{__filter__}) {
435             $code->(
436             tag => $tag,
437             args => \%args,
438             );
439             }
440              
441             for my $key (keys %args) {
442             my %hash = %{$self->{__data__}};
443             if ($args{$key} =~ /\@\@([A-Za-z][A-Za-z0-9_-][^.@]*)\.?([^@]*)\@\@/) {
444             die q(Can't do this);
445             }
446             $args{$key} =~ s/\@\@([A-Za-z][A-Za-z0-9_-][^.@]*)\.?([^@]*)\@\@/
447             my @extra = split('\.', $2);
448             my $name = $1;
449             my $extra = $2;
450             my $type = $hash{$1};
451             if (defined $type) {
452             $type;
453             } else {
454             qq(-- $1 --);
455             }
456             /xge;
457             }
458             my %local = ();
459              
460             my $child = $top->child(
461             tag => $tag,
462             args => \%args,
463             );
464             if (my $id = $child->id) {
465             warn "Duplicate id: $id" if exists $self->{__ids__}{$id};
466             $self->{__ids__}{$args{id}}++;
467             $self->{__idr__}{$id} = $child;
468             }
469             if (exists($self->{_inform_}) && $child->name && $child->id) {
470             $self->{_inform_}->{_ids_}{$child->id} = $child->name;
471             $self->{_inform_}->{_names_}{$child->name} = $child->id;
472             }
473             if (exists($self->{_inform_}) && $child->name) {
474             my $form_id = $self->{_inform_id_};
475             if ($form_id) {
476             $self->{_inform_ids_}{$form_id}{$child->name} = $tag;
477             } else {
478             warn "Fix this";
479             }
480             }
481             push(@{$self->{__context__}},
482             $child,
483             );
484             if ($tag eq 'form') {
485             $self->xpcroak('embeded form') if ($self->{_inform_});
486             $self->{_inform_} = $child;
487             if (my $id = $args{id} || $args{name}) {
488             $self->{_inform_id_} = $id;
489             $self->{_inform_ids_}{$id} = {};
490             }
491             }
492             return undef;
493             }
494              
495             {
496             package
497             XML::Parser::Expat;
498              
499             sub clone {
500             my $self = shift;
501             my $parser = new XML::Parser::Expat(
502             NoExpand => $self->{'NoExpand'},
503             ErrorContext => $self->{'ErrorContext'},
504             ProtocolEncoding => $self->{'ProtocolEncoding'},
505             );
506             $parser->{__data__} = {};
507             $parser->{__top__} = XHTML::Instrumented::Entry->new(
508             tag => 'div',
509             flags => {},
510             args => {},
511             );
512             $parser->{__context__} = [ $parser->{__top__} ];
513             return $parser;
514             }
515             }
516              
517             sub _eh
518             {
519             my $self = shift;
520             my $tag = shift;
521             my $current = pop(@{$self->{__context__}});
522             my $parent = $self->{__context__}->[-1];
523              
524             my $args = { $current->args };
525              
526             die "mismatched tags $tag " . $current->tag unless $tag eq $current->tag;
527              
528             if ($args->{class} && grep(/:removetag/, split('\s+', $args->{class}))) {
529             $parent->append(@{$current->{data} || []});
530             return;
531             }
532             if ($args->{class} && grep(/:remove/, split('\s+', $args->{class}))) {
533             return;
534             }
535              
536             if ($args->{class} && (my @names = grep(/:replace/, split('\s+', $args->{class})))) {
537             my $out;
538             die "Only one replace per tag" if @names != 1;
539              
540             my $gargs = $self->{__args__};
541             my $default = $gargs->{default_replace};
542             my ($name, $file) = split('\.', $names[0]);
543              
544             $file ||= $self->{__args__}->{replace_name} || die;
545              
546             if ($self->{__args__}{name} ne $file) {
547             $out = XHTML::Instrumented->new(
548             path => $self->{path},
549             cachepath => $self->{cachepath},
550             %{$gargs},
551             name => $file,
552             );
553             } else {
554             }
555              
556             if ($out) {
557             my $id = $args->{id};
558             die 'Need an id for :replace' unless defined $id;
559             die 'Replacement not found' unless $out->{parsed}{idr}{$id};
560              
561             $current = $out->{parsed}{idr}{$id};
562             }
563             }
564              
565             $parent->append($current);
566              
567             if ($tag eq 'form') {
568             delete $self->{_inform_};
569             }
570             }
571              
572             sub _ah
573             {
574             my $self = shift;
575              
576             die q(We don't do these here);
577             }
578              
579             sub _ch
580             {
581             my $self = shift;
582             my $context = $self->{__context__}->[-1];
583             my $data = shift;
584             my %hash = %{$self->{__data__}};
585              
586             my @ret;
587              
588             $data = join('', _fixup($data));
589              
590             if ($context->{flags} & 1) {
591             ;
592             } else {
593             my @x = split(/(\@\@[A-Za-z][A-Za-z0-9_-][^.@]*\.?[^@]*\@\@)/, $data);
594             if (@x > 1) {
595             for my $p (@x) {
596             if ($p =~ m/\@\@([A-Za-z][A-Za-z0-9_-][^.@]*)\.?([^@]*)\@\@/) {
597             push @ret,
598             XHTML::Instrumented::Entry->new(
599             tag => '__special__',
600             flags => {rs => 1},
601             args => {},
602             data => [ "-- $p --" ],
603             id => $1,
604             );
605             } else {
606             push @ret, $p;
607             }
608             }
609             } else {
610             push @ret, $data;
611             }
612             $data =~ s/\@\@([A-Za-z][A-Za-z0-9_-][^.@]*)\.?([^@]*)\@\@/
613             my @extra = split('\.', $2);
614             my $name = $1;
615             my $extra = $2;
616             my $type = $hash{$1};
617             XHTML::Instrumented::Entry->new(
618             tag => '__special__',
619             flags => {},
620             args => {},
621             id => $name,
622             );
623             /xge;
624             }
625             push(@{$context->{data}}, @ret);
626             }
627              
628             1;
629             __END__