| blib/lib/String/Tagged/HTML.pm | |||
|---|---|---|---|
| Criterion | Covered | Total | % |
| statement | 54 | 56 | 96.4 |
| branch | 19 | 24 | 79.1 |
| condition | n/a | ||
| subroutine | 8 | 8 | 100.0 |
| pod | 2 | 2 | 100.0 |
| total | 83 | 90 | 92.2 |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | # You may distribute under the terms of either the GNU General Public License | ||||||
| 2 | # or the Artistic License (the same terms as Perl itself) | ||||||
| 3 | # | ||||||
| 4 | # (C) Paul Evans, 2011 -- leonerd@leonerd.org.uk | ||||||
| 5 | |||||||
| 6 | package String::Tagged::HTML; | ||||||
| 7 | |||||||
| 8 | 4 | 4 | 92395 | use strict; | |||
| 4 | 138 | ||||||
| 4 | 160 | ||||||
| 9 | 4 | 4 | 21 | use warnings; | |||
| 4 | 7 | ||||||
| 4 | 161 | ||||||
| 10 | |||||||
| 11 | 4 | 4 | 27 | use base qw( String::Tagged ); | |||
| 4 | 7 | ||||||
| 4 | 4928 | ||||||
| 12 | String::Tagged->VERSION( '0.07' ); | ||||||
| 13 | |||||||
| 14 | our $VERSION = '0.01'; | ||||||
| 15 | |||||||
| 16 | =head1 NAME | ||||||
| 17 | |||||||
| 18 | C |
||||||
| 19 | |||||||
| 20 | =head1 SYNOPSIS | ||||||
| 21 | |||||||
| 22 | use String::Tagged::HTML; | ||||||
| 23 | |||||||
| 24 | my $st = String::Tagged::HTML->new( "An important message" ); | ||||||
| 25 | |||||||
| 26 | $st->apply_tag( 3, 9, b => 1 ); | ||||||
| 27 | |||||||
| 28 | print $st->as_html( "h1" ); | ||||||
| 29 | |||||||
| 30 | =head1 DESCRIPTION | ||||||
| 31 | |||||||
| 32 | This subclass of L |
||||||
| 33 | the string as an HTML fragment, using the tags to provide formatting. For | ||||||
| 34 | example, the SYNOPSIS example will produce the output | ||||||
| 35 | |||||||
| 36 | An important message |
||||||
| 37 | |||||||
| 38 | With the exception of tags named C |
||||||
| 39 | C |
||||||
| 40 | same name. If the tag's value is a C |
||||||
| 41 | used to provide additional attributes for the HTML element. | ||||||
| 42 | |||||||
| 43 | my $str = String::Tagged::HTML->new( "click here" ); | ||||||
| 44 | $str->apply_tag( 6, 4, a => { href => "/see/other.html" } ); | ||||||
| 45 | |||||||
| 46 | print $str->as_html( "p" ); | ||||||
| 47 | |||||||
| 48 | Z<> | ||||||
| 49 | |||||||
| 50 | click here |
||||||
| 51 | |||||||
| 52 | If it is not a C |
||||||
| 53 | true value, such as C<1>. | ||||||
| 54 | |||||||
| 55 | The special tag named C |
||||||
| 56 | |||||||
| 57 | my $str = String::Tagged::HTML->new( "This |
||||||
| 58 | |||||||
| 59 | my $br = String::Tagged::HTML->new( " " ); |
||||||
| 60 | $br->apply_tag( 0, $br->length, raw => 1 ); | ||||||
| 61 | |||||||
| 62 | print +( $str . $br )->as_html( "p" ); | ||||||
| 63 | |||||||
| 64 | Z<> | ||||||
| 65 | |||||||
| 66 | This <content> is escaped |
||||||
| 67 | |||||||
| 68 | =head2 Tag Nesting | ||||||
| 69 | |||||||
| 70 | Because of the arbitrary way that C |
||||||
| 71 | compared to the strict nesting requirements in HTML, the C |
||||||
| 72 | have to break a single C |
||||||
| 73 | following example, the C tag has been split in two to allow it to overlap | ||||||
| 74 | correctly with C. | ||||||
| 75 | |||||||
| 76 | my $str = String::Tagged::HTML->new( "bbb b+i iii" ); | ||||||
| 77 | $str->apply_tag( 0, 7, b => 1 ); | ||||||
| 78 | $str->apply_tag( 4, 7, i => 1 ); | ||||||
| 79 | |||||||
| 80 | print $str->as_html | ||||||
| 81 | |||||||
| 82 | Z<> | ||||||
| 83 | |||||||
| 84 | bbb b+i iii | ||||||
| 85 | |||||||
| 86 | =cut | ||||||
| 87 | |||||||
| 88 | =head1 CONSTRUCTORS | ||||||
| 89 | |||||||
| 90 | As well as the standard C |
||||||
| 91 | L |
||||||
| 92 | |||||||
| 93 | =cut | ||||||
| 94 | |||||||
| 95 | =head2 $st = String::Tagged::HTML->new_raw( $str ) | ||||||
| 96 | |||||||
| 97 | Returns a new C |
||||||
| 98 | over its entire length. This convenience is provided for creating objects | ||||||
| 99 | containing already-rendered HTML fragments. | ||||||
| 100 | |||||||
| 101 | =cut | ||||||
| 102 | |||||||
| 103 | sub new_raw | ||||||
| 104 | { | ||||||
| 105 | 1 | 1 | 1 | 3 | my $class = shift; | ||
| 106 | 1 | 2 | my ( $str ) = @_; | ||||
| 107 | 1 | 11 | return $class->new_tagged( $str, raw => 1 ); | ||||
| 108 | } | ||||||
| 109 | |||||||
| 110 | =head1 METHODS | ||||||
| 111 | |||||||
| 112 | The following methods are provided in addition to those provided by | ||||||
| 113 | L |
||||||
| 114 | |||||||
| 115 | =cut | ||||||
| 116 | |||||||
| 117 | sub _escape_html | ||||||
| 118 | { | ||||||
| 119 | 24 | 24 | 33 | my $s = $_[0]; | |||
| 120 | 24 | 50 | 51 | $s =~ s/([<>&"'])/$1 eq "<" ? "<" : | |||
| 8 | 100 | 45 | |||||
| 100 | |||||||
| 100 | |||||||
| 100 | |||||||
| 121 | $1 eq ">" ? ">" : | ||||||
| 122 | $1 eq "&" ? "&" : | ||||||
| 123 | $1 eq '"' ? """ : | ||||||
| 124 | $1 eq "'" ? "'" : ""/eg; | ||||||
| 125 | 24 | 88 | $s; | ||||
| 126 | } | ||||||
| 127 | |||||||
| 128 | sub _cmp_tag_values | ||||||
| 129 | { | ||||||
| 130 | 3 | 3 | 4 | my $self = shift; | |||
| 131 | 3 | 6 | my ( $name, $v1, $v2 ) = @_; | ||||
| 132 | |||||||
| 133 | 3 | 50 | 5 | return ( $v1 == $v2 ) if grep { $name eq $_ } qw( b i u small ); | |||
| 12 | 33 | ||||||
| 134 | 0 | 0 | 0 | return ( $v1->{href} eq $v2->{href} ) if $name eq "a"; | |||
| 135 | 0 | 0 | die "Unknown tag name $name\n"; | ||||
| 136 | } | ||||||
| 137 | |||||||
| 138 | =head2 $html = $st->as_html( $element ) | ||||||
| 139 | |||||||
| 140 | Returns a string containing an HTML rendering of the current contents of the | ||||||
| 141 | object. If C<$element> is provided, the output will be wrapped in an element | ||||||
| 142 | of the given name. If not defined, no outer wrapping will be performed. | ||||||
| 143 | |||||||
| 144 | =cut | ||||||
| 145 | |||||||
| 146 | sub as_html | ||||||
| 147 | { | ||||||
| 148 | 14 | 14 | 1 | 1721 | my $self = shift; | ||
| 149 | 14 | 24 | my ( $elem ) = @_; | ||||
| 150 | |||||||
| 151 | 14 | 19 | my $ret = ""; | ||||
| 152 | |||||||
| 153 | 14 | 14 | my @tags_in_effect; # of [ $name, $value ] | ||||
| 154 | |||||||
| 155 | $self->iter_extents_nooverlap( | ||||||
| 156 | sub { | ||||||
| 157 | 20 | 20 | 769 | my ( $e, %tags ) = @_; | |||
| 158 | |||||||
| 159 | # Look for the first tag that no longer applies, as we'll have to | ||||||
| 160 | # unwind the entire tag stack to that point | ||||||
| 161 | |||||||
| 162 | 20 | 24 | my $i; | ||||
| 163 | 20 | 59 | for( $i = 0; $i < @tags_in_effect; $i++ ) { | ||||
| 164 | 5 | 6 | my ( $tag, $value ) = @{ $tags_in_effect[$i] }; | ||||
| 5 | 9 | ||||||
| 165 | 5 | 100 | 12 | last if !exists $tags{$tag}; | |||
| 166 | 3 | 50 | 8 | last if !$self->_cmp_tag_values( $tag, $value, $tags{$tag} ); | |||
| 167 | 3 | 12 | delete $tags{$tag}; | ||||
| 168 | } | ||||||
| 169 | |||||||
| 170 | 20 | 51 | while( @tags_in_effect > $i ) { | ||||
| 171 | 3 | 5 | my ( $tag ) = @{ pop @tags_in_effect }; | ||||
| 3 | 3 | ||||||
| 172 | 3 | 9 | $ret .= "$tag>"; | ||||
| 173 | } | ||||||
| 174 | |||||||
| 175 | # TODO: Sort these into an optimal order | ||||||
| 176 | 20 | 45 | foreach my $tag ( keys %tags ) { | ||||
| 177 | 12 | 16 | my $value = $tags{$tag}; | ||||
| 178 | 12 | 100 | 31 | if( ref $value eq "HASH" ) { | |||
| 179 | 3 | 12 | my $attrs = join "", map { qq( $_=") . _escape_html($value->{$_}) . q(") } sort keys %$value; | ||||
| 3 | 561 | ||||||
| 180 | 3 | 22 | $ret .= "<$tag$attrs>"; | ||||
| 181 | } | ||||||
| 182 | else { | ||||||
| 183 | 9 | 16 | $ret .= "<$tag>"; | ||||
| 184 | } | ||||||
| 185 | 12 | 39 | push @tags_in_effect, [ $tag, $value ]; | ||||
| 186 | } | ||||||
| 187 | |||||||
| 188 | $self->iter_substr_nooverlap( | ||||||
| 189 | sub { | ||||||
| 190 | 23 | 1206 | my ( $str, %tags ) = @_; | ||||
| 191 | 23 | 100 | 68 | $ret .= ( $tags{raw} ? $str : _escape_html( $str ) ); | |||
| 192 | }, | ||||||
| 193 | 20 | 108 | start => $e->start, | ||||
| 194 | end => $e->end, | ||||||
| 195 | ); | ||||||
| 196 | }, | ||||||
| 197 | 14 | 125 | except => [qw( raw )], | ||||
| 198 | ); | ||||||
| 199 | |||||||
| 200 | 14 | 401 | while( @tags_in_effect ) { | ||||
| 201 | 9 | 11 | my ( $tag ) = @{ pop @tags_in_effect }; | ||||
| 9 | 18 | ||||||
| 202 | 9 | 31 | $ret .= "$tag>"; | ||||
| 203 | } | ||||||
| 204 | |||||||
| 205 | 14 | 100 | 35 | return "<$elem>$ret$elem>" if defined $elem; | |||
| 206 | 13 | 67 | return "$ret"; | ||||
| 207 | } | ||||||
| 208 | |||||||
| 209 | =head1 AUTHOR | ||||||
| 210 | |||||||
| 211 | Paul Evans |
||||||
| 212 | |||||||
| 213 | =cut | ||||||
| 214 | |||||||
| 215 | 0x55AA; |