File Coverage

blib/lib/SVGPDF/CSS.pm
Criterion Covered Total %
statement 135 168 80.3
branch 53 82 64.6
condition 29 42 69.0
subroutine 14 17 82.3
pod 0 11 0.0
total 231 320 72.1


line stmt bran cond sub pod time code
1             #! perl
2              
3 8     8   912093 use v5.26;
  8         34  
4 8     8   4709 use Object::Pad;
  8         137532  
  8         87  
5 8     8   4567 use utf8;
  8         2329  
  8         69  
6 8     8   365 use Carp;
  8         23  
  8         2274  
7              
8             class SVGPDF::CSS;
9              
10 10 50   10 0 75 field $css :accessor;
  10         164  
11 0 0   0 0 0 field $errstr :accessor;
  0         0  
12 12     12 0 49 field $base :mutator;
  12         138  
13 2 50   2 0 12 field $ctx :accessor;
  2         16  
14             field @stack;
15 0 0   0 0 0 field $ffam :accessor;
  0         0  
16              
17             BUILD {
18             $css = {};
19             $base =
20             { 'font-family' => 'serif',
21             'font-size' => '10',
22             'color' => 'black',
23             'background-color' => 'none',
24             'fill' => 'currentColor',
25             'stroke' => 'none',
26             'line-width' => 1,
27             };
28             $ctx = {};
29             $ffam = [];
30             $self->push( @_ ) if @_;
31             }
32              
33             # Parse a string with one or more styles. Augments.
34 14     14 0 183 method read_string ( $string ) {
  14         61  
  14         31  
  14         42  
35              
36 14         60 state $ffi = "face000"; # for unique font-face ids
37              
38 14   66     81 $css->{'*'} //= $base;
39              
40             # Flatten whitespace and remove /* comment */ style comments.
41 14         160 $string =~ s/\s+/ /g;
42 14         47 $string =~ s!/\*.*?\*\/!!g;
43              
44             # Hide semicolon in url(data:application/octet-stream;base64,...)
45 14         44 $string =~ s/(url\(['"]data:.*?\/.*?);(.*?),/$1\x{ff1b}$2,/g;
46              
47             # Split into styles.
48 14         187 foreach ( grep { /\S/ } split /(?<=\})/, $string ) {
  27         109  
49 21 50       198 unless ( /^\s*([^{]+?)\s*\{(.*)\}\s*$/ ) {
50 0         0 $errstr = "Invalid or unexpected style data '$_'";
51 0         0 return;
52             }
53              
54             # Split in such a way as to support grouped styles.
55 21         74 my $style = $1;
56 21         78 my $properties = $2;
57 21         59 $style =~ s/\s{2,}/ /g;
58             my @styles =
59 23         67 grep { s/\s+/ /g; 1; }
  23         77  
60 21         80 grep { /\S/ }
  23         84  
61             split( /\s*,\s*/, $style );
62 21         53 foreach ( @styles ) {
63             # Give @font-face rules an unique id.
64 23 50       71 if ( $_ eq '@font-face' ) {
65 0         0 $_ = '@font-'.$ffi;
66 0         0 $ffi++;
67             }
68 23   100     150 $css->{$_} //= {};
69             }
70              
71             # Split into properties.
72 21         76 foreach ( grep { /\S/ } split /\;/, $properties ) {
  27         113  
73 25 50       223 unless ( /^\s*(\*?[\w._-]+)\s*:\s*(.*?)\s*$/ ) {
74 0         0 $errstr = "Invalid or unexpected property '$_' in style '$style'";
75 0         0 return;
76             }
77              
78 25         89 my $s = lc($1);
79 25         122 my %s = ( $s => $2 );
80              
81             # Split font shorthand.
82 25 100       102 if ( $s eq "font" ) {
    50          
83 8     8   24323 use Text::ParseWords qw(shellwords);
  8         27721  
  8         8312  
84 9         55 my @spec = shellwords($s{$s});
85              
86 9         2971 foreach my $spec ( @spec ) {
87 26         52 $spec =~ s/;$//;
88 26 100       281 if ( $spec =~ /^([.\d]+)px/ ) {
    100          
    50          
    50          
    100          
    100          
    100          
    50          
89 7         31 $s{'font-size'} = $1;
90             }
91             elsif ( $spec eq "bold" ) {
92 2         8 $s{'font-weight'} = "bold";
93             }
94             elsif ( $spec eq "italic" ) {
95 0         0 $s{'font-style'} = "italic";
96             }
97             elsif ( $spec eq "bolditalic" ) {
98 0         0 $s{'font-weight'} = "bold";
99 0         0 $s{'font-style'} = "italic";
100             }
101             elsif ( $spec =~ /^(?:text,)?serif$/i ) {
102 2         10 $s{'font-family'} = "serif";
103             }
104             elsif ( $spec =~ /^(?:text,)?sans(?:-serif)?$/i ) {
105 5         21 $s{'font-family'} = "sans";
106             }
107              
108             # These are for ABC SVG processing.
109             elsif ( $spec =~ /^abc2svg(?:\.ttf)?$/i ) {
110 2         9 $s{'font-family'} = "abc2svg";
111             }
112             elsif ( lc($spec) =~ /^musejazz\s*text$/i ) {
113 0         0 $s{'font-family'} = "musejazztext";
114             }
115             else {
116 8         17 $s{'font-family'} = $spec;
117             }
118             }
119              
120             # Remove the shorthand if we found something.
121 9 50       53 delete($s{$s}) if keys(%s) > 1;
122             }
123              
124             # Split outline shorthand.
125             elsif ( $s eq "outline" ) {
126 8     8   79 use Text::ParseWords qw(shellwords);
  8         19  
  8         42606  
127 0         0 my @spec = shellwords($s{$s});
128              
129 0         0 foreach my $spec ( @spec ) {
130 0         0 $spec =~ s/;$//;
131 0 0       0 if ( $spec =~ /^([.\d]+)px/ ) {
    0          
132 0         0 $s{'outline-width'} = $1;
133             }
134             elsif ( $spec =~ /^(dotted|dashed|solid|double|groove|ridge|inset|outset)$/i ) {
135 0         0 $s{'outline-style'} = $1;
136             }
137             else {
138 0         0 $s{'outline-color'} = $spec;
139             }
140             }
141              
142             # Remove the shorthand if we found something.
143 0 0       0 delete($s{$s}) if keys(%s) > 1;
144             }
145              
146 25         71 foreach my $k ( keys %s ) {
147 34         69 foreach ( @styles ) {
148 38         185 $css->{$_}->{$k} = $s{$k};
149             }
150             }
151             }
152             }
153              
154 14         70 my @keys = keys( %$css );
155 14         36 for my $k ( @keys ) {
156 56 50       133 if ( $k =~ /^\@font-face/ ) {
157             # Unhide semicolons.
158 0         0 s/\x{ff1b}/;/g for values( %{$css->{$k}} );
  0         0  
159 0         0 push( @$ffam, $css->{$k} );
160 0         0 delete $css->{$k};
161             }
162             }
163 14         48 for my $k ( @keys ) {
164 56 100       296 next unless $k =~ /^(\S+)\s+(\S+)$/;
165 3   50     63 $css->{$1}->{" $2"} //= {};
166 3         48 $self->merge( $css->{$1}->{" $2"}, $css->{$k} );
167 3         13 delete ( $css->{$k} );
168             }
169              
170 14         70 1;
171             }
172              
173             # Merge hashes (and only hashes), recursive.
174 644     644 0 1094 method merge ( $left, $right ) {
  644         1406  
  644         915  
  644         1008  
  644         901  
175 644 50       1324 return unless defined $right;
176 644 50 33     3213 if ( ref($left) eq 'HASH' && ref($right) eq 'HASH' ) {
177 644         2346 for ( keys %$right ) {
178 3699 100 100     13094 if ( exists $left->{$_}
      66        
179             && ref($left->{$_}) eq 'HASH'
180             && ref($right->{$_}) eq 'HASH' ) {
181 196         735 $self->merge( $left->{$_}, $right->{$_} );
182             }
183             else {
184 3503         7510 $left->{$_} = $right->{$_};
185             }
186             }
187 644         1990 return;
188             }
189 0         0 croak("Cannot merge " . ref($left) . " and " . ref($right));
190             }
191              
192 76     76 0 202 method find ( $arg ) {
  76         365  
  76         202  
  76         130  
193 76   33     276 $css->{'*'} //= $base;
194 76         143 my $ret = { %{$css->{'*'}} };
  76         719  
195 76 100       305 if ( exists( $css->{_} ) ) {
196 74         265 $self->merge( $ret, $css->{_} );
197             }
198 76         209 $ctx = $ret;
199 76         363 $ret->{$arg};
200             }
201              
202 191     191 0 382 method push ( @args ) {
  191         536  
  191         679  
  191         315  
203 191 100       1115 my $args = ref($args[0]) eq 'HASH' ? $args[0] : { @args };
204 191   66     805 $css->{'*'} //= $base;
205 191         290 my $ret;
206              
207             # CSS defaults.
208 191         382 while ( my($k,$v) = each %{$css->{'*'}} ) {
  1528         4629  
209 1337   33     4932 $ret->{$k} //= $v;
210             }
211              
212             ## Parent.
213 191 100       591 if ( exists( $css->{_} ) ) {
214 155         585 $self->merge( $ret, $css->{_} );
215             }
216              
217             ## Presentation attributes.
218 191         624 for ( keys %$args ) {
219 429 100       1894 next if /^(element|class|style|id)$/;
220 215         645 $ret->{$_} = $args->{$_};
221             }
222              
223             ## Tag style.
224 191 100 100     1177 if ( $args->{element} && exists( $css->{$args->{element}} ) ) {
225 6         34 $self->merge( $ret, $css->{$args->{element}} );
226             }
227 191 100 100     1107 if ( $args->{element} && exists( $css->{_}->{" ".$args->{element}} ) ) {
228 3         19 $self->merge( $ret, $css->{_}->{" ".$args->{element}} );
229             }
230              
231             ## Class style.
232 191 100       517 if ( $args->{class} ) {
233 8         35 for ( split( ' ', $args->{class} ), "svg" ) {
234             $self->merge( $ret, $css->{".$_"} )
235 18 100       84 if exists( $css->{".$_"} );
236             $self->merge( $ret, $css->{$args->{element}.".$_"} )
237 18 50 66     90 if $args->{element} && exists( $css->{$args->{element}.".$_"} );
238             }
239             }
240              
241             ## ID (generic).
242 191 50 66     746 if ( $args->{id} && exists( $css->{ "#" . $args->{id} } ) ) {
243 0         0 $self->merge( $ret, $css->{ "#" . $args->{id} } );
244             }
245              
246             ## ID (specific).
247 191 50 66     640 if ( $args->{id} && exists( $css->{ $args->{element} . "#" . $args->{id} } ) ) {
248 0         0 $self->merge( $ret, $css->{ $args->{element} . "#" . $args->{id} } );
249             }
250              
251             ## Style attribute.
252 191 100       697 if ( $args->{style} ) {
253 8 50       134 $self->read_string( "__ {" . $args->{style} . "}" )
254             or croak($errstr);
255 8         44 $self->merge( $ret, delete $css->{__} );
256             }
257              
258 191 50       628 $ret->{'@font-face'} = $ffam if $ffam;
259 191   100     382 push( @stack, { %{$css->{_}//{}} } );
  191         1471  
260 191         976 $self->merge( $css, { _ => $ret } );
261 191         1514 $ctx = $ret;
262             }
263              
264 185     185 0 313 method pop () {
  185         507  
  185         259  
265 185 50       516 Carp::croak("CSS stack underflow") unless @stack;
266 185         1693 $ctx = $css->{_} = pop(@stack);
267             }
268              
269 0     0 0   method level () {
  0            
  0            
270 0           0+@stack;
271             }
272              
273             1;