• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

i-net-software / jlessc / 281

pending completion
281

push

travis-ci-com

volker
Handle doubled rule correctly in the compressed formatter if previous
rule ends with an block

5 of 5 new or added lines in 1 file covered. (100.0%)

3586 of 3873 relevant lines covered (92.59%)

2.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.31
/src/com/inet/lib/less/UrlUtils.java
1
/**
2
 * MIT License (MIT)
3
 *
4
 * Copyright (c) 2014 - 2020 Volker Berlin
5
 *
6
 * Permission is hereby granted, free of charge, to any person obtaining a copy
7
 * of this software and associated documentation files (the "Software"), to deal
8
 * in the Software without restriction, including without limitation the rights
9
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
 * copies of the Software, and to permit persons to whom the Software is
11
 * furnished to do so, subject to the following conditions:
12
 * 
13
 * The above copyright notice and this permission notice shall be included in
14
 * all copies or substantial portions of the Software.
15
 * 
16
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
 * UT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
 * THE SOFTWARE.
23
 *
24
 * @author Volker Berlin
25
 * @license: The MIT license <http://opensource.org/licenses/MIT>
26
 */
27
package com.inet.lib.less;
28

29
import java.io.ByteArrayOutputStream;
30
import java.io.IOException;
31
import java.io.InputStream;
32
import java.lang.reflect.Method;
33
import java.net.MalformedURLException;
34
import java.net.URL;
35
import java.nio.charset.StandardCharsets;
36
import java.util.ArrayList;
37
import java.util.List;
38

39
import javax.annotation.Nonnull;
40

41
/**
42
 * Implementation of the function svg-Gradient and other URL utils.
43
 */
44
class UrlUtils {
×
45

46
    private static Method base64;
47
    private static Object encoder;
48

49
    /**
50
     * Remove a quote if exists.
51
     * 
52
     * @param str a string
53
     * @return the str without quotes
54
     */
55
    static @Nonnull String removeQuote( @Nonnull String str ) {
56
        if( str.length() > 1 ) {
3✔
57
            char ch = str.charAt( 0 );
3✔
58
            if( ch == '\'' || ch == '\"' ) {
3✔
59
                if( str.charAt( str.length() - 1 ) == ch ) {
3✔
60
                    return str.substring( 1, str.length() - 1 );
3✔
61
                }
62
            }
63
        }
64
        return str;
3✔
65
    }
66

67
    /**
68
     * Implementation of the function svg-Gradient.
69
     * 
70
     * @param formatter current formatter
71
     * @param parameters function parameters
72
     * @throws LessException if parameter list is wrong
73
     */
74
    static void svgGradient( CssFormatter formatter, List<Expression> parameters ) {
75
        if( parameters.size() < 3 ) {
3✔
76
            throw new LessException( "error evaluating function `svg-gradient expects direction, start_color [start_position], [color position,]..., end_color " );
×
77
        }
78
        String direction = parameters.get( 0 ).stringValue( formatter );
3✔
79
        String gradientType = "linear";
3✔
80
        String rectangleDimension = "x=\"0\" y=\"0\" width=\"1\" height=\"1\"";
3✔
81
        String gradientDirection;
82
        switch( direction ) {
3✔
83
            case "to bottom":
84
                gradientDirection = "x1=\"0%\" y1=\"0%\" x2=\"0%\" y2=\"100%\"";
3✔
85
                break;
3✔
86
            case "to right":
87
                gradientDirection = "x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\"";
×
88
                break;
×
89
            case "to bottom right":
90
                gradientDirection = "x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\"";
×
91
                break;
×
92
            case "to top right":
93
                gradientDirection = "x1=\"0%\" y1=\"100%\" x2=\"100%\" y2=\"0%\"";
×
94
                break;
×
95
            case "ellipse":
96
            case "ellipse at center":
97
                gradientType = "radial";
3✔
98
                gradientDirection = "cx=\"50%\" cy=\"50%\" r=\"75%\"";
3✔
99
                rectangleDimension = "x=\"-50\" y=\"-50\" width=\"101\" height=\"101\"";
3✔
100
                break;
3✔
101
            default:
102
                throw new LessException( "error evaluating function `svg-gradient`: svg-gradient direction must be 'to bottom', 'to right', 'to bottom right', 'to top right' or 'ellipse at center'" );
×
103
        }
104
        StringBuilder builder = new StringBuilder( "<?xml version=\"1.0\" ?><svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"100%\" height=\"100%\" viewBox=\"0 0 1 1\" preserveAspectRatio=\"none\">" );
3✔
105
        builder.append( '<' ).append( gradientType ).append( "Gradient id=\"gradient\" gradientUnits=\"userSpaceOnUse\" " ).append( gradientDirection ).append( '>' );
3✔
106

107
        for( int i = 1; i < parameters.size(); i++ ) {
3✔
108
            Expression param = parameters.get( i ).unpack( formatter );
3✔
109
            double color;
110
            double position;
111
            if( param.getClass() == Operation.class && ((Operation)param).getOperator() == ' ' ) {
3✔
112
                ArrayList<Expression> operands = ((Operation)param).getOperands();
3✔
113
                color = ColorUtils.getColor( operands.get( 0 ), formatter );
3✔
114
                position = ColorUtils.getPercent( operands.get( 1 ), formatter );
3✔
115
            } else {
3✔
116
                color = ColorUtils.getColor( param, formatter );
3✔
117
                position = (i - 1) / (parameters.size() - 2.0);
3✔
118
            }
119
            builder.append( "<stop offset=\"" );
3✔
120
            position *= 100;
3✔
121
            if( position == (int)position ) {
3✔
122
                builder.append( (int)position );
3✔
123
            } else {
124
                builder.append( formatter.getFormat().format( position ) );
×
125
            }
126
            builder.append( "%\" stop-color=\"" );
3✔
127
            formatter.addOutput();
3✔
128
            formatter.appendColor( color, null );
3✔
129
            builder.append( formatter.releaseOutput() );
3✔
130
            builder.append( '\"' );
3✔
131
            double alpha = ColorUtils.alpha( color );
3✔
132
            if( alpha < 1 ) {
3✔
133
                builder.append( " stop-opacity=\"" + alpha ).append( '\"' );
×
134
            }
135
            builder.append( "/>" );
3✔
136
        }
137
        builder.append( "</" ).append( gradientType ).append( "Gradient><rect " ).append( rectangleDimension ).append( " fill=\"url(#gradient)\" /></svg>" );
3✔
138

139
        byte[] bytes = builder.toString().getBytes( StandardCharsets.UTF_8 );
3✔
140

141
        formatter.append( "url('data:image/svg+xml;base64," );
3✔
142
        formatter.append( toBase64( bytes ) );
3✔
143
        formatter.append( "\')" );
3✔
144
    }
3✔
145

146
    /**
147
     * Implementation of the function data-uri.
148
     * 
149
     * @param formatter      current formatter
150
     * @param relativeUrlStr relative URL of the less script. Is used as base URL
151
     * @param urlString      the url parameter of the function
152
     * @param type           the mime type
153
     * @throws IOException If any I/O errors occur on reading the content
154
     */
155
    static void dataUri( CssFormatter formatter, String relativeUrlStr, final String urlString, String type ) throws IOException {
156
        String urlStr = removeQuote( urlString );
3✔
157
        InputStream input = null;
3✔
158
        try {
159
            try {
160
                URL url = formatter.getBaseURL();
3✔
161
                url = new URL( url, urlStr );
3✔
162
                if( !formatter.isRewriteUrlOff() ) {
3✔
163
                    input = formatter.getReaderFactory().openStream(new URL( formatter.getBaseURL(), relativeUrlStr ), urlStr, "" );
3✔
164
                }
165
                else {
166
                         input = formatter.getReaderFactory().openStream( formatter.getBaseURL(),  urlStr, relativeUrlStr );
3✔
167
                }
168
            } catch( Exception e ) {
3✔
169
                // try to do the default without rewrite, also if is a root url, remove that to see if the file can be found right besides the base less file.
170
                input = formatter.getReaderFactory().openStream( formatter.getBaseURL(),  urlStr.startsWith( "/" ) ? urlStr.substring( 1 ): urlStr, relativeUrlStr );
3✔
171
            }
3✔
172
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
3✔
173

174
            int count;
175
            byte[] data = new byte[16384];
3✔
176

177
            while( (count = input.read( data, 0, data.length )) > 0 ) {
3✔
178
                buffer.write( data, 0, count );
3✔
179
                if( buffer.size() >= 32 * 1024 )
3✔
180
                    break;
3✔
181
            }
182

183
            byte[] bytes = buffer.toByteArray();
3✔
184

185
            if( bytes.length >= 32 * 1024 ) {
3✔
186
                String rewrittenUrl = getRewrittenUrl( formatter, relativeUrlStr, urlString, urlStr );
3✔
187
                formatter.append( "url(" ).append( rewrittenUrl ).append( ')' );
3✔
188
            } else {
3✔
189
                dataUri( formatter, bytes, urlStr, type );
3✔
190
            }
191
        } catch( Exception e ) {
3✔
192
            String rewrittenUrl = getRewrittenUrl( formatter, relativeUrlStr, urlString, urlStr );
3✔
193
            formatter.append( "url(" ).append( rewrittenUrl ).append( ')' );
3✔
194
            return;
3✔
195
        } finally {
196
            if( input != null )
3✔
197
                input.close();
3✔
198
        }
199
    }
3✔
200

201
    /**
202
     * Create the rewritten URL
203
     * @param formatter current formatter
204
     * @param relativeUrlStr relative URL of the less script. Is used as base URL
205
     * @param urlString the url parameter of the function
206
     * @param urlStr the url parameter of the function
207
     * @return the new URL
208
     * @throws MalformedURLException if there any problems with the URL
209
     */
210
    private static String getRewrittenUrl( CssFormatter formatter, String relativeUrlStr, final String urlString, String urlStr ) throws MalformedURLException {
211
        boolean quote = urlString != urlStr;
3✔
212
        String rewrittenUrl;
213
        if( formatter.isRewriteUrl( urlStr ) ) {
3✔
214
            URL relativeUrl = new URL( relativeUrlStr );
3✔
215
            relativeUrl = new URL( relativeUrl, urlStr );
3✔
216
            rewrittenUrl = relativeUrl.getPath();
3✔
217
            rewrittenUrl = quote ? urlString.charAt( 0 ) + rewrittenUrl + urlString.charAt( 0 ) : rewrittenUrl;
3✔
218
        } else {
3✔
219
            rewrittenUrl = urlString;
3✔
220
        }
221
        return rewrittenUrl;
3✔
222
    }
223

224
    /**
225
     * Write the bytes as inline url.
226
     * 
227
     * @param formatter current formatter
228
     * @param bytes the bytes
229
     * @param urlStr used if mime type is null to detect the mime type
230
     * @param type the mime type
231
     */
232
    static void dataUri( CssFormatter formatter, byte[] bytes, String urlStr, String type ) {
233
        if( type == null ) {
3✔
234
            switch( urlStr.substring( urlStr.lastIndexOf( '.' ) + 1 ) ) {
3✔
235
                case "gif": 
236
                    type = "image/gif;base64";
×
237
                    break;
×
238
                case "png": 
239
                    type = "image/png;base64";
×
240
                    break;
×
241
                case "jpg":
242
                case "jpeg":
243
                    type = "image/jpeg;base64";
3✔
244
                    break;
3✔
245
                case "webp":
246
                    type = "image/webp;base64";
×
247
                    break;
×
248
                case "svg":
249
                    type = "image/svg+xml";
×
250
                    break;
×
251
                default: 
252
                    type = "text/html";
3✔
253
            }
254
        } else {
255
            type = removeQuote( type );
3✔
256
        }
257

258
        if( type.endsWith( "base64" ) ) {
3✔
259
            formatter.append( "url(\"data:" ).append( type ).append( ',' );
3✔
260
            formatter.append( toBase64( bytes ) );
3✔
261
            formatter.append( "\")" );
3✔
262
        } else {
263
            formatter.append( "url(\"data:" ).append( type ).append( ',' );
3✔
264
            appendEncode( formatter, bytes );
3✔
265
            formatter.append( "\")" );
3✔
266
        }
267
    }
3✔
268

269
    /**
270
     * Append the bytes URL encoded.
271
     * 
272
     * @param formatter current formatter
273
     * @param bytes the bytes
274
     */
275
    private static void appendEncode( CssFormatter formatter, byte[] bytes ) {
276
        for( byte b : bytes ) {
3✔
277
            if ((b >= 'a' && b <= 'z' ) || (b >= 'A' && b <= 'Z' ) || (b >= '0' && b <= '9' )) {
3✔
278
                formatter.append( (char )b );
3✔
279
            } else {
280
                switch( b ) {
3✔
281
                    case '-':
282
                    case '_':
283
                    case '*':
284
                    case '.':
285
                        formatter.append( (char )b );
3✔
286
                        break;
3✔
287
                    default:
288
                        formatter.append( '%' );
3✔
289
                        formatter.append( Character.toUpperCase( Character.forDigit((b >> 4) & 0xF, 16) ) );
3✔
290
                        formatter.append( Character.toUpperCase( Character.forDigit(b & 0xF, 16) ) );
3✔
291
                }
292
            }
293
        }
294
    }
3✔
295

296
    /**
297
     * Hack for base64 in Java 7 to Java 9.
298
     * <ul>
299
     * <li>In Java 7 we use: javax.xml.bind.DatatypeConverter.printBase64Binary( byte[] ) 
300
     * <li>in Java 8-9 we use: java.util.Base64.getEncoder().encodeToString( byte[] )
301
     * </ul>
302
     * @param bytes the bytes to converted
303
     * @return the base64 encoded string
304
     * @throws LessException if the reflection does not work
305
     */
306
    private static String toBase64( byte[] bytes ) {
307
        if( base64 == null ) {
3✔
308
            try {
309
                Class<?> clazz = Class.forName( "java.util.Base64" );
3✔
310
                encoder = clazz.getMethod( "getEncoder" ).invoke( null );
3✔
311
                base64 = encoder.getClass().getMethod( "encodeToString", byte[].class );
3✔
312
                base64.setAccessible( true ); /// performance optimizing
3✔
313
            } catch( Throwable th1 ) {
×
314
                try {
315
                    Class<?> clazz = Class.forName( "javax.xml.bind.DatatypeConverter" );
×
316
                    base64 = clazz.getMethod( "printBase64Binary", byte[].class );
×
317
                    encoder = null;
×
318
                    base64.setAccessible( true ); /// performance optimizing
×
319
                } catch( Throwable th2 ) {
×
320
                    th1.addSuppressed( th2 );
×
321
                    throw new LessException( th1 );
×
322
                }
×
323
            }
3✔
324
        }
325
        try {
326
            return (String)base64.invoke( encoder, bytes );
3✔
327
        } catch( Throwable th ) {
×
328
            throw new LessException( th );
×
329
        }
330
    }
331
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc