File Coverage

lib/ChordPro/lib/SVGPDF/CSS.pm
Criterion Covered Total %
statement 17 166 10.2
branch 0 78 0.0
condition 0 36 0.0
subroutine 6 17 35.2
pod 0 11 0.0
total 23 308 7.4


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