Frameworks/AutoHyperlinks Framework/Source/AHHyperlinkScanner.m
author Stephen Holt <sholt@adium.im>
Wed Aug 12 13:32:55 2009 -0400 (2009-08-12)
changeset 2603 aac234118c92
parent 2588 705099bf70ca
parent 2602 19c704d73e7f
child 2637 1832ae51377b
permissions -rw-r--r--
Add em and en dashes to the start set. Fixes #11490
(transplanted from 19c704d73e7f771dc4899d7b31a091db79030fc0)
     1 /*
     2  * The AutoHyperlinks Framework is the legal property of its developers (DEVELOPERS), whose names are listed in the
     3  * copyright file included with this source distribution.
     4  *
     5  * Redistribution and use in source and binary forms, with or without
     6  * modification, are permitted provided that the following conditions are met:
     7  *     * Redistributions of source code must retain the above copyright
     8  *       notice, this list of conditions and the following disclaimer.
     9  *     * Redistributions in binary form must reproduce the above copyright
    10  *       notice, this list of conditions and the following disclaimer in the
    11  *       documentation and/or other materials provided with the distribution.
    12  *     * Neither the name of the AutoHyperlinks Framework nor the
    13  *       names of its contributors may be used to endorse or promote products
    14  *       derived from this software without specific prior written permission.
    15  *
    16  * THIS SOFTWARE IS PROVIDED BY ITS DEVELOPERS ``AS IS'' AND ANY
    17  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    18  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    19  * DISCLAIMED. IN NO EVENT SHALL ITS DEVELOPERS BE LIABLE FOR ANY
    20  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    21  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    22  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
    23  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    24  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    25  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    26  */
    27 
    28 #import "AHHyperlinkScanner.h"
    29 #import "AHLinkLexer.h"
    30 #import "AHMarkedHyperlink.h"
    31 
    32 #define	DEFAULT_URL_SCHEME	@"http://"
    33 #define ENC_INDEX_KEY @"encIndex"
    34 #define ENC_CHAR_KEY @"encChar"
    35 
    36 @interface AHHyperlinkScanner (PRIVATE)
    37 - (NSRange)_longestBalancedEnclosureInRange:(NSRange)inRange;
    38 - (BOOL)_scanString:(NSString *)inString upToCharactersFromSet:(NSCharacterSet *)inCharSet intoRange:(NSRange *)outRangeRef fromIndex:(unsigned long *)idx;
    39 - (BOOL)_scanString:(NSString *)inString charactersFromSet:(NSCharacterSet *)inCharSet intoRange:(NSRange *)outRangeRef fromIndex:(unsigned long *)idx;
    40 @end
    41 
    42 @implementation AHHyperlinkScanner
    43 #pragma mark static variables
    44 	static NSCharacterSet	*skipSet = nil;
    45 	static NSCharacterSet	*endSet = nil;
    46 	static NSCharacterSet	*startSet = nil;
    47 	static NSCharacterSet	*puncSet = nil;
    48 	static NSCharacterSet	*hostnameComponentSeparatorSet = nil;
    49 	static NSArray			*enclosureStartArray = nil;
    50 	static NSCharacterSet	*enclosureSet = nil;
    51 	static NSArray			*enclosureStopArray = nil;
    52 	static NSArray			*encKeys = nil;
    53 	
    54 #pragma mark Class Methods
    55 + (id)hyperlinkScannerWithString:(NSString *)inString
    56 {
    57 	return [[[[self class] alloc] initWithString:inString usingStrictChecking:NO] autorelease];
    58 }
    59 
    60 + (id)strictHyperlinkScannerWithString:(NSString *)inString
    61 {
    62 	return [[[[self class] alloc] initWithString:inString usingStrictChecking:YES] autorelease];
    63 }
    64 
    65 + (id)hyperlinkScannerWithAttributedString:(NSAttributedString *)inString
    66 {
    67 	return [[[[self class] alloc] initWithAttributedString:inString usingStrictChecking:NO] autorelease];
    68 }
    69 
    70 + (id)strictHyperlinkScannerWithAttributedString:(NSAttributedString *)inString
    71 {
    72 	return [[[[self class] alloc] initWithAttributedString:inString usingStrictChecking:NO] autorelease];
    73 }
    74 
    75 #pragma mark Initialization
    76 + (void)initialize
    77 {
    78 	if ((self == [AHHyperlinkScanner class])) {
    79 		if (!skipSet) {
    80 			NSMutableCharacterSet *mutableSkipSet = [[NSMutableCharacterSet alloc] init];
    81 			[mutableSkipSet formUnionWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
    82 			[mutableSkipSet formUnionWithCharacterSet:[NSCharacterSet illegalCharacterSet]];
    83 			[mutableSkipSet formUnionWithCharacterSet:[NSCharacterSet controlCharacterSet]];
    84 			[mutableSkipSet formUnionWithCharacterSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];
    85 			skipSet = [[NSCharacterSet characterSetWithBitmapRepresentation:[mutableSkipSet bitmapRepresentation]] retain];
    86 			[mutableSkipSet release];
    87 		}
    88 		
    89 		if (!endSet) {
    90 			endSet = [[NSCharacterSet characterSetWithCharactersInString:@"\"',:;>)]}.?!@"] retain];
    91 		}
    92 		
    93 		if (!startSet) {
    94 			NSMutableCharacterSet *mutableStartSet = [[NSMutableCharacterSet alloc] init];
    95 			[mutableStartSet formUnionWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
    96 			[mutableStartSet formUnionWithCharacterSet:[NSCharacterSet characterSetWithCharactersInString:[NSString stringWithFormat:@"\"'.,:;<?!-@%C%C", 0x2014, 0x2013]]];
    97 			startSet = [[NSCharacterSet characterSetWithBitmapRepresentation:[mutableStartSet bitmapRepresentation]] retain];
    98 			[mutableStartSet release];
    99 		}
   100 		
   101 		if (!puncSet) {
   102 			puncSet = [[NSCharacterSet characterSetWithCharactersInString:@"\"'.,:;<?!"] retain];
   103 		}
   104 		
   105 		if (!hostnameComponentSeparatorSet) {
   106 			hostnameComponentSeparatorSet = [[NSCharacterSet characterSetWithCharactersInString:@"./"] retain];
   107 		}
   108 		
   109 		if(!enclosureStartArray){
   110 			enclosureStartArray = [[NSArray arrayWithObjects:@"(",@"[",@"{",nil] retain];
   111 		}
   112 		
   113 		if(!enclosureSet){
   114 			enclosureSet = [[NSCharacterSet characterSetWithCharactersInString:@"()[]{}"] retain];
   115 		}
   116 		
   117 		if(!enclosureStopArray){
   118 			enclosureStopArray = [[NSArray arrayWithObjects:@")",@"]",@"}",nil] retain];
   119 		}
   120 		
   121 		if(!encKeys){
   122 			encKeys = [[NSArray arrayWithObjects:ENC_INDEX_KEY, ENC_CHAR_KEY, nil] retain];
   123 		}		
   124 	}
   125 }
   126 
   127 #pragma mark Init/Dealloc
   128 
   129 
   130 - (id)initWithString:(NSString *)inString usingStrictChecking:(BOOL)flag
   131 {
   132 	if((self = [self init])){
   133 		m_scanString = [inString retain];
   134 		m_scanAttrString = nil;
   135 		m_urlSchemes = [[NSDictionary alloc] initWithObjectsAndKeys:
   136 			@"ftp://", @"ftp",
   137 			nil];
   138 		m_strictChecking = flag;
   139 		m_scanLocation = 0;
   140 		m_scanStringLength = [m_scanString length];
   141 	}
   142 	return self;
   143 }
   144 
   145 - (id)initWithAttributedString:(NSAttributedString *)inString usingStrictChecking:(BOOL)flag
   146 {
   147 	if((self = [self init])){
   148 		m_scanString = [[inString string] retain];
   149 		m_scanAttrString = [inString retain];
   150 		m_urlSchemes = [[NSDictionary alloc] initWithObjectsAndKeys:
   151 			@"ftp://", @"ftp",
   152 			nil];
   153 		m_strictChecking = flag;
   154 		m_scanLocation = 0;
   155 		m_scanStringLength = [m_scanString length];
   156 	}
   157 	return self;
   158 }
   159 
   160 - (void)dealloc
   161 {
   162 	[m_scanString release];
   163 	[m_urlSchemes release];
   164 	if(m_scanAttrString) [m_scanAttrString release];
   165 	[super dealloc];
   166 }
   167 
   168 #pragma mark URI Verification
   169 
   170 - (BOOL)isValidURI
   171 {
   172 	return [AHHyperlinkScanner isStringValidURI:m_scanString usingStrict:m_strictChecking fromIndex:nil withStatus:nil];
   173 }
   174 
   175 + (BOOL)isStringValidURI:(NSString *)inString usingStrict:(BOOL)useStrictChecking fromIndex:(unsigned long *)index withStatus:(AH_URI_VERIFICATION_STATUS *)validStatus
   176 {
   177     AH_BUFFER_STATE	 buf;  // buffer for flex to scan from
   178 	yyscan_t		 scanner; // pointer to the flex scanner opaque type
   179 	const char		*inStringEnc;
   180     unsigned long	 encodedLength;
   181 
   182 	if(!validStatus){
   183 		AH_URI_VERIFICATION_STATUS newStatus = AH_URL_INVALID;
   184 		validStatus = &newStatus;
   185 	}
   186 	
   187 	*validStatus = AH_URL_INVALID; // assume the URL is invalid
   188 
   189 	// Find the fastest 8-bit wide encoding possible for the c string
   190 	NSStringEncoding stringEnc = [inString fastestEncoding];
   191 	if([@" " lengthOfBytesUsingEncoding:stringEnc] > 1U)
   192 		stringEnc = NSUTF8StringEncoding;
   193 
   194 	if (!(inStringEnc = [inString cStringUsingEncoding:stringEnc])) {
   195 		return NO;
   196 	}
   197 	
   198 	
   199 	encodedLength = strlen(inStringEnc); // length of the string in utf-8
   200     
   201 	// initialize the buffer (flex automatically switches to the buffer in this function)
   202 	AHlex_init(&scanner);
   203     buf = AH_scan_string(inStringEnc, scanner);
   204 
   205     // call flex to parse the input
   206     *validStatus = AHlex(scanner);
   207 	if(index) *index += AHget_leng(scanner);
   208 	
   209     // condition for valid URI's
   210     if(*validStatus == AH_URL_VALID || *validStatus == AH_MAILTO_VALID || *validStatus == AH_FILE_VALID){
   211         AH_delete_buffer(buf, scanner); //remove the buffer from flex.
   212         buf = NULL; //null the buffer pointer for safty's sake.
   213         
   214         // check that the whole string was matched by flex.
   215         // this prevents silly things like "blah...com" from being seen as links
   216         if(AHget_leng(scanner) == encodedLength){
   217 			AHlex_destroy(scanner);
   218             return YES;
   219         }
   220     // condition for degenerate URL's (A.K.A. URI's sans specifiers), requres strict checking to be NO.
   221     }else if((*validStatus == AH_URL_DEGENERATE || *validStatus == AH_MAILTO_DEGENERATE) && !useStrictChecking){
   222         AH_delete_buffer(buf, scanner);
   223         buf = NULL;
   224         if(AHget_leng(scanner) == encodedLength){
   225 			AHlex_destroy(scanner);
   226             return YES;
   227         }
   228     // if it ain't vaild, and it ain't degenerate, then it's invalid.
   229     }else{
   230         AH_delete_buffer(buf, scanner);
   231         buf = NULL;
   232 		AHlex_destroy(scanner);
   233         return NO;
   234     }
   235     // default case, if the range checking above fails.
   236 	AHlex_destroy(scanner);
   237     return NO;
   238 }
   239 
   240 #pragma mark Accessors
   241 
   242 - (AHMarkedHyperlink *)nextURI
   243 {
   244 	NSRange	scannedRange;
   245 	unsigned long scannedLocation = m_scanLocation;
   246 	
   247     // scan upto the next whitespace char so that we don't unnecessarity confuse flex
   248     // otherwise we end up validating urls that look like this "http://www.adium.im/ <--cool"
   249 	[self _scanString:m_scanString charactersFromSet:startSet intoRange:nil fromIndex:&scannedLocation];
   250 
   251 	// main scanning loop
   252 	while([self _scanString:m_scanString upToCharactersFromSet:skipSet intoRange:&scannedRange fromIndex:&scannedLocation]) {
   253 		BOOL foundUnpairedEnclosureCharacter = NO;
   254 
   255 		// Check for and filter enclosures.  We can't add (, [, etc. to the skipSet as they may be in a URI
   256 		if([enclosureSet characterIsMember:[m_scanString characterAtIndex:scannedRange.location]]){
   257 			unsigned long encIdx = [enclosureStartArray indexOfObject:[m_scanString substringWithRange:NSMakeRange(scannedRange.location, 1)]];
   258 			NSRange encRange;
   259 			if(NSNotFound != encIdx) {
   260 				encRange = [m_scanString rangeOfString:[enclosureStopArray objectAtIndex:encIdx] options:NSBackwardsSearch range:scannedRange];
   261 				if(NSNotFound != encRange.location){
   262 					scannedRange.location++; scannedRange.length -= 2;
   263 				}else{
   264 					foundUnpairedEnclosureCharacter = YES;
   265 				}
   266 			}
   267 		}
   268 		if(!scannedRange.length) break;
   269 				
   270 		// Find balanced enclosure chars
   271 		NSRange longestEnclosure = [self _longestBalancedEnclosureInRange:scannedRange];
   272 		while (scannedRange.length > 2 && [endSet characterIsMember:[m_scanString characterAtIndex:(scannedRange.location + scannedRange.length - 1)]]) {
   273 			if((longestEnclosure.location + longestEnclosure.length) < scannedRange.length){
   274 				scannedRange.length--;
   275 				foundUnpairedEnclosureCharacter = NO;
   276 			}else break;
   277 		}
   278 		
   279         // if we have a valid URL then save the scanned string, and make a SHMarkedHyperlink out of it.
   280         // this way, we can preserve things like the matched string (to be converted to a NSURL),
   281         // parent string, its validation status (valid, file, degenerate, etc), and its range in the parent string
   282 		AH_URI_VERIFICATION_STATUS	 validStatus;
   283 		NSString					*_scanString = nil;
   284 		if(3 < scannedRange.length) _scanString = [m_scanString substringWithRange:scannedRange];
   285 
   286         if((3 < scannedRange.length) && [[self class] isStringValidURI:_scanString usingStrict:m_strictChecking fromIndex:&m_scanLocation withStatus:&validStatus]){
   287             AHMarkedHyperlink	*markedLink;
   288 			
   289             //insert typical specifiers if the URL is degenerate
   290             switch(validStatus){
   291                 case AH_URL_DEGENERATE:
   292                 {
   293                     NSString *scheme = DEFAULT_URL_SCHEME;
   294 					unsigned long i = 0;
   295 
   296                     NSRange  firstComponent;
   297 					[self		  _scanString:_scanString
   298 						upToCharactersFromSet:hostnameComponentSeparatorSet
   299 									intoRange:&firstComponent
   300 									fromIndex:&i];
   301 
   302                     if(NSNotFound != firstComponent.location) {
   303                     	NSString *hostnameScheme = [m_urlSchemes objectForKey:[_scanString substringWithRange:firstComponent]];
   304                     	if(hostnameScheme) scheme = hostnameScheme;
   305                     }
   306 
   307                     _scanString = [scheme stringByAppendingString:_scanString];
   308 
   309                     break;
   310                 }
   311 
   312                 case AH_MAILTO_DEGENERATE:
   313 					_scanString = [@"mailto:" stringByAppendingString:_scanString];
   314                     break;
   315                 default:
   316                     break;
   317             }
   318             
   319             //make a marked link
   320             markedLink = [[[AHMarkedHyperlink alloc] initWithString:_scanString
   321 											  withValidationStatus:validStatus
   322 													  parentString:m_scanString
   323 														  andRange:scannedRange] autorelease];
   324             return [markedLink URL]? markedLink : nil;
   325         }
   326 
   327 		//step location after scanning a string
   328 		if (foundUnpairedEnclosureCharacter){
   329 			m_scanLocation++;
   330 		}else{
   331 			NSRange startRange = [m_scanString rangeOfCharacterFromSet:puncSet options:NSLiteralSearch range:scannedRange];
   332 			if (startRange.location != NSNotFound)
   333 				m_scanLocation = startRange.location + startRange.length;
   334 			else
   335 				m_scanLocation += scannedRange.length;
   336 		}
   337 			
   338 		scannedLocation = m_scanLocation;
   339     }
   340 	
   341     // if we're here, then NSScanner hit the end of the string
   342     // set AHStringOffset to the string length here so we avoid potential infinite looping with many trailing spaces.
   343     m_scanLocation = m_scanStringLength;
   344     return nil;
   345 }
   346 
   347 -(NSArray *)allURIs
   348 {
   349     NSMutableArray		*rangeArray = [NSMutableArray array];
   350     AHMarkedHyperlink	*markedLink;
   351 	unsigned long		 _holdOffset = m_scanLocation; // store location for later restoration;
   352 	m_scanLocation = 0; //set the offset to 0.
   353     
   354     //build an array of marked links.
   355 	while((markedLink = [self nextURI])){
   356 		[rangeArray addObject:markedLink];
   357 	}
   358     m_scanLocation = _holdOffset; // reset scanLocation
   359 	return rangeArray;
   360 }
   361 
   362 -(NSAttributedString *)linkifiedString
   363 {
   364 	NSMutableAttributedString	*linkifiedString;
   365 	AHMarkedHyperlink			*markedLink;
   366 	BOOL						_didFindLinks = NO;
   367 	unsigned long				_holdOffset = m_scanLocation; // store location for later restoration;
   368 	
   369 	m_scanLocation = 0;
   370 
   371 	if(m_scanAttrString) {
   372 		linkifiedString = [[m_scanAttrString mutableCopy] autorelease];
   373 	} else {
   374 		linkifiedString = [[[NSMutableAttributedString alloc] initWithString:m_scanString] autorelease];
   375 	}
   376 
   377 	//for each SHMarkedHyperlink, add the proper URL to the proper range in the string.
   378 	for(markedLink in self) {
   379 		NSURL *markedLinkURL;
   380 		_didFindLinks = YES;
   381 		if((markedLinkURL = [markedLink URL])) {
   382 			[linkifiedString addAttribute:NSLinkAttributeName
   383 									value:markedLinkURL
   384 									range:[markedLink range]];
   385 		}
   386 	}
   387 	
   388 	m_scanLocation = _holdOffset; // reset scanLocation
   389 		
   390 	return _didFindLinks? linkifiedString :
   391 						  m_scanAttrString ? [[m_scanAttrString retain] autorelease] : [[[NSMutableAttributedString alloc] initWithString:m_scanString] autorelease];
   392 }
   393 
   394 -(unsigned long)scanLocation
   395 {
   396 	return m_scanLocation;
   397 }
   398 
   399 - (void)setScanLocation:(unsigned int)location
   400 {
   401 	m_scanLocation = location;
   402 }
   403 
   404 #pragma mark NSFastEnumeration
   405 - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len
   406 {
   407 	AHMarkedHyperlink	*currentLink;
   408 	
   409 	NSUInteger fastEnumCount = 0;
   410 	while (fastEnumCount < len && nil != (currentLink = [self nextURI])) {
   411 		stackbuf[fastEnumCount] = currentLink;
   412 		++fastEnumCount;
   413 	}
   414 	
   415 	state->state = (nil == currentLink)? (NSUInteger)currentLink : NSNotFound;
   416 	state->itemsPtr = stackbuf;
   417 	state->mutationsPtr = (unsigned long *)self;
   418 	
   419 	return fastEnumCount;
   420 }
   421 
   422 #pragma mark Below Here There Be Private Methods
   423 
   424 - (NSRange)_longestBalancedEnclosureInRange:(NSRange)inRange
   425 {
   426 	NSMutableArray	*enclosureStack = nil, *enclosureArray = nil;
   427 	NSString  *matchChar = nil;
   428 	NSDictionary *encDict;
   429 	unsigned long encScanLocation = inRange.location;
   430 	
   431 	while(encScanLocation < inRange.length + inRange.location) {
   432 		[self _scanString:m_scanString upToCharactersFromSet:enclosureSet intoRange:nil fromIndex:&encScanLocation];
   433 			
   434 		if(encScanLocation >= (inRange.location + inRange.length)) break;
   435 			
   436 		matchChar = [m_scanString substringWithRange:NSMakeRange(encScanLocation, 1)];
   437 			
   438 		if([enclosureStartArray containsObject:matchChar]) {
   439 			encDict = [NSDictionary	dictionaryWithObjects:[NSArray arrayWithObjects:[NSNumber numberWithUnsignedLong:encScanLocation], matchChar, nil]
   440 												forKeys:encKeys];
   441 			if(!enclosureStack) enclosureStack = [NSMutableArray arrayWithCapacity:1];
   442 			[enclosureStack addObject:encDict];
   443 		}else if([enclosureStopArray containsObject:matchChar]) {
   444 			NSEnumerator *encEnumerator = [enclosureStack objectEnumerator];
   445 			while ((encDict = [encEnumerator nextObject])) {
   446 				unsigned long encTagIndex = [(NSNumber *)[encDict objectForKey:ENC_INDEX_KEY] unsignedLongValue];
   447 				unsigned long encStartIndex = [enclosureStartArray indexOfObjectIdenticalTo:[encDict objectForKey:ENC_CHAR_KEY]];
   448 				if([enclosureStopArray indexOfObjectIdenticalTo:matchChar] == encStartIndex) {
   449 					NSRange encRange = NSMakeRange(encTagIndex, encScanLocation - encTagIndex + 1);
   450 					if(!enclosureStack) enclosureStack = [NSMutableArray arrayWithCapacity:1];
   451 					if(!enclosureArray) enclosureArray = [NSMutableArray arrayWithCapacity:1];
   452 					[enclosureStack removeObject:encDict];
   453 					[enclosureArray addObject:NSStringFromRange(encRange)];
   454 					break;
   455 				}
   456 			}
   457 		}
   458 		if(encScanLocation < inRange.length + inRange.location)
   459 			encScanLocation++;
   460 	}
   461 	return (enclosureArray && [enclosureArray count])? NSRangeFromString([enclosureArray lastObject]) : NSMakeRange(0, 0);
   462 }
   463 
   464 // functional replacement for -[NSScanner scanUpToCharactersFromSet:intoString:]
   465 - (BOOL)_scanString:(NSString *)inString upToCharactersFromSet:(NSCharacterSet *)inCharSet intoRange:(NSRange *)outRangeRef fromIndex:(unsigned long *)idx
   466 {
   467 	unichar			_curChar;
   468 	NSRange			_outRange;
   469 	unsigned long	_scanLength = [inString length];
   470 	unsigned long	_idx;
   471 	
   472 	if(_scanLength <= *idx) return NO;
   473 
   474 	// Asorb skipSet
   475 	for(_idx = *idx; _scanLength > _idx; _idx++) {
   476 		_curChar = [inString characterAtIndex:_idx];
   477 		if(![skipSet characterIsMember:_curChar]) break;
   478 	}
   479 
   480 	// scanUpTo:
   481 	for(*idx = _idx; _scanLength > _idx; _idx++) {
   482 		_curChar = [inString characterAtIndex:_idx];
   483 		if([inCharSet characterIsMember:_curChar] || [skipSet characterIsMember:_curChar]) break;
   484 	}
   485 	
   486 	_outRange = NSMakeRange(*idx, _idx - *idx);
   487 	*idx = _idx;
   488 	
   489 	if(_outRange.length) {
   490 		if(outRangeRef) *outRangeRef = _outRange;
   491 		return YES;
   492 	} else {
   493 		return NO;
   494 	}
   495 }
   496 
   497 // functional replacement for -[NSScanner scanCharactersFromSet:intoString:]
   498 - (BOOL)_scanString:(NSString *)inString charactersFromSet:(NSCharacterSet *)inCharSet intoRange:(NSRange *)outRangeRef fromIndex:(unsigned long *)idx
   499 {
   500 	unichar			_curChar;
   501 	NSRange			_outRange;
   502 	unsigned long	_scanLength = [inString length];
   503 	unsigned long	_idx = *idx;
   504 	
   505 	if(_scanLength <= _idx) return NO;
   506 
   507 	// Asorb skipSet
   508 	for(_idx = *idx; _scanLength > _idx; _idx++) {
   509 		_curChar = [inString characterAtIndex:_idx];
   510 		if(![skipSet characterIsMember:_curChar]) break;
   511 	}
   512 
   513 	// scanCharacters:
   514 	for(*idx = _idx; _scanLength > _idx; _idx++) {
   515 		_curChar = [inString characterAtIndex:_idx];
   516 		if(![inCharSet characterIsMember:_curChar]) break;
   517 	}
   518 
   519 	_outRange = NSMakeRange(*idx, _idx - *idx);
   520 	*idx = _idx;
   521 	
   522 	if(_outRange.length) {
   523 		if(outRangeRef) *outRangeRef = _outRange;
   524 		return YES;
   525 	} else {
   526 		return NO;
   527 	}
   528 }
   529 @end