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 .= ""; | ||||
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 .= ""; | ||||
203 | } | ||||||
204 | |||||||
205 | 14 | 100 | 35 | return "<$elem>$ret" if defined $elem; | |||
206 | 13 | 67 | return "$ret"; | ||||
207 | } | ||||||
208 | |||||||
209 | =head1 AUTHOR | ||||||
210 | |||||||
211 | Paul Evans |
||||||
212 | |||||||
213 | =cut | ||||||
214 | |||||||
215 | 0x55AA; |