File: | NSString_BDSKExtensions.m |
Location: | line 1173, column 16 |
Description: | Memory Leak |
Code is compiled without garbage collection. |
1 | // NSString_BDSKExtensions.m |
2 | |
3 | // Created by Michael McCracken on Sun Jul 21 2002. |
4 | /* |
5 | This software is Copyright (c) 2002-2008 |
6 | Michael O. McCracken. All rights reserved. |
7 | |
8 | Redistribution and use in source and binary forms, with or without |
9 | modification, are permitted provided that the following conditions |
10 | are met: |
11 | |
12 | - Redistributions of source code must retain the above copyright |
13 | notice, this list of conditions and the following disclaimer. |
14 | |
15 | - Redistributions in binary form must reproduce the above copyright |
16 | notice, this list of conditions and the following disclaimer in |
17 | the documentation and/or other materials provided with the |
18 | distribution. |
19 | |
20 | - Neither the name of Michael O. McCracken nor the names of any |
21 | contributors may be used to endorse or promote products derived |
22 | from this software without specific prior written permission. |
23 | |
24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
25 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
26 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
27 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
28 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
29 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
30 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
31 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
32 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
33 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
34 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
35 | */ |
36 | |
37 | #import "NSString_BDSKExtensions.h" |
38 | #import <OmniFoundation/NSString-OFExtensions.h> |
39 | #import <Cocoa/Cocoa.h> |
40 | #import <AGRegex/AGRegex.h> |
41 | #import <OmniFoundation/OFCharacterSet.h> |
42 | #import <OmniFoundation/OFStringScanner.h> |
43 | #import "BDSKStringConstants.h" |
44 | #import "CFString_BDSKExtensions.h" |
45 | #import "OFCharacterSet_BDSKExtensions.h" |
46 | #import "NSURL_BDSKExtensions.h" |
47 | #import "NSScanner_BDSKExtensions.h" |
48 | #import "html2tex.h" |
49 | #import "NSDictionary_BDSKExtensions.h" |
50 | #import "NSWorkspace_BDSKExtensions.h" |
51 | #import "BDSKStringEncodingManager.h" |
52 | #import "BDSKTypeManager.h" |
53 | #import "NSFileManager_BDSKExtendedAttributes.h" |
54 | |
55 | static NSString *yesString = nil( ( void * ) 0 ); |
56 | static NSString *noString = nil( ( void * ) 0 ); |
57 | static NSString *mixedString = nil( ( void * ) 0 ); |
58 | |
59 | @implementation NSString (BDSKExtensions) |
60 | |
61 | + (void)didLoad |
62 | { |
63 | yesString = [NSLocalizedString[ [ NSBundle mainBundle ] localizedStringForKey : ( @ "Yes" ) value : @ "" table : ( ( void * ) 0 ) ](@"Yes", @"") copy]; |
64 | noString = [NSLocalizedString[ [ NSBundle mainBundle ] localizedStringForKey : ( @ "No" ) value : @ "" table : ( ( void * ) 0 ) ](@"No", @"") copy]; |
65 | mixedString = [NSLocalizedString[ [ NSBundle mainBundle ] localizedStringForKey : ( @ "-" ) value : @ "" table : ( ( void * ) 0 ) ](@"-", @"indeterminate or mixed value indicator") copy]; |
66 | |
67 | } |
68 | |
69 | + (NSString *)hexStringForCharacter:(unichar)ch{ |
70 | NSMutableString *string = [NSMutableString stringWithCapacity:4]; |
71 | [string appendFormat:@"%X", ch]; |
72 | while([string length] < 4) |
73 | [string insertString:@"0" atIndex:0]; |
74 | [string insertString:@"0x" atIndex:0]; |
75 | return string; |
76 | } |
77 | |
78 | static int MAX_RATING = 5; |
79 | + (NSString *)ratingStringWithInteger:(int)rating; |
80 | { |
81 | NSParameterAssertdo { if ( ! ( ( rating <= MAX_RATING ) ) ) { [ [ NSAssertionHandler currentHandler ] handleFailureInMethod : _cmd object : self file : [ NSString stringWithUTF8String : "/Volumes/Local/Users/amaxwell/build/bibdesk-clean/NSString_BDSKExtensions.m" ] lineNumber : 81 description : ( @ "Invalid parameter not satisfying: %s" ) , ( "rating <= MAX_RATING" ) , ( 0 ) , ( 0 ) , ( 0 ) , ( 0 ) ] ; } } while ( 0 )(rating <= MAX_RATING); |
82 | static CFMutableDictionaryRef ratings = NULL( ( void * ) 0 ); |
83 | if(ratings == NULL( ( void * ) 0 )){ |
84 | ratings = CFDictionaryCreateMutable(CFAllocatorGetDefault(), MAX_RATING + 1, &OFIntegerDictionaryKeyCallbacks, &OFNSObjectDictionaryValueCallbacks); |
85 | int i = 0; |
86 | NSMutableString *ratingString = [NSMutableString string]; |
87 | do { |
88 | CFDictionaryAddValue(ratings, (const void *)i, (const void *)[[ratingString copy] autorelease]); |
89 | [ratingString appendCharacter:(0x278A + i)]; |
90 | } while(i++ < MAX_RATING); |
91 | OBPOSTCONDITIONdo { if ( ! ( ( int ) [ ( id ) ratings count ] == MAX_RATING + 1 ) ) OBAssertFailed ( "POSTCONDITION" , "(int)[(id)ratings count] == MAX_RATING + 1" , "/Volumes/Local/Users/amaxwell/build/bibdesk-clean/NSString_BDSKExtensions.m" , 91 ) ; } while ( ( BOOL ) 0 )((int)[(id)ratings count] == MAX_RATING + 1); |
92 | } |
93 | return (NSString *)CFDictionaryGetValue(ratings, (const void *)rating); |
94 | } |
95 | |
96 | + (NSString *)stringWithBool:(BOOL)boolValue { |
97 | return boolValue ? yesString : noString; |
98 | } |
99 | |
100 | + (NSString *)stringWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)encoding guessEncoding:(BOOL)try; |
101 | { |
102 | return [[self alloc] initWithContentsOfFile:path encoding:encoding guessEncoding:try]; |
103 | } |
104 | |
105 | + (NSString *)stringWithFileSystemRepresentation:(const char *)cstring; |
106 | { |
107 | NSParameterAssertdo { if ( ! ( ( cstring != ( ( void * ) 0 ) ) ) ) { [ [ NSAssertionHandler currentHandler ] handleFailureInMethod : _cmd object : self file : [ NSString stringWithUTF8String : "/Volumes/Local/Users/amaxwell/build/bibdesk-clean/NSString_BDSKExtensions.m" ] lineNumber : 107 description : ( @ "Invalid parameter not satisfying: %s" ) , ( "cstring != NULL" ) , ( 0 ) , ( 0 ) , ( 0 ) , ( 0 ) ] ; } } while ( 0 )(cstring != NULL); |
108 | return [(id)CFStringCreateWithFileSystemRepresentation(CFAllocatorGetDefault(), cstring) autorelease]; |
109 | } |
110 | |
111 | + (NSString *)stringWithTriStateValue:(NSCellStateValue)triStateValue { |
112 | switch (triStateValue) { |
113 | case NSOffState: |
114 | return noString; |
115 | break; |
116 | case NSOnState: |
117 | return yesString; |
118 | break; |
119 | case NSMixedState: |
120 | default: |
121 | return mixedString; |
122 | break; |
123 | } |
124 | } |
125 | |
126 | + (NSString *)unicodeNameOfCharacter:(unichar)ch; |
127 | { |
128 | CFMutableStringRef charString = CFStringCreateMutable(CFAllocatorGetDefault(), 0); |
129 | CFStringAppendCharacters(charString, &ch, 1); |
130 | |
131 | // ignore failures for now |
132 | CFStringTransform(charString, NULL( ( void * ) 0 ), kCFStringTransformToUnicodeName, FALSE0); |
133 | |
134 | return [(id)charString autorelease]; |
135 | } |
136 | |
137 | + (NSString *)IANACharSetNameForEncoding:(NSStringEncoding)enc; |
138 | { |
139 | CFStringEncoding cfEnc = CFStringConvertNSStringEncodingToEncoding(enc); |
140 | NSString *encName = nil( ( void * ) 0 ); |
141 | if (kCFStringEncodingInvalidId( 0xffffffffU ) != cfEnc) |
142 | encName = (NSString *)CFStringConvertEncodingToIANACharSetName(cfEnc); |
143 | return encName; |
144 | } |
145 | |
146 | + (NSStringEncoding)encodingForIANACharSetName:(NSString *)name |
147 | { |
148 | NSStringEncoding nsEnc = 0; |
149 | CFStringEncoding cfEnc = kCFStringEncodingInvalidId( 0xffffffffU ); |
150 | |
151 | if (name) |
152 | cfEnc = CFStringConvertIANACharSetNameToEncoding((CFStringRef)name); |
153 | |
154 | if (kCFStringEncodingInvalidId( 0xffffffffU ) != cfEnc) |
155 | nsEnc = CFStringConvertEncodingToNSStringEncoding(cfEnc); |
156 | |
157 | return nsEnc; |
158 | } |
159 | |
160 | static inline BOOL dataHasUnicodeByteOrderMark(NSData *data) |
161 | { |
162 | unsigned len = [data length]; |
163 | size_t size = sizeof(UniChar); |
164 | BOOL rv = NO( BOOL ) 0; |
165 | if(len >= size){ |
166 | const UniChar bigEndianBOM = 0xfeff; |
167 | const UniChar littleEndianBOM = 0xfffe; |
168 | |
169 | UniChar possibleBOM = 0; |
170 | [data getBytes:&possibleBOM length:size]; |
171 | rv = (possibleBOM == bigEndianBOM || possibleBOM == littleEndianBOM); |
172 | } |
173 | return rv; |
174 | } |
175 | |
176 | - (NSString *)initWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)encoding guessEncoding:(BOOL)try; |
177 | { |
178 | if(self = [self init]){ |
179 | NSData *data = [[NSData alloc] initWithContentsOfFile:path options:NSMappedRead error:NULL( ( void * ) 0 )]; |
180 | |
181 | NSString *string = nil( ( void * ) 0 ); |
182 | // zero encoding is never valid |
183 | if(encoding > 0) |
184 | string = [[NSString alloc] initWithData:data encoding:encoding]; |
185 | // read com.apple.TextEncoding on Leopard, or when reading a Tiger file saved on Leopard |
186 | if(nil( ( void * ) 0 ) == string) { |
187 | encoding = [[NSFileManager defaultManager] appleStringEncodingAtPath:path error:NULL( ( void * ) 0 )]; |
188 | if (encoding > 0) |
189 | string = [[NSString alloc] initWithData:data encoding:encoding]; |
190 | } |
191 | // if the string is nil at this point, the passed in encoding was not valid/correct and com.apple.TextEncoding wasn't present |
192 | if(nil( ( void * ) 0 ) == string && try && dataHasUnicodeByteOrderMark(data) && encoding != NSUnicodeStringEncoding) |
193 | string = [[NSString alloc] initWithData:data encoding:NSUnicodeStringEncoding]; |
194 | if(nil( ( void * ) 0 ) == string && try && encoding != NSUTF8StringEncoding) |
195 | string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; |
196 | if(nil( ( void * ) 0 ) == string && try && encoding != [NSString defaultCStringEncoding]) |
197 | string = [[NSString alloc] initWithData:data encoding:[NSString defaultCStringEncoding]]; |
198 | if(nil( ( void * ) 0 ) == string && try && encoding != [BDSKStringEncodingManager defaultEncoding]) |
199 | string = [[NSString alloc] initWithData:data encoding:[BDSKStringEncodingManager defaultEncoding]]; |
200 | if(nil( ( void * ) 0 ) == string && try && encoding != NSISOLatin1StringEncoding) |
201 | string = [[NSString alloc] initWithData:data encoding:NSISOLatin1StringEncoding]; |
202 | |
203 | [data release]; |
204 | [self release]; |
205 | self = string; |
206 | } |
207 | return self; |
208 | } |
209 | |
210 | |
211 | #pragma mark TeX cleaning |
212 | |
213 | - (NSString *)stringByConvertingDoubleHyphenToEndash{ |
214 | NSString *string = self; |
215 | NSString *doubleHyphen = @"--"; |
216 | NSRange range = [self rangeOfString:doubleHyphen]; |
217 | if (range.location != NSNotFound) { |
218 | NSMutableString *mutString = [[self mutableCopy] autorelease]; |
219 | do { |
220 | [mutString replaceCharactersInRange:range withString:[NSString endashString]]; |
221 | range = [mutString rangeOfString:doubleHyphen]; |
222 | } while (range.location != NSNotFound); |
223 | string = mutString; |
224 | } |
225 | return string; |
226 | } |
227 | |
228 | - (NSString *)stringByRemovingCurlyBraces{ |
229 | return [self stringByRemovingCharactersInOFCharacterSet:[OFCharacterSet curlyBraceCharacterSet]]; |
230 | } |
231 | |
232 | - (NSString *)stringByRemovingTeX{ |
233 | NSRange searchRange = NSMakeRange(0, [self length]); |
234 | NSRange foundRange = [self rangeOfTeXCommandInRange:searchRange]; |
235 | |
236 | if (foundRange.length == 0 && [self rangeOfCharacterFromSet:[NSCharacterSet curlyBraceCharacterSet]].length == 0) |
237 | return self; |
238 | |
239 | NSMutableString *mutableString = [[self mutableCopy] autorelease]; |
240 | while(foundRange.length){ |
241 | [mutableString replaceCharactersInRange:foundRange withString:@""]; |
242 | searchRange.length = NSMaxRange(searchRange) - NSMaxRange(foundRange); |
243 | searchRange.location = foundRange.location; |
244 | foundRange = [mutableString rangeOfTeXCommandInRange:searchRange]; |
245 | } |
246 | [mutableString deleteCharactersInCharacterSet:[NSCharacterSet curlyBraceCharacterSet]]; |
247 | return mutableString; |
248 | } |
249 | |
250 | #pragma mark TeX parsing |
251 | |
252 | - (NSString *)entryType; |
253 | { |
254 | // we could save a little memory by using a case-insensitive dictionary, but this is faster (and these strings are small) |
255 | static NSMutableDictionary *entryDictionary = nil( ( void * ) 0 ); |
256 | if (nil( ( void * ) 0 ) == entryDictionary) |
257 | entryDictionary = [[NSMutableDictionary alloc] initWithCapacity:100]; |
258 | |
259 | NSString *entryType = [entryDictionary objectForKey:self]; |
260 | if (nil( ( void * ) 0 ) == entryType) { |
261 | entryType = [self lowercaseString]; |
262 | [entryDictionary setObject:entryType forKey:self]; |
263 | } |
264 | return entryType; |
265 | } |
266 | |
267 | - (NSString *)fieldName; |
268 | { |
269 | // we could save a little memory by using a case-insensitive dictionary, but this is faster (and these strings are small) |
270 | static NSMutableDictionary *fieldDictionary = nil( ( void * ) 0 ); |
271 | if (nil( ( void * ) 0 ) == fieldDictionary) |
272 | fieldDictionary = [[NSMutableDictionary alloc] initWithCapacity:100]; |
273 | |
274 | NSString *fieldName = [fieldDictionary objectForKey:self]; |
275 | if (nil( ( void * ) 0 ) == fieldName) { |
276 | fieldName = [self capitalizedString]; |
277 | [fieldDictionary setObject:fieldName forKey:self]; |
278 | } |
279 | return fieldName; |
280 | } |
281 | |
282 | - (NSString *)localizedFieldName; |
283 | { |
284 | // this is used for display, for now we don't do anything |
285 | return self; |
286 | } |
287 | |
288 | - (unsigned)indexOfRightBraceMatchingLeftBraceAtIndex:(unsigned int)startLoc |
289 | { |
290 | return [self indexOfRightBraceMatchingLeftBraceInRange:NSMakeRange(startLoc, [self length] - startLoc)]; |
291 | } |
292 | |
293 | - (unsigned)indexOfRightBraceMatchingLeftBraceInRange:(NSRange)range |
294 | { |
295 | |
296 | CFStringInlineBuffer inlineBuffer; |
297 | CFIndex length = CFStringGetLength((CFStringRef)self); |
298 | CFIndex startLoc = range.location; |
299 | CFIndex endLoc = NSMaxRange(range); |
300 | CFIndex cnt; |
301 | BOOL matchFound = NO( BOOL ) 0; |
302 | |
303 | CFStringInitInlineBuffer((CFStringRef)self, &inlineBuffer, CFRangeMake(0, length)); |
304 | UniChar ch; |
305 | int nesting = 0; |
306 | |
307 | if(CFStringGetCharacterFromInlineBuffer(&inlineBuffer, startLoc) != '{') |
308 | [NSException raise:NSInternalInconsistencyException format:@"character at index %i is not a brace", startLoc]; |
309 | |
310 | // we don't consider escaped braces yet |
311 | for(cnt = startLoc; cnt < endLoc; cnt++){ |
312 | ch = CFStringGetCharacterFromInlineBuffer(&inlineBuffer, cnt); |
313 | if(ch == '\\') |
314 | cnt++; |
315 | else if(ch == '{') |
316 | nesting++; |
317 | else if(ch == '}') |
318 | nesting--; |
319 | if(nesting == 0){ |
320 | //NSLog(@"match found at index %i", cnt); |
321 | matchFound = YES( BOOL ) 1; |
322 | break; |
323 | } |
324 | } |
325 | |
326 | return (matchFound == YES( BOOL ) 1) ? cnt : NSNotFound; |
327 | } |
328 | |
329 | - (BOOL)isStringTeXQuotingBalancedWithBraces:(BOOL)braces connected:(BOOL)connected{ |
330 | return [self isStringTeXQuotingBalancedWithBraces:braces connected:connected range:NSMakeRange(0,[self length])]; |
331 | } |
332 | |
333 | - (BOOL)isStringTeXQuotingBalancedWithBraces:(BOOL)braces connected:(BOOL)connected range:(NSRange)range{ |
334 | int nesting = 0; |
335 | NSCharacterSet *delimCharSet; |
336 | unichar rightDelim; |
337 | |
338 | if (braces) { |
339 | delimCharSet = [NSCharacterSet curlyBraceCharacterSet]; |
340 | rightDelim = '}'; |
341 | } else { |
342 | delimCharSet = [NSCharacterSet characterSetWithCharactersInString:@"\""]; |
343 | rightDelim = '"'; |
344 | } |
345 | |
346 | NSRange delimRange = [self rangeOfCharacterFromSet:delimCharSet options:NSLiteralSearch range:range]; |
347 | int delimLoc = delimRange.location; |
348 | |
349 | while (delimLoc != NSNotFound) { |
350 | if (delimLoc == 0 || braces || [self characterAtIndex:delimLoc - 1] != '\\') { |
351 | // we found an unescaped delimiter |
352 | if (connected && nesting == 0) // connected quotes cannot have a nesting of 0 in the middle |
353 | return NO( BOOL ) 0; |
354 | if ([self characterAtIndex:delimLoc] == rightDelim) { |
355 | --nesting; |
356 | } else { |
357 | ++nesting; |
358 | } |
359 | if (nesting < 0) // we should never get a negative nesting |
360 | return NO( BOOL ) 0; |
361 | } |
362 | // set the range to the part of the range after the last found brace |
363 | range = NSMakeRange(delimLoc + 1, range.length - delimLoc + range.location - 1); |
364 | // search for the next brace |
365 | delimRange = [self rangeOfCharacterFromSet:delimCharSet options:NSLiteralSearch range:range]; |
366 | delimLoc = delimRange.location; |
367 | } |
368 | |
369 | return (nesting == 0); |
370 | } |
371 | |
372 | // transforms a bibtex string to have temp cite keys, using the method in openWithPhoneyKeys. |
373 | - (NSString *)stringWithPhoneyCiteKeys:(NSString *)tmpKey{ |
374 | // ^(@[[:alpha:]]+{),?$ will grab either "@type{,eol" or "@type{eol", which is what we get |
375 | // from Bookends and EndNote, respectively. |
376 | AGRegex *theRegex = [AGRegex regexWithPattern:@"^([ \\t]*@[[:alpha:]]+[ \\t]*{)[ \\t]*,?$" options:AGRegexCaseInsensitive]; |
377 | |
378 | // should assert that the noKeysString matches theRegex |
379 | //NSAssert([theRegex findInString:self] != nil, @"stringWithPhoneyCiteKeys called on non-matching string"); |
380 | |
381 | // replace with "@type{FixMe,eol" (add the comma in, since we remove it if present) |
382 | NSCharacterSet *newlineCharacterSet = [NSCharacterSet newlineCharacterSet]; |
383 | |
384 | // do not use NSCharacterSets with OFStringScanners! |
385 | OFCharacterSet *newlineOFCharset = [[[OFCharacterSet alloc] initWithCharacterSet:newlineCharacterSet] autorelease]; |
386 | |
387 | OFStringScanner *scanner = [[[OFStringScanner alloc] initWithString:self] autorelease]; |
388 | NSMutableString *mutableFileString = [NSMutableString stringWithCapacity:[self length]]; |
389 | NSString *tmp = nil( ( void * ) 0 ); |
390 | int scanLocation = 0; |
391 | NSString *replaceRegex = [NSString stringWithFormat:@"$1%@,", tmpKey]; |
392 | |
393 | // we scan up to an (newline@) sequence, then to a newline; we then replace only in that line using theRegex, which is much more efficient than using AGRegex to find/replace in the entire string |
394 | do { |
395 | // append the previous part to the mutable string |
396 | tmp = [scanner readFullTokenWithDelimiterCharacter:'@']; |
397 | if(tmp) [mutableFileString appendString:tmp]; |
398 | |
399 | scanLocation = scannerScanLocation(scanner); |
400 | if(scanLocation == 0 || [newlineCharacterSet characterIsMember:[self characterAtIndex:scanLocation - 1]]){ |
401 | |
402 | tmp = [scanner readFullTokenWithDelimiterOFCharacterSet:newlineOFCharset]; |
403 | |
404 | // if we read something between the @ and newline, see if we can do the regex find/replace |
405 | if(tmp){ |
406 | // this should be a noop if the pattern isn't matched |
407 | tmp = [theRegex replaceWithString:replaceRegex inString:tmp]; |
408 | [mutableFileString appendString:tmp]; // guaranteed non-nil result from AGRegex |
409 | } |
410 | } else |
411 | scannerReadCharacter(scanner); |
412 | |
413 | } while(scannerHasData(scanner)); |
414 | |
415 | NSString *toReturn = [NSString stringWithString:mutableFileString]; |
416 | |
417 | return toReturn; |
418 | } |
419 | |
420 | - (NSRange)rangeOfTeXCommandInRange:(NSRange)searchRange; |
421 | { |
422 | static CFCharacterSetRef nonLetterCharacterSet = NULL( ( void * ) 0 ); |
423 | |
424 | if (NULL( ( void * ) 0 ) == nonLetterCharacterSet) { |
425 | CFMutableCharacterSetRef letterCFCharacterSet = CFCharacterSetCreateMutableCopy(CFAllocatorGetDefault(), CFCharacterSetGetPredefined(kCFCharacterSetLetter)); |
426 | CFCharacterSetInvert(letterCFCharacterSet); |
427 | nonLetterCharacterSet = CFCharacterSetCreateCopy(CFAllocatorGetDefault(), letterCFCharacterSet); |
428 | CFRelease(letterCFCharacterSet); |
429 | } |
430 | |
431 | CFRange bsSearchRange = *(CFRange*)&searchRange; |
432 | CFRange cmdStartRange, cmdEndRange; |
433 | CFIndex endLoc = NSMaxRange(searchRange); |
434 | |
435 | while(bsSearchRange.length > 4 && BDStringFindCharacter((CFStringRef)self, '\\', bsSearchRange, &cmdStartRange) && |
436 | CFStringFindCharacterFromSet((CFStringRef)self, nonLetterCharacterSet, CFRangeMake(cmdStartRange.location + 1, endLoc - cmdStartRange.location - 1), 0, &cmdEndRange)){ |
437 | // if the char right behind the backslash is a non-letter char, it's a one-letter command |
438 | if(cmdEndRange.location == cmdStartRange.location + 1) |
439 | cmdEndRange.location++; |
440 | // see if we found a left brace, we ignore commands like \LaTeX{} which we want to keep |
441 | if('{' == CFStringGetCharacterAtIndex((CFStringRef)self, cmdEndRange.location) && |
442 | '}' != CFStringGetCharacterAtIndex((CFStringRef)self, cmdEndRange.location + 1)) |
443 | return NSMakeRange(cmdStartRange.location, cmdEndRange.location - cmdStartRange.location); |
444 | |
445 | bsSearchRange = CFRangeMake(cmdEndRange.location, endLoc - cmdEndRange.location); |
446 | } |
447 | |
448 | return NSMakeRange(NSNotFound, 0); |
449 | } |
450 | |
451 | - (NSString *)stringByBackslashEscapingTeXSpecials; |
452 | { |
453 | static NSCharacterSet *charSet = nil( ( void * ) 0 ); |
454 | // We could really go crazy with this, but the main need is to escape characters that commonly appear in titles and journal names when importing from z39.50 and other non-RIS/non-BibTeX search group sources. Those sources aren't processed by the HTML->TeX path that's used for RIS, since they generally don't have embedded HTML. |
455 | if (nil( ( void * ) 0 ) == charSet) |
456 | charSet = [[NSCharacterSet characterSetWithCharactersInString:@"%&"] copy]; |
457 | return [self stringByBackslashEscapingCharactersInSet:charSet]; |
458 | } |
459 | |
460 | - (NSString *)stringByBackslashEscapingCharactersInSet:(NSCharacterSet *)charSet; |
461 | { |
462 | NSRange r = [self rangeOfCharacterFromSet:charSet options:NSLiteralSearch]; |
463 | if (r.location == NSNotFound) |
464 | return self; |
465 | |
466 | NSMutableString *toReturn = [self mutableCopy]; |
467 | while (r.length) { |
468 | unsigned start; |
469 | if (r.location == 0 || [toReturn characterAtIndex:(r.location - 1)] != '\\') { |
470 | // insert the backslash, then advance the search range by two characters |
471 | [toReturn replaceCharactersInRange:NSMakeRange(r.location, 0) withString:@"\\"]; |
472 | start = r.location + 2; |
473 | r = [toReturn rangeOfCharacterFromSet:charSet options:NSLiteralSearch range:NSMakeRange(start, [toReturn length] - start)]; |
474 | } else { |
475 | // this one was already escaped, so advance a character and repeat the search, unless that puts us over the end |
476 | if (r.location < [toReturn length]) { |
477 | start = r.location + 1; |
478 | r = [toReturn rangeOfCharacterFromSet:charSet options:NSLiteralSearch range:NSMakeRange(start, [toReturn length] - start)]; |
479 | } else { |
480 | r = NSMakeRange(NSNotFound, 0); |
481 | } |
482 | } |
483 | } |
484 | return [toReturn autorelease]; |
485 | } |
486 | |
487 | - (NSString *)stringByConvertingHTMLToTeX; |
488 | { |
489 | static NSCharacterSet *asciiSet = nil( ( void * ) 0 ); |
490 | if(asciiSet == nil( ( void * ) 0 )) |
491 | asciiSet = [[NSCharacterSet characterSetWithRange:NSMakeRange(0, 127)] retain]; |
492 | |
493 | // set these up here, so we don't autorelease them every time we parse an entry |
494 | // Some entries from Compendex have spaces in the tags, which is why we match 0-1 spaces between each character. |
495 | static AGRegex *findSubscriptLeadingTag = nil( ( void * ) 0 ); |
496 | if(findSubscriptLeadingTag == nil( ( void * ) 0 )) |
497 | findSubscriptLeadingTag = [[AGRegex alloc] initWithPattern:@"< ?s ?u ?b ?>"]; |
498 | static AGRegex *findSubscriptOrSuperscriptTrailingTag = nil( ( void * ) 0 ); |
499 | if(findSubscriptOrSuperscriptTrailingTag == nil( ( void * ) 0 )) |
500 | findSubscriptOrSuperscriptTrailingTag = [[AGRegex alloc] initWithPattern:@"< ?/ ?s ?u ?[bp] ?>"]; |
501 | static AGRegex *findSuperscriptLeadingTag = nil( ( void * ) 0 ); |
502 | if(findSuperscriptLeadingTag == nil( ( void * ) 0 )) |
503 | findSuperscriptLeadingTag = [[AGRegex alloc] initWithPattern:@"< ?s ?u ?p ?>"]; |
504 | |
505 | // This one might require some explanation. An entry with TI of "Flapping flight as a bifurcation in Re<sub>ω</sub>" |
506 | // was run through the html conversion to give "...Re<sub>$\omega$</sub>", then the find sub/super regex replaced the sub tags to give |
507 | // "...Re$_$omega$$", which LaTeX barfed on. So, we now search for <sub></sub> tags with matching dollar signs inside, and remove the inner |
508 | // dollar signs, since we'll use the dollar signs from our subsequent regex search and replace; however, we have to |
509 | // reject the case where there is a <sub><\sub> by matching [^<]+ (at least one character which is not <), or else it goes to the next </sub> tag |
510 | // and deletes dollar signs that it shouldn't touch. Yuck. |
511 | static AGRegex *findNestedDollar = nil( ( void * ) 0 ); |
512 | if(findNestedDollar == nil( ( void * ) 0 )) |
513 | findNestedDollar = [[AGRegex alloc] initWithPattern:@"(< ?s ?u ?[bp] ?>[^<]+)(\\$)(.*)(\\$)(.*< ?/ ?s ?u ?[bp] ?>)"]; |
514 | |
515 | // Run the value string through the HTML2LaTeX conversion, to clean up θ and friends. |
516 | // NB: do this before the regex find/replace on <sub> and <sup> tags, or else your LaTeX math |
517 | // stuff will get munged. Unfortunately, the C code for HTML2LaTeX will destroy accented characters, so we only send it ASCII, and just keep |
518 | // the accented characters to let BDSKConverter deal with them later. |
519 | |
520 | NSScanner *scanner = [[NSScanner alloc] initWithString:self]; |
521 | NSString *asciiAndHTMLChars, *nonAsciiAndHTMLChars; |
522 | NSMutableString *fullString = [[NSMutableString alloc] initWithCapacity:[self length]]; |
523 | |
524 | while(![scanner isAtEnd]){ |
525 | if([scanner scanCharactersFromSet:asciiSet intoString:&asciiAndHTMLChars]) |
526 | [fullString appendString:[NSString TeXStringWithHTMLString:asciiAndHTMLChars ]]; |
527 | if([scanner scanUpToCharactersFromSet:asciiSet intoString:&nonAsciiAndHTMLChars]) |
528 | [fullString appendString:nonAsciiAndHTMLChars]; |
529 | } |
530 | [scanner release]; |
531 | |
532 | NSString *newValue = [[fullString copy] autorelease]; |
533 | [fullString release]; |
534 | |
535 | // see if we have nested math modes and try to fix them; see note earlier on findNestedDollar |
536 | if([findNestedDollar findInString:newValue] != nil( ( void * ) 0 )){ |
537 | NSLog(@"WARNING: found nested math mode; trying to repair..."); |
538 | newValue = [findNestedDollar replaceWithString:@"$1$3$5" |
539 | inString:newValue]; |
540 | } |
541 | |
542 | // Do a regex find and replace to put LaTeX subscripts and superscripts in place of the HTML |
543 | // that Compendex (and possibly others) give us. |
544 | newValue = [findSubscriptLeadingTag replaceWithString:@"\\$_{" inString:newValue]; |
545 | newValue = [findSuperscriptLeadingTag replaceWithString:@"\\$^{" inString:newValue]; |
546 | newValue = [findSubscriptOrSuperscriptTrailingTag replaceWithString:@"}\\$" inString:newValue]; |
547 | |
548 | return newValue; |
549 | } |
550 | |
551 | + (NSString *)TeXStringWithHTMLString:(NSString *)htmlString; |
552 | { |
553 | const char *str = [htmlString UTF8String]; |
554 | int ln = strlen(str); |
555 | FILE *freport = stdout__stdoutp; |
556 | char *html_fn = NULL( ( void * ) 0 ); |
557 | BOOL in_math = NO( BOOL ) 0; |
558 | BOOL in_verb = NO( BOOL ) 0; |
559 | BOOL in_alltt = NO( BOOL ) 0; |
560 | |
561 | /* ARM: this code was taken directly from HTML2LaTeX. I modified it to return |
562 | an NSString object, since working with FILE* streams led to really nasty problems |
563 | with NSPipe needing asynchronous reads to avoid blocking. |
564 | The NSMutableString appendFormat method was used to replace all of the calls to |
565 | fputc, fprintf, and fputs. |
566 | |
567 | Frans Faase, the author of HTML2LaTeX, gave permission to include this in BibDesk under the BSD license. |
568 | The following copyright notice was taken verbatim from the HTML2LaTeX code: |
569 | |
570 | HTML2LaTeX -- Converting HTML files to LaTeX |
571 | Copyright (C) 1995-2003 Frans Faase |
572 | |
573 | This program is free software; you can redistribute it and/or modify |
574 | it under the terms of the GNU General Public License as published by |
575 | the Free Software Foundation; either version 2 of the License, or |
576 | (at your option) any later version. |
577 | |
578 | This program is distributed in the hope that it will be useful, |
579 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
580 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
581 | GNU General Public License for more details. |
582 | |
583 | You should have received a copy of the GNU General Public License |
584 | along with this program; if not, write to the Free Software |
585 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. |
586 | |
587 | GNU General Public License: |
588 | http://home.planet.nl/~faase009/GNU.txt |
589 | */ |
590 | |
591 | |
592 | NSMutableString *mString = [NSMutableString stringWithCapacity:ln]; |
593 | |
594 | BOOL option_warn = YES( BOOL ) 1; |
595 | |
596 | for(; *str; str++) |
597 | { BOOL special = NO( BOOL ) 0; |
598 | int v = 0; |
599 | char ch = '\0'; |
600 | char html_ch[10]; |
601 | html_ch[0] = '\0'; |
602 | |
603 | if (*str == '&') |
604 | { int i = 0; |
605 | BOOL correct = NO( BOOL ) 0; |
606 | |
607 | if (isalpha(str[1])) |
608 | { for (i = 0; i < 9; i++) |
609 | if (isalpha(str[i+1])) |
610 | html_ch[i] = str[i+1]; |
611 | else |
612 | break; |
613 | html_ch[i] = '\0'; |
614 | for (v = 0; v < NR_CH_TABLE170; v++) |
615 | if ( ch_table[v].html_ch != NULL( ( void * ) 0 ) |
616 | && !strcmp(html_ch, ch_table[v].html_ch)) |
617 | { special = YES( BOOL ) 1; |
618 | correct = YES( BOOL ) 1; |
619 | ch = ch_table[v].ch; |
620 | break; |
621 | } |
622 | } |
623 | else if (str[1] == '#') |
624 | { int code = 0; |
625 | html_ch[0] = '#'; |
626 | for (i = 1; i < 9; i++) |
627 | if (isdigit(str[i+1])) |
628 | { html_ch[i] = str[i+1]; |
629 | code = code * 10 + str[i+1] - '0'; |
630 | } |
631 | else |
632 | break; |
633 | if ((code >= ' ' && code < 127) || code == 8) |
634 | { correct = YES( BOOL ) 1; |
635 | ch = code; |
636 | } |
637 | else if (code >= 160 && code <= 255) |
638 | { |
639 | correct = YES( BOOL ) 1; |
640 | special = YES( BOOL ) 1; |
641 | v = code - 160; |
642 | ch = ch_table[v].ch; |
643 | } |
644 | } |
645 | html_ch[i] = '\0'; |
646 | |
647 | if (correct) |
648 | { str += i; |
649 | if (str[1] == ';') |
650 | str++; |
651 | } |
652 | else |
653 | { if (freport != NULL( ( void * ) 0 ) && option_warn) |
654 | if (html_ch[0] == '\0') |
655 | fprintf(freport, |
656 | "%s (%d) : Replace `&' by `&'.\n", |
657 | html_fn, ln); |
658 | else |
659 | fprintf(freport, |
660 | "%s (%d) : Unknown sequence `&%s;'.\n", |
661 | html_fn, ln, html_ch); |
662 | ch = *str; |
663 | } |
664 | } |
665 | else if (((unsigned char)*str >= ' ' && (unsigned char)*str <= HIGHASCII126) || *str == '\t') |
666 | ch = *str; |
667 | else if (option_warn && freport != NULL( ( void * ) 0 )) |
668 | fprintf(freport, |
669 | "%s (%d) : Unknown character %d (decimal)\n", |
670 | html_fn, ln, (unsigned char)*str); |
671 | if (mString) |
672 | { if (in_verb) |
673 | { |
674 | [mString appendFormat:@"%c", ch != '\0' ? ch : ' ']; |
675 | if ( special && freport != NULL( ( void * ) 0 ) && option_warn |
676 | && v < NR_CH_M159) |
677 | { fprintf(freport, "%s (%d) : ", html_fn, ln); |
678 | if (html_ch[0] == '\0') |
679 | fprintf(freport, "character %d (decimal)", |
680 | (unsigned char) *str); |
681 | else |
682 | fprintf(freport, "sequence `&%s;'", html_ch); |
683 | fprintf(freport, " rendered as `%c' in verbatim\n", |
684 | ch != '\0' ? ch : ' '); |
685 | } |
686 | } |
687 | else if (in_alltt) |
688 | { if (special) |
689 | { char *o = ch_table[v].tex_ch; |
690 | if (o != NULL( ( void * ) 0 )) |
691 | if (*o == '$') |
692 | [mString appendFormat:@"\\(%s\\)", o + 1]; |
693 | else |
694 | [mString appendFormat:@"%s", o]; |
695 | } |
696 | else if (ch == '{' || ch == '}') |
697 | [mString appendFormat:@"\\%c", ch]; |
698 | else if (ch == '\\') |
699 | [mString appendFormat:@"\\%c", ch]; |
700 | else if (ch != '\0') |
701 | [mString appendFormat:@"%c", ch]; |
702 | } |
703 | else if (special) |
704 | { char *o = ch_table[v].tex_ch; |
705 | if (o == NULL( ( void * ) 0 )) |
706 | { if (freport != NULL( ( void * ) 0 ) && option_warn) |
707 | { fprintf(freport, |
708 | "%s (%d) : no LaTeX representation for ", |
709 | html_fn, ln); |
710 | if (html_ch[0] == '\0') |
711 | fprintf(freport, "character %d (decimal)\n", |
712 | (unsigned char) *str); |
713 | else |
714 | fprintf(freport, "sequence `&%s;'\n", html_ch); |
715 | } |
716 | } |
717 | else if (*o == '$') |
718 | if (in_math) |
719 | [mString appendFormat:@"%s", o+1]; |
720 | else |
721 | [mString appendFormat:@"{%s$}", o]; |
722 | else |
723 | [mString appendFormat:@"%s", o]; |
724 | } |
725 | else if (in_math) |
726 | { if (ch == '#' || ch == '%') |
727 | [mString appendFormat:@"\\%c", ch]; |
728 | else |
729 | [mString appendFormat:@"%c", ch]; |
730 | } |
731 | else |
732 | { switch(ch) |
733 | { case '\0' : break; |
734 | case '\t': [mString appendString:@" "]; break; |
735 | case '_': case '{': case '}': |
736 | case '#': case '$': case '%': |
737 | [mString appendFormat:@"{\\%c}", ch]; break; |
738 | case '@' : [mString appendFormat:@"{\\char64}"]; break; |
739 | case '[' : |
740 | case ']' : [mString appendFormat:@"{$%c$}", ch]; break; |
741 | case '~' : [mString appendString:@"\\~{}"]; break; |
742 | case '^' : [mString appendString:@"\\^{}"]; break; |
743 | case '|' : [mString appendString:@"{$|$}"]; break; |
744 | case '\\': [mString appendString:@"{$\\backslash$}"]; break; |
745 | case '&' : [mString appendString:@"\\&"]; break; |
746 | default: [mString appendFormat:@"%c", ch]; break; |
747 | } |
748 | } |
749 | } |
750 | } |
751 | return mString; |
752 | } |
753 | |
754 | - (NSArray *)sourceLinesBySplittingString; |
755 | { |
756 | // ARM: This code came from Art Isbell to cocoa-dev on Tue Jul 10 22:13:11 2001. Comments are his. |
757 | // We were using componentsSeparatedByString:@"\r", but this is not robust. Files from ScienceDirect |
758 | // have \n as newlines, so this code handles those cases as well as PubMed. |
759 | unsigned stringLength = [self length]; |
760 | unsigned startIndex; |
761 | unsigned lineEndIndex = 0; |
762 | unsigned contentsEndIndex; |
763 | NSRange range; |
764 | NSMutableArray *sourceLines = [NSMutableArray array]; |
765 | |
766 | // There is more than one way to terminate this loop. Beware of an |
767 | // invalid termination test which might exist in this untested example :-) |
768 | while (lineEndIndex < stringLength) |
769 | { |
770 | // Include only a single character in range. Not sure whether |
771 | // this will work with empty lines, but if not, try a length of 0. |
772 | range = NSMakeRange(lineEndIndex, 1); |
773 | [self getLineStart:&startIndex |
774 | end:&lineEndIndex |
775 | contentsEnd:&contentsEndIndex |
776 | forRange:range]; |
777 | |
778 | // If you want to exclude line terminators... |
779 | [sourceLines addObject:[self substringWithRange:NSMakeRange(startIndex, contentsEndIndex - startIndex)]]; |
780 | } |
781 | return sourceLines; |
782 | } |
783 | |
784 | - (NSString *)stringByEscapingGroupPlistEntities{ |
785 | NSMutableString *escapedValue = [self mutableCopy]; |
786 | // escape braces as they can give problems with btparse |
787 | [escapedValue replaceAllOccurrencesOfString:@"%" withString:@"%25"]; // this should come first |
788 | [escapedValue replaceAllOccurrencesOfString:@"{" withString:@"%7B"]; |
789 | [escapedValue replaceAllOccurrencesOfString:@"}" withString:@"%7D"]; |
790 | [escapedValue replaceAllOccurrencesOfString:@"<" withString:@"%3C"]; |
791 | [escapedValue replaceAllOccurrencesOfString:@">" withString:@"%3E"]; |
792 | return [escapedValue autorelease]; |
793 | } |
794 | |
795 | - (NSString *)stringByUnescapingGroupPlistEntities{ |
796 | NSMutableString *escapedValue = [self mutableCopy]; |
797 | // escape braces as they can give problems with btparse, and angles as they can give problems with the plist xml |
798 | [escapedValue replaceAllOccurrencesOfString:@"%7B" withString:@"{"]; |
799 | [escapedValue replaceAllOccurrencesOfString:@"%7D" withString:@"}"]; |
800 | [escapedValue replaceAllOccurrencesOfString:@"%3C" withString:@"<"]; |
801 | [escapedValue replaceAllOccurrencesOfString:@"%3E" withString:@">"]; |
802 | [escapedValue replaceAllOccurrencesOfString:@"%25" withString:@"%"]; // this should come last |
803 | return [escapedValue autorelease]; |
804 | } |
805 | |
806 | - (NSString *)lossyASCIIString{ |
807 | return [[[NSString alloc] initWithData:[self dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES( BOOL ) 1] encoding:NSASCIIStringEncoding] autorelease]; |
808 | } |
809 | |
810 | #pragma mark Comparisons |
811 | |
812 | - (NSComparisonResult)localizedCaseInsensitiveNumericCompare:(NSString *)aStr{ |
813 | return [self compare:aStr |
814 | options:NSCaseInsensitiveSearch | NSNumericSearch |
815 | range:NSMakeRange(0, [self length]) |
816 | locale:[[NSUserDefaults standardUserDefaults] dictionaryRepresentation]]; |
817 | } |
818 | |
819 | // -[NSString compare: options:NSNumericSearch] is buggy for string literals (tested on 10.4.3), but CFStringCompare() works and returns the same comparison constants |
820 | - (NSComparisonResult)numericCompare:(NSString *)otherString{ |
821 | return CFStringCompare((CFStringRef)self, (CFStringRef)otherString, kCFCompareNumerically); |
822 | } |
823 | |
824 | - (NSString *)stringByRemovingTeXAndStopWords; |
825 | { |
826 | CFMutableStringRef modifiedSelf = CFStringCreateMutableCopy(CFAllocatorGetDefault(), CFStringGetLength((CFStringRef)self), (CFStringRef)self); |
827 | BDDeleteArticlesForSorting(modifiedSelf); |
828 | BDDeleteTeXForSorting(modifiedSelf); |
829 | return [(id)modifiedSelf autorelease]; |
830 | } |
831 | |
832 | - (NSComparisonResult)localizedCaseInsensitiveNonTeXNonArticleCompare:(NSString *)otherString; |
833 | { |
834 | |
835 | // Check before passing to CFStringCompare, as a nil argument causes a crash. The caller has to handle nil comparisons. |
836 | NSParameterAssertdo { if ( ! ( ( otherString != ( ( void * ) 0 ) ) ) ) { [ [ NSAssertionHandler currentHandler ] handleFailureInMethod : _cmd object : self file : [ NSString stringWithUTF8String : "/Volumes/Local/Users/amaxwell/build/bibdesk-clean/NSString_BDSKExtensions.m" ] lineNumber : 836 description : ( @ "Invalid parameter not satisfying: %s" ) , ( "otherString != nil" ) , ( 0 ) , ( 0 ) , ( 0 ) , ( 0 ) ] ; } } while ( 0 )(otherString != nil); |
837 | |
838 | CFAllocatorRef allocator = CFAllocatorGetDefault(); |
839 | CFMutableStringRef modifiedSelf = CFStringCreateMutableCopy(allocator, CFStringGetLength((CFStringRef)self), (CFStringRef)self); |
840 | CFMutableStringRef modifiedOther = CFStringCreateMutableCopy(allocator, CFStringGetLength((CFStringRef)otherString), (CFStringRef)otherString); |
841 | |
842 | BDDeleteArticlesForSorting(modifiedSelf); |
843 | BDDeleteArticlesForSorting(modifiedOther); |
844 | BDDeleteTeXForSorting(modifiedSelf); |
845 | BDDeleteTeXForSorting(modifiedOther); |
846 | |
847 | // the mutating functions above should only create an empty string, not a nil string |
848 | OBASSERTdo { if ( ! ( modifiedSelf != ( ( void * ) 0 ) ) ) OBAssertFailed ( "ASSERT" , "modifiedSelf != nil" , "/Volumes/Local/Users/amaxwell/build/bibdesk-clean/NSString_BDSKExtensions.m" , 848 ) ; } while ( ( BOOL ) 0 )(modifiedSelf != nil); |
849 | OBASSERTdo { if ( ! ( modifiedOther != ( ( void * ) 0 ) ) ) OBAssertFailed ( "ASSERT" , "modifiedOther != nil" , "/Volumes/Local/Users/amaxwell/build/bibdesk-clean/NSString_BDSKExtensions.m" , 849 ) ; } while ( ( BOOL ) 0 )(modifiedOther != nil); |
850 | |
851 | // CFComparisonResult returns same values as NSComparisonResult |
852 | CFComparisonResult result = CFStringCompare(modifiedSelf, modifiedOther, kCFCompareCaseInsensitive | kCFCompareLocalized); |
853 | CFRelease(modifiedSelf); |
854 | CFRelease(modifiedOther); |
855 | |
856 | return result; |
857 | } |
858 | |
859 | - (NSComparisonResult)sortCompare:(NSString *)other{ |
860 | BOOL otherIsEmpty = [NSString isEmptyString:other]; |
861 | if ([self isEqualToString:@""]) { |
862 | return (otherIsEmpty)? NSOrderedSame : NSOrderedDescending; |
863 | } else if (otherIsEmpty) { |
864 | return NSOrderedAscending; |
865 | } |
866 | return [self localizedCaseInsensitiveNumericCompare:other]; |
867 | } |
868 | |
869 | - (NSComparisonResult)extensionCompare:(NSString *)other{ |
870 | NSString *myExtension = [self pathExtension]; |
871 | NSString *otherExtension = [other pathExtension]; |
872 | BOOL otherIsEmpty = [NSString isEmptyString:otherExtension]; |
873 | if ([myExtension isEqualToString:@""]) |
874 | return otherIsEmpty ? NSOrderedSame : NSOrderedDescending; |
875 | if (otherIsEmpty) |
876 | return NSOrderedAscending; |
877 | return [myExtension localizedCaseInsensitiveCompare:otherExtension]; |
878 | } |
879 | |
880 | - (NSComparisonResult)triStateCompare:(NSString *)other{ |
881 | // we order increasingly as 0, -1, 1 |
882 | int myValue = [self triStateValue]; |
883 | int otherValue = [other triStateValue]; |
884 | if (myValue == otherValue) |
885 | return NSOrderedSame; |
886 | else if (myValue == 0 || otherValue == 1) |
887 | return NSOrderedAscending; |
888 | else |
889 | return NSOrderedDescending; |
890 | } |
891 | |
892 | static NSURL *CreateFileURLFromPathOrURLString(NSString *aPath, NSString *basePath) |
893 | { |
894 | // default return values |
895 | NSURL *fileURL = nil( ( void * ) 0 ); |
896 | |
897 | if ([aPath hasPrefix:@"file://"]) { |
898 | fileURL = [[NSURL alloc] initWithString:aPath]; |
899 | } else if ([aPath length]) { |
900 | unichar ch = [aPath characterAtIndex:0]; |
901 | if ('/' != ch && '~' != ch) |
902 | aPath = [basePath stringByAppendingPathComponent:aPath]; |
903 | if (aPath) |
904 | fileURL = [[NSURL alloc] initFileURLWithPath:[aPath stringByStandardizingPath]]; |
905 | } |
906 | return fileURL; |
907 | } |
908 | |
909 | static NSString *UTIForPathOrURLString(NSString *aPath, NSString *basePath) |
910 | { |
911 | NSString *theUTI = nil( ( void * ) 0 ); |
912 | NSURL *fileURL = nil( ( void * ) 0 ); |
913 | // !!! We return nil when a file doesn't exist if it's a properly resolvable path/URL, but we have no way of checking existence with a relative path. Returning nil is preferable, since then nonexistent files will be sorted to the top or bottom and they're easy to find. |
914 | if (fileURL = CreateFileURLFromPathOrURLString(aPath, basePath)) { |
915 | // UTI will be nil for a file that doesn't exist, yet had an absolute/resolvable path |
916 | if (fileURL) { |
917 | theUTI = [[NSWorkspace sharedWorkspace] UTIForURL:fileURL error:NULL( ( void * ) 0 )]; |
918 | [fileURL release]; |
919 | } |
920 | |
921 | } else { |
922 | |
923 | // fall back to extension; this is probably a relative path, so we'll assume it exists |
924 | NSString *extension = [aPath pathExtension]; |
925 | if ([extension isEqualToString:@""] == NO( BOOL ) 0) |
926 | theUTI = [[NSWorkspace sharedWorkspace] UTIForPathExtension:extension]; |
927 | } |
928 | return theUTI; |
929 | } |
930 | |
931 | - (NSComparisonResult)UTICompare:(NSString *)other{ |
932 | return [self UTICompare:other basePath:nil( ( void * ) 0 )]; |
933 | } |
934 | |
935 | - (NSComparisonResult)UTICompare:(NSString *)other basePath:(NSString *)basePath{ |
936 | NSString *otherUTI = UTIForPathOrURLString(other, basePath); |
937 | NSString *selfUTI = UTIForPathOrURLString(self, basePath); |
938 | if (nil( ( void * ) 0 ) == selfUTI) |
939 | return (nil( ( void * ) 0 ) == otherUTI ? NSOrderedSame : NSOrderedDescending); |
940 | if (nil( ( void * ) 0 ) == otherUTI) |
941 | return NSOrderedAscending; |
942 | return [selfUTI caseInsensitiveCompare:otherUTI]; |
943 | } |
944 | |
945 | #pragma mark - |
946 | |
947 | - (BOOL)booleanValue{ |
948 | // Omni's boolValue method uses YES, Y, yes, y and 1 with isEqualToString |
949 | if([self compare:[NSString stringWithBool:YES( BOOL ) 1] options:NSCaseInsensitiveSearch] == NSOrderedSame || |
950 | [self compare:@"y" options:NSCaseInsensitiveSearch] == NSOrderedSame || |
951 | [self compare:@"yes" options:NSCaseInsensitiveSearch] == NSOrderedSame || |
952 | [self isEqualToString:@"1"]) |
953 | return YES( BOOL ) 1; |
954 | else |
955 | return NO( BOOL ) 0; |
956 | } |
957 | |
958 | - (NSCellStateValue)triStateValue{ |
959 | if([self booleanValue] == YES( BOOL ) 1){ |
960 | return NSOnState; |
961 | }else if([self isEqualToString:@""] || |
962 | [self compare:[NSString stringWithBool:NO( BOOL ) 0] options:NSCaseInsensitiveSearch] == NSOrderedSame || |
963 | [self compare:@"n" options:NSCaseInsensitiveSearch] == NSOrderedSame || |
964 | [self compare:@"no" options:NSCaseInsensitiveSearch] == NSOrderedSame || |
965 | [self isEqualToString:@"0"]){ |
966 | return NSOffState; |
967 | }else{ |
968 | return NSMixedState; |
969 | } |
970 | } |
971 | |
972 | - (NSString *)acronymValueIgnoringWordLength:(unsigned int)ignoreLength{ |
973 | NSMutableString *result = [NSMutableString string]; |
974 | NSArray *allComponents = [self componentsSeparatedByString:@" "]; // single whitespace |
975 | NSEnumerator *e = [allComponents objectEnumerator]; |
976 | NSString *component = nil( ( void * ) 0 ); |
977 | unsigned int currentIgnoreLength; |
978 | |
979 | while(component = [e nextObject]){ |
980 | currentIgnoreLength = ignoreLength; |
981 | if(![component isEqualToString:@""]) // stringByTrimmingCharactersInSet will choke on an empty string |
982 | component = [component stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; |
983 | if([component length] > 1 && [component characterAtIndex:[component length] - 1] == '.') |
984 | currentIgnoreLength = 0; |
985 | if(![component isEqualToString:@""]) |
986 | component = [component stringByTrimmingCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]]; |
987 | if([component length] > currentIgnoreLength){ |
988 | [result appendString:[[component substringToIndex:1] uppercaseString]]; |
989 | } |
990 | } |
991 | return result; |
992 | } |
993 | |
994 | #pragma mark - |
995 | |
996 | - (BOOL)containsString:(NSString *)searchString options:(unsigned int)mask range:(NSRange)aRange{ |
997 | return !searchString || [searchString length] == 0 || [self rangeOfString:searchString options:mask range:aRange].length > 0; |
998 | } |
999 | |
1000 | - (BOOL)containsWord:(NSString *)aWord{ |
1001 | |
1002 | NSRange subRange = [self rangeOfString:aWord]; |
1003 | |
1004 | if(subRange.location == NSNotFound) |
1005 | return NO( BOOL ) 0; |
1006 | |
1007 | CFIndex wordLength = [aWord length]; |
1008 | CFIndex myLength = [self length]; |
1009 | |
1010 | // trivial case; we contain the word, and have the same length |
1011 | if(myLength == wordLength) |
1012 | return YES( BOOL ) 1; |
1013 | |
1014 | CFIndex beforeIndex, afterIndex; |
1015 | |
1016 | beforeIndex = subRange.location - 1; |
1017 | afterIndex = NSMaxRange(subRange); |
1018 | |
1019 | UniChar beforeChar = '\0', afterChar = '\0'; |
1020 | |
1021 | if(beforeIndex >= 0) |
1022 | beforeChar = [self characterAtIndex:beforeIndex]; |
1023 | |
1024 | if(afterIndex < myLength) |
1025 | afterChar = [self characterAtIndex:afterIndex]; |
1026 | |
1027 | static NSCharacterSet *wordTestSet = nil( ( void * ) 0 ); |
1028 | if(wordTestSet == nil( ( void * ) 0 )){ |
1029 | NSMutableCharacterSet *set = [[NSCharacterSet punctuationCharacterSet] mutableCopy]; |
1030 | [set formUnionWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; |
1031 | wordTestSet = [set copy]; |
1032 | [set release]; |
1033 | } |
1034 | |
1035 | // if a character appears before the start of the substring match, see if it is punctuation or whitespace |
1036 | if(beforeChar && [wordTestSet characterIsMember:beforeChar] == NO( BOOL ) 0) |
1037 | return NO( BOOL ) 0; |
1038 | |
1039 | // now check after the substring match |
1040 | if(afterChar && [wordTestSet characterIsMember:afterChar] == NO( BOOL ) 0) |
1041 | return NO( BOOL ) 0; |
1042 | |
1043 | return YES( BOOL ) 1; |
1044 | } |
1045 | |
1046 | - (BOOL)hasCaseInsensitivePrefix:(NSString *)prefix; |
1047 | { |
1048 | unsigned int length = [prefix length]; |
1049 | if(prefix == nil( ( void * ) 0 ) || length > [self length]) |
1050 | return NO( BOOL ) 0; |
1051 | |
1052 | return (CFStringCompareWithOptions((CFStringRef)self,(CFStringRef)prefix, CFRangeMake(0, length), kCFCompareCaseInsensitive) == kCFCompareEqualTo ? YES( BOOL ) 1 : NO( BOOL ) 0); |
1053 | } |
1054 | |
1055 | #pragma mark - |
1056 | |
1057 | - (NSArray *)componentsSeparatedByCharactersInSet:(NSCharacterSet *)charSet trimWhitespace:(BOOL)trim; |
1058 | { |
1059 | return [(id)BDStringCreateComponentsSeparatedByCharacterSetTrimWhitespace(CFAllocatorGetDefault(), (CFStringRef)self, (CFCharacterSetRef)charSet, trim) autorelease]; |
1060 | } |
1061 | |
1062 | - (NSArray *)componentsSeparatedByStringCaseInsensitive:(NSString *)separator; |
1063 | { |
1064 | return [(id)BDStringCreateArrayBySeparatingStringsWithOptions(CFAllocatorGetDefault(), (CFStringRef)self, (CFStringRef)separator, kCFCompareCaseInsensitive) autorelease]; |
1065 | } |
1066 | |
1067 | - (NSArray *)componentsSeparatedByFieldSeparators; |
1068 | { |
1069 | NSCharacterSet *acSet = [[BDSKTypeManager sharedManager] separatorCharacterSetForField:BDSKKeywordsString]; |
1070 | if([self containsCharacterInSet:acSet]) |
1071 | return [self componentsSeparatedByCharactersInSet:acSet trimWhitespace:YES( BOOL ) 1]; |
1072 | else |
1073 | return [self componentsSeparatedByStringCaseInsensitive:@" and "]; |
1074 | } |
1075 | |
1076 | - (NSArray *)componentsSeparatedByAnd; |
1077 | { |
1078 | return [self componentsSeparatedByStringCaseInsensitive:@" and "]; |
1079 | } |
1080 | |
1081 | - (NSArray *)componentsSeparatedByComma; |
1082 | { |
1083 | return [self componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@","] trimWhitespace:YES( BOOL ) 1]; |
1084 | } |
1085 | |
1086 | - (NSString *)fastStringByCollapsingWhitespaceAndRemovingSurroundingWhitespace; |
1087 | { |
1088 | return [(id)BDStringCreateByCollapsingAndTrimmingWhitespace(CFAllocatorGetDefault(), (CFStringRef)self) autorelease]; |
1089 | } |
1090 | |
1091 | - (NSString *)fastStringByCollapsingWhitespaceAndNewlinesAndRemovingSurroundingWhitespaceAndNewlines; |
1092 | { |
1093 | return [(id)BDStringCreateByCollapsingAndTrimmingWhitespaceAndNewlines(CFAllocatorGetDefault(), (CFStringRef)self) autorelease]; |
1094 | } |
1095 | |
1096 | - (NSString *)stringByNormalizingSpacesAndLineBreaks; |
1097 | { |
1098 | return [(id)BDStringCreateByNormalizingWhitespaceAndNewlines(CFAllocatorGetDefault(), (CFStringRef)self) autorelease]; |
1099 | } |
1100 | |
1101 | - (NSString *)stringByAppendingEllipsis{ |
1102 | return [self stringByAppendingString:[NSString horizontalEllipsisString]]; |
1103 | } |
1104 | |
1105 | - (NSString *)stringBySurroundingWithSpacesIfNotEmpty |
1106 | { |
1107 | return [self isEqualToString:@""] ? self : [NSString stringWithFormat:@" %@ ", self]; |
1108 | } |
1109 | |
1110 | - (NSString *)stringByAppendingSpaceIfNotEmpty |
1111 | { |
1112 | return [self isEqualToString:@""] ? self : [self stringByAppendingString:@" "]; |
1113 | } |
1114 | |
1115 | - (NSString *)stringByAppendingDoubleSpaceIfNotEmpty |
1116 | { |
1117 | return [self isEqualToString:@""] ? self : [self stringByAppendingString:@" "]; |
1118 | } |
1119 | |
1120 | - (NSString *)stringByPrependingSpaceIfNotEmpty |
1121 | { |
1122 | return [self isEqualToString:@""] ? self : [NSString stringWithFormat:@" %@", self]; |
1123 | } |
1124 | |
1125 | - (NSString *)stringByAppendingCommaIfNotEmpty |
1126 | { |
1127 | return [self isEqualToString:@""] ? self : [self stringByAppendingString:@","]; |
1128 | } |
1129 | |
1130 | - (NSString *)stringByAppendingFullStopIfNotEmpty |
1131 | { |
1132 | return [self isEqualToString:@""] ? self : [self stringByAppendingString:@"."]; |
1133 | } |
1134 | |
1135 | - (NSString *)stringByAppendingCommaAndSpaceIfNotEmpty |
1136 | { |
1137 | return [self isEqualToString:@""] ? self : [self stringByAppendingString:@", "]; |
1138 | } |
1139 | |
1140 | - (NSString *)stringByAppendingFullStopAndSpaceIfNotEmpty |
1141 | { |
1142 | return [self isEqualToString:@""] ? self : [self stringByAppendingString:@". "]; |
1143 | } |
1144 | |
1145 | - (NSString *)stringByPrependingCommaAndSpaceIfNotEmpty |
1146 | { |
1147 | return [self isEqualToString:@""] ? self : [NSString stringWithFormat:@", %@", self]; |
1148 | } |
1149 | |
1150 | - (NSString *)stringByPrependingFullStopAndSpaceIfNotEmpty |
1151 | { |
1152 | return [self isEqualToString:@""] ? self : [NSString stringWithFormat:@". %@", self]; |
1153 | } |
1154 | |
1155 | - (NSString *)quotedStringIfNotEmpty |
1156 | { |
1157 | return [self isEqualToString:@""] ? self : [NSString stringWithFormat:@"\"%@\"", self]; |
1158 | } |
1159 | |
1160 | - (NSString *)parenthesizedStringIfNotEmpty |
1161 | { |
1162 | return [self isEqualToString:@""] ? self : [NSString stringWithFormat:@"(%@)", self]; |
1163 | } |
1164 | |
1165 | - (NSString *)titlecaseString; |
1166 | { |
1167 | CFAllocatorRef alloc = CFGetAllocator((CFStringRef)self); |
[1] Function call returns an object with a +1 retain count (owning reference). | |
1168 | CFMutableStringRef mutableString = CFStringCreateMutableCopy(alloc, 0, (CFStringRef)self); |
1169 | CFLocaleRef locale = CFLocaleCopyCurrent(); |
1170 | CFStringCapitalize(mutableString, locale); |
1171 | CFRelease(locale); |
1172 | |
[2] Object allocated on line 1168 and stored into 'mutableString' is no longer referenced after this point and has a retain count of +1 (object leaked). | |
1173 | CFArrayRef comp = BDStringCreateComponentsSeparatedByCharacterSetTrimWhitespace(alloc, mutableString, CFCharacterSetGetPredefined(kCFCharacterSetWhitespace), TRUE1); |
1174 | NSMutableArray *words = nil( ( void * ) 0 ); |
1175 | |
1176 | if (comp) { |
1177 | words = (NSMutableArray *)CFArrayCreateMutableCopy(alloc, CFArrayGetCount(comp), comp); |
1178 | CFRelease(comp); |
1179 | } |
1180 | |
1181 | const NSString *uppercaseWords[] = { |
1182 | @"A", |
1183 | @"An", |
1184 | @"The", |
1185 | @"Of", |
1186 | @"And", |
1187 | }; |
1188 | |
1189 | const NSString *lowercaseWords[] = { |
1190 | @"a", |
1191 | @"an", |
1192 | @"the", |
1193 | @"of", |
1194 | @"and", |
1195 | }; |
1196 | |
1197 | unsigned i, j, iMax = sizeof(uppercaseWords) / sizeof(NSString *); |
1198 | |
1199 | for (i = 0; i < iMax; i++) { |
1200 | |
1201 | const NSString *ucWord = uppercaseWords[i]; |
1202 | const NSString *lcWord = lowercaseWords[i]; |
1203 | |
1204 | // omit the first word, since it should always be capitalized |
1205 | while (NSNotFound != (j = [words indexOfObject:ucWord]) && j > 0) |
1206 | [words replaceObjectAtIndex:j withObject:lcWord]; |
1207 | } |
1208 | |
1209 | NSString *toReturn = nil( ( void * ) 0 ); |
1210 | if (words) { |
1211 | toReturn = (NSString *)CFStringCreateByCombiningStrings(alloc, (CFArrayRef)words, CFSTR( ( CFStringRef ) __builtin___CFStringMakeConstantString ( "" " " "" ) )(" ")); |
1212 | [words release]; |
1213 | } |
1214 | return [toReturn autorelease]; |
1215 | } |
1216 | |
1217 | - (NSString *)stringByTrimmingFromLastPunctuation{ |
1218 | NSRange range = [self rangeOfCharacterFromSet:[NSCharacterSet punctuationCharacterSet] options:NSBackwardsSearch]; |
1219 | |
1220 | if(range.location != NSNotFound && (range.location += 1) < [self length]) |
1221 | return [self substringWithRange:NSMakeRange(range.location, [self length] - range.location)]; |
1222 | else |
1223 | return self; |
1224 | } |
1225 | |
1226 | - (NSString *)stringByTrimmingPrefixCharactersFromSet:(NSCharacterSet *)characterSet; |
1227 | { |
1228 | NSString *string = nil( ( void * ) 0 ); |
1229 | NSScanner *scanner = [[NSScanner alloc] initWithString:self]; |
1230 | [scanner setCharactersToBeSkipped:nil( ( void * ) 0 )]; |
1231 | [scanner scanCharactersFromSet:characterSet intoString:nil( ( void * ) 0 )]; |
1232 | NSRange range = NSMakeRange(0, [scanner scanLocation]); |
1233 | [scanner release]; |
1234 | |
1235 | if(range.length){ |
1236 | NSMutableString *mutableCopy = [self mutableCopy]; |
1237 | [mutableCopy deleteCharactersInRange:range]; |
1238 | string = [mutableCopy autorelease]; |
1239 | } |
1240 | return string ? string : self; |
1241 | } |
1242 | |
1243 | #pragma mark HTML/XML |
1244 | |
1245 | - (NSString *)stringByConvertingHTMLLineBreaks{ |
1246 | NSMutableString *rv = [self mutableCopy]; |
1247 | [rv replaceOccurrencesOfString:@"\n" |
1248 | withString:@"<br>" |
1249 | options:NSCaseInsensitiveSearch |
1250 | range:NSMakeRange(0,[self length])]; |
1251 | return [rv autorelease]; |
1252 | } |
1253 | |
1254 | - (NSString *)stringByEscapingBasicXMLEntitiesUsingUTF8; |
1255 | { |
1256 | return [OFXMLCreateStringWithEntityReferencesInCFEncoding(self, OFXMLBasicEntityMask( ( 0x01 ) | ( ( 0x00 ) << ( 8 ) ) | ( ( 0x00 ) << ( 16 ) ) ), nil( ( void * ) 0 ), kCFStringEncodingUTF8) autorelease]; |
1257 | } |
1258 | |
1259 | #define APPEND_PREVIOUS() \ |
1260 | string = [[NSString alloc] initWithCharacters:begin length:(ptr - begin)]; \ |
1261 | [result appendString:string]; \ |
1262 | [string release]; \ |
1263 | begin = ptr + 1; |
1264 | |
1265 | // Stolen and modified from the OmniFoundation -htmlString. |
1266 | - (NSString *)xmlString; |
1267 | { |
1268 | unichar *ptr, *begin, *end; |
1269 | NSMutableString *result; |
1270 | NSString *string; |
1271 | int length; |
1272 | |
1273 | length = [self length]; |
1274 | ptr = NSZoneMalloc([self zone], length * sizeof(unichar)); |
1275 | void *originalPtr = ptr; |
1276 | end = ptr + length; |
1277 | [self getCharacters:ptr]; |
1278 | result = [NSMutableString stringWithCapacity:length]; |
1279 | |
1280 | begin = ptr; |
1281 | while (ptr < end) { |
1282 | if (*ptr > 127) { |
1283 | APPEND_PREVIOUSstring = [ [ NSString alloc ] initWithCharacters : begin length : ( ptr - begin ) ] ; [ result appendString : string ] ; [ string release ] ; begin = ptr + 1 ;(); |
1284 | [result appendFormat:@"&#%d;", (int)*ptr]; |
1285 | } else if (*ptr == '&') { |
1286 | APPEND_PREVIOUSstring = [ [ NSString alloc ] initWithCharacters : begin length : ( ptr - begin ) ] ; [ result appendString : string ] ; [ string release ] ; begin = ptr + 1 ;(); |
1287 | [result appendString:@"&"]; |
1288 | } else if (*ptr == '\"') { |
1289 | APPEND_PREVIOUSstring = [ [ NSString alloc ] initWithCharacters : begin length : ( ptr - begin ) ] ; [ result appendString : string ] ; [ string release ] ; begin = ptr + 1 ;(); |
1290 | [result appendString:@"""]; |
1291 | } else if (*ptr == '<') { |
1292 | APPEND_PREVIOUSstring = [ [ NSString alloc ] initWithCharacters : begin length : ( ptr - begin ) ] ; [ result appendString : string ] ; [ string release ] ; begin = ptr + 1 ;(); |
1293 | [result appendString:@"<"]; |
1294 | } else if (*ptr == '>') { |
1295 | APPEND_PREVIOUSstring = [ [ NSString alloc ] initWithCharacters : begin length : ( ptr - begin ) ] ; [ result appendString : string ] ; [ string release ] ; begin = ptr + 1 ;(); |
1296 | [result appendString:@">"]; |
1297 | } else if (*ptr == '\n') { |
1298 | APPEND_PREVIOUSstring = [ [ NSString alloc ] initWithCharacters : begin length : ( ptr - begin ) ] ; [ result appendString : string ] ; [ string release ] ; begin = ptr + 1 ;(); |
1299 | if (ptr + 1 != end && *(ptr + 1) == '\n') { |
1300 | [result appendString:@"<p>"]; |
1301 | ptr++; |
1302 | } else |
1303 | [result appendString:@"<br>"]; |
1304 | } |
1305 | ptr++; |
1306 | } |
1307 | APPEND_PREVIOUSstring = [ [ NSString alloc ] initWithCharacters : begin length : ( ptr - begin ) ] ; [ result appendString : string ] ; [ string release ] ; begin = ptr + 1 ;(); |
1308 | NSZoneFree([self zone], originalPtr); |
1309 | return result; |
1310 | } |
1311 | |
1312 | - (NSString *)csvString; |
1313 | { |
1314 | unichar *ptr, *begin, *end; |
1315 | NSMutableString *result; |
1316 | NSString *string; |
1317 | int length; |
1318 | BOOL isQuoted, needsSpace; |
1319 | |
1320 | length = [self length]; |
1321 | ptr = NSZoneMalloc([self zone], length * sizeof(unichar)); |
1322 | void *originalPtr = ptr; |
1323 | end = ptr + length; |
1324 | [self getCharacters:ptr]; |
1325 | result = [NSMutableString stringWithCapacity:length]; |
1326 | isQuoted = length > 0 && (*ptr == ' ' || *(end-1) == ' '); |
1327 | needsSpace = NO( BOOL ) 0; |
1328 | |
1329 | if(isQuoted == NO( BOOL ) 0 && [self containsCharacterInSet:[NSCharacterSet characterSetWithCharactersInString:@"\n\r\t\","]] == NO( BOOL ) 0) { |
1330 | NSZoneFree([self zone], originalPtr); |
1331 | return self; |
1332 | } |
1333 | |
1334 | begin = ptr; |
1335 | while (ptr < end) { |
1336 | switch (*ptr) { |
1337 | case '\n': |
1338 | case '\r': |
1339 | APPEND_PREVIOUSstring = [ [ NSString alloc ] initWithCharacters : begin length : ( ptr - begin ) ] ; [ result appendString : string ] ; [ string release ] ; begin = ptr + 1 ;(); |
1340 | if (needsSpace) |
1341 | [result appendString:@" "]; |
1342 | case ' ': |
1343 | case '\t': |
1344 | needsSpace = NO( BOOL ) 0; |
1345 | break; |
1346 | case '"': |
1347 | APPEND_PREVIOUSstring = [ [ NSString alloc ] initWithCharacters : begin length : ( ptr - begin ) ] ; [ result appendString : string ] ; [ string release ] ; begin = ptr + 1 ;(); |
1348 | [result appendString:@"\"\""]; |
1349 | case ',': |
1350 | isQuoted = YES( BOOL ) 1; |
1351 | default: |
1352 | needsSpace = YES( BOOL ) 1; |
1353 | break; |
1354 | } |
1355 | ptr++; |
1356 | } |
1357 | APPEND_PREVIOUSstring = [ [ NSString alloc ] initWithCharacters : begin length : ( ptr - begin ) ] ; [ result appendString : string ] ; [ string release ] ; begin = ptr + 1 ;(); |
1358 | if (isQuoted) { |
1359 | [result insertString:@"\"" atIndex:0]; |
1360 | [result appendString:@"\""]; |
1361 | } |
1362 | NSZoneFree([self zone], originalPtr); |
1363 | return result; |
1364 | } |
1365 | |
1366 | - (NSString *)tsvString; |
1367 | { |
1368 | if([self containsCharacterInSet:[NSCharacterSet characterSetWithCharactersInString:@"\t\n\r"]] == NO( BOOL ) 0) |
1369 | return self; |
1370 | |
1371 | unichar *ptr, *begin, *end; |
1372 | NSMutableString *result; |
1373 | NSString *string; |
1374 | int length; |
1375 | BOOL needsSpace; |
1376 | |
1377 | length = [self length]; |
1378 | ptr = NSZoneMalloc([self zone], length * sizeof(unichar)); |
1379 | void *originalPtr = ptr; |
1380 | end = ptr + length; |
1381 | [self getCharacters:ptr]; |
1382 | result = [NSMutableString stringWithCapacity:length]; |
1383 | needsSpace = NO( BOOL ) 0; |
1384 | |
1385 | begin = ptr; |
1386 | while (ptr < end) { |
1387 | switch (*ptr) { |
1388 | case '\t': |
1389 | needsSpace = YES( BOOL ) 1; |
1390 | case '\n': |
1391 | case '\r': |
1392 | APPEND_PREVIOUSstring = [ [ NSString alloc ] initWithCharacters : begin length : ( ptr - begin ) ] ; [ result appendString : string ] ; [ string release ] ; begin = ptr + 1 ;(); |
1393 | if (needsSpace) |
1394 | [result appendString:@" "]; |
1395 | case ' ': |
1396 | needsSpace = NO( BOOL ) 0; |
1397 | break; |
1398 | default: |
1399 | needsSpace = YES( BOOL ) 1; |
1400 | break; |
1401 | } |
1402 | ptr++; |
1403 | } |
1404 | APPEND_PREVIOUSstring = [ [ NSString alloc ] initWithCharacters : begin length : ( ptr - begin ) ] ; [ result appendString : string ] ; [ string release ] ; begin = ptr + 1 ;(); |
1405 | NSZoneFree([self zone], originalPtr); |
1406 | return result; |
1407 | } |
1408 | |
1409 | #pragma mark - |
1410 | #pragma mark Script arguments |
1411 | |
1412 | // parses a space separated list of shell script argments |
1413 | // allows quoting parts of an argument and escaped characters outside quotes, according to shell rules |
1414 | - (NSArray *)shellScriptArgumentsArray { |
1415 | static NSCharacterSet *specialChars = nil( ( void * ) 0 ); |
1416 | static NSCharacterSet *quoteChars = nil( ( void * ) 0 ); |
1417 | |
1418 | if (specialChars == nil( ( void * ) 0 )) { |
1419 | NSMutableCharacterSet *tmpSet = [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy]; |
1420 | [tmpSet addCharactersInString:@"\\\"'`"]; |
1421 | specialChars = [tmpSet copy]; |
1422 | [tmpSet release]; |
1423 | quoteChars = [[NSCharacterSet characterSetWithCharactersInString:@"\"'`"] retain]; |
1424 | } |
1425 | |
1426 | NSScanner *scanner = [NSScanner scannerWithString:self]; |
1427 | NSString *s = nil( ( void * ) 0 ); |
1428 | unichar ch = 0; |
1429 | NSMutableString *currArg = [scanner isAtEnd] ? nil( ( void * ) 0 ) : [NSMutableString string]; |
1430 | NSMutableArray *arguments = [NSMutableArray array]; |
1431 | |
1432 | [scanner setCharactersToBeSkipped:nil( ( void * ) 0 )]; |
1433 | [scanner scanCharactersFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet] intoString:NULL( ( void * ) 0 )]; |
1434 | |
1435 | while ([scanner isAtEnd] == NO( BOOL ) 0) { |
1436 | if ([scanner scanUpToCharactersFromSet:specialChars intoString:&s]) |
1437 | [currArg appendString:s]; |
1438 | if ([scanner scanCharacter:&ch] == NO( BOOL ) 0) |
1439 | break; |
1440 | if ([[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:ch]) { |
1441 | // argument separator, add the last one we found and ignore more whitespaces |
1442 | [scanner scanCharactersFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet] intoString:NULL( ( void * ) 0 )]; |
1443 | [arguments addObject:currArg]; |
1444 | currArg = [scanner isAtEnd] ? nil( ( void * ) 0 ) : [NSMutableString string]; |
1445 | } else if (ch == '\\') { |
1446 | // escaped character |
1447 | if ([scanner scanCharacter:&ch] == NO( BOOL ) 0) |
1448 | [NSException raise:NSInternalInconsistencyException format:@"Missing character"]; |
1449 | if ([currArg length] == 0 && [[NSCharacterSet newlineCharacterSet] characterIsMember:ch]) |
1450 | // ignore escaped newlines between arguments, as they should be considered whitespace |
1451 | [scanner scanCharactersFromSet:[NSCharacterSet newlineCharacterSet] intoString:NULL( ( void * ) 0 )]; |
1452 | else // real escaped character, just add the character, so we can ignore it if it is a special character |
1453 | [currArg appendFormat:@"%C", ch]; |
1454 | } else if ([quoteChars characterIsMember:ch]) { |
1455 | // quoted part of an argument, scan up to the matching quote |
1456 | if ([scanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithRange:NSMakeRange(ch, 1)] intoString:&s]) |
1457 | [currArg appendString:s]; |
1458 | if ([scanner scanCharacter:NULL( ( void * ) 0 )] == NO( BOOL ) 0) |
1459 | [NSException raise:NSInternalInconsistencyException format:@"Unmatched %C", ch]; |
1460 | } |
1461 | } |
1462 | if (currArg) |
1463 | [arguments addObject:currArg]; |
1464 | return arguments; |
1465 | } |
1466 | |
1467 | // parses a comma separated list of AppleScript type arguments |
1468 | - (NSArray *)appleScriptArgumentsArray { |
1469 | NSMutableArray *arguments = [NSMutableArray array]; |
1470 | NSScanner *scanner = [NSScanner scannerWithString:self]; |
1471 | unichar ch = 0; |
1472 | id object; |
1473 | |
1474 | [scanner setCharactersToBeSkipped:nil( ( void * ) 0 )]; |
1475 | |
1476 | while ([scanner isAtEnd] == NO( BOOL ) 0) { |
1477 | if ([scanner scanAppleScriptValueUpToCharactersInSet:[NSCharacterSet commaCharacterSet] intoObject:&object]) |
1478 | [arguments addObject:object]; |
1479 | if ([scanner scanCharacter:&ch] == NO( BOOL ) 0) |
1480 | break; |
1481 | if (ch != ',') |
1482 | [NSException raise:NSInternalInconsistencyException format:@"Missing ,"]; |
1483 | } |
1484 | return arguments; |
1485 | } |
1486 | |
1487 | #pragma mark Empty lines |
1488 | |
1489 | // whitespace at the beginning of the string up to and including a newline |
1490 | - (NSRange)rangeOfLeadingEmptyLine { |
1491 | return [self rangeOfLeadingEmptyLineRequiringNewline:YES( BOOL ) 1]; |
1492 | } |
1493 | |
1494 | - (NSRange)rangeOfLeadingEmptyLineRequiringNewline:(BOOL)requireNL { |
1495 | return [self rangeOfLeadingEmptyLineRequiringNewline:requireNL range:NSMakeRange(0, [self length])]; |
1496 | } |
1497 | |
1498 | - (NSRange)rangeOfLeadingEmptyLineInRange:(NSRange)range { |
1499 | return [self rangeOfLeadingEmptyLineRequiringNewline:YES( BOOL ) 1 range:range]; |
1500 | } |
1501 | |
1502 | - (NSRange)rangeOfLeadingEmptyLineRequiringNewline:(BOOL)requireNL range:(NSRange)range { |
1503 | NSRange firstCharRange = [self rangeOfCharacterFromSet:[NSCharacterSet nonWhitespaceCharacterSet] options:0 range:range]; |
1504 | NSRange wsRange = NSMakeRange(NSNotFound, 0); |
1505 | unsigned int start = range.location; |
1506 | if (firstCharRange.location == NSNotFound) { |
1507 | if (requireNL == NO( BOOL ) 0) |
1508 | wsRange = range; |
1509 | } else { |
1510 | unichar firstChar = [self characterAtIndex:firstCharRange.location]; |
1511 | unsigned int rangeEnd = NSMaxRange(firstCharRange); |
1512 | if([[NSCharacterSet newlineCharacterSet] characterIsMember:firstChar]) { |
1513 | if (firstChar == '\r' && rangeEnd < NSMaxRange(range) && [self characterAtIndex:rangeEnd] == '\n') |
1514 | wsRange = NSMakeRange(start, rangeEnd + 1 - start); |
1515 | else |
1516 | wsRange = NSMakeRange(start, rangeEnd - start); |
1517 | } |
1518 | } |
1519 | return wsRange; |
1520 | } |
1521 | |
1522 | // whitespace at the end of the string after a newline |
1523 | - (NSRange)rangeOfTrailingEmptyLine { |
1524 | return [self rangeOfTrailingEmptyLineRequiringNewline:YES( BOOL ) 1]; |
1525 | } |
1526 | |
1527 | - (NSRange)rangeOfTrailingEmptyLineRequiringNewline:(BOOL)requireNL { |
1528 | return [self rangeOfTrailingEmptyLineRequiringNewline:requireNL range:NSMakeRange(0, [self length])]; |
1529 | } |
1530 | |
1531 | - (NSRange)rangeOfTrailingEmptyLineInRange:(NSRange)range { |
1532 | return [self rangeOfTrailingEmptyLineRequiringNewline:YES( BOOL ) 1 range:range]; |
1533 | } |
1534 | |
1535 | - (NSRange)rangeOfTrailingEmptyLineRequiringNewline:(BOOL)requireNL range:(NSRange)range { |
1536 | NSRange lastCharRange = [self rangeOfCharacterFromSet:[NSCharacterSet nonWhitespaceCharacterSet] options:NSBackwardsSearch range:range]; |
1537 | NSRange wsRange = NSMakeRange(NSNotFound, 0); |
1538 | unsigned int end = NSMaxRange(range); |
1539 | if (lastCharRange.location == NSNotFound) { |
1540 | if (requireNL == NO( BOOL ) 0) |
1541 | wsRange = range; |
1542 | } else { |
1543 | unichar lastChar = [self characterAtIndex:lastCharRange.location]; |
1544 | unsigned int rangeEnd = NSMaxRange(lastCharRange); |
1545 | if ([[NSCharacterSet newlineCharacterSet] characterIsMember:lastChar]) |
1546 | wsRange = NSMakeRange(rangeEnd, end - rangeEnd); |
1547 | } |
1548 | return wsRange; |
1549 | } |
1550 | |
1551 | #pragma mark Some convenience keys for templates |
1552 | |
1553 | - (NSURL *)url { |
1554 | NSURL *url = nil( ( void * ) 0 ); |
1555 | if ([self rangeOfString:@"://"].location != NSNotFound) |
1556 | url = [NSURL URLWithStringByNormalizingPercentEscapes:self]; |
1557 | else |
1558 | url = [NSURL fileURLWithPath:[self stringByExpandingTildeInPath]]; |
1559 | return url; |
1560 | } |
1561 | |
1562 | - (NSAttributedString *)linkedText { |
1563 | return [[[NSAttributedString alloc] initWithString:self attributeName:NSLinkAttributeName attributeValue:[self url]] autorelease]; |
1564 | } |
1565 | |
1566 | - (NSAttributedString *)icon { |
1567 | return [[self url] icon]; |
1568 | } |
1569 | |
1570 | - (NSAttributedString *)smallIcon { |
1571 | return [[self url] smallIcon]; |
1572 | } |
1573 | |
1574 | - (NSAttributedString *)linkedIcon { |
1575 | return [[self url] linkedIcon]; |
1576 | } |
1577 | |
1578 | - (NSAttributedString *)linkedSmallIcon { |
1579 | return [[self url] linkedSmallIcon]; |
1580 | } |
1581 | |
1582 | - (NSString *)textSkimNotes { |
1583 | return [[self url] textSkimNotes]; |
1584 | } |
1585 | |
1586 | - (NSAttributedString *)richTextSkimNotes { |
1587 | return [[self url] richTextSkimNotes]; |
1588 | } |
1589 | |
1590 | - (NSString *)titleCapitalizedString { |
1591 | NSScanner *scanner = [[NSScanner alloc] initWithString:self]; |
1592 | NSString *s = nil( ( void * ) 0 ); |
1593 | NSMutableString *returnString = [NSMutableString stringWithCapacity:[self length]]; |
1594 | int nesting = 0; |
1595 | unichar ch; |
1596 | unsigned location; |
1597 | NSRange range; |
1598 | BOOL foundFirstLetter = NO( BOOL ) 0; |
1599 | |
1600 | [scanner setCharactersToBeSkipped:nil( ( void * ) 0 )]; |
1601 | |
1602 | while([scanner isAtEnd] == NO( BOOL ) 0){ |
1603 | if([scanner scanUpToCharactersFromSet:[NSCharacterSet curlyBraceCharacterSet] intoString:&s]) |
1604 | [returnString appendString:nesting == 0 ? [s lowercaseString] : s]; |
1605 | if (foundFirstLetter == NO( BOOL ) 0) { |
1606 | range = [returnString rangeOfCharacterFromSet:[NSCharacterSet letterCharacterSet]]; |
1607 | if (range.location != NSNotFound) { |
1608 | foundFirstLetter = YES( BOOL ) 1; |
1609 | if (nesting == 0) |
1610 | [returnString replaceCharactersInRange:range withString:[[returnString substringWithRange:range] uppercaseString]]; |
1611 | } |
1612 | } |
1613 | if([scanner scanCharacter:&ch] == NO( BOOL ) 0) |
1614 | break; |
1615 | [returnString appendFormat:@"%C", ch]; |
1616 | location = [scanner scanLocation]; |
1617 | if(location > 0 && [self characterAtIndex:location - 1] == '\\') |
1618 | continue; |
1619 | if(ch == '{') |
1620 | nesting++; |
1621 | else |
1622 | nesting--; |
1623 | } |
1624 | |
1625 | [scanner release]; |
1626 | |
1627 | return returnString; |
1628 | } |
1629 | |
1630 | - (NSString *)firstLetter{ |
1631 | return [self length] ? [self substringToIndex:1] : nil( ( void * ) 0 ); |
1632 | } |
1633 | |
1634 | - (NSString *)stringByAddingPercentEscapesIncludingReserved{ |
1635 | return [(NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)self, NULL( ( void * ) 0 ), CFSTR( ( CFStringRef ) __builtin___CFStringMakeConstantString ( "" ";/?:@&=+$," "" ) )(";/?:@&=+$,"), kCFStringEncodingUTF8) autorelease]; |
1636 | } |
1637 | |
1638 | - (NSString *)stringByAddingPercentEscapes{ |
1639 | return [self stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; |
1640 | } |
1641 | |
1642 | - (NSString *)stringByReplacingPercentEscapes{ |
1643 | return [self stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; |
1644 | } |
1645 | |
1646 | @end |
1647 | |
1648 | |
1649 | @implementation NSMutableString (BDSKExtensions) |
1650 | |
1651 | - (BOOL)isMutableString; |
1652 | { |
1653 | int isMutable = YES( BOOL ) 1; |
1654 | @try{ |
1655 | [self appendCharacter:'X']; |
1656 | } |
1657 | @catch(NSException *localException){ |
1658 | if([[localException name] isEqual:NSInvalidArgumentException]) |
1659 | isMutable = NO( BOOL ) 0; |
1660 | else |
1661 | @throw; |
1662 | } |
1663 | @catch(id localException){ |
1664 | @throw; |
1665 | } |
1666 | |
1667 | [self deleteCharactersInRange:NSMakeRange([self length] - 1, 1)]; |
1668 | return isMutable; |
1669 | } |
1670 | |
1671 | - (void)deleteCharactersInCharacterSet:(NSCharacterSet *)characterSet; |
1672 | { |
1673 | BDDeleteCharactersInCharacterSet((CFMutableStringRef)self, (CFCharacterSetRef)characterSet); |
1674 | } |
1675 | |
1676 | @end |