1 /* ***** BEGIN LICENSE BLOCK ***** 2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 3 * 4 * The contents of this file are subject to the Mozilla Public License Version 5 * 1.1 (the "License"); you may not use this file except in compliance with 6 * the License. You may obtain a copy of the License at 7 * http://www.mozilla.org/MPL/ 8 * 9 * Software distributed under the License is distributed on an "AS IS" basis, 10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 11 * for the specific language governing rights and limitations under the 12 * License. 13 * 14 * The Original Code is Chimera code. 15 * 16 * The Initial Developer of the Original Code is 17 * Netscape Communications Corporation. 18 * Portions created by the Initial Developer are Copyright (C) 2002 19 * the Initial Developer. All Rights Reserved. 20 * 21 * Contributor(s): 22 * Simon Fraser <sfraser (at) netscape.com> 23 * David Haas <haasd (at) cae.wisc.edu> 24 * 25 * Alternatively, the contents of this file may be used under the terms of 26 * either the GNU General Public License Version 2 or later (the "GPL"), or 27 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 28 * in which case the provisions of the GPL or the LGPL are applicable instead 29 * of those above. If you wish to allow use of your version of this file only 30 * under the terms of either the GPL or the LGPL, and not to allow others to 31 * use your version of this file under the terms of the MPL, indicate your 32 * decision by deleting the provisions above and replace them with the notice 33 * and other provisions required by the GPL or the LGPL. If you do not delete 34 * the provisions above, a recipient may use your version of this file under 35 * the terms of any one of the MPL, the GPL or the LGPL. 36 * 37 * ***** END LICENSE BLOCK ***** */ 38 39 #import <AppKit/AppKit.h> // for NSStringDrawing.h 40 41 #import "NSString+Utils.h" 42 #include "url/gurl.h" 43 44 45 @implementation NSString (ChimeraStringUtils) 46 47 + (id)ellipsisString 48 { 49 static NSString* sEllipsisString = nil; 50 if (!sEllipsisString) { 51 unichar ellipsisChar = 0x2026; 52 sEllipsisString = [[NSString alloc] initWithCharacters:&ellipsisChar length:1]; 53 } 54 55 return sEllipsisString; 56 } 57 58 + (NSString*)stringWithUUID 59 { 60 NSString* uuidString = nil; 61 CFUUIDRef newUUID = CFUUIDCreate(kCFAllocatorDefault); 62 if (newUUID) { 63 uuidString = (NSString *)CFUUIDCreateString(kCFAllocatorDefault, newUUID); 64 CFRelease(newUUID); 65 } 66 return [uuidString autorelease]; 67 } 68 69 - (BOOL)isEqualToStringIgnoringCase:(NSString*)inString 70 { 71 return ([self compare:inString options:NSCaseInsensitiveSearch] == NSOrderedSame); 72 } 73 74 - (BOOL)hasCaseInsensitivePrefix:(NSString*)inString 75 { 76 if ([self length] < [inString length]) 77 return NO; 78 return ([self compare:inString options:NSCaseInsensitiveSearch range:NSMakeRange(0, [inString length])] == NSOrderedSame); 79 } 80 81 - (BOOL)isLooselyValidatedURI 82 { 83 return ([self hasCaseInsensitivePrefix:@"javascript:"] || [self hasCaseInsensitivePrefix:@"data:"]); 84 } 85 86 - (BOOL)isPotentiallyDangerousURI 87 { 88 return ([self hasCaseInsensitivePrefix:@"javascript:"] || [self hasCaseInsensitivePrefix:@"data:"]); 89 } 90 91 - (BOOL)isValidURI 92 { 93 // isValid() will only be true for valid, well-formed URI strings 94 GURL testURL([self UTF8String]); 95 96 // |javascript:| and |data:| URIs might not have passed the test, 97 // but spaces will work OK, so evaluate them separately. 98 if ((testURL.is_valid()) || [self isLooselyValidatedURI]) { 99 return YES; 100 } 101 return NO; 102 } 103 104 - (NSString *)stringByRemovingCharactersInSet:(NSCharacterSet*)characterSet 105 { 106 NSScanner* cleanerScanner = [NSScanner scannerWithString:self]; 107 NSMutableString* cleanString = [NSMutableString stringWithCapacity:[self length]]; 108 // Make sure we don't skip whitespace, which NSScanner does by default 109 [cleanerScanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@""]]; 110 111 while (![cleanerScanner isAtEnd]) { 112 NSString* stringFragment; 113 if ([cleanerScanner scanUpToCharactersFromSet:characterSet intoString:&stringFragment]) 114 [cleanString appendString:stringFragment]; 115 116 [cleanerScanner scanCharactersFromSet:characterSet intoString:nil]; 117 } 118 119 return cleanString; 120 } 121 122 - (NSString *)stringByReplacingCharactersInSet:(NSCharacterSet*)characterSet 123 withString:(NSString*)string 124 { 125 NSScanner* cleanerScanner = [NSScanner scannerWithString:self]; 126 NSMutableString* cleanString = [NSMutableString stringWithCapacity:[self length]]; 127 // Make sure we don't skip whitespace, which NSScanner does by default 128 [cleanerScanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@""]]; 129 130 while (![cleanerScanner isAtEnd]) 131 { 132 NSString* stringFragment; 133 if ([cleanerScanner scanUpToCharactersFromSet:characterSet intoString:&stringFragment]) 134 [cleanString appendString:stringFragment]; 135 136 if ([cleanerScanner scanCharactersFromSet:characterSet intoString:nil]) 137 [cleanString appendString:string]; 138 } 139 140 return cleanString; 141 } 142 143 - (NSString*)stringByTruncatingTo:(unsigned int)maxCharacters at:(ETruncationType)truncationType 144 { 145 if ([self length] > maxCharacters) 146 { 147 NSMutableString *mutableCopy = [self mutableCopy]; 148 [mutableCopy truncateTo:maxCharacters at:truncationType]; 149 return [mutableCopy autorelease]; 150 } 151 152 return self; 153 } 154 155 - (NSString *)stringByTruncatingToWidth:(float)inWidth at:(ETruncationType)truncationType 156 withAttributes:(NSDictionary *)attributes 157 { 158 if ([self sizeWithAttributes:attributes].width > inWidth) 159 { 160 NSMutableString *mutableCopy = [self mutableCopy]; 161 [mutableCopy truncateToWidth:inWidth at:truncationType withAttributes:attributes]; 162 return [mutableCopy autorelease]; 163 } 164 165 return self; 166 } 167 168 - (NSString *)stringByTrimmingWhitespace 169 { 170 return [self stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; 171 } 172 173 -(NSString *)stringByRemovingAmpEscapes 174 { 175 NSMutableString* dirtyStringMutant = [NSMutableString stringWithString:self]; 176 [dirtyStringMutant replaceOccurrencesOfString:@"&" 177 withString:@"&" 178 options:NSLiteralSearch 179 range:NSMakeRange(0,[dirtyStringMutant length])]; 180 [dirtyStringMutant replaceOccurrencesOfString:@""" 181 withString:@"\"" 182 options:NSLiteralSearch 183 range:NSMakeRange(0,[dirtyStringMutant length])]; 184 [dirtyStringMutant replaceOccurrencesOfString:@"<" 185 withString:@"<" 186 options:NSLiteralSearch 187 range:NSMakeRange(0,[dirtyStringMutant length])]; 188 [dirtyStringMutant replaceOccurrencesOfString:@">" 189 withString:@">" 190 options:NSLiteralSearch 191 range:NSMakeRange(0,[dirtyStringMutant length])]; 192 [dirtyStringMutant replaceOccurrencesOfString:@"—" 193 withString:@"-" 194 options:NSLiteralSearch 195 range:NSMakeRange(0,[dirtyStringMutant length])]; 196 [dirtyStringMutant replaceOccurrencesOfString:@"'" 197 withString:@"'" 198 options:NSLiteralSearch 199 range:NSMakeRange(0,[dirtyStringMutant length])]; 200 // fix import from old Firefox versions, which exported ' instead of a plain apostrophe 201 [dirtyStringMutant replaceOccurrencesOfString:@"'" 202 withString:@"'" 203 options:NSLiteralSearch 204 range:NSMakeRange(0,[dirtyStringMutant length])]; 205 return [dirtyStringMutant stringByRemovingCharactersInSet:[NSCharacterSet controlCharacterSet]]; 206 } 207 208 -(NSString *)stringByAddingAmpEscapes 209 { 210 NSMutableString* dirtyStringMutant = [NSMutableString stringWithString:self]; 211 [dirtyStringMutant replaceOccurrencesOfString:@"&" 212 withString:@"&" 213 options:NSLiteralSearch 214 range:NSMakeRange(0,[dirtyStringMutant length])]; 215 [dirtyStringMutant replaceOccurrencesOfString:@"\"" 216 withString:@""" 217 options:NSLiteralSearch 218 range:NSMakeRange(0,[dirtyStringMutant length])]; 219 [dirtyStringMutant replaceOccurrencesOfString:@"<" 220 withString:@"<" 221 options:NSLiteralSearch 222 range:NSMakeRange(0,[dirtyStringMutant length])]; 223 [dirtyStringMutant replaceOccurrencesOfString:@">" 224 withString:@">" 225 options:NSLiteralSearch 226 range:NSMakeRange(0,[dirtyStringMutant length])]; 227 return [NSString stringWithString:dirtyStringMutant]; 228 } 229 230 @end 231 232 233 @implementation NSMutableString (ChimeraMutableStringUtils) 234 235 - (void)truncateTo:(unsigned)maxCharacters at:(ETruncationType)truncationType 236 { 237 if ([self length] <= maxCharacters) 238 return; 239 240 NSRange replaceRange; 241 replaceRange.length = [self length] - maxCharacters; 242 243 switch (truncationType) { 244 case kTruncateAtStart: 245 replaceRange.location = 0; 246 break; 247 248 case kTruncateAtMiddle: 249 replaceRange.location = maxCharacters / 2; 250 break; 251 252 case kTruncateAtEnd: 253 replaceRange.location = maxCharacters; 254 break; 255 256 default: 257 #if DEBUG 258 NSLog(@"Unknown truncation type in stringByTruncatingTo::"); 259 #endif 260 replaceRange.location = maxCharacters; 261 break; 262 } 263 264 [self replaceCharactersInRange:replaceRange withString:[NSString ellipsisString]]; 265 } 266 267 268 - (void)truncateToWidth:(float)maxWidth 269 at:(ETruncationType)truncationType 270 withAttributes:(NSDictionary *)attributes 271 { 272 // First check if we have to truncate at all. 273 if ([self sizeWithAttributes:attributes].width <= maxWidth) 274 return; 275 276 // Essentially, we perform a binary search on the string length 277 // which fits best into maxWidth. 278 279 float width = maxWidth; 280 int lo = 0; 281 int hi = [self length]; 282 int mid; 283 284 // Make a backup copy of the string so that we can restore it if we fail low. 285 NSMutableString *backup = [self mutableCopy]; 286 287 while (hi >= lo) { 288 mid = (hi + lo) / 2; 289 290 // Cut to mid chars and calculate the resulting width 291 [self truncateTo:mid at:truncationType]; 292 width = [self sizeWithAttributes:attributes].width; 293 294 if (width > maxWidth) { 295 // Fail high - string is still to wide. For the next cut, we can simply 296 // work on the already cut string, so we don't restore using the backup. 297 hi = mid - 1; 298 } 299 else if (width == maxWidth) { 300 // Perfect match, abort the search. 301 break; 302 } 303 else { 304 // Fail low - we cut off too much. Restore the string before cutting again. 305 lo = mid + 1; 306 [self setString:backup]; 307 } 308 } 309 // Perform the final cut (unless this was already a perfect match). 310 if (width != maxWidth) 311 [self truncateTo:hi at:truncationType]; 312 [backup release]; 313 } 314 315 @end 316 317 @implementation NSString (ChimeraFilePathStringUtils) 318 319 - (NSString*)volumeNamePathComponent 320 { 321 // if the file doesn't exist, then componentsToDisplayForPath will return nil, 322 // so back up to the nearest existing dir 323 NSString* curPath = self; 324 while (![[NSFileManager defaultManager] fileExistsAtPath:curPath]) 325 { 326 NSString* parentDirPath = [curPath stringByDeletingLastPathComponent]; 327 if ([parentDirPath isEqualToString:curPath]) 328 break; // avoid endless loop 329 curPath = parentDirPath; 330 } 331 332 NSArray* displayComponents = [[NSFileManager defaultManager] componentsToDisplayForPath:curPath]; 333 if ([displayComponents count] > 0) 334 return [displayComponents objectAtIndex:0]; 335 336 return self; 337 } 338 339 - (NSString*)displayNameOfLastPathComponent 340 { 341 return [[NSFileManager defaultManager] displayNameAtPath:self]; 342 } 343 344 @end 345 346 @implementation NSString (CaminoURLStringUtils) 347 348 - (BOOL)isBlankURL 349 { 350 return ([self isEqualToString:@"about:blank"] || [self isEqualToString:@""]); 351 } 352 353 // Excluded character list comes from RFC2396 and by examining Safari's behaviour 354 - (NSString*)unescapedURI 355 { 356 NSString *unescapedURI = (NSString*)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault, 357 (CFStringRef)self, 358 CFSTR(" \"\';/?:@&=+$,#"), 359 kCFStringEncodingUTF8); 360 return unescapedURI ? [unescapedURI autorelease] : self; 361 } 362 363 @end 364