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   496724 use v5.26;
  8         21  
4 8     8   3208 use Object::Pad;
  8         58206  
  8         41  
5 8     8   2957 use utf8;
  8         1699  
  8         52  
6 8     8   229 use Carp;
  8         14  
  8         1601  
7              
8             class SVGPDF::CSS;
9              
10 10 50   10 0 43 field $css :accessor;
  10         83  
11 0 0   0 0 0 field $errstr :accessor;
  0         0  
12 12     12 0 26 field $base :mutator;
  12         81  
13 2 50   2 0 8 field $ctx :accessor;
  2         9  
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 16     16 0 146 method read_string ( $string ) {
  16         29  
  16         21  
  16         15  
35              
36 16         39 state $ffi = "face000"; # for unique font-face ids
37              
38 16   66     55 $css->{'*'} //= $base;
39              
40             # Flatten whitespace and remove /* comment */ style comments.
41 16         117 $string =~ s/\s+/ /g;
42 16         31 $string =~ s!/\*.*?\*\/!!g;
43              
44             # Hide semicolon in url(data:application/octet-stream;base64,...)
45 16         35 $string =~ s/(url\(['"]data:.*?\/.*?);(.*?),/$1\x{ff1b}$2,/g;
46              
47             # Split into styles.
48 16         178 foreach ( grep { /\S/ } split /(?<=\})/, $string ) {
  39         98  
49 31 50       145 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 31         54 my $style = $1;
56 31         46 my $properties = $2;
57 31         50 $style =~ s/\s{2,}/ /g;
58             my @styles =
59 33         49 grep { s/\s+/ /g; 1; }
  33         64  
60 31         57 grep { /\S/ }
  33         69  
61             split( /\s*,\s*/, $style );
62 31         48 foreach ( @styles ) {
63             # Give @font-face rules an unique id.
64 33 50       53 if ( $_ eq '@font-face' ) {
65 0         0 $_ = '@font-'.$ffi;
66 0         0 $ffi++;
67             }
68 33   100     114 $css->{$_} //= {};
69             }
70              
71             # Split into properties.
72 31         62 foreach ( grep { /\S/ } split /\;/, $properties ) {
  37         77  
73 35 50       141 unless ( /^\s*(\*?[\w._-]+)\s*:\s*(.*?)\s*$/ ) {
74 0         0 $errstr = "Invalid or unexpected property '$_' in style '$style'";
75 0         0 return;
76             }
77              
78 35         72 my $s = lc($1);
79 35         74 my %s = ( $s => $2 );
80              
81             # Split font shorthand.
82 35 100       83 if ( $s eq "font" ) {
    50          
83 8     8   16075 use Text::ParseWords qw(shellwords);
  8         14542  
  8         5400  
84 9         35 my @spec = shellwords($s{$s});
85              
86 9         1754 foreach my $spec ( @spec ) {
87 26         35 $spec =~ s/;$//;
88 26 100       194 if ( $spec =~ /^([.\d]+)px/ ) {
    100          
    50          
    50          
    100          
    100          
    100          
    50          
89 7         17 $s{'font-size'} = $1;
90             }
91             elsif ( $spec eq "bold" ) {
92 2         4 $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         4 $s{'font-family'} = "serif";
103             }
104             elsif ( $spec =~ /^(?:text,)?sans(?:-serif)?$/i ) {
105 5         13 $s{'font-family'} = "sans";
106             }
107              
108             # These are for ABC SVG processing.
109             elsif ( $spec =~ /^abc2svg(?:\.ttf)?$/i ) {
110 2         6 $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         14 $s{'font-family'} = $spec;
117             }
118             }
119              
120             # Remove the shorthand if we found something.
121 9 50       33 delete($s{$s}) if keys(%s) > 1;
122             }
123              
124             # Split outline shorthand.
125             elsif ( $s eq "outline" ) {
126 8     8   56 use Text::ParseWords qw(shellwords);
  8         12  
  8         25810  
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 35         79 foreach my $k ( keys %s ) {
147 44         53 foreach ( @styles ) {
148 48         139 $css->{$_}->{$k} = $s{$k};
149             }
150             }
151             }
152             }
153              
154 16         66 my @keys = keys( %$css );
155 16         25 for my $k ( @keys ) {
156 69 50       103 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 16         25 for my $k ( @keys ) {
164 69 100       143 next unless $k =~ /^(\S+)\s+(\S+)$/;
165 3   50     21 $css->{$1}->{" $2"} //= {};
166 3         32 $self->merge( $css->{$1}->{" $2"}, $css->{$k} );
167 3         9 delete ( $css->{$k} );
168             }
169              
170 16         45 1;
171             }
172              
173             # Merge hashes (and only hashes), recursive.
174 759     759 0 728 method merge ( $left, $right ) {
  759         930  
  759         691  
  759         684  
  759         587  
175 759 50       1000 return unless defined $right;
176 759 50 33     1950 if ( ref($left) eq 'HASH' && ref($right) eq 'HASH' ) {
177 759         1633 for ( keys %$right ) {
178 4217 100 100     8818 if ( exists $left->{$_}
      66        
179             && ref($left->{$_}) eq 'HASH'
180             && ref($right->{$_}) eq 'HASH' ) {
181 222         429 $self->merge( $left->{$_}, $right->{$_} );
182             }
183             else {
184 3995         5032 $left->{$_} = $right->{$_};
185             }
186             }
187 759         1318 return;
188             }
189 0         0 croak("Cannot merge " . ref($left) . " and " . ref($right));
190             }
191              
192 78     78 0 106 method find ( $arg ) {
  78         218  
  78         86  
  78         71  
193 78   33     194 $css->{'*'} //= $base;
194 78         83 my $ret = { %{$css->{'*'}} };
  78         497  
195 78 100       232 if ( exists( $css->{_} ) ) {
196 76         172 $self->merge( $ret, $css->{_} );
197             }
198 78         147 $ctx = $ret;
199 78         201 $ret->{$arg};
200             }
201              
202 217     217 0 239 method push ( @args ) {
  217         472  
  217         434  
  217         236  
203 217 100       838 my $args = ref($args[0]) eq 'HASH' ? $args[0] : { @args };
204 217   66     547 $css->{'*'} //= $base;
205 217         223 my $ret;
206              
207             # CSS defaults.
208 217         235 while ( my($k,$v) = each %{$css->{'*'}} ) {
  1736         3282  
209 1519   33     3709 $ret->{$k} //= $v;
210             }
211              
212             ## Parent.
213 217 100       358 if ( exists( $css->{_} ) ) {
214 180         450 $self->merge( $ret, $css->{_} );
215             }
216              
217             ## Presentation attributes.
218 217         416 for ( keys %$args ) {
219 490 100       1228 next if /^(element|class|style|id)$/;
220 236         402 $ret->{$_} = $args->{$_};
221             }
222              
223             ## Tag style.
224 217 100 100     811 if ( $args->{element} && exists( $css->{$args->{element}} ) ) {
225 6         19 $self->merge( $ret, $css->{$args->{element}} );
226             }
227 217 100 100     766 if ( $args->{element} && exists( $css->{_}->{" ".$args->{element}} ) ) {
228 3         10 $self->merge( $ret, $css->{_}->{" ".$args->{element}} );
229             }
230              
231             ## Class style.
232 217 100       392 if ( $args->{class} ) {
233 21         61 for ( split( ' ', $args->{class} ), "svg" ) {
234             $self->merge( $ret, $css->{".$_"} )
235 68 100       176 if exists( $css->{".$_"} );
236             $self->merge( $ret, $css->{$args->{element}.".$_"} )
237 68 50 66     194 if $args->{element} && exists( $css->{$args->{element}.".$_"} );
238             }
239             }
240              
241             ## ID (generic).
242 217 50 66     480 if ( $args->{id} && exists( $css->{ "#" . $args->{id} } ) ) {
243 0         0 $self->merge( $ret, $css->{ "#" . $args->{id} } );
244             }
245              
246             ## ID (specific).
247 217 50 66     449 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 217 100       414 if ( $args->{style} ) {
253 8 50       81 $self->read_string( "__ {" . $args->{style} . "}" )
254             or croak($errstr);
255 8         25 $self->merge( $ret, delete $css->{__} );
256             }
257              
258 217 50       429 $ret->{'@font-face'} = $ffam if $ffam;
259 217   100     284 push( @stack, { %{$css->{_}//{}} } );
  217         1235  
260 217         740 $self->merge( $css, { _ => $ret } );
261 217         1040 $ctx = $ret;
262             }
263              
264 211     211 0 239 method pop () {
  211         386  
  211         195  
265 211 50       350 Carp::croak("CSS stack underflow") unless @stack;
266 211         1472 $ctx = $css->{_} = pop(@stack);
267             }
268              
269 0     0 0   method level () {
  0            
  0            
270 0           0+@stack;
271             }
272              
273             1;