@@ -84,6 +84,16 @@ public static String extractStringValue (@Nullable final String sStr)
8484 return sStr ;
8585 }
8686
87+ private static boolean _isWhitespace (final char c )
88+ {
89+ return c == '\n' || c == '\t' || c == ' ' ;
90+ }
91+
92+ private static boolean _isHexChar (final char c )
93+ {
94+ return (c >= '0' && c <= '9' ) || (c >= 'A' && c <= 'F' ) || (c >= 'a' && c <= 'f' );
95+ }
96+
8797 /**
8898 * Unescape all escaped characters in a CSS URL. All characters masked with a
8999 * '\\' character replaced.
@@ -96,28 +106,63 @@ public static String extractStringValue (@Nullable final String sStr)
96106 @ Nonnull
97107 public static String unescapeURL (@ Nonnull final String sEscapedURL )
98108 {
99- int nIndex = sEscapedURL .indexOf (URL_ESCAPE_CHAR );
109+ final int nIndex = sEscapedURL .indexOf (URL_ESCAPE_CHAR );
100110 if (nIndex < 0 )
101111 {
102112 // No escape sequence found
103113 return sEscapedURL ;
104114 }
105115
106- final StringBuilder aSB = new StringBuilder (sEscapedURL .length ());
107- int nPrevIndex = 0 ;
108- do
116+ // The source length is always longer
117+ final int nSrcLen = sEscapedURL .length ();
118+ final StringBuilder aSB = new StringBuilder (nSrcLen );
119+ int nCharIndex = 0 ;
120+ while (nCharIndex < nSrcLen )
109121 {
110- // Append everything before the first quote char
111- aSB .append (sEscapedURL , nPrevIndex , nIndex );
112- // Append the quoted char itself
113- aSB .append (sEscapedURL , nIndex + 1 , nIndex + 2 );
114- // The new position to start searching
115- nPrevIndex = nIndex + 2 ;
116- // Search the next escaped char
117- nIndex = sEscapedURL .indexOf (URL_ESCAPE_CHAR , nPrevIndex );
118- } while (nIndex >= 0 );
119- // Append the rest
120- aSB .append (sEscapedURL .substring (nPrevIndex ));
122+ final char c = sEscapedURL .charAt (nCharIndex );
123+ nCharIndex ++;
124+
125+ if (c == URL_ESCAPE_CHAR )
126+ {
127+ int nCodePoint = 0 ;
128+ int nHexCount = 0 ;
129+ while (nHexCount <= 6 )
130+ {
131+ final char cNext = sEscapedURL .charAt (nCharIndex );
132+ if (_isHexChar (cNext ))
133+ {
134+ nHexCount ++;
135+ nCharIndex ++;
136+ nCodePoint = (nCodePoint * 16 ) + StringHelper .getHexValue (cNext );
137+ }
138+ else
139+ break ;
140+ }
141+
142+ if (nHexCount > 0 )
143+ {
144+ // Check for a trailing whitespace and evtl. skip it
145+ final char cNext = sEscapedURL .charAt (nCharIndex );
146+ if (_isWhitespace (cNext ))
147+ nCharIndex ++;
148+
149+ if (nCodePoint > '\uFFFF' )
150+ aSB .append (Character .toChars (nCodePoint ));
151+ else
152+ aSB .append ((char ) nCodePoint );
153+ }
154+ else
155+ {
156+ // Append \ verbose
157+ aSB .append (c );
158+ }
159+ }
160+ else
161+ {
162+ // Copy as is
163+ aSB .append (c );
164+ }
165+ }
121166 return aSB .toString ();
122167 }
123168
@@ -134,7 +179,9 @@ public static String unescapeURL (@Nonnull final String sEscapedURL)
134179 public static String trimUrl (@ Nonnull final CharSequence s )
135180 {
136181 // Extract from "url(...)"
137- final String sTrimmed = _trimBy (s , CCSSValue .PREFIX_URL_OPEN .length (), CCSSValue .SUFFIX_URL_CLOSE .length ()).trim ();
182+ final String sTrimmed = _trimBy (s ,
183+ CCSSValue .PREFIX_URL_OPEN .length (),
184+ CCSSValue .SUFFIX_URL_CLOSE .length ()).trim ();
138185 // Remove the trailing quotes (if any)
139186 final String sUnquoted = extractStringValue (sTrimmed );
140187 // Unescape all escaped chars
@@ -181,7 +228,8 @@ public static String validateIdentifier (@Nonnull final StringBuilder aPattern)
181228 if (c1 == '-' || c1 == '$' || c1 == '*' )
182229 {
183230 if (nLength > 1 && Character .isDigit (c2 ))
184- throw new IllegalArgumentException ("Identifier may not start with a hyphen/dollar/star and a digit: " + aPattern );
231+ throw new IllegalArgumentException ("Identifier may not start with a hyphen/dollar/star and a digit: " +
232+ aPattern );
185233 }
186234 else
187235 {
0 commit comments