File Coverage

blib/lib/XML/XSS.pm
Criterion Covered Total %
statement 5 7 71.4
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 8 10 80.0


line stmt bran cond sub pod time code
1             package XML::XSS;
2             BEGIN {
3 9     9   229061 $XML::XSS::AUTHORITY = 'cpan:YANICK';
4             }
5             {
6             $XML::XSS::VERSION = '0.3.4';
7             }
8             # ABSTRACT: XML stylesheet system
9              
10              
11 9     9   152 use 5.10.0;
  9         32  
  9         370  
12              
13 9     9   15391 use MooseX::SemiAffordanceAccessor;
  0            
  0            
14             use Moose;
15             use MooseX::ClassAttribute;
16             use Moose::Exporter;
17              
18             use XML::LibXML;
19              
20             use XML::XSS::Element;
21             use XML::XSS::Document;
22             use XML::XSS::Text;
23             use XML::XSS::Comment;
24             use XML::XSS::ProcessingInstruction;
25              
26             use XML::XSS::Template;
27              
28             use MooseX::Clone;
29              
30             with 'MooseX::Clone';
31              
32             no warnings qw/ uninitialized /;
33              
34             Moose::Exporter->setup_import_methods(
35             with_meta => [ 'style' ],
36             as_is => ['xsst'],
37             );
38              
39             sub style {
40             my $metaclass = shift;
41             my $master = ($metaclass->linearized_isa)[0]->master;
42              
43             my $element = shift;
44              
45             my %attr = @_;
46              
47             $master->set( $element, \%attr );
48              
49             }
50              
51             #class_has 'master' => (
52             # is => 'ro',
53             # lazy => 1,
54             # lazy_build => 1,
55             #);
56              
57             sub _build_master {
58             my $self = shift;
59              
60             return XML::XSS->new;
61              
62             }
63              
64             sub master {
65             my $class = shift;
66             $class = ref $class if ref $class;
67              
68             my $var = '$'.$class.'::master';
69              
70             my $master = eval $var;
71              
72             return $master if $master;
73              
74             $master = $class->new;
75              
76             for my $super ( reverse grep { $_->isa('XML::XSS') } $class->meta->superclasses ) {
77             $master->include( $super->master ) if $super->has_master;
78             }
79              
80             eval "$var = \$master";
81              
82             return $master;
83             }
84              
85             sub has_master {
86             my $class = shift;
87             $class = ref $class if ref $class;
88              
89             return eval '$'.$class.'::master';
90             }
91              
92             sub include {
93             my $self = shift;
94             my $to_include = shift;
95              
96             for my $elt ( $to_include->element_keys ) {
97             $self->_set_element( $elt, $to_include->_element( $elt )->clone );
98             }
99              
100             $self->set_comment( $to_include->comment->style_attribute_hash );
101             $self->set_pi( $to_include->pi->style_attribute_hash );
102             $self->set_text( $to_include->text->style_attribute_hash );
103              
104              
105             }
106              
107             around new => sub {
108             my $orig = shift;
109             my $self = shift;
110              
111             if ( $self->has_master ) {
112             my $self = $self->master->clone;
113             $self->BUILDALL( $self->BUILDARGS(@_) );
114             return $self;
115             }
116              
117             return $self->$orig(@_);
118              
119             };
120              
121              
122             has document => (
123             is => 'ro',
124             default => sub {
125             XML::XSS::Document->new( stylesheet => $_[0] );
126             },
127             traits => [ 'Clone' ],
128             );
129              
130              
131              
132             has 'text' => (
133             is => 'ro',
134             default => sub { XML::XSS::Text->new( stylesheet => $_[0] ) },
135             handles => {
136             set_text => 'set',
137             clear_text => 'clear',
138             },
139             traits => [ 'Clone' ],
140             );
141              
142              
143             has comment => (
144             is => 'ro',
145             default =>
146             sub { XML::XSS::Comment->new( stylesheet => $_[0] ) },
147             handles => {
148             set_comment => 'set',
149             },
150             traits => [ 'Clone' ],
151             );
152              
153              
154             has '_elements' => (
155             isa => 'HashRef[XML::XSS::Element]',
156             default => sub { {} },
157             handles => {
158             '_set_element' => 'set',
159             '_element' => 'get',
160             'element_keys' => 'keys',
161             },
162             traits => [ 'Clone', 'Hash' ],
163             );
164              
165              
166             sub element {
167             my ( $self, $name ) = @_;
168             my $elt = $self->_element($name);
169             unless ($elt) {
170             $elt = XML::XSS::Element->new( stylesheet => $self );
171             $self->_set_element( $name => $elt );
172             }
173             return $elt;
174             }
175              
176             sub set_element {
177             my $self = shift;
178             my ( $name, $args ) = @_;
179              
180             if ( ref $args eq 'HASH' ) {
181             $self->element($name)->set(%$args);
182             }
183             else {
184             $self->_set_element( $name => $args );
185             }
186             }
187              
188              
189             has 'catchall_element' => (
190             is => 'rw',
191             isa => 'XML::XSS::Element',
192             default => sub {
193             XML::XSS::Element->new( stylesheet => $_[0] );
194             },
195             lazy => 1,
196             traits => [ 'Clone' ],
197             );
198              
199             has pi => (
200             is => 'ro',
201             default => sub {
202             XML::XSS::ProcessingInstruction->new( stylesheet => $_[0] );
203             },
204             traits => [ 'Clone' ],
205             handles => {
206             set_pi => 'set',
207             },
208             );
209              
210              
211             has stash => (
212             is => 'ro',
213             writer => '_set_stash',
214             isa => 'HashRef',
215             default => sub { {} },
216             );
217              
218             sub clear_stash { $_[0]->_set_stash( {} ) }
219              
220              
221             use overload
222             '.' => sub { $_[0]->get($_[1]) },
223             '""' => sub { return ref shift };
224              
225              
226             sub set {
227             my $self = shift;
228              
229             while ( @_ ) {
230             my $name = shift;
231             my $attrs = shift;
232              
233             $self->get($name)->set(%$attrs);
234             }
235             }
236              
237             sub get {
238             my ( $self, $name ) = @_;
239              
240             given ( $name ) {
241             when ( '#document' ) {
242             return $self->document;
243             }
244             when( '#text' ) {
245             return $self->text;
246             }
247             when( '#comment' ) {
248             return $self->comment;
249             }
250             when( '#pi' ) {
251             return $self->pi;
252             }
253             when( '*' ) {
254             return $self->catchall_element;
255             }
256             default {
257             return $self->element($name);
258             }
259             }
260              
261              
262             }
263              
264              
265             sub render {
266             my $self = shift;
267              
268             my $args = ref( $_[-1] ) eq 'HASH' ? pop @_ : {};
269              
270             if ( @_ == 1 and not ref $_[0] ) {
271             @_ = ( XML::LibXML->load_xml( string => $_[0] ) );
272             }
273              
274             my $output;
275              
276             for my $node (@_) {
277              
278             my $renderer = $self->resolve($node);
279              
280             $output .= $renderer->apply( $node, $args );
281             }
282              
283             return $output;
284             }
285              
286             sub detach {
287             my ( $self, $node ) = @_;
288              
289             # iterate through the nodes and replace the node by a copy
290              
291             my $copy = $node->clone;
292             $node->set_is_detached(1);
293              
294             if ( ref $node eq 'XML::XSS::Text' ) {
295             $self->set_text($copy);
296             return;
297             }
298             elsif ( ref $node eq 'XML::XSS::Element' ) {
299             for ( $self->element_keys ) {
300             if ( $self->element($_) eq $node ) { # FIXME
301             # FIXME set_element in Stylesheet
302             $self->set_element( $_ => $copy );
303             }
304             }
305             if ( $self->catchall_element eq $node ) {
306             $self->set_catchall_element( $copy );
307             }
308             }
309             else {
310             die;
311             }
312              
313             }
314              
315              
316             sub resolve {
317             my ( $self, $node ) = @_;
318              
319             my $type = ref $node;
320              
321             given ($type) {
322             when ('XML::LibXML::Document') {
323             return $self->document;
324             }
325             when ('XML::LibXML::Element') {
326             my $name = $node->nodeName;
327             return $self->_element($name) || $self->catchall_element;
328             }
329             when ('XML::LibXML::Text') {
330             return $self->text;
331             }
332             when ('XML::LibXML::CDATASection') {
333             return $self->text;
334             }
335             when ( 'XML::LibXML::Comment' ) {
336             return $self->comment;
337             }
338             when ( 'XML::LibXML::PI' ) {
339             return $self->pi;
340             }
341             default {
342             die "unknown node type: $type";
343             }
344             }
345              
346             }
347              
348              
349             1;
350              
351             __END__
352              
353             =pod
354              
355             =head1 NAME
356              
357             XML::XSS - XML stylesheet system
358              
359             =head1 VERSION
360              
361             version 0.3.4
362              
363             =head1 SYNOPSIS
364              
365             use XML::XSS;
366              
367             my $xss = XML::XSS->new;
368              
369             $xss->set( pod => {
370             pre => "=pod\n",
371             post => "=cut\n",
372             } );
373              
374             $xss->set( section => {
375             pre => \&pre_section
376             } );
377              
378             sub pre_section {
379             my ( $self, $node, $args ) = @_;
380              
381             return "=head1 " . $node->findvalue( '@title' ) . "\n\n";
382             }
383              
384             print $xss->render( <<'END_XML' );
385             <pod>
386             <section title="NAME">XML::XSS - a XML stylesheet system</section>
387             ...
388             </pod>
389             END_XML
390              
391             =head1 DESCRIPTION
392              
393             Caution: this is alpha-quality software. Here be enough dragons to send
394             Beowulf packing. Caveat maximus emptor.
395              
396             C<XML::XSS> is a XML stylesheet system loosely similar to
397             CSS and XSLT. A C<XML::XSS> object is made up of
398             rendering rules that dictate how the different nodes of
399             an XML document are to be rendered, and can be applied
400             against one or many XML documents.
401              
402             C<XML::XSS> is a rewrite of L<XML::XPathScript>, which was
403             initially part of the L<AxKit> framework.
404              
405             =head2 The XML Document
406              
407             C<XML::XSS> uses L<XML::LibXML> under the hood as its XML DOM
408             API. Documents can be passed as strings, in which case the creation
409             of the XML::LibXML object will be done behind the curtain
410              
411             $xss->render( '<foo>yadah</foo>' );
412              
413             or the L<XML::LibXML> object can be passed directly
414              
415             my $doc = XML::LibXML->load_xml( location => 'foo.xml' );
416             $xss->render( $doc );
417              
418             =head2 Stylesheet Rules
419              
420             C<XML::XSS> has 5 different kinds of rules that reflect the
421             different kinds of nodes that a XML document can have (as per
422             L<XML::LibXML>): L<XML::XSS::Document>, L<XML::XSS::Text>,
423             L<XML::XSS::Comment>, L<XML::XSS::ProcessingInstruction> and
424             L<XML::XSS::Element>. Whereas there are can many C<XML::LibXML::Element>
425             rules, there is only one instance of each of the first 4 rules per
426             stylesheet. In addition of the regular C<XML::LibXML::Element> rules,
427             a special I<catch-all> C<XML::LibXML::Element> also exists that will
428             be applied to any document element not explicitly matched by one of the
429             element rules.
430              
431             =head2 Rules Style Attributes
432              
433             Each rule has a set of style attributes that control how the matching
434             document node is transformed. The different types of rule
435             (L<XML::XSS::Document>, L<XML::XSS::Element>,
436             L<XML::XSS::Text>, L<XML::XSS::Comment> and L<XML::XSS::ProcessingInstruction>)
437             have each a different set of style attributes, which are
438             described in their relative manpages.
439              
440             Unless specified otherwise, a style attribute can be assigned a
441             scalar value or a reference to a sub. In the second case, the sub will
442             be evaluated in the context of the processed node and its return value will
443             be used as the style attribute value.
444              
445             Upon execution, the sub references will be passed three parameters:
446             the invoking rule, the C<XML::LibXML> node it is rendering and the arguments
447             ref given to C<render()>.
448              
449             $css->set( 'foo' => {
450             pre => '[[[',
451             post => sub {
452             my ( $self, $node, $args ) = @_;
453             return $node->findvalue( '@bar' );
454             }
455             } );
456              
457             =head2 Modifying Rules While Rendering
458              
459             Rules attributes changed while rendering only apply to
460             the current element.
461              
462             $xss->set( 'section' => {
463             process => sub {
464             my ( $self, $node ) = @_;
465             $self->stash->{section_nbr}++;
466             if ( $self->stash->{section_nbr} == 5 ) {
467             # only applies to the one section
468             $self->set_pre( '>>> this is the fifth section <<<' );
469             }
470             return 1;
471             }
472             } );
473              
474             If you want to change the global rule, you have to access the rule
475             from the stylesheet, like so
476              
477             $xss->set( 'section' => {
478             process => sub {
479             my ( $self, $node ) = @_;
480             $self->stash->{section_nbr}++;
481             if ( $self->stash->{section_nbr} == 6 ) {
482             $self->stylesheet->element('section')->set_pre(
483             '>>> this is after the fifth section <<<'
484             );
485             }
486             return 1;
487             }
488             } );
489              
490             =head1 ATTRIBUTES
491              
492             =head2 document
493              
494             The document rule. Note that this matches against the
495             C<XML::LibXML::Document> node, not the root element node of
496             the document.
497              
498             =head3 document()
499              
500             Attribute getter.
501              
502             =head2 text
503              
504             The text rule.
505              
506             =head3 text()
507              
508             Attribute getter.
509              
510             =head3 set_text( ... )
511              
512             Shortcut for
513              
514             $xss->text->set( ... );
515              
516             =head3 clear_text()
517              
518             Shortcut for
519              
520             $xss->text->clear;
521              
522             =head2 comment
523              
524             The comment rule.
525              
526             =head3 comment()
527              
528             Attribute getter.
529              
530             =head3 set_comment( ... )
531              
532             Shortcut for
533              
534             $xss->comment->set( ... )
535              
536             =head2 elements
537              
538             The collection of user-defined element rules.
539              
540             =head3 element( $name )
541              
542             Returns the L<XML::XSS::Element> node associated to the tag C<$name>.
543             If the element didn't already exist, it is automatically created.
544              
545             my $elt = $xss->element( 'foo' ); # element for <foo>
546             $elt->set( pre => '[foo]' );
547              
548             =head2 catchall_element
549              
550             The catch-all element rule, which is applied to
551             all the element nodes that aren't explicitly matched.
552              
553             # change all tags to <unknown> except for <foo>
554             $xss->set( 'foo' => { showtag => 1 } );
555             $xss->set( '*' => { rename => 'unknown' } );
556              
557             =head3 catchall_element()
558              
559             The attribute getter.
560              
561             =head2 stash
562              
563             The stylesheet has a stash (an hashref) that is accessible to all the
564             rules during the rendering of a document, and can be used to pass
565             information back and forth.
566              
567             $xss->set( section => {
568             intro => \&section_title,
569             } );
570              
571             # turns <section title="blah"> ...
572             # into 1. blah
573             sub section_title {
574             my ( $self, $node, $args ) = @_;
575              
576             my $section_nbr = $self->stash->{section_nbr}++;
577              
578             return $section_nbr . ". " . $node->findvalue( '@title' );
579             }
580              
581             By default, the stash is cleared when rendering a document.
582             To change this behavior, see L<XML::XSS::Document/use_clean_stash>.
583              
584             =head3 stash()
585              
586             The attribute getter.
587              
588             =head3 clear_stash()
589              
590             Clear the stash.
591              
592             =head1 OVERLOADING
593              
594             =head2 Concatenation (.)
595              
596             The concatenation operator is overloaded to behave as an alias for C<get()>.
597              
598             my $chapter = $xss.'chapter'; # just like $xss->get('chapter')
599              
600             $chapter->set_pre( '<div class="chapter">' );
601             $chapter->set_post( '</div>' );
602              
603             Gets really powerful when used in concert with the overloading of the rules
604             and style attributes:
605              
606             # equivalent as example above
607             $xss.'chapter'.'pre' *= '<div class="chapter">';
608             $xss.'chapter'.'post' *= '</div>';
609              
610             =head1 METHODS
611              
612             =head2 set( $element_1 => \%attrs, $element_2 => \%attrs_2, ... )
613              
614             Sets attributes for a rendering node.
615              
616             The C<$name> can be
617             an XML element name, or one of the special keywords C<#document>,
618             C<#text>, C<#comment>, C<#pi> or C<*> (for the
619             I<catch-all> element),
620             which will resolve to the corresponding rendering object.
621              
622             $xss->set( 'foo' => { rename => 'bar' } );
623             # same as $xss->element('foo')->set( rename => 'bar' );
624              
625             $xss->set( '#text' => { filter => { uc shift } } );
626             # same as $xss->text->set( filter => { uc shift } );
627              
628             Note that subsequent calls to C<set()> are additive. I.e.:
629              
630             $xss->set( foo => { pre => 'X' } );
631             $xss->set( foo => { post => 'Y' } ); # pre is still set to 'X'
632              
633             If you want to delete an attribute, passes it C<undef> as its
634             value.
635              
636             =head2 render( $xml, \%args )
637              
638             Returns the output produced by the application of the
639             stylesheet to the xml document. The xml can
640             be passed as a string, or as a C<XML::LibXML> object.
641             Several C<XML::LibXML> objects can also be passed, in
642             which case the return value will be the concatenation
643             of their transformations.
644              
645             my $sections = $xss->render( $doc->findnodes( 'section' ) );
646              
647             The C<%args> is optional, and will defaults to an empty
648             hash if not provided. The reference to C<%args> is also passed to
649             the recursive calls to C<render()> for the children of the processed
650             node, which allows for another way for parent/children nodes to pass
651             information in addition to the C<stash>.
652              
653             # count the descendents of all nodes
654             $xss->set(
655             '*' => {
656             process => sub {
657             my ( $self, $node, $attrs ) = @_;
658             $attrs->{children}++;
659             return 1;
660             },
661             content => sub {
662             my ( $self, $node, $attrs ) = @_;
663              
664             my %c_attrs;
665             my $c_ref = \%c_attrs;
666             my $output = $self->render( $node->childNodes, $c_ref );
667              
668             $attrs->{children} += $c_ref->{children};
669              
670             $self->{post} =
671             "\n>>> node has "
672             . ($c_ref->{children}||0)
673             . " descendents\n";
674              
675             return $output;
676             },
677             } );
678              
679             =head1 AUTHOR
680              
681             Yanick Champoux <yanick@cpan.org>
682              
683             =head1 COPYRIGHT AND LICENSE
684              
685             This software is copyright (c) 2013 by Yanick Champoux.
686              
687             This is free software; you can redistribute it and/or modify it under
688             the same terms as the Perl 5 programming language system itself.
689              
690             =cut