Home | History | Annotate | Download | only in mozilla
      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:@"&amp;"
    177                                      withString:@"&"
    178                                         options:NSLiteralSearch
    179                                           range:NSMakeRange(0,[dirtyStringMutant length])];
    180   [dirtyStringMutant replaceOccurrencesOfString:@"&quot;"
    181                                      withString:@"\""
    182                                         options:NSLiteralSearch
    183                                           range:NSMakeRange(0,[dirtyStringMutant length])];
    184   [dirtyStringMutant replaceOccurrencesOfString:@"&lt;"
    185                                      withString:@"<"
    186                                         options:NSLiteralSearch
    187                                           range:NSMakeRange(0,[dirtyStringMutant length])];
    188   [dirtyStringMutant replaceOccurrencesOfString:@"&gt;"
    189                                      withString:@">"
    190                                         options:NSLiteralSearch
    191                                           range:NSMakeRange(0,[dirtyStringMutant length])];
    192   [dirtyStringMutant replaceOccurrencesOfString:@"&mdash;"
    193                                      withString:@"-"
    194                                         options:NSLiteralSearch
    195                                           range:NSMakeRange(0,[dirtyStringMutant length])];
    196   [dirtyStringMutant replaceOccurrencesOfString:@"&apos;"
    197                                      withString:@"'"
    198                                         options:NSLiteralSearch
    199                                           range:NSMakeRange(0,[dirtyStringMutant length])];
    200   // fix import from old Firefox versions, which exported &#39; instead of a plain apostrophe
    201   [dirtyStringMutant replaceOccurrencesOfString:@"&#39;"
    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:@"&amp;"
    213                                         options:NSLiteralSearch
    214                                           range:NSMakeRange(0,[dirtyStringMutant length])];
    215   [dirtyStringMutant replaceOccurrencesOfString:@"\""
    216                                      withString:@"&quot;"
    217                                         options:NSLiteralSearch
    218                                           range:NSMakeRange(0,[dirtyStringMutant length])];
    219   [dirtyStringMutant replaceOccurrencesOfString:@"<"
    220                                      withString:@"&lt;"
    221                                         options:NSLiteralSearch
    222                                           range:NSMakeRange(0,[dirtyStringMutant length])];
    223   [dirtyStringMutant replaceOccurrencesOfString:@">"
    224                                      withString:@"&gt;"
    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