File Coverage

blib/lib/CSS/Janus.pm
Criterion Covered Total %
statement 178 187 95.1
branch 32 38 84.2
condition 4 6 66.6
subroutine 24 24 100.0
pod 2 18 11.1
total 240 273 87.9


line stmt bran cond sub pod time code
1             #-*- perl -*-
2             #-*- coding: us-ascii -*-
3              
4             =encoding us-ascii
5              
6             =head1 NAME
7              
8             CSS::Janus - Converts a left-to-right Cascading Style Sheet (CSS) into a right-to-left one
9              
10             =head1 SYNOPSIS
11              
12             use CSS::Janus;
13            
14             $janus = CSS::Janus->new;
15             $css_source_rtl = $janus->transform($css_source);
16              
17             =head1 DESCRIPTION
18              
19             As Janus have two faces, horizontal texts can run in two directions:
20             left to right and right to left.
21              
22             CSS::Janus replaces "left" directed things in a Cascading Style Sheet (CSS)
23             file such as float, padding, margin with "right" directed values, and vice
24             versa.
25              
26             This module is a Perl port of CSSJanus by Lindsey Simon .
27              
28             =cut
29              
30 25     25   609418 use 5.005; # qr{} and $10 are required.
  25         96  
  25         1214  
31              
32             package CSS::Janus;
33              
34 25     25   147 use strict;
  25         50  
  25         1041  
35             #use warnings;
36 25     25   206 use Carp qw(carp croak);
  25         43  
  25         2378  
37 25     25   13561 use CSS::Janus::Consts;
  25         70  
  25         11646  
38              
39             # To be compatible with Perl 5.5.
40 25     25   245 use vars qw($VERSION $BASE_REVISION);
  25         44  
  25         61873  
41             $VERSION = '0.04';
42             $BASE_REVISION = 'http://cssjanus.googlecode.com/svn/trunk@31';
43              
44             =head2 Constructor
45              
46             =over 4
47              
48             =item new ( [ options... ] )
49              
50             Creates new CSS::Janus object.
51             Following options are available.
52              
53             =over 4
54              
55             =item swap_left_right_in_url =E 0|1
56              
57             Fixes "left"/"right" string within URLs.
58             Default is C<0>, won't fix.
59              
60             =item swap_ltr_rtl_in_url =E 0|1
61              
62             Fixes "ltr"/"rtl" string within URLs.
63             Default is C<0>, won't fix.
64              
65             =item ignore_bad_bgp =E 0|1
66              
67             Ignores unmirrorable background-position values.
68             Default is C<0>, won't ignore and will croak it.
69              
70             =back
71              
72             =back
73              
74             =cut
75              
76             sub new {
77 25     25 1 299 my $pkg = shift;
78 25         140 bless {@_} => $pkg;
79             }
80              
81             # Substituttion of CSS gradients which cannot be performed only by regexp
82             # because they can contain nested parentheses.
83              
84             my $GRADIENT_RE = qr<$IDENT[\.-]gradient\s*\(>i;
85              
86             sub substituteGradient {
87 120     120 0 168 my $self = shift;
88 120         159 my $match_function = shift;
89 120         163 my $input_string = shift;
90              
91 120         482 pos($input_string) = 0;
92 120         218 my $output = '';
93 120         206 my ($other, $match, $paren_count);
94              
95 120         9867 while ($input_string =~ m{\G(.*?)($GRADIENT_RE)}cg) {
96 2         25 ($other, $match) = ($1, $2);
97              
98 2         2 $paren_count = 1;
99 2   66     20 while ($paren_count and $input_string =~ m{\G(\(|\)|[^()]+)}cg) {
100 11 100       42 if ($1 eq '(') {
    100          
101 2         3 $paren_count++;
102             } elsif ($1 eq ')') {
103 4         6 $paren_count--;
104             }
105 11         58 $match .= $1;
106             }
107              
108             # pos() is at last closing parenthesis (or end of text).
109 2         7 $output .= $other . &$match_function($match);
110             }
111 120         866 return $output . substr($input_string, pos($input_string));
112             }
113              
114             # fixBodyDirectionLtrAndRtl ($line)
115             #
116             # Replaces ltr with rtl and vice versa ONLY in the body direction:
117             # 'body { direction:ltr }' => 'body { direction:rtl }'
118              
119             sub fixBodyDirectionLtrAndRtl {
120 120     120 0 174 my $self = shift;
121 120         157 my $line = shift;
122              
123 120         449 $line =~ s{$BODY_DIRECTION_LTR_RE}{$1$2$3~TMP~}g;
124 120         349 $line =~ s{$BODY_DIRECTION_RTL_RE}{$1$2$3ltr}g;
125 120         238 $line =~ s{~TMP~}{rtl}g;
126              
127 120         238 return $line;
128             }
129              
130             # fixLeftAndRight ($line)
131             #
132             # Replaces left with right and vice versa in line, e,g,:
133             # 'padding-left: 2px; margin-right: 1px;' =>
134             # 'padding-right: 2px; margin-left: 1px;'
135              
136             sub fixLeftAndRight {
137 120     120 0 160 my $self = shift;
138 120         162 my $line = shift;
139              
140 120         2239 $line =~ s{$LEFT_RE}{$1~TMP~}g;
141 120         1253 $line =~ s{$RIGHT_RE}{$1left}g;
142 120         568 $line =~ s{~TMP~}{right}g;
143              
144 120         264 return $line;
145             }
146              
147             # fixLeftAndRightInUrl ($line)
148             #
149             # Replaces left with right and vice versa within background URLs, e.g.:
150             # 'background:url(right.png)' => 'background:url(left.png)'
151              
152             sub fixLeftAndRightInUrl {
153 11     11 0 12 my $self = shift;
154 11         16 my $line = shift;
155              
156 11         82 $line =~ s{$LEFT_IN_URL_RE}{~TMP~}g;
157 11         55 $line =~ s{$RIGHT_IN_URL_RE}{left}g;
158 11         24 $line =~ s{~TMP~}{right}g;
159              
160 11         28 return $line;
161             }
162              
163             # fixLtrAndRtlInUrl ($line)
164             #
165             # Replaces ltr with rtl and vice versa within background URLs, e.g.:
166             # 'background:url(rtl.png)' => 'background:url(ltr.png)'
167              
168             sub fixLtrAndRtlInUrl {
169 11     11 0 11 my $self = shift;
170 11         13 my $line = shift;
171              
172 11         77 $line =~ s{$LTR_IN_URL_RE}{~TMP~}g;
173 11         58 $line =~ s{$RTL_IN_URL_RE}{ltr}g;
174 11         27 $line =~ s{~TMP~}{rtl}g;
175              
176 11         27 return $line;
177             }
178              
179             # fixCursorProperties ($line)
180             #
181             # Changes directional CSS cursor properties:
182             # 'cursor: ne-resize' => 'cursor: nw-resize'
183              
184             sub fixCursorProperties {
185 120     120 0 169 my $self = shift;
186 120         164 my $line = shift;
187              
188 120         408 $line =~ s{$CURSOR_EAST_RE}{$1~TMP~}g;
189 120         340 $line =~ s{$CURSOR_WEST_RE}{$1e-resize}g;
190 120         194 $line =~ s{~TMP~}{w-resize}g;
191              
192 120         281 return $line;
193             }
194              
195             # fixBorderRadius ($line)
196             #
197             # Changes border-radius and its browser-specific variants, e.g.:
198             # 'border-radius: 1px 2px 3px 4px / 5px 6px 7px' =>
199             # 'border-radius: 2px 1px 4px 3px / 6px 5px 6px 7px'
200              
201             sub fixBorderRadius {
202 120     120 0 181 my $self = shift;
203 120         144 my $line = shift;
204              
205 120         3638 $line =~ s{$BORDER_RADIUS_RE}{
206 8         31 reorderBorderRadius($&, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
207             }eg;
208              
209 120         246 return $line;
210             }
211              
212             # fixFourPartNotation ($line)
213             #
214             # Fixes the second and fourth positions in four-part CSS notation, e.g.:
215             # 'padding: 1px 2px 3px 4px' => 'padding: 1px 4px 3px 2px'
216              
217             sub fixFourPartNotation {
218 120     120 0 163 my $self = shift;
219 120         235 my $line = shift;
220              
221 120         2422 $line =~ s{$FOUR_NOTATION_QUANTITY_RE}{$1 $4 $3 $2}g;
222 120         427 $line =~ s{$FOUR_NOTATION_COLOR_RE}{$1$2 $5 $4 $3}g;
223              
224 120         256 return $line;
225             }
226              
227             # fixBackgroundPosition ($line)
228             #
229             # METHOD. Changes horizontal background values in line.
230             #
231             # If value is not replaceable, croak it (by default) or carp it (if
232             # 'ignore_bad_bgp' option is set).
233              
234             sub fixBackgroundPosition {
235 120     120 0 175 my $self = shift;
236 120         168 my $line = shift;
237              
238 120         1145 $line =~ s{$BG_HORIZONTAL_PERCENTAGE_RE}{
239 9         23 calculateNewBackgroundPosition($&, $1, $2, $3, $4, $5)
240             }eg;
241 120         339 $line =~ s{$BG_HORIZONTAL_PERCENTAGE_X_RE}{
242 1         28 calculateNewBackgroundPositionX($&, $1, $2)
243             }eg;
244 120         766 $line =~ s{$BG_HORIZONTAL_LENGTH_RE}{
245 12         32 $self->calculateNewBackgroundLengthPosition($&, $1, $2, $3, $4, $5)
246             }eg;
247 119         332 $line =~ s{$BG_HORIZONTAL_LENGTH_X_RE}{
248 2         6 $self->calculateNewBackgroundLengthPositionX($&, $1, $2)
249             }eg;
250              
251 119         241 return $line;
252             }
253              
254             # Takes a list of zero to four border radius parts and returns a string of
255             # them reordered for bidi mirroring.
256              
257             sub reorderBorderRadiusPart {
258 22     22 0 56 my @part = @_;
259              
260             # Remove any piece which may be 'None'
261 22 100       34 @part = grep { defined $_ and length $_ } @part;
  82         263  
262              
263 22 100       86 if (scalar @part == 4) {
    100          
    100          
    100          
    50          
264 8         33 return "$part[1] $part[0] $part[3] $part[2]";
265             } elsif (scalar @part == 3) {
266 4         19 return "$part[1] $part[0] $part[1] $part[2]";
267             } elsif (scalar @part == 2) {
268 3         12 return "$part[1] $part[0]";
269             } elsif (scalar @part == 1) {
270 3         10 return $part[0];
271             } elsif (scalar @part == 0) {
272 4         11 return '';
273             } else {
274 0         0 croak "This can't happen!";
275             }
276             }
277              
278             # Receives a match object for a border-radius element and reorders it pieces.
279             sub reorderBorderRadius {
280 9     9 0 60 my @m = @_;
281              
282 9         25 my $first_group = reorderBorderRadiusPart(@m[3 .. 6]);
283 9         28 my $second_group = reorderBorderRadiusPart(@m[7 .. $#m]);
284 9 100       21 if ($second_group eq '') {
285 4         41 return sprintf '%sborder-radius%s%s', $_[1], $_[2], $first_group;
286             } else {
287 5         34 return sprintf '%sborder-radius%s%s / %s', $_[1], $_[2],
288             $first_group, $second_group;
289             }
290             }
291              
292             # calculateNewBackgroundPosition ($&, $1, $2, $3, $4, $5)
293             #
294             # Changes horizontal background-position percentages, e.g.:
295             # 'background-position: 75% 50%' => 'background-position: 25% 50%'
296              
297             sub calculateNewBackgroundPosition {
298 9     9 0 42 my @m = @_;
299 9         11 my $new_x;
300             my $position_string;
301              
302             # The flipped value is the offset from 100%
303 9         13 $new_x = 100 - int($m[4]);
304              
305             # Since m.group(1) may very well be None type and we need a string..
306 9 100       14 if ($m[1]) {
307 4         5 $position_string = $m[1];
308             } else {
309 5         6 $position_string = '';
310             }
311              
312 9         68 return sprintf 'background%s%s%s%s%%%s',
313             $position_string, $m[2], $m[3], $new_x, $m[5];
314             }
315              
316             # calculateNewBackgroundPositionX ($&, $1, $2)
317             #
318             # Fixes percent based background-position-x, e.g.:
319             # 'background-position-x: 75%' => 'background-position-x: 25%'
320              
321             sub calculateNewBackgroundPositionX {
322 1     1 0 6 my @m = @_;
323 1         2 my $new_x;
324              
325             # The flipped value is the offset from 100%
326 1         2 $new_x = 100 - int($m[2]);
327              
328 1         8 return sprintf 'background-position-x%s%s%%', $m[1], $new_x;
329             }
330              
331             my $BACKGROUND_POSITION_ERROR_MESSAGE =
332             "Unmirrorable horizonal value \"%s\": %s\n";
333              
334             sub warnForBackgroundPosition {
335 1     1 0 2 my $self = shift;
336 1         2 my $bad_length = shift;
337 1         2 my $whole_value = shift;
338              
339 1         8 my $msg = sprintf $BACKGROUND_POSITION_ERROR_MESSAGE, $bad_length,
340             $whole_value;
341 1 50       5 if ($self->{'ignore_bad_bgp'}) {
342 0         0 $@ = $msg;
343 0         0 carp $msg;
344             } else {
345 1         196 croak $msg;
346             }
347             }
348              
349             # calculateNewBackgroundLengthPosition ($&, $1, $2, $3, $4, $5)
350             #
351             # Changes horizontal background-position lengths, e.g.:
352             # 'background-position: 0px 10px' => 'background-position: 100% 10px'
353             #
354             # If value is not replaceable, croak it (by default) or carp it (if
355             # 'ignore_bad_bgp' option is set).
356              
357             sub calculateNewBackgroundLengthPosition {
358 12     12 0 13 my $self = shift;
359 12         85 my @m = @_;
360 12         12 my $position_string;
361              
362             # croak if the length is not zero-valued
363 12 100       116 unless ($m[4] =~ m{^$ZERO_LENGTH}) {
364 1         5 $self->warnForBackgroundPosition($m[4], $m[0]);
365 0         0 return $m[0];
366             }
367              
368 11 100 66     54 if (defined $m[1] and length $m[1]) {
369 6         8 $position_string = $m[1];
370             } else {
371 5         8 $position_string = '';
372             }
373              
374 11         86 return sprintf 'background%s%s%s100%%%s',
375             $position_string, $m[2], $m[3], $m[5];
376             }
377              
378             # calculateNewBackgroundLengthPositionX ($&, $1, $2)
379             #
380             # Fixes background-position-x lengths, e.g.:
381             # 'background-position-x: 0' => 'background-position-x: 100%'
382             #
383             # If value is not replaceable, croak it (by default) or carp it (if
384             # 'ignore_bad_bgp' option is set).
385              
386             sub calculateNewBackgroundLengthPositionX {
387 2     2 0 3 my $self = shift;
388 2         8 my @m = @_;
389              
390             # croak if the length is not zero-valued
391 2 50       56 unless ($m[2] =~ m{^$ZERO_LENGTH}) {
392 0         0 $self->warnForBackgroundPosition($m[2], $m[0]);
393 0         0 return $m[0];
394             }
395              
396 2         13 return sprintf 'background-position-x%s100%%', $m[1];
397             }
398              
399             =head2 Method
400              
401             =over 4
402              
403             =item transform ( $lines, [ options... ] )
404              
405             Runs the fixing functions against CSS source.
406              
407             $lines is a string.
408             Following options are available.
409              
410             =over 4
411              
412             =item swap_ltr_rtl_in_url =E 0|1
413              
414             Overrides this flag if param is set.
415              
416             =item swap_left_right_in_url =E 0|1
417              
418             Overrides this flag if param is set.
419              
420             =back
421              
422             Returns same lines directions (left and right) are changed.
423              
424             =back
425              
426             =cut
427              
428             sub transform {
429 120     120 1 1339 my $self = shift;
430 120         173 my $line = shift;
431 120         261 my %opts = @_;
432              
433 120 50       308 return undef unless defined $line;
434              
435             # Possibly override flags with params.
436 120         190 my $swap_ltr_rtl_in_url = $opts{'swap_ltr_rtl_in_url'};
437 120         168 my $swap_left_right_in_url = $opts{'swap_left_right_in_url'};
438 120 50       277 unless (defined $swap_ltr_rtl_in_url) {
439 120         292 $swap_ltr_rtl_in_url = $self->{'swap_ltr_rtl_in_url'};
440             }
441 120 50       344 unless (defined $swap_left_right_in_url) {
442 120         183 $swap_left_right_in_url = $self->{'swap_left_right_in_url'};
443             }
444              
445 120         202 my @originals = ();
446              
447             # Tokenize tokens tokenizer can be confused.
448 120         249 $line =~ s{(~[A-Z_\d]+~)}{
449 0         0 push @originals, $1;
450 0         0 '~X_' . (scalar @originals) . '~'
451             }eg;
452              
453             # Tokenize any single line rules with the /* noflip */ annotation.
454 120         1192 $line =~ s{$NOFLIP_SINGLE_RE}{
455 4         9 push @originals, $1;
456 4         17 '~NOFLIP_SINGLE_' . (scalar @originals) . '~'
457             }eg;
458              
459             # Tokenize any class rules with the /* noflip */ annotation.
460 120         509 $line =~ s{$NOFLIP_CLASS_RE}{
461 5         15 push @originals, $1;
462 5         26 '~NOFLIP_CLASS_' . (scalar @originals) . '~'
463             }eg;
464              
465             # Tokenize the comments so we can preserve them through the changes.
466 120         378 $line =~ s{$COMMENT_RE}{
467 8         25 push @originals, $1;
468 8         37 '~C_' . (scalar @originals) . '~'
469             }eg;
470              
471             # Tokenize gradients since we don't want to mirror the values inside
472             $line = $self->substituteGradient(
473             sub {
474 2     2   4 push @originals, shift;
475 2         19 '~GRADIENT_' . (scalar @originals) . '~';
476             },
477 120         766 $line
478             );
479              
480             # Here starteth the various left/right direction fixes.
481 120         680 $line = $self->fixBodyDirectionLtrAndRtl($line);
482              
483 120 100       275 if ($swap_left_right_in_url) {
484 11         25 $line = $self->fixLeftAndRightInUrl($line);
485             }
486              
487 120 100       245 if ($swap_ltr_rtl_in_url) {
488 11         28 $line = $self->fixLtrAndRtlInUrl($line);
489             }
490              
491 120         315 $line = $self->fixLeftAndRight($line);
492 120         353 $line = $self->fixCursorProperties($line);
493              
494 120         308 $line = $self->fixBorderRadius($line);
495              
496             # Since FourPartNotation conflicts with BorderRadius, we tokenize
497             # border-radius properties here.
498 120         3174 $line =~ s{$BORDER_RADIUS_TOKENIZER_RE}{
499 8         17 push @originals, $1;
500 8         28 '~BORDER_RADIUS_' . (scalar @originals) . '~'
501             }eg;
502 120         332 $line = $self->fixFourPartNotation($line);
503 120         231 $line =~ s{~BORDER_RADIUS_(\d+)~}{$originals[$1 - 1]}eg;
  8         32  
504              
505 120         300 $line = $self->fixBackgroundPosition($line);
506              
507             # DeTokenize gradients
508 119         211 $line =~ s{~GRADIENT_(\d+)~}{$originals[$1 - 1]}eg;
  2         10  
509              
510             # DeTokenize the single line noflips.
511 119         189 $line =~ s{~NOFLIP_SINGLE_(\d+)~}{$originals[$1 - 1]}eg;
  4         20  
512              
513             # DeTokenize the class-level noflips.
514 119         185 $line =~ s{~NOFLIP_CLASS_(\d+)~}{$originals[$1 - 1]}eg;
  5         24  
515              
516             # DeTokenize the comments.
517 119         190 $line =~ s{~C_(\d+)~}{$originals[$1 - 1]}eg;
  8         42  
518              
519             # Detokenize tokens tokenizer can be confused.
520 119         200 $line =~ s{~X_(\d+)~}{$originals[$1 - 1]}eg;
  0         0  
521              
522 119         761 return $line;
523             }
524              
525             =head1 VERSION
526              
527             Consult C<$VERSION> variable.
528              
529             =head1 SEE ALSO
530              
531             CSSJanus L.
532              
533             A PHP port of CSSJanus L.
534              
535             =head1 AUTHOR
536              
537             Hatuka*nezumi - IKEDA Soji .
538              
539             =head1 COPYRIGHT
540              
541             Copyright (C) 2013 Hatuka*nezumi - IKEDA Soji.
542              
543             This program is free software; you can redistribute
544             it and/or modify it under the same terms as Perl itself.
545              
546             =cut
547              
548             1;