diff options
author | rowanbeentje <rowan@beent.je> | 2009-02-18 21:07:43 +0000 |
---|---|---|
committer | rowanbeentje <rowan@beent.je> | 2009-02-18 21:07:43 +0000 |
commit | 2525366dbfed3aef78beaed89630ea543389cec1 (patch) | |
tree | 68dc953f1bc0c127c05e618072dc8936d8a8cf0e /Source/SPSQLParser.m | |
parent | 1dae450e32b269cb95c47a4274de9641ea0e779a (diff) | |
download | sequelpro-2525366dbfed3aef78beaed89630ea543389cec1.tar.gz sequelpro-2525366dbfed3aef78beaed89630ea543389cec1.tar.bz2 sequelpro-2525366dbfed3aef78beaed89630ea543389cec1.zip |
Visible improvements in this build:
- Significantly reduce the queries that have to be performed, improving lag - especially over slow connections (Issue #118; see new controller info under headline code changes).
- Fix Issue #117 properly (export numeric quoting - we now have access to column types and so can quote appropriately).
- Fix Issue #145 (loss of unsigned/null/default attributes when reordering columns).
- Fixes Issue #90 (support for filtering DECIMAL column types)
- Improve table scrolling speed when the table contains long items. (Added a NSFormatter to automatically truncate strings > 150 chars for display purposes only)
- Improved SQL compatibility - for example /* C style comments */ are now correctly ignored in imports and custom queries.
- Add text and symbols emphasising that the table info pane / status view row count is an approximation (partially addresses Issue #141)
- Fixes a major memory leak whenever opening or scrolling tables containing text/blob data.
- SQL import is now faster (SQL parsing part is 3x faster).
- Speed up SQL export (1.5x faster for numeric data; 1.1x faster for string data) and slightly speed up CSV export (~1.1x faster).
- Display sizes on the status view using the byte size formatter, as per table info pane.
Headline code changes:
- Add a new NSMutableString subclass, SPSQLParser. See the header file for documentation and overview, but in short it's a centralised place for SQL parsing. Centralises and improves parsing, improves comment support, improves quoting support. Despite the improved featureset this is also faster than the previous distributed implementations - for example, when used to replace the old splitQueries:, > 3x speedup.
- Implement a new controller which handles a structure and status cache for the current table, and provides structure parsing for specified tables. This cache is now used throughout the code, reducing the queries that have to be performed and providing additional information about the table structure for use; I think it also improves column type format slightly.
- The table info pane and the status view now draw all their data from the cache.
Tweaks:
- Table encoding is now detected directly instead of being derived from the collation - increased accuracy and cope with the DEFAULT encoding.
- Comments and formatting cleaned up in bits I was working on, obviously.
- A couple of methods - particularly [tablesListInstance table] and [tableDocument encoding] - have been renamed to avoid conflicts and fix code warnings.
Future improvements now possible:
- As we now have access to column types and other information, we can provide per-type behaviour where desired.
- The table parsing doesn't currently pull out comments or table indices, together with one or two other attributes. Some of this would be useful for display; some, such as indices, could be used to draw the table structure view as long as we're happy discarding a couple of columns (ie cardinality!)
Diffstat (limited to 'Source/SPSQLParser.m')
-rw-r--r-- | Source/SPSQLParser.m | 655 |
1 files changed, 655 insertions, 0 deletions
diff --git a/Source/SPSQLParser.m b/Source/SPSQLParser.m new file mode 100644 index 00000000..9828f529 --- /dev/null +++ b/Source/SPSQLParser.m @@ -0,0 +1,655 @@ +// +// SPSQLParsing.m +// sequel-pro +// +// Created by Rowan Beentje on 18/01/2009. +// Copyright 2009 Rowan Beentje. All rights reserved. +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPSQLParser.h" + +/* + * Please see the header files for a general description of the purpose of this class, + * and increased overview detail for the functions below. + */ +@implementation SPSQLParser : NSMutableString + + + +/* + * Removes comments within the current string, trimming "#", "--[/s]", and "/* * /" style strings. + */ +- (void) deleteComments +{ + long currentStringIndex, commentEndIndex, quotedStringEndIndex; + unichar currentCharacter; + long stringLength = [string length]; + + // Walk along the string, processing characters. + for (currentStringIndex = 0; currentStringIndex < stringLength; currentStringIndex++) { + currentCharacter = [string characterAtIndex:currentStringIndex]; + switch (currentCharacter) { + + // When quote characters are encountered walk to the end of the quoted string. + case '\'': + case '"': + case '`': + quotedStringEndIndex = [self endIndexOfStringQuotedByCharacter:currentCharacter startingAtIndex:currentStringIndex+1]; + if (quotedStringEndIndex == NSNotFound) { + return; + } + currentStringIndex = quotedStringEndIndex; + break; + + // For comments starting "--[\s]", ensure the start syntax is valid before proceeding. + case '-': + if (stringLength < currentStringIndex + 2) break; + if ([string characterAtIndex:currentStringIndex+1] != '-') break; + if (![[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:[string characterAtIndex:currentStringIndex+2]]) break; + commentEndIndex = [self endIndexOfCommentOfType:SPDoubleDashComment startingAtIndex:currentStringIndex]; + + // Remove the comment + [string deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)]; + stringLength -= commentEndIndex - currentStringIndex + 1; + currentStringIndex--; + break; + + case '#': + commentEndIndex = [self endIndexOfCommentOfType:SPHashComment startingAtIndex:currentStringIndex]; + + // Remove the comment + [string deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)]; + stringLength -= commentEndIndex - currentStringIndex + 1; + currentStringIndex--; + break; + + // For comments starting "/*", ensure the start syntax is valid before proceeding. + case '/': + if (stringLength < currentStringIndex + 1) break; + if ([string characterAtIndex:currentStringIndex+1] != '*') break; + commentEndIndex = [self endIndexOfCommentOfType:SPCStyleComment startingAtIndex:currentStringIndex]; + + // Remove the comment + [string deleteCharactersInRange:NSMakeRange(currentStringIndex, commentEndIndex - currentStringIndex + 1)]; + stringLength -= commentEndIndex - currentStringIndex + 1; + currentStringIndex--; + break; + } + } +} + + +/* + * Removes quotes surrounding the string if present, and un-escapes internal occurrences of the quote character before returning. + */ +- (NSString *) unquotedString +{ + NSMutableString *returnString; + long stringEndIndex; + unichar quoteCharacter; + + if (![string length]) return nil; + + // If the first character is not a quote character, return the entire string. + quoteCharacter = [string characterAtIndex:0]; + if (quoteCharacter != '`' && quoteCharacter != '"' && quoteCharacter != '\'') { + return [NSString stringWithString:string]; + } + + // Get the end of the string + stringEndIndex = [self endIndexOfStringQuotedByCharacter:quoteCharacter startingAtIndex:1]; + if (stringEndIndex == NSNotFound) { + return [NSString stringWithString:string]; + } + + // Trim the string appropriately + returnString = [NSMutableString stringWithString:[string substringWithRange:NSMakeRange(1, stringEndIndex-1)]]; + + // Remove escaped characters and escaped strings as appropriate + if (quoteCharacter == '`' || quoteCharacter == '"' || quoteCharacter == '\'') { + [returnString replaceOccurrencesOfString:[NSString stringWithFormat:@"%C%C", quoteCharacter, quoteCharacter] withString:[NSString stringWithFormat:@"%C", quoteCharacter] options:0 range:NSMakeRange(0, [returnString length])]; + } + if (quoteCharacter == '"') { + [returnString replaceOccurrencesOfString:@"\\\"" withString:@"\"" options:0 range:NSMakeRange(0, [returnString length])]; + [returnString replaceOccurrencesOfString:@"\\\\" withString:@"\\" options:0 range:NSMakeRange(0, [returnString length])]; + } else if (quoteCharacter == '\'') { + [returnString replaceOccurrencesOfString:@"\\'" withString:@"'" options:0 range:NSMakeRange(0, [returnString length])]; + [returnString replaceOccurrencesOfString:@"\\\\" withString:@"\\" options:0 range:NSMakeRange(0, [returnString length])]; + } + + return returnString; +} + + +/* + * Removes characters from the string up to the first occurrence of the supplied character. + */ +- (BOOL) trimToCharacter:(unichar)character inclusively:(BOOL)inclusive +{ + return [self trimToCharacter:character inclusively:inclusive ignoringQuotedStrings:YES]; +} + + +/* + * As trimToCharacter: ..., but allows control over whether characters within quoted + * strings are ignored. + */ +- (BOOL) trimToCharacter:(unichar)character inclusively:(BOOL)inclusive ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + long stringIndex; + + // Get the first occurrence of the specified character, returning NO if not found + stringIndex = [self firstOccurrenceOfCharacter:character ignoringQuotedStrings:ignoreQuotedStrings]; + if (stringIndex == NSNotFound) return NO; + + // If it has been found, trim the string appropriately and return YES + [string deleteCharactersInRange:NSMakeRange(0, stringIndex + (inclusive?1:0))]; + return YES; +} + + +/* + * Returns an NSString containing characters from the string up to the first occurrence of the supplied character. + */ +- (NSString *) stringToCharacter:(unichar)character inclusively:(BOOL)inclusive +{ + return [self stringToCharacter:character inclusively:inclusive ignoringQuotedStrings:YES]; +} + + +/* + * As stringToCharacter: ..., but allows control over whether characters within quoted strings + * are ignored. + */ +- (NSString *) stringToCharacter:(unichar)character inclusively:(BOOL)inclusive ignoringQuotedStrings:(BOOL)ignoreQuotedStrings { + long stringIndex; + + // Get the first occurrence of the specified character, returning nil if not found + stringIndex = [self firstOccurrenceOfCharacter:character ignoringQuotedStrings:ignoreQuotedStrings]; + if (stringIndex == NSNotFound) return nil; + + // If it has been found, return the appropriate string range + return [string substringWithRange:NSMakeRange(0, stringIndex + (inclusive?1:0))]; +} + + +/* + * Returns an NSString containing characters from the string up to the first occurrence of the supplied + * character, also removing them from the string. + */ +- (NSString *) trimAndReturnStringToCharacter:(unichar)character trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn +{ + return [self trimAndReturnStringToCharacter:character trimmingInclusively:inclusiveTrim returningInclusively:inclusiveReturn ignoringQuotedStrings:YES]; +} + + +/* + * As trimAndReturnStringToCharacter: ..., but allows control over whether characters within quoted + * strings are ignored. + */ +- (NSString *) trimAndReturnStringToCharacter:(unichar)character trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + long stringIndex; + NSString *resultString; + + // Get the first occurrence of the specified character, returning nil if it could not be found + stringIndex = [self firstOccurrenceOfCharacter:character ignoringQuotedStrings:ignoreQuotedStrings]; + if (stringIndex == NSNotFound) return nil; + + // Select the appropriate string range, truncate the current string, and return the selected string + resultString = [NSString stringWithString:[string substringWithRange:NSMakeRange(0, stringIndex + (inclusiveReturn?1:0))]]; + [string deleteCharactersInRange:NSMakeRange(0, stringIndex + (inclusiveTrim?1:0))]; + return resultString; +} + + +/* + * Returns characters from the string up to and from the first occurrence of the supplied opening character + * to the appropriate occurrence of the supplied closing character. "inclusively" controls whether the supplied + * characters should also be returned. + */ +- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive +{ + return [self stringFromCharacter:fromCharacter toCharacter:toCharacter inclusively:inclusive skippingBrackets:NO ignoringQuotedStrings:YES]; +} + + +/* + * As stringFromCharacter: toCharacter: ..., but allows control over whether to skip + * over bracket-enclosed characters, as in subqueries, enums, definitions or groups + */ +- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive skippingBrackets:(BOOL)skipBrackets +{ + return [self stringFromCharacter:fromCharacter toCharacter:toCharacter inclusively:inclusive skippingBrackets:skipBrackets ignoringQuotedStrings:YES]; +} + + +/* + * As stringFromCharacter: toCharacter: ..., but allows control over whether characters within quoted + * strings are ignored. + */ +- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + return [self stringFromCharacter:fromCharacter toCharacter:toCharacter inclusively:inclusive skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings]; +} + + +/* + * As stringFromCharacter: toCharacter: ..., but allows control over both bracketing and quoting. + */ +- (NSString *) stringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter inclusively:(BOOL)inclusive skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + long fromCharacterIndex, toCharacterIndex; + + // Look for the first occurrence of the from: character + fromCharacterIndex = [self firstOccurrenceOfCharacter:fromCharacter afterIndex:-1 skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings]; + if (fromCharacterIndex == NSNotFound) return nil; + + // Look for the first/balancing occurrence of the to: character + toCharacterIndex = [self firstOccurrenceOfCharacter:toCharacter afterIndex:fromCharacterIndex skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings]; + if (toCharacterIndex == NSNotFound) return nil; + + // Return the correct part of the string. + return [string substringWithRange:NSMakeRange(fromCharacterIndex + (inclusive?0:1), toCharacterIndex + (inclusive?1:-1) - fromCharacterIndex)]; +} + + +/* + * As stringFromCharacter: toCharacter: ..., but also trims the string up to the "to" character and + * up to or including the "from" character, depending on whether "trimmingInclusively" is set. + */ +- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn +{ + return [self trimAndReturnStringFromCharacter:fromCharacter toCharacter:toCharacter trimmingInclusively:inclusiveTrim returningInclusively:inclusiveReturn skippingBrackets:NO ignoringQuotedStrings:YES]; +} + + +/* + * As trimAndReturnStringFromCharacter: toCharacter: ..., but allows control over whether to + * skip over bracket-enclosed characters, as in subqueries, enums, definitions or groups. + */ +- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn skippingBrackets:(BOOL)skipBrackets +{ + return [self trimAndReturnStringFromCharacter:fromCharacter toCharacter:toCharacter trimmingInclusively:inclusiveTrim returningInclusively:inclusiveReturn skippingBrackets:skipBrackets ignoringQuotedStrings:YES]; +} + + +/* + * As trimAndReturnStringFromCharacter: toCharacter: ..., but allows control over whether characters + * within quoted strings are ignored. + */ +- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + return [self trimAndReturnStringFromCharacter:fromCharacter toCharacter:toCharacter trimmingInclusively:inclusiveTrim returningInclusively:inclusiveReturn skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings]; +} + + +/* + * As trimAndReturnStringFromCharacter: toCharacter: ..., but allows control over both bracketing + * and quoting. + */ +- (NSString *) trimAndReturnStringFromCharacter:(unichar)fromCharacter toCharacter:(unichar)toCharacter trimmingInclusively:(BOOL)inclusiveTrim returningInclusively:(BOOL)inclusiveReturn skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + long fromCharacterIndex, toCharacterIndex; + NSString *resultString; + + // Look for the first occurrence of the from: character + fromCharacterIndex = [self firstOccurrenceOfCharacter:fromCharacter afterIndex:-1 skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings]; + if (fromCharacterIndex == NSNotFound) return nil; + + // Look for the first/balancing occurrence of the to: character + toCharacterIndex = [self firstOccurrenceOfCharacter:toCharacter afterIndex:fromCharacterIndex skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings]; + if (toCharacterIndex == NSNotFound) return nil; + + // Select the correct part of the string, truncate the current string, and return the selected string. + resultString = [string substringWithRange:NSMakeRange(fromCharacterIndex + (inclusiveReturn?0:1), toCharacterIndex + (inclusiveReturn?1:-1) - fromCharacterIndex)]; + [string deleteCharactersInRange:NSMakeRange(fromCharacterIndex + (inclusiveTrim?0:1), toCharacterIndex + (inclusiveTrim?1:-1) - fromCharacterIndex)]; + return resultString; +} + +/* + * Split a string on the boundaries formed by the supplied character, returning an array of strings. + */ +- (NSArray *) splitStringByCharacter:(unichar)character +{ + return [self splitStringByCharacter:character skippingBrackets:NO ignoringQuotedStrings:YES]; +} + +/* + * As splitStringByCharacter: ..., but allows control over whether to skip over bracket-enclosed + * characters, as in subqueries, enums, definitions or groups. + */ +- (NSArray *) splitStringByCharacter:(unichar)character skippingBrackets:(BOOL)skipBrackets +{ + return [self splitStringByCharacter:character skippingBrackets:skipBrackets ignoringQuotedStrings:YES]; +} + + +/* + * As splitStringByCharacter:, but allows control over whether characters + * within quoted strings are ignored. + */ +- (NSArray *) splitStringByCharacter:(unichar)character ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + return [self splitStringByCharacter:character skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings]; +} + + +/* + * As splitStringByCharacter: ..., but allows control over both bracketing and quoting. + */ +- (NSArray *) splitStringByCharacter:(unichar)character skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + NSMutableArray *resultsArray = [NSMutableArray array]; + long stringIndex = -1, nextIndex = 0; + + // Walk through the string finding the character to split by, and add non-zero length strings. + while (1) { + nextIndex = [self firstOccurrenceOfCharacter:character afterIndex:stringIndex skippingBrackets:skipBrackets ignoringQuotedStrings:ignoreQuotedStrings]; + if (nextIndex == NSNotFound) { + break; + } + + if (nextIndex - stringIndex - 1 > 0) { + [resultsArray addObject:[string substringWithRange:NSMakeRange(stringIndex+1, nextIndex - stringIndex - 1)]]; + } + stringIndex = nextIndex; + } + + // Add the end of the string after the previously matched character where appropriate. + if (stringIndex + 1 < [string length]) { + [resultsArray addObject:[string substringWithRange:NSMakeRange(stringIndex+1, [string length] - stringIndex - 1)]]; + } + + return resultsArray; +} + + +/* + * A method intended for use by the functions above. + */ +- (long) firstOccurrenceOfCharacter:(unichar)character ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + return [self firstOccurrenceOfCharacter:character afterIndex:-1 skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings]; +} + + +/* + * A method intended for use by the functions above. + */ +- (long) firstOccurrenceOfCharacter:(unichar)character afterIndex:(long)startIndex ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + return [self firstOccurrenceOfCharacter:character afterIndex:startIndex skippingBrackets:NO ignoringQuotedStrings:ignoreQuotedStrings]; +} + + +/* + * A method intended for use by the functions above. + */ +- (long) firstOccurrenceOfCharacter:(unichar)character afterIndex:(long)startIndex skippingBrackets:(BOOL)skipBrackets ignoringQuotedStrings:(BOOL)ignoreQuotedStrings +{ + long currentStringIndex, quotedStringEndIndex; + unichar currentCharacter; + long stringLength = [string length]; + int bracketingLevel = 0; + + // Sanity check inputs + if (startIndex < -1) startIndex = -1; + + // Walk along the string, processing characters + for (currentStringIndex = startIndex + 1; currentStringIndex < stringLength; currentStringIndex++) { + currentCharacter = [string characterAtIndex:currentStringIndex]; + + // Check for the ending character, and if it has been found and quoting/brackets is valid, return. + if (currentCharacter == character) { + if (!skipBrackets || bracketingLevel <= 0) { + return currentStringIndex; + } + } + + // Process strings and comments as appropriate + switch (currentCharacter) { + + // When quote characters are encountered and strings are not being ignored, walk to the end of the quoted string. + case '\'': + case '"': + case '`': + if (!ignoreQuotedStrings) break; + quotedStringEndIndex = [self endIndexOfStringQuotedByCharacter:currentCharacter startingAtIndex:currentStringIndex+1]; + if (quotedStringEndIndex == NSNotFound) { + return NSNotFound; + } + currentStringIndex = quotedStringEndIndex; + break; + + // For opening brackets increment the bracket count + case '(': + bracketingLevel++; + break; + + // For closing brackets decrement the bracket count + case ')': + bracketingLevel--; + + // For comments starting "--[\s]", ensure the start syntax is valid before proceeding. + case '-': + if (stringLength < currentStringIndex + 2) break; + if ([string characterAtIndex:currentStringIndex+1] != '-') break; + if (![[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:[string characterAtIndex:currentStringIndex+2]]) break; + currentStringIndex = [self endIndexOfCommentOfType:SPDoubleDashComment startingAtIndex:currentStringIndex]; + break; + + case '#': + currentStringIndex = [self endIndexOfCommentOfType:SPHashComment startingAtIndex:currentStringIndex]; + break; + + // For comments starting "/*", ensure the start syntax is valid before proceeding. + case '/': + if (stringLength < currentStringIndex + 1) break; + if ([string characterAtIndex:currentStringIndex+1] != '*') break; + currentStringIndex = [self endIndexOfCommentOfType:SPCStyleComment startingAtIndex:currentStringIndex]; + break; + } + } + + // If no matches have been made in this string, return NSNotFound. + return NSNotFound; +} + +/* + * A method intended for use by the functions above. + */ +- (long) endIndexOfStringQuotedByCharacter:(unichar)quoteCharacter startingAtIndex:(long)index +{ + long currentStringIndex, stringLength, i, quotedStringLength; + BOOL characterIsEscaped; + unichar currentCharacter; + + stringLength = [string length]; + + // Walk the string looking for the string end + for ( currentStringIndex = index; currentStringIndex < stringLength; currentStringIndex++) { + currentCharacter = [string characterAtIndex:currentStringIndex]; + + // If the string end is a backtick and one has been encountered, treat it as end of string + if (quoteCharacter == '`' && currentCharacter == '`') { + + // ...as long as the next character isn't also a backtick, in which case it's being quoted. Skip both. + if ((currentStringIndex + 1) < stringLength && [string characterAtIndex:currentStringIndex+1] == '`') { + currentStringIndex++; + continue; + } + + return currentStringIndex; + + // Otherwise, prepare to treat the string as ended when meeting the correct boundary character.... + } else if (currentCharacter == quoteCharacter) { + + // ...but only if the string end isn't escaped with an *odd* number of escaping characters... + characterIsEscaped = NO; + i = 1; + quotedStringLength = currentStringIndex - 1; + while ((quotedStringLength - i) > 0 && [string characterAtIndex:currentStringIndex - i] == '\\') { + characterIsEscaped = !characterIsEscaped; + i++; + } + + // If an even number have been found, it may be the end of the string - as long as the subsequent character + // isn't also the same character, in which case it's another form of escaping. + if (!characterIsEscaped) { + if ((currentStringIndex + 1) < stringLength && [string characterAtIndex:currentStringIndex+1] == quoteCharacter) { + currentStringIndex++; + continue; + } + + // Really is the end of the string. + return currentStringIndex; + } + } + } + + return NSNotFound; +} + +/* + * A method intended for use by the functions above. + */ +- (long) endIndexOfCommentOfType:(SPCommentType)commentType startingAtIndex:(long)index +{ + long stringLength = [string length]; + unichar currentCharacter; + + switch (commentType) { + + // For comments of type "--[\s]", start the comment processing two characters in to match the start syntax, + // then flow into the Hash comment handling (looking for first newline). + case SPDoubleDashComment: + index = index+2; + + // For comments starting "--[\s]" and "#", continue until the first newline. + case SPHashComment: + index++; + for ( ; index < stringLength; index++ ) { + currentCharacter = [string characterAtIndex:index]; + if (currentCharacter == '\r' || currentCharacter == '\n') { + return index-1; + } + } + break; + + // For comments starting "/*", start the comment processing one character in to match the start syntax, then + // continue until the first matching "*/". + case SPCStyleComment: + index = index+2; + for ( ; index < stringLength; index++ ) { + if ([string characterAtIndex:index] == '*') { + if ((stringLength > index + 1) && [string characterAtIndex:index+1] == '/') { + return (index+1); + } + } + } + } + + // If no match has been found, the comment must continue until the very end of the string. + return (stringLength-1); +} + + +/* Required and primitive methods to allow subclassing class cluster */ +#pragma mark - +- (id) init { + if (self = [super init]) { + string = [[NSMutableString string] retain]; + } + return self; +} +- (id) initWithBytes:(const void *)bytes length:(unsigned int)length encoding:(NSStringEncoding)encoding { + if (self = [super init]) { + string = [[NSMutableString alloc] initWithBytes:bytes length:length encoding:encoding]; + } + return self; +} +- (id) initWithBytesNoCopy:(void *)bytes length:(unsigned int)length encoding:(NSStringEncoding)encoding freeWhenDone:(BOOL)flag { + if (self = [super init]) { + string = [[NSMutableString alloc] initWithBytesNoCopy:bytes length:length encoding:encoding freeWhenDone:flag]; + } + return self; +} +- (id) initWithCapacity:(unsigned int)capacity { + if (self = [super init]) { + string = [[NSMutableString stringWithCapacity:capacity] retain]; + } + return self; +} +- (id) initWithCharactersNoCopy:(unichar *)characters length:(unsigned int)length freeWhenDone:(BOOL)flag { + if (self = [super init]) { + string = [[NSMutableString alloc] initWithCharactersNoCopy:characters length:length freeWhenDone:flag]; + } + return self; +} +- (id) initWithContentsOfFile:(id)path { + return [self initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL]; +} +- (id) initWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)encoding error:(NSError **)error { + if (self = [super init]) { + string = [[NSMutableString alloc] initWithContentsOfFile:path encoding:encoding error:error]; + } + return self; +} +- (id) initWithCString:(const char *)nullTerminatedCString encoding:(NSStringEncoding)encoding { + if (self = [super init]) { + string = [[NSMutableString alloc] initWithCString:nullTerminatedCString encoding:encoding]; + } + return self; +} +- (id) initWithFormat:(NSString *)format, ... { + va_list argList; + va_start(argList, format); + id str = [self initWithFormat:format arguments:argList]; + va_end(argList); + return str; +} +- (id) initWithFormat:(NSString *)format arguments:(va_list)argList { + if (self = [super init]) { + string = [[NSMutableString alloc] initWithFormat:format arguments:argList]; + } + return self; +} +- (unsigned int) length { + return [string length]; +} +- (unichar) characterAtIndex:(unsigned int)index { + return [string characterAtIndex:index]; +} +- (id) description { + return [string description]; +} +- (unsigned int) replaceOccurrencesOfString:(NSString *)target withString:(NSString *)replacement options:(unsigned)options range:(NSRange)searchRange { + return [string replaceOccurrencesOfString:target withString:replacement options:options range:searchRange]; +} +- (void) setString:(NSString *)aString { + [string setString:aString]; +} +- (void) replaceCharactersInRange:(NSRange)range withString:(NSString *)aString { + [string replaceCharactersInRange:range withString:aString]; +} +- (void) dealloc { + [string release]; + [super dealloc]; +} +@end
\ No newline at end of file |