File Coverage

blib/lib/I22r/Translate/Filter/HTML.pm
Criterion Covered Total %
statement 55 103 53.4
branch 18 46 39.1
condition 4 24 16.6
subroutine 6 6 100.0
pod 0 2 0.0
total 83 181 45.8


line stmt bran cond sub pod time code
1             package I22r::Translate::Filter::HTML;
2 2     2   333 use Carp;
  2         2  
  2         102  
3 2     2   449 use Moose;
  2         325508  
  2         11  
4             with 'I22r::Translate::Filter';
5              
6             our $VERSION = '0.95';
7              
8             sub apply {
9 18     18 0 768 my ($self, $req, $key) = @_;
10 18         16 local $_;
11 18         418 my $otext = $_ = $req->text->{$key};
12 18   50     73 my $keymap = $self->{map}{$key} //= {};
13              
14 18         13 my $html_transform_count = 0;
15 18         77 while ( s{
16             < (\w+) (\b[^>]*)? > # OPEN TAG
17             (?!.*?<\1\b) # TAG INTERIOR does not contain OPEN TAG
18             (.*?) # TAG INTERIOR
19             </ \1 > # CLOSE TAG
20             }[
21 12         21 $self->_html_transform( $otext, $_, $keymap, $1, $2, $3 );
22             ]gexs) {
23              
24 12 50       37 if (++$html_transform_count > 500) {
25 0         0 Carp::cluck "I22r::Translate::Filter::HTML: ",
26             "endless html_transform loop for data\n---\n$_\n---\n\n";
27 0         0 last;
28             }
29             }
30              
31             # XXX - protect singleton <tag/> <tag attr="value"/> tags?
32              
33             # protect any remaining HTML tags at the end
34 18         30 1 while s{<.*?>(?:&nbsp;?|\s)*$}(
35 0   0     0 $keymap->{__end__} //= [];
36 0         0 unshift @{$keymap->{__end__}}, ${^MATCH};
  0         0  
37 0         0 '' )psex;
38              
39             # protect any remaining HTML tags at the beginning
40 18         33 1 while s{^(?:&nbsp;?|\s)*<.*?>}(
41 1   50     6 $keymap->{__begin__} //= [];
42 1         1 push @{$keymap->{__begin__}}, ${^MATCH};
  1         4  
43 1         5 '' )psex;
44              
45 18         430 $req->text->{$key} = $_;
46             }
47              
48             sub unapply {
49 18     18 0 1671 my ($self, $req, $key) = @_;
50              
51             ### - unnecessary to restore $req->text->{$key}, it is always
52             ### done in Request::unapply_filters . When the bare filter
53             ### tests are fixed so they aren't testing this anymore,
54             ### this line can be removed.
55             $req->text->{$key} =
56 18         404 $self->_unapply( $req, $key, $req->text->{$key} );
57 18 100       385 if (defined $req->results->{$key}) {
58             $req->results->{$key}{text} =
59 15         314 $self->_unapply( $req, $key, $req->results->{$key}->text );
60             }
61             }
62              
63             sub _unapply {
64 33     33   34 my ($self, $req, $key, $topic) = @_;
65 33         28 local $_ = $topic;
66 33         30 my $keymap = $self->{map}{$key};
67 33 100       270 return $_ unless $keymap->{__keys__};
68 22         45 foreach my $enc (reverse @{$keymap->{__keys__}}) {
  22         31  
69 22         14 my $mapping = $keymap->{$enc};
70 22 50       34 next if !defined $mapping;
71              
72 22         24 my ($open, $close) = ($mapping->[1], $mapping->[3]);
73 22         15 my $unmap = 0;
74 22 50       51 if ($enc eq q/""/) {
    50          
    50          
    0          
    0          
    0          
75 0         0 $unmap = s/"(.*?)"/$open$1$close/;
76             } elsif ($enc eq q/''/) {
77 0         0 $unmap = s/'(.*?)'/$open$1$close/;
78             } elsif ($enc eq q/()/) {
79 22         99 $unmap = s/\((.*?)\)/$open$1$close/;
80             } elsif ($enc eq q/[]/) {
81 0         0 $unmap = s/\[(.*?)\]/$open$1$close/;
82             } elsif ($enc eq q/{}/) {
83 0         0 $unmap = s/\{(.*?)\}/$open$1$close/;
84             } elsif ($enc =~ /[\x{9FD0}-\x{9FFF}]/) {
85 0         0 my ($c1,$c2) = split //, $enc;
86             # $unmap = s/$c1(.*?)$c2/$open$1$close/;
87 0         0 $unmap = s/ ?$c1 ?(.*?) ?$c2 ?/$open$1$close/;
88             }
89 22 50       46 if (!$unmap) {
90 0         0 carp "Could not find place to restore html tags ",
91             "'$open' and '$close' with encoding $enc ",
92             "in translated text result $_\n";
93             }
94             }
95 22 100       36 if ($keymap->{__begin__}) {
96 1         1 $_ = join('', @{$keymap->{__begin__}}) . $_;
  1         3  
97             }
98 22 50       28 if ($keymap->{__end__}) {
99 0         0 $_ .= join('', @{$keymap->{__end__}});
  0         0  
100             }
101 22         510 return $_;
102             }
103              
104             sub _html_transform {
105 12     12   27 my ($self, $source1,$source2,$map,$tag,$attr,$element) = @_;
106 12         13 my $interior = $element;
107 12 50       20 $element = '' if $element eq "\x{00}\x{00}";
108 12         18 my $source = $source1 . $source2;
109              
110 12 50       114 if ($interior =~ /<$tag/) {
111 0         0 carp "Detected nested <$tag></$tag> tags!\n";
112 0         0 return "<$tag$attr>$element</$tag>";
113             }
114              
115 12 50 33     26 if (defined($map->{$element}) && $map->{$element}[0] eq 'literal') {
116 0         0 $interior = $map->{$element}[1];
117             }
118 12 50       49 my $mapping = [ 'html', "<$tag$attr>",
119             $interior eq "\x{00}\x{00}"
120             ? ('','') : ($interior,"</$tag>") ];
121              
122 12 50 33     47 if (!defined $map->{q/()/} && $source !~ /[()]/) {
123 12         15 $map->{q/()/} = $mapping;
124 12         8 push @{$map->{__keys__}}, q/()/;
  12         27  
125 12         52 return qq/($element)/;
126             }
127 0 0 0       if (!defined $map->{q/[]/} && $source !~ /\[|\]/) {
128 0           $map->{q/[]/} = $mapping;
129 0           push @{$map->{__keys__}}, q/[]/;
  0            
130 0           return qq/[$element]/;
131             }
132 0 0 0       if (!defined $map->{q/{}/} && $source !~ /\{|\}/) {
133 0           $map->{q/{}/} = $mapping;
134 0           push @{$map->{__keys__}}, q/{}/;
  0            
135 0           return qq/{$element}/;
136             }
137 0 0 0       if (!defined $map->{q/""/} && $source !~ /\"/) {
138 0           $map->{q/""/} = $mapping;
139 0           push @{$map->{__keys__}}, q/""/;
  0            
140 0           return qq/"$element"/;
141             }
142 0 0 0       if (!defined $map->{q/''/} && $source !~ /\'/) {
143 0           $map->{q/''/} = $mapping;
144 0           push @{$map->{__keys__}}, q/''/;
  0            
145 0           return qq/'$element'/;
146             }
147              
148             # other good ranges:
149             # 0x0860 - 0x08FF
150             # 0xA6A0 - 0xA6FF
151             # 0xAAE0 - 0xABBF
152             # 0xD800 - 0xDFFF? (reserved for UTF-16 surrogate pairs)
153             # 0xE000 - 0xF8FF? ("private use area")
154             # 0x104B0 - 0x107FF
155             # 0x10E80 - 0x10FFF
156 0           for (my $q = 0x9FD0; $q <= 0x9FFF; $q += 2) {
157 0           my $c1 = chr($q);
158 0           my $c2 = chr($q+1);
159 0 0         if (!defined $map->{"$c1$c2"}) {
160 0           $map->{"$c1$c2"} = $mapping;
161 0           push @{$map->{__keys__}}, "$c1$c2";
  0            
162 0           return " $c1 " . $element . " $c2 ";
163             }
164             }
165              
166 0           carp "cannot transform html expression <$tag$attr> $element </$tag> ",
167             "in source text $source1!\n";
168 0           return "<$tag$attr>$element</$tag>";
169             }
170              
171             1;
172             __END__
173              
174             =head1 NAME
175              
176             I22r::Translate::Filter::HTML - protect HTML tags in input to I22r::Translate
177              
178             =head1 SYNOPSIS
179              
180             I22r::Translate->config(
181             ...,
182             filter => [ 'HTML' ]
183             );
184              
185             $t = I22r::Translate->translate_string(
186             src => ..., dest => ..., text => 'string that might have HTML markup',
187             filter => [ 'HTML' ] )
188              
189             =head1 DESCRIPTION
190              
191             A preprocessing and postprocessing filter that protects
192             HTML tags from being altered in a translation engine.
193              
194             Sometimes, content that you wish to translate may have
195             HTML tags or other markup. Consider this English text:
196              
197             <strong>Roses</strong> are <a href="http://red.com/" style="">red</a>.
198              
199             If you wished to translate this text into, say, Spanish, you would
200             probably B<not> want to translate the words inside the HTML tags,
201             even though some of those words are recognizably English. That is,
202             you would hope the translator would output something like
203              
204             <strong>Rosas</strong> son <a href="http://red.com/" style="">rojas</a>.
205              
206             rather than (in the worst case)
207              
208             <fuerte>Rosas</fuerte> son <un href="http://rojo.com/" estilo="">rojas</a>.
209              
210             which would surely not be rendered correctly in a web browser.
211              
212             This C<I22r::Translate::Filter::HTML> module is a
213             L<filter|I22r::Translate::Filter> that can hide HTML tags from
214             a translation backend, but restore the HTML in the appropriate
215             place in the translation output.
216              
217             =head1 SEE ALSO
218              
219             L<I22r::Translate::Filter>, L<I22r::Translate::Filter::Literal>,
220             L<I22r::Translate>.
221              
222             =cut