diff --git a/lib/XML/XSS.pm b/lib/XML/XSS.pm index 44e9f77..7ab41dc 100644 --- a/lib/XML/XSS.pm +++ b/lib/XML/XSS.pm @@ -1,5 +1,6 @@ package XML::XSS; + =head1 NAME XML::XSS - XML Stylesheet System @@ -136,6 +137,7 @@ use MooseX::SemiAffordanceAccessor; use Moose; use MooseX::AttributeHelpers; use MooseX::ClassAttribute; +use Moose::Exporter; use XML::LibXML; @@ -143,10 +145,18 @@ use XML::XSS::Element; use XML::XSS::Document; use XML::XSS::Text; use XML::XSS::Comment; +use XML::XSS::ProcessingInstruction; no warnings qw/ uninitialized /; -with qw/ MooseX::LogDispatch::Levels /; +with qw/ MooseX::LogDispatch::Levels + XML::XSS::Role::Template +/; + +Moose::Exporter->setup_import_methods( + as_is => ['xsst'], +); + has log_dispatch_conf => ( is => 'ro', @@ -165,6 +175,15 @@ has log_dispatch_conf => ( has log_level => ( is => 'rw', default => 'info' ); +use overload + '.' => '_concat_overload', + '""' => sub { return ref shift }; + +sub _concat_overload { + my ( $self, $elt ) = @_; + return $self->get($elt); +} + =head1 ATTRIBUTES =head2 document @@ -316,6 +335,13 @@ has 'catchall_element' => ( lazy => 1, ); +has pi => ( + is => 'ro', + default => sub { + XML::XSS::ProcessingInstruction->new( stylesheet => $_[0] ); + }, +); + =head2 stash The stylesheet has a stash (an hashref) that is accessible to all the @@ -366,7 +392,7 @@ Sets attributes for a rendering node. The C<$name> can be an XML element name, or one of the special keywords C<#document>, -C<#text>, C<#comment>, C<#processing_instruction> or C<*> (for the +C<#text>, C<#comment>, C<#pi> or C<*> (for the I element), which will resolve to the corresponding rendering object. @@ -389,17 +415,34 @@ value. sub set { my ( $self, $name, $attrs ) = @_; - given ($name) { - when ('#document') { - $self->document->set(%$attrs); + $self->get($name)->set(%$attrs); +} + +sub get { + my ( $self, $name ) = @_; + + given ( $name ) { + when ( '#document' ) { + return $self->document; + } + when( '#text' ) { + return $self->text; } - when ('*') { - $self->catchall_element->set( %$attrs ); + when( '#comment' ) { + return $self->comment; + } + when( '#pi' ) { + return $self->pi; + } + when( '*' ) { + return $self->catchall_element; } default { - $self->element($name)->set(%$attrs); + return $self->element($name); } } + + } =head2 render( $xml, \%args ) @@ -517,6 +560,9 @@ sub resolve { when ( 'XML::LibXML::Comment' ) { return $self->comment; } + when ( 'XML::LibXML::PI' ) { + return $self->pi; + } default { die "unknown node type: $type"; } diff --git a/lib/XML/XSS/Element.pm b/lib/XML/XSS/Element.pm index b09315e..6694cab 100644 --- a/lib/XML/XSS/Element.pm +++ b/lib/XML/XSS/Element.pm @@ -95,8 +95,11 @@ If set to a true value, the open and closing tags of the xml element are printed if not, they are omited. C defaults to true -unless the attribute C
 is defined.  This exception is to accomodate the
-common use of C
 to replace the tags with something else.
+unless either the attribute C
 or C is defined. 
+This exception is to accomodate the
+common use of C
 to replace the tags with something else, or
+when C is used to provide a templated replacement for the
+element.
 
     $css->set( 'subsection' => { 
         pre  => '
', @@ -192,7 +195,8 @@ sub apply { my $w = XML::Writer->new( OUTPUT => \$output ); my $showtag = - $self->has_showtag ? $self->showtag : $self->has_pre ? 0 : 1; + $self->has_showtag ? $self->showtag + : $self->has_pre || $self->has_content ? 0 : 1; my $name; if ($showtag) { @@ -237,6 +241,46 @@ sub clone { return bless {%$self}, ref $self; } +use overload + '.' => '_concat_overload', + 'bool' => sub { 1 }; + +sub _concat_overload { + my ( $self, $attr ) = @_; + + my $magic = bless { + object => $self, + 'method' => 'set_' . $attr, + }, 'XML::XSS::Role::RenderAttribute::Sugar'; + + return $magic; +} + +package XML::XSS::Role::RenderAttribute::Sugar; + +use XML::XSS; + +use overload + '<<=' => '_set_verbatim', + '<<' => '_set_xsst', + '=' => sub { shift }, + 'bool' => sub { 1 }; + +sub _set_verbatim { + my ( $self, $value ) = @_; + + my $method = $self->{'method'}; + my $object = $self->{object}; + $object->$method( $value ); + + return $self; +} + +sub _set_xsst { + my ( $self, $value ) = @_; + + $self <<= XML::XSS::xsst( $value ); +} 1; diff --git a/lib/XML/XSS/ProcessingInstruction.pm b/lib/XML/XSS/ProcessingInstruction.pm new file mode 100644 index 0000000..26eb9bd --- /dev/null +++ b/lib/XML/XSS/ProcessingInstruction.pm @@ -0,0 +1,21 @@ +package XML::XSS::ProcessingInstruction; + +use 5.10.0; + +use Moose; +use MooseX::SemiAffordanceAccessor; + +with 'XML::XSS::Role::Renderer'; + +no warnings qw/ uninitialized /; + +sub apply { + my ( $self, $node, $args ) = @_; + $args ||= {}; + + return $node->toString; +} + +1; + + diff --git a/lib/XML/XSS/Role/Template.pm b/lib/XML/XSS/Role/Template.pm new file mode 100644 index 0000000..d71857c --- /dev/null +++ b/lib/XML/XSS/Role/Template.pm @@ -0,0 +1,50 @@ +package XML::XSS::Role::Template; + +use Moose::Role; + +Moose::Exporter->setup_import_methods( + as_is => ['xsst'], +); + +sub xsst($) { + my $template = shift; + + my $code = _parse($template); + + my $sub = eval <<"END_SUB"; +sub { +my ( \$r, \$node, \$args ) = \@_; +local *STDOUT; +my \$output; +open STDOUT, '>', \\\$output or die; +$code; +return \$output; +} +END_SUB + + die $@ if $@; + + return $sub; +} + +sub _parse { + my $template = shift; + + my @tokens = split /(<-?%\S*|%-?>)/, $template; + + my @pieces; + + while( @tokens ) { + if ( $tokens[0] =~ /^<-?%/ ) { + } + else { + push @pieces, [ 1, shift @tokens ]; + } + } + + $template =~ s/\|/\\\|/g; + + return "print q|$template|;" +} + +1; diff --git a/parse.pl b/parse.pl new file mode 100644 index 0000000..c016f68 --- /dev/null +++ b/parse.pl @@ -0,0 +1,109 @@ +#!/usr/bin/perl + +use strict; +use warnings; + + +<% $x = 3; %> +<%= $i %> +<%~ foo/bar %> +<%@ @text %> +<%# blah blah %> +<%! foo +yadah yadah +%> + +sub extract { + my ($self,$stylesheet,@includestack) = @_; + + my $filename = $self->{stylesheet_dependencies}[0] || "stylesheet"; + + my $contents = $self->read_stylesheet( $stylesheet ); + + my @tokens = split /(<%[-=~#@]*|-?%>)/, $contents; + + no warnings qw/ uninitialized /; + + my $script; + my $line = 1; + TOKEN: + while ( @tokens ) { + my $token = shift @tokens; + + if ( -1 == index $token, '<%' ) { + $line += $token =~ tr/\n//; + $token =~ s/\s+$// if -1 < index $tokens[0], '<%' + and -1 < index $tokens[0], '-'; + $token =~ s/\|/\\\|/g; + # check for include + $token =~ s{} + { '|);' + . $self->include_file( $2, @includestack) + . 'print(q|'}seg; + $script .= 'print(q|'.$token.'|);' if length $token; + + next TOKEN; + } + + $script .= "\n#line $line $filename\n"; + + my $opening_tag = $token; + my $code; + my $closing_tag; + my $level = 1; + while( @tokens ) { + my $t = shift @tokens; + $level++ if -1 < index $t, '<%'; + $level-- if -1 < index $t, '%>'; + if ( $level == 0 ) { + $closing_tag = $t; + last; + } + $code .= $t; + } + + die "stylesheet <% %>s are unbalanced: $opening_tag$code\n" + unless $closing_tag; + + $line += $code =~ tr/\n//; + + if ( -1 < index $opening_tag, '=' ) { + $script .= 'print( '.$code.' );'; + } + elsif ( -1 < index $opening_tag, '~' ) { + $code =~ s/^\s+//; + $code =~ s/\s+$//; + $script .= 'print $processor->apply_templates( qq<'. $code .'> );'; + } + elsif( -1 < index $opening_tag, '#' ) { + # do nothing + } + elsif( -1 < index $opening_tag, '@' ) { + $code =~ s/^\s+(\S+).*?\n//; # strip first line + my $tag = $1 + or die "tag name missing in <%\@ %> at line $line\n"; + + my $here_delimiter = 'END_TAG'; + while ( $code =~ /$here_delimiter/ ) { + $here_delimiter .= 'x'; + } + $script .= <set( $tag => { content => <<'$here_delimiter' } ); +$code +$here_delimiter +END_SNIPPET + } + else { + # always add a ';', just in case + $script .= $code . ';'; + } + + if ( -1 < index $closing_tag, '-' ) { + $tokens[0] =~ s/^\s*//; + my $temp = $&; + $line += $temp =~ tr/\n//; + } + } + + return $script; + diff --git a/project.vim b/project.vim index 133b060..9843aeb 100644 --- a/project.vim +++ b/project.vim @@ -5,12 +5,17 @@ lib Files=lib { XML/XSS/Text.pm XML/XSS/Comment.pm XML/XSS/Document.pm + XML/XSS/ProcessingInstruction.pm XML/XSS/Role/Renderer.pm XML/XSS/Role/RenderAttribute.pm + XML/XSS/Role/Template.pm } test Files=t { basic.t text.t + lib/XML/XSSTest.pm lib/XML/XSS/CommentTest.pm + lib/XML/XSS/TemplateTest.pm + lib/XML/XSS/OverloadTest.pm } } diff --git a/t/lib/XML/XSS/OverloadTest.pm b/t/lib/XML/XSS/OverloadTest.pm index fe20cbc..faa363a 100644 --- a/t/lib/XML/XSS/OverloadTest.pm +++ b/t/lib/XML/XSS/OverloadTest.pm @@ -16,7 +16,15 @@ sub overload_basic :Tests { my $xss = $self->{xss}; - $xss.'foo'.'pre' = 'X'; + isa_ok $xss.'foo' => 'XML::XSS::Element'; + + isa_ok $xss.'foo'.'pre' => 'XML::XSS::Role::RenderAttribute::Sugar'; + + $xss.'foo'.'pre' <<= 'X'; + + $self->render_ok( 'X' ); + + $xss.'foo'.'pre' << 'X'; $self->render_ok( 'X' ); diff --git a/t/lib/XML/XSS/TemplateTest.pm b/t/lib/XML/XSS/TemplateTest.pm new file mode 100644 index 0000000..c17ae5a --- /dev/null +++ b/t/lib/XML/XSS/TemplateTest.pm @@ -0,0 +1,57 @@ +package XML::XSS::TemplateTest; + +use strict; +use warnings; + +no warnings qw/ uninitialized /; + +use base qw/ My::Test::Class /; + +use Test::More; + +use XML::XSS; + +sub xsst_basic :Tests { + my $code = xsst q{Foo!}; + is ref $code => 'CODE', 'produces a sub ref'; + + is $code->() => 'Foo!', 'and returns the right stuff'; + +} + + +sub simple_string :Tests { + my $self = shift; + + $self->{foo}->set( 'content' => xsst q{Hello world} ); + + $self->render_ok( 'Hello world' ); +} + + +sub simple_evaluation :Tests { + my $self = shift; + + $self->{foo}->set( 'content' => xsst q{X<% 1 + 2 %>X} ); + + $self->render_ok( 'XX' ); +} + + + +sub render_ok { + my ( $self, $expected, $comment ) = @_; + + is $self->{xss}->render( $self->{doc} ), $expected, $comment; +} + +sub create_xss : Test(setup) { + my $self = shift; + $self->{xss} = XML::XSS->new; + $self->{foo} = $self->{xss}->element('foo'); + $self->{xss}->set( 'doc' => { showtag => 0 } ); + $self->{doc} = 'bar'; +} + +1; + diff --git a/t/lib/XML/XSSTest.pm b/t/lib/XML/XSSTest.pm index 3e06307..623fe77 100644 --- a/t/lib/XML/XSSTest.pm +++ b/t/lib/XML/XSSTest.pm @@ -11,12 +11,33 @@ use Test::More; use XML::XSS; +sub get :Tests { + my $self = shift; + + my $xss = $self->{xss}; + + my %type = ( + 'foo' => 'XML::XSS::Element', + '*' => 'XML::XSS::Element', + '#document' => 'XML::XSS::Document', + '#text' => 'XML::XSS::Text', + '#comment' => 'XML::XSS::Comment', + '#pi' => 'XML::XSS::ProcessingInstruction', + ); + + while( my ( $type, $class ) = each %type ) { + isa_ok $xss->get($type) => $class; + } + +} + sub render_many :Tests { my $self = shift; $self->{doc} = ''; $self->{xss}->set( foo => { + showtag => 1, content => sub { $_[0]->render( $_[1]->findnodes('bar') ) } }, ); @@ -29,7 +50,7 @@ sub all_types :Tests { local $TODO = "some types left to deal with"; my $self = shift; - $self->{doc} = <<'END_XML'; + my $doc = $self->{doc} = <<'END_XML'; @@ -37,8 +58,9 @@ sub all_types :Tests { END_XML - ok eval { $self->render_ok( 'unknown' ); 1 }; - + chop $doc; + + $self->render_ok( $doc ); }