From: Joseph Spiros Date: Sun, 1 Mar 2009 10:35:43 +0000 (-0500) Subject: Implemented searching notification for characters which should use the AppleGothic... X-Git-Tag: v1.0~4 X-Git-Url: http://git.ithinksw.org/GrowlITTSW.git/commitdiff_plain/2802d8d58f43f9b0b5c01c0c5ade24fdbecf17c7?ds=inline Implemented searching notification for characters which should use the AppleGothic font, and the status window now positions itself according to global Growl positioning preferences. --- diff --git a/English.lproj/GrowlITTSWPrefs.nib/info.nib b/English.lproj/GrowlITTSWPrefs.nib/info.nib index 5155f4a..61d25e2 100644 --- a/English.lproj/GrowlITTSWPrefs.nib/info.nib +++ b/English.lproj/GrowlITTSWPrefs.nib/info.nib @@ -5,12 +5,12 @@ IBFramework Version 672 IBLastKnownRelativeProjectPath - ../../../../Growl.xcodeproj + ../GrowlITTSW.xcodeproj IBOldestOS 3 IBOpenObjects - 6 + 244 IBSystem Version 9G55 diff --git a/English.lproj/GrowlITTSWPrefs.nib/keyedobjects.nib b/English.lproj/GrowlITTSWPrefs.nib/keyedobjects.nib index 8994fc5..d017b7d 100644 Binary files a/English.lproj/GrowlITTSWPrefs.nib/keyedobjects.nib and b/English.lproj/GrowlITTSWPrefs.nib/keyedobjects.nib differ diff --git a/GrowlITTSW.xcodeproj/project.pbxproj b/GrowlITTSW.xcodeproj/project.pbxproj index 479620c..d2fc688 100644 --- a/GrowlITTSW.xcodeproj/project.pbxproj +++ b/GrowlITTSW.xcodeproj/project.pbxproj @@ -11,13 +11,15 @@ 8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */; }; FA4A47250F5A3C2A00F37A2B /* GrowlITTSWDisplay.m in Sources */ = {isa = PBXBuildFile; fileRef = FA4A471E0F5A3C2A00F37A2B /* GrowlITTSWDisplay.m */; }; FA4A47270F5A3C2A00F37A2B /* GrowlITTSWController.m in Sources */ = {isa = PBXBuildFile; fileRef = FA4A47220F5A3C2A00F37A2B /* GrowlITTSWController.m */; }; - FA4A47280F5A3C2A00F37A2B /* GrowlITTSWWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = FA4A47240F5A3C2A00F37A2B /* GrowlITTSWWindow.m */; }; FA4A47400F5A3C7100F37A2B /* ITKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA4A473D0F5A3C6900F37A2B /* ITKit.framework */; }; FA4A47410F5A3C7300F37A2B /* ITFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA4A47340F5A3C5900F37A2B /* ITFoundation.framework */; }; FA4A47510F5A3CFC00F37A2B /* GrowlITTSWPrefs.nib in Resources */ = {isa = PBXBuildFile; fileRef = FA4A474F0F5A3CFC00F37A2B /* GrowlITTSWPrefs.nib */; }; FA5074430F5A56DA008DEAD9 /* ITFoundation.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = FA4A47340F5A3C5900F37A2B /* ITFoundation.framework */; }; FA5074440F5A56DD008DEAD9 /* ITKit.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = FA4A473D0F5A3C6900F37A2B /* ITKit.framework */; }; FA50774D0F5A7A2D008DEAD9 /* PreferencePanes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA50774C0F5A7A2D008DEAD9 /* PreferencePanes.framework */; }; + FA7C86AB0F5A936900213B2E /* RegexKitLite.m in Sources */ = {isa = PBXBuildFile; fileRef = FA7C86A90F5A936900213B2E /* RegexKitLite.m */; }; + FA7C86D00F5A93DF00213B2E /* libicucore.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = FA7C86CF0F5A93DF00213B2E /* libicucore.dylib */; }; + FA7C87DA0F5A9C3400213B2E /* GrowlITTSWWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = FA4A47240F5A3C2A00F37A2B /* GrowlITTSWWindow.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -98,6 +100,10 @@ FA5073AA0F5A4D9A008DEAD9 /* GrowlApplicationNotification.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GrowlApplicationNotification.h; sourceTree = ""; }; FA5074060F5A5562008DEAD9 /* GrowlDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GrowlDefines.h; sourceTree = ""; }; FA50774C0F5A7A2D008DEAD9 /* PreferencePanes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PreferencePanes.framework; path = /System/Library/Frameworks/PreferencePanes.framework; sourceTree = ""; }; + FA7C86A90F5A936900213B2E /* RegexKitLite.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RegexKitLite.m; sourceTree = ""; }; + FA7C86AA0F5A936900213B2E /* RegexKitLite.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RegexKitLite.h; sourceTree = ""; }; + FA7C86CF0F5A93DF00213B2E /* libicucore.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libicucore.dylib; path = /usr/lib/libicucore.dylib; sourceTree = ""; }; + FA7C87E80F5A9FCB00213B2E /* GrowlPositioningDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GrowlPositioningDefines.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -109,6 +115,7 @@ FA4A47400F5A3C7100F37A2B /* ITKit.framework in Frameworks */, FA4A47410F5A3C7300F37A2B /* ITFoundation.framework in Frameworks */, FA50774D0F5A7A2D008DEAD9 /* PreferencePanes.framework in Frameworks */, + FA7C86D00F5A93DF00213B2E /* libicucore.dylib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -166,6 +173,7 @@ 1058C7ACFEA557BF11CA2CBB /* Linked Frameworks */ = { isa = PBXGroup; children = ( + FA7C86CF0F5A93DF00213B2E /* libicucore.dylib */, 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */, FA50774C0F5A7A2D008DEAD9 /* PreferencePanes.framework */, ); @@ -193,8 +201,11 @@ 32C88E010371C26100C91783 /* Other Sources */ = { isa = PBXGroup; children = ( + FA7C86AA0F5A936900213B2E /* RegexKitLite.h */, + FA7C86A90F5A936900213B2E /* RegexKitLite.m */, FA5074060F5A5562008DEAD9 /* GrowlDefines.h */, FA50739B0F5A4D74008DEAD9 /* GrowlDefinesInternal.h */, + FA7C87E80F5A9FCB00213B2E /* GrowlPositioningDefines.h */, FA5073AA0F5A4D9A008DEAD9 /* GrowlApplicationNotification.h */, FA4A472B0F5A3C3100F37A2B /* GrowlPlugin.h */, FA4A472C0F5A3C3100F37A2B /* GrowlDisplayPlugin.h */, @@ -331,7 +342,8 @@ files = ( FA4A47250F5A3C2A00F37A2B /* GrowlITTSWDisplay.m in Sources */, FA4A47270F5A3C2A00F37A2B /* GrowlITTSWController.m in Sources */, - FA4A47280F5A3C2A00F37A2B /* GrowlITTSWWindow.m in Sources */, + FA7C86AB0F5A936900213B2E /* RegexKitLite.m in Sources */, + FA7C87DA0F5A9C3400213B2E /* GrowlITTSWWindow.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/GrowlITTSWController.m b/GrowlITTSWController.m index 0f1cd0e..3cf14a3 100644 --- a/GrowlITTSWController.m +++ b/GrowlITTSWController.m @@ -7,11 +7,18 @@ // #import "GrowlITTSWController.h" -#import "GrowlITTSWWindow.h" #import #import +#import "RegexKitLite.h" + +#import "GrowlPositioningDefines.h" + +@interface GrowlPositionController ++ (enum GrowlPosition)selectedOriginPosition; +@end + @implementation NSImage (SmoothAdditions) - (NSImage *)imageScaledSmoothlyToSize:(NSSize)scaledSize @@ -46,9 +53,6 @@ [_window setExitMode:ITTransientStatusWindowExitAfterDelay]; [_window setExitDelay:4.0]; - [_window setHorizontalPosition:ITWindowPositionRight]; - [_window setVerticalPosition:ITWindowPositionTop]; - [_window setSizing:ITTransientStatusWindowMini]; [_window setEntryEffect:[[[NSClassFromString(@"ITSlideVerticallyWindowEffect") alloc] initWithWindow:_window] autorelease]]; @@ -72,7 +76,6 @@ - (void)showWindowWithText:(NSString *)text image:(NSImage *)image { - NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text]; NSSize newSize; NSSize oldSize = [image size]; @@ -84,6 +87,57 @@ image = [[[[NSImage alloc] initWithData:[image TIFFRepresentation]] autorelease] imageScaledSmoothlyToSize:newSize]; + NSArray *gothicChars = [NSArray arrayWithObjects:[NSString stringWithUTF8String:"☆"], [NSString stringWithUTF8String:"★"], nil]; + NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text]; + + if (([gothicChars count] > 0) && ([text length] > 0)) { + NSMutableString *gothicRegex = [[NSMutableString alloc] init]; + + [gothicRegex appendString:@"["]; + for (NSString *gothicChar in gothicChars) { + [gothicRegex appendString:gothicChar]; + } + [gothicRegex appendString:@"]+"]; + + NSUInteger endOfLastRange = 0; + NSRange foundRange; + while (endOfLastRange != NSNotFound) { + foundRange = [text rangeOfRegex:gothicRegex inRange:NSMakeRange(endOfLastRange, ([text length] - endOfLastRange))]; + if (foundRange.location != NSNotFound) { + [attributedText setAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[NSFont fontWithName:@"AppleGothic" size:(18.0 / MINI_DIVISOR)], NSFontAttributeName, nil, nil] range:foundRange]; + endOfLastRange = foundRange.location+foundRange.length; + if (endOfLastRange >= [text length]) { + endOfLastRange = NSNotFound; + } + } else { + endOfLastRange = NSNotFound; + } + } + } + + switch ([GrowlPositionController selectedOriginPosition]) { + case GrowlMiddleColumnPosition: + [_window setVerticalPosition:ITWindowPositionMiddle]; + [_window setHorizontalPosition:ITWindowPositionCenter]; + break; + case GrowlTopLeftPosition: + [_window setVerticalPosition:ITWindowPositionTop]; + [_window setHorizontalPosition:ITWindowPositionLeft]; + break; + case GrowlBottomRightPosition: + [_window setVerticalPosition:ITWindowPositionBottom]; + [_window setHorizontalPosition:ITWindowPositionRight]; + break; + case GrowlTopRightPosition: + [_window setVerticalPosition:ITWindowPositionTop]; + [_window setHorizontalPosition:ITWindowPositionRight]; + break; + case GrowlBottomLeftPosition: + [_window setVerticalPosition:ITWindowPositionBottom]; + [_window setHorizontalPosition:ITWindowPositionLeft]; + break; + } + [_window setImage:image]; [_window buildTextWindowWithString:attributedText]; [_window appear:self]; diff --git a/GrowlITTSWWindow.m b/GrowlITTSWWindow.m index 0616224..db280be 100644 --- a/GrowlITTSWWindow.m +++ b/GrowlITTSWWindow.m @@ -60,6 +60,9 @@ - (void)setImage:(NSImage *)newImage { + if (!newImage) { + newImage = [NSImage imageNamed:@"NSApplicationIcon"]; + } [_image autorelease]; _image = [newImage copy]; } diff --git a/GrowlPositioningDefines.h b/GrowlPositioningDefines.h new file mode 100644 index 0000000..d2b04e0 --- /dev/null +++ b/GrowlPositioningDefines.h @@ -0,0 +1,60 @@ +/*! @header GrowlPositioningDefines.h +* @abstract Defines all the positioning-related enumerators. +* @discussion Defines all the positioning-related enumerators for position, +expansion, and origin selection. +* @updated 2006-11-24 +*/ + +/*! +* @typedef GrowlPosition + * @abstract Represents a general position on the screen for display plugins. + * + * @constant GrowlTopLeftPosition The top left square of the screen. + * @constant GrowlTopMiddlePosition The top middle square of the screen. + * @constant GrowlTopRightPosition The top right square of the screen. + * @constant GrowlCenterLeftPosition The center left square of the screen. + * @constant GrowlCenterMiddlePosition The center middle square of the screen. + * @constant GrowlCenterRightPosition The center right square of the screen. + * @constant GrowlBottomLeftPosition The bottom left square of the screen. + * @constant GrowlBottomMiddlePosition The bottom left middle of the screen. + * @constant GrowlBottomRightPosition The bottom right square of the screen. + * @constant GrowlTopRowPosition The top oblong (row) of the screen. + * @constant GrowlCenterRowPosition The center oblong (row) of the screen. + * @constant GrowlBottomRowPosition The bottom oblong (row) of the screen. + * @constant GrowlLeftColumnPosition The left oblong (column) of the screen. + * @constant GrowlMiddleColumnPosition The middle oblong (column) of the screen. + * @constant GrowlRightColumnPosition The right oblong (column) of the screen. + */ +enum GrowlPosition { + GrowlTopLeftPosition, + GrowlTopMiddlePosition, + GrowlTopRightPosition, + GrowlCenterLeftPosition, + GrowlCenterMiddlePosition, + GrowlCenterRightPosition, + GrowlBottomLeftPosition, + GrowlBottomMiddlePosition, + GrowlBottomRightPosition, + GrowlTopRowPosition, + GrowlCenterRowPosition, + GrowlBottomRowPosition, + GrowlLeftColumnPosition, + GrowlMiddleColumnPosition, + GrowlRightColumnPosition +}; + +enum GrowlExpansionDirection { + GrowlNoExpansionDirection, + GrowlDownExpansionDirection, + GrowlUpExpansionDirection, + GrowlLeftExpansionDirection, + GrowlRightExpansionDirection +}; + +enum GrowlPositionOrigin { + GrowlNoOrigin, + GrowlTopLeftCorner, + GrowlBottomRightCorner, + GrowlTopRightCorner, + GrowlBottomLeftCorner +}; diff --git a/RegexKitLite.h b/RegexKitLite.h new file mode 100644 index 0000000..f69021b --- /dev/null +++ b/RegexKitLite.h @@ -0,0 +1,186 @@ +// +// RegexKitLite.h +// http://regexkit.sourceforge.net/ +// Licensed under the terms of the BSD License, as specified below. +// + +/* + Copyright (c) 2008, John Engelhart + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the Zang Industries nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#ifdef __OBJC__ +#import +#import +#import +#endif // __OBJC__ + +#include +#include +#include + +#ifndef REGEXKITLITE_VERSION_DEFINED +#define REGEXKITLITE_VERSION_DEFINED + +#define REGEXKITLITE_VERSION_MAJOR 2 +#define REGEXKITLITE_VERSION_MINOR 2 + +#define REGEXKITLITE_VERSION_CSTRING _RKL_VERSION_STRING(REGEXKITLITE_VERSION_MAJOR, REGEXKITLITE_VERSION_MINOR) +#define REGEXKITLITE_VERSION_NSSTRING @REGEXKITLITE_VERSION_CSTRING + +#define _RKL__STRINGIFY(b) #b +#define _RKL_STRINGIFY(a) _RKL__STRINGIFY(a) +#define _RKL_JOIN_VERSION(a,b) _RKL_STRINGIFY(a##.##b) +#define _RKL_VERSION_STRING(a,b) _RKL_JOIN_VERSION(a,b) + +#endif // REGEXKITLITE_VERSION_DEFINED + +#ifdef __cplusplus +extern "C" { +#endif + +// For Mac OS X < 10.5. +#ifndef NSINTEGER_DEFINED +#define NSINTEGER_DEFINED +#ifdef __LP64__ || NS_BUILD_32_LIKE_64 +typedef long NSInteger; +typedef unsigned long NSUInteger; +#define NSIntegerMin LONG_MIN +#define NSIntegerMax LONG_MAX +#define NSUIntegerMax ULONG_MAX +#else // 32-bit +typedef int NSInteger; +typedef unsigned int NSUInteger; +#define NSIntegerMin INT_MIN +#define NSIntegerMax INT_MAX +#define NSUIntegerMax UINT_MAX +#endif // __LP64__ || NS_BUILD_32_LIKE_64 +#endif // NSINTEGER_DEFINED + +#ifndef RKLREGEXOPTIONS_DEFINED +#define RKLREGEXOPTIONS_DEFINED + +// These must be idential to their ICU regex counterparts. See http://www.icu-project.org/userguide/regexp.html +enum { + RKLNoOptions = 0, + RKLCaseless = 2, + RKLComments = 4, + RKLDotAll = 32, + RKLMultiline = 8, + RKLUnicodeWordBoundaries = 256 +}; +typedef uint32_t RKLRegexOptions; + +#endif // RKLREGEXOPTIONS_DEFINED + +#ifndef _REGEXKITLITE_H_ +#define _REGEXKITLITE_H_ + +#ifdef __OBJC__ + +@class NSError; + +// NSException exception name. +extern NSString * const RKLICURegexException; + +// NSError error domains and user info keys. +extern NSString * const RKLICURegexErrorDomain; + +extern NSString * const RKLICURegexErrorCodeErrorKey; +extern NSString * const RKLICURegexErrorNameErrorKey; +extern NSString * const RKLICURegexLineErrorKey; +extern NSString * const RKLICURegexOffsetErrorKey; +extern NSString * const RKLICURegexPreContextErrorKey; +extern NSString * const RKLICURegexPostContextErrorKey; +extern NSString * const RKLICURegexRegexErrorKey; +extern NSString * const RKLICURegexRegexOptionsErrorKey; + +// If it looks like low memory notifications might be available, add code to register and respond to them. +// This is (should be) harmless if it turns out that this isn't the case, since the notification that we register for, +// UIApplicationDidReceiveMemoryWarningNotification, is dynamically looked up via dlsym(). +#if (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE) && (!defined(RKL_REGISTER_FOR_IPHONE_LOWMEM_NOTIFICATIONS) || (RKL_REGISTER_FOR_IPHONE_LOWMEM_NOTIFICATIONS != 0)) +#define RKL_REGISTER_FOR_IPHONE_LOWMEM_NOTIFICATIONS 1 +#endif + +#ifdef RKL_PREPEND_TO_METHODS +// This requires a few levels of rewriting to get the desired results. +#define RKL_METHOD_PREPEND_2(c,d) c ## d +#define RKL_METHOD_PREPEND_1(a,b) RKL_METHOD_PREPEND_2(a,b) +#define RKL_METHOD_PREPEND(x) RKL_METHOD_PREPEND_1(RKL_PREPEND_TO_METHODS, x) +#else +#define RKL_METHOD_PREPEND(x) x +#endif + +@interface NSString (RegexKitLiteAdditions) + ++ (void)RKL_METHOD_PREPEND(clearStringCache); + ++ (NSInteger)RKL_METHOD_PREPEND(captureCountForRegex):(NSString *)regex; ++ (NSInteger)RKL_METHOD_PREPEND(captureCountForRegex):(NSString *)regex options:(RKLRegexOptions)options error:(NSError **)error; + +- (NSArray *)RKL_METHOD_PREPEND(componentsSeparatedByRegex):(NSString *)regex; +- (NSArray *)RKL_METHOD_PREPEND(componentsSeparatedByRegex):(NSString *)regex range:(NSRange)range; +- (NSArray *)RKL_METHOD_PREPEND(componentsSeparatedByRegex):(NSString *)regex options:(RKLRegexOptions)options range:(NSRange)range error:(NSError **)error; + +- (BOOL)RKL_METHOD_PREPEND(isMatchedByRegex):(NSString *)regex; +- (BOOL)RKL_METHOD_PREPEND(isMatchedByRegex):(NSString *)regex inRange:(NSRange)range; +- (BOOL)RKL_METHOD_PREPEND(isMatchedByRegex):(NSString *)regex options:(RKLRegexOptions)options inRange:(NSRange)range error:(NSError **)error; + +- (NSRange)RKL_METHOD_PREPEND(rangeOfRegex):(NSString *)regex; +- (NSRange)RKL_METHOD_PREPEND(rangeOfRegex):(NSString *)regex capture:(NSInteger)capture; +- (NSRange)RKL_METHOD_PREPEND(rangeOfRegex):(NSString *)regex inRange:(NSRange)range; +- (NSRange)RKL_METHOD_PREPEND(rangeOfRegex):(NSString *)regex options:(RKLRegexOptions)options inRange:(NSRange)range capture:(NSInteger)capture error:(NSError **)error; + +- (NSString *)RKL_METHOD_PREPEND(stringByMatching):(NSString *)regex; +- (NSString *)RKL_METHOD_PREPEND(stringByMatching):(NSString *)regex capture:(NSInteger)capture; +- (NSString *)RKL_METHOD_PREPEND(stringByMatching):(NSString *)regex inRange:(NSRange)range; +- (NSString *)RKL_METHOD_PREPEND(stringByMatching):(NSString *)regex options:(RKLRegexOptions)options inRange:(NSRange)range capture:(NSInteger)capture error:(NSError **)error; + +- (NSString *)RKL_METHOD_PREPEND(stringByReplacingOccurrencesOfRegex):(NSString *)regex withString:(NSString *)replacement; +- (NSString *)RKL_METHOD_PREPEND(stringByReplacingOccurrencesOfRegex):(NSString *)regex withString:(NSString *)replacement range:(NSRange)searchRange; +- (NSString *)RKL_METHOD_PREPEND(stringByReplacingOccurrencesOfRegex):(NSString *)regex withString:(NSString *)replacement options:(RKLRegexOptions)options range:(NSRange)searchRange error:(NSError **)error; + +@end + +@interface NSMutableString (RegexKitLiteAdditions) + +- (NSUInteger)RKL_METHOD_PREPEND(replaceOccurrencesOfRegex):(NSString *)regex withString:(NSString *)replacement; +- (NSUInteger)RKL_METHOD_PREPEND(replaceOccurrencesOfRegex):(NSString *)regex withString:(NSString *)replacement range:(NSRange)searchRange; +- (NSUInteger)RKL_METHOD_PREPEND(replaceOccurrencesOfRegex):(NSString *)regex withString:(NSString *)replacement options:(RKLRegexOptions)options range:(NSRange)searchRange error:(NSError **)error; + +@end + +#endif // __OBJC__ + +#endif // _REGEXKITLITE_H_ + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/RegexKitLite.m b/RegexKitLite.m new file mode 100644 index 0000000..6bf66af --- /dev/null +++ b/RegexKitLite.m @@ -0,0 +1,1013 @@ +// +// RegexKitLite.m +// http://regexkit.sourceforge.net/ +// Licensed under the terms of the BSD License, as specified below. +// + +/* + Copyright (c) 2008, John Engelhart + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the Zang Industries nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#ifdef __OBJC_GC__ +#import +#endif +#import +#import +#import +#import +#import +#import +#import "RegexKitLite.h" + +// Compile time tuneables. + +#ifndef RKL_CACHE_SIZE +#define RKL_CACHE_SIZE 23 +#endif + +#ifndef RKL_FIXED_LENGTH +#define RKL_FIXED_LENGTH 2048 +#endif + +#ifndef RKL_STACK_LIMIT +#define RKL_STACK_LIMIT (128 * 1024) +#endif + +#define SCRATCH_BUFFERS 4 + +// These macros are nearly identical to their NSCParameterAssert siblings. +// This is required because nearly everything is done while cacheSpinLock is locked. +// We need to safely unlock before throwing any of these exceptions. +// @try {} @finally {} significantly slows things down so it's not used. +#define RKLCAssert(d, ...) RKLCAssertDictionary(__PRETTY_FUNCTION__, __FILE__, __LINE__, (d), ##__VA_ARGS__) +#ifdef NS_BLOCK_ASSERTIONS +#define _RKLCDelayedAssertBody(c, e, g, d, ...) +#else +#define _RKLCDelayedAssertBody(c, e, g, d, ...) do { id *_e=(e); if(*_e!=NULL) { goto g; } if(!(c)) { *_e = RKLCAssert((d), ##__VA_ARGS__); goto g; } } while(0) +#endif // NS_BLOCK_ASSERTIONS +#define RKLCDelayedAssert(c, e, g) _RKLCDelayedAssertBody(c, e, g, @"Invalid parameter not satisfying: %s", #c) + +#define RKLRaiseException(e, f, ...) [[NSException exceptionWithName:(e) reason:RKLStringFromClassAndMethod((self), (_cmd), (f), ##__VA_ARGS__) userInfo:NULL] raise] + +// Ugly macros to keep other parts clean. + +#define NSMaxRange(r) ((r).location + (r).length) +#define NSRangeInsideRange(in, win) (((((in).location - (win).location) <= (win).length) && ((NSMaxRange(in) - (win).location) <= (win).length))) +#define NSEqualRanges(r1, r2) ((((r1).location == (r2).location) && ((r1).length == (r2).length))) +#define NSMakeRange(loc, len) ((NSRange){(NSUInteger)(loc), (NSUInteger)(len)}) +#define CFMakeRange(loc, len) ((CFRange){ (CFIndex)(loc), (CFIndex)(len)}) +#define NSNotFoundRange ((NSRange){NSNotFound, 0 }) +#define NSMaxiumRange ((NSRange){ 0, NSUIntegerMax}) + +#if defined (__GNUC__) && (__GNUC__ >= 4) +#define RKL_PREFETCH(ptr, off) { const char *p = ((const char *)(ptr)) + ((off) + 64); __builtin_prefetch(p); __builtin_prefetch(p + 64); } +#else +#define RKL_PREFETCH(ptr, off) +#endif + +// If the gcc flag -mmacosx-version-min is used with, for example, '=10.2', give a warning that the libicucore.dylib is only available on >= 10.3. +// If you are reading this comment because of this warning, this is to let you know that linking to /usr/lib/libicucore.dylib will cause your executable to fail on < 10.3. +// You will need to build your own version of the ICU library and link to that in order for RegexKitLite to work successfully on < 10.3. This is not simple. + +#if MAC_OS_X_VERSION_MIN_REQUIRED < 1030 +#warning The ICU dynamic shared library, /usr/lib/libicucore.dylib, is only available on Mac OS X 10.3 and later. +#warning You will need to supply a version of the ICU library to use RegexKitLite on Mac OS X 10.2 and earlier. +#endif + +#define RKLGetRangeForCapture(re, s, c, r) ({ int32_t start = uregex_start((re), (int32_t)(c), (s)); if(start == -1) { r = NSNotFoundRange; } else { r.location = (NSUInteger)start; r.length = (NSUInteger)uregex_end((re), (int32_t)(c), (s)) - r.location; } *(s); }) + +// Exported symbols. Exception names, error domains, keys, etc. +NSString * const RKLICURegexException = @"RKLICURegexException"; + +NSString * const RKLICURegexErrorDomain = @"RKLICURegexErrorDomain"; + +NSString * const RKLICURegexErrorCodeErrorKey = @"RKLICURegexErrorCode"; +NSString * const RKLICURegexErrorNameErrorKey = @"RKLICURegexErrorName"; +NSString * const RKLICURegexLineErrorKey = @"RKLICURegexLine"; +NSString * const RKLICURegexOffsetErrorKey = @"RKLICURegexOffset"; +NSString * const RKLICURegexPreContextErrorKey = @"RKLICURegexPreContext"; +NSString * const RKLICURegexPostContextErrorKey = @"RKLICURegexPostContext"; +NSString * const RKLICURegexRegexErrorKey = @"RKLICURegexRegex"; +NSString * const RKLICURegexRegexOptionsErrorKey = @"RKLICURegexRegexOptions"; + +// Type / struct definitions + +typedef struct uregex uregex; // Opaque ICU regex type. + +#define U_BUFFER_OVERFLOW_ERROR 15 + +#define U_PARSE_CONTEXT_LEN 16 + +typedef struct UParseError { + int32_t line; + int32_t offset; + UniChar preContext[U_PARSE_CONTEXT_LEN]; + UniChar postContext[U_PARSE_CONTEXT_LEN]; +} UParseError; + +enum { + RKLSplitOp = 1, + RKLReplaceOp = 2, + RKLRangeOp = 3, + RKLMaskOp = 0xf, + RKLReplaceMutable = 1 << 4, +}; +typedef NSUInteger RKLRegexOp; + +typedef struct { + CFStringRef string; + CFHashCode hash; + CFIndex length; + UniChar *uniChar; +} RKLBuffer; + +typedef struct { + CFStringRef regexString; + RKLRegexOptions options; + uregex *icu_regex; + NSInteger captureCount; + + CFStringRef setToString; + CFHashCode setToHash; + CFIndex setToLength; + NSUInteger setToIsImmutable:1; + NSUInteger setToNeedsConversion:1; + const UniChar *setToUniChar; + NSRange setToRange, lastFindRange, lastMatchRange; + NSUInteger pad[1]; // For 32 bits, this makes the struct 64 bytes exactly, which is good for cache line alignment. +} RKLCacheSlot; + +// ICU functions. See http://www.icu-project.org/apiref/icu4c/uregex_8h.html Tweaked slightly from the originals, but functionally identical. +const char *u_errorName (int32_t status); +int32_t u_strlen (const UniChar *s); +int32_t uregex_appendReplacement (uregex *regexp, const UniChar *replacementText, int32_t replacementLength, UniChar **destBuf, int32_t *destCapacity, int32_t *status); +int32_t uregex_appendTail (uregex *regexp, UniChar **destBuf, int32_t *destCapacity, int32_t *status); +void uregex_close (uregex *regexp); +int32_t uregex_end (uregex *regexp, int32_t groupNum, int32_t *status); +BOOL uregex_find (uregex *regexp, int32_t location, int32_t *status); +BOOL uregex_findNext (uregex *regexp, int32_t *status); +int32_t uregex_groupCount (uregex *regexp, int32_t *status); +uregex *uregex_open (const UniChar *pattern, int32_t patternLength, RKLRegexOptions flags, UParseError *parseError, int32_t *status); +void uregex_reset (uregex *regexp, int32_t newIndex, int32_t *status); +void uregex_setText (uregex *regexp, const UniChar *text, int32_t textLength, int32_t *status); +int32_t uregex_split (uregex *regexp, UniChar *destBuf, int32_t destCapacity, int32_t *requiredCapacity, UniChar *destFields[], int32_t destFieldsCapacity, int32_t *status); +int32_t uregex_start (uregex *regexp, int32_t groupNum, int32_t *status); + + +static RKLCacheSlot *getCachedRegex (NSString *regexString, RKLRegexOptions options, NSError **error, id *exception); +static BOOL setCacheSlotToString (RKLCacheSlot *cacheSlot, const NSRange *range, int32_t *status, id *exception); +static RKLCacheSlot *getCachedRegexSetToString (NSString *regexString, RKLRegexOptions options, NSString *matchString, NSUInteger *matchLengthPtr, NSRange *matchRange, NSError **error, id *exception, int32_t *status); +static id performRegexOp (id self, SEL _cmd, RKLRegexOp doRegexOp, NSString *regexString, RKLRegexOptions options, NSInteger capture, id matchString, NSRange *matchRange, NSString *replacementString, NSError **error, void **result); + +static void rkl_find (RKLCacheSlot *cacheSlot, NSInteger capture, NSRange searchRange, NSRange *resultRange, id *exception, int32_t *status); +static NSArray *rkl_splitArray (RKLCacheSlot *cacheSlot, id *exception, int32_t *status); +static NSString *rkl_replaceString (RKLCacheSlot *cacheSlot, id searchString, NSUInteger searchU16Length, NSString *replacementString, NSUInteger replacementU16Length, NSUInteger *replacedCount, int replaceMutable, id *exception, int32_t *status); +static int32_t rkl_replaceAll (RKLCacheSlot *cacheSlot, const UniChar *replacementUniChar, int32_t replacementU16Length, UniChar *replacedUniChar, int32_t replacedU16Capacity, NSUInteger *replacedCount, id *exception, int32_t *status); + +static void rkl_clearStringCache (void); +static void clearBuffer (RKLBuffer *buffer, int freeDynamicBuffer); +static void clearCacheSlotRegex (RKLCacheSlot *cacheSlot); +static void clearCacheSlotSetTo (RKLCacheSlot *cacheSlot); + +static NSDictionary *userInfoDictionary (NSString *regexString, RKLRegexOptions options, const UParseError *parseError, int status, ...); +static NSError *RKLNSErrorForRegex (NSString *regexString, RKLRegexOptions options, const UParseError *parseError, int status); +static NSException *RKLNSExceptionForRegex (NSString *regexString, RKLRegexOptions options, const UParseError *parseError, int status); +static NSDictionary *RKLCAssertDictionary (const char *function, const char *file, int line, NSString *format, ...); +static NSString *RKLStringFromClassAndMethod(id object, SEL selector, NSString *format, ...); + +#ifdef __OBJC_GC__ +// If compiled with Garbage Collection, we need to be able to do a few things slightly differently. +// The basic premiss is that under GC we use a trampoline function pointer which is set to a _start function to catch the first invocation. +// The _start function checks if GC is running and then overwrites the function pointer with the appropriate routine. Think of it as 'lazy linking'. + +// rkl_collectingEnabled uses objc_getClass() to get the NSGarbageCollector class, which doesn't exist on earlier systems. +// This allows for graceful failure should we find ourselves running on an earlier version of the OS without NSGarbageCollector. +static BOOL rkl_collectingEnabled_first (void); +static BOOL rkl_collectingEnabled_yes (void) { return(YES); } +static BOOL rkl_collectingEnabled_no (void) { return(NO); } +static BOOL(*rkl_collectingEnabled) (void) = rkl_collectingEnabled_first; +static BOOL rkl_collectingEnabled_first (void) { return((([objc_getClass("NSGarbageCollector") defaultCollector]!=NULL) ? (rkl_collectingEnabled=rkl_collectingEnabled_yes) : (rkl_collectingEnabled=rkl_collectingEnabled_no))()); } + +static void *rkl_realloc_first (void **ptr, size_t size, NSUInteger flags); +static void *rkl_realloc_std (void **ptr, size_t size, NSUInteger flags) { flags=flags; /*unused*/ return((*ptr = reallocf(*ptr, size))); } +static void *rkl_realloc_gc (void **ptr, size_t size, NSUInteger flags) { void *p=NULL; if(flags!=0) { p=NSAllocateCollectable((NSUInteger)size,flags); if(*ptr!=NULL) { free(*ptr); *ptr=NULL; } } else { p=*ptr=reallocf(*ptr, size); } return(p); } +static void *(*rkl_realloc) (void **ptr, size_t size, NSUInteger flags) = rkl_realloc_first; +static void *rkl_realloc_first (void **ptr, size_t size, NSUInteger flags) { return(((rkl_collectingEnabled()==YES) ? (rkl_realloc=rkl_realloc_gc) : (rkl_realloc=rkl_realloc_std))(ptr, size, flags)); } + +static id rkl_CFAutorelease_first (CFTypeRef obj); +static id rkl_CFAutorelease_std (CFTypeRef obj) { return([(id)obj autorelease]); } +static id rkl_CFAutorelease_gc (CFTypeRef obj) { return((id)CFMakeCollectable(obj)); } +static id(*rkl_CFAutorelease) (CFTypeRef obj) = rkl_CFAutorelease_first; +static id rkl_CFAutorelease_first (CFTypeRef obj) { return(((rkl_collectingEnabled()==YES) ? (rkl_CFAutorelease=rkl_CFAutorelease_gc) : (rkl_CFAutorelease=rkl_CFAutorelease_std))(obj)); } + +#else // __OBJC_GC__ not defined + +static void *rkl_realloc (void **ptr, size_t size, NSUInteger flags) { flags=flags; /*unused*/ return((*ptr = reallocf(*ptr, size))); } +static id rkl_CFAutorelease (CFTypeRef obj) { return([(id)obj autorelease]); } + +#endif // __OBJC_GC__ + +#ifdef RKL_FAST_MUTABLE_CHECK +// We use a trampoline function pointer to check at run time if the function __CFStringIsMutable is available. +// If it is, the trampoline function pointer is replaced with the address of that function. +// Otherwise, we assume the worst case that ever string is mutable. +// This hopefully helps to protect us since we're using an undocumented, non-public API call. +// We will keep on working if it ever does go away, just with a bit less performance due to the overhead of mutable checks. +static BOOL rkl_CFStringIsMutable_first (CFStringRef str); +static BOOL rkl_CFStringIsMutable_yes (CFStringRef str) { str=str; /*unused*/ return(YES); } +static BOOL(*rkl_CFStringIsMutable) (CFStringRef str) = rkl_CFStringIsMutable_first; +static BOOL rkl_CFStringIsMutable_first (CFStringRef str) { if((rkl_CFStringIsMutable = dlsym(RTLD_DEFAULT, "__CFStringIsMutable")) == NULL) { rkl_CFStringIsMutable = rkl_CFStringIsMutable_yes; } return(rkl_CFStringIsMutable(str)); } +#else // RKL_FAST_MUTABLE_CHECK is not defined. Assume that all strings are potentially mutable. +#define rkl_CFStringIsMutable(s) (YES) +#endif +BOOL __CFStringIsMutable(CFStringRef str); + +// Translation unit scope global variables. + +static UniChar fixedUniChar[(RKL_FIXED_LENGTH)]; // This is the fixed sized UTF-16 conversion buffer. +static RKLCacheSlot RKLCache[(RKL_CACHE_SIZE)], *lastCacheSlot; +static OSSpinLock cacheSpinLock = OS_SPINLOCK_INIT; +static RKLBuffer dynamicBuffer, fixedBuffer = {NULL, 0UL, 0L, &fixedUniChar[0]}; +static const UniChar emptyUniCharString[1]; // For safety, icu_regexes are 'set' to this when the string they were searched is cleared. +static void *scratchBuffer[(SCRATCH_BUFFERS)]; // Used to hold temporary allocations that are allocated via reallocf(). + +// These are used when running under manual memory management for the array that rkl_splitArray creates. +// The split strings are created, but not autoreleased. The (immutable) array is created using these callbacks, which skips the CFRetain() call. +// For each split string this saves the overhead of an autorelease, then an array retain, then a autoreleasepool release. This is good for a ~30% speed increase. +static Boolean RKLCFArrayEqualCallBack (const void *value1, const void *value2) { return(CFEqual(value1, value2)); } +static void RKLCFArrayRelease (CFAllocatorRef allocator, const void *ptr) { allocator=allocator;/*unused*/ CFRelease(ptr); } +static CFArrayCallBacks transferOwnershipArrayCallBacks = { 0, NULL, RKLCFArrayRelease, NULL, RKLCFArrayEqualCallBack }; + +#if defined(RKL_REGISTER_FOR_IPHONE_LOWMEM_NOTIFICATIONS) && (RKL_REGISTER_FOR_IPHONE_LOWMEM_NOTIFICATIONS == 1) + +// The next few lines are specifically for the iPhone to catch low memory conditions. +// The basic idea is that rkl_RegisterForLowMemoryNotifications() is set to be run once by the linker at load time via __attribute((constructor)). +// rkl_RegisterForLowMemoryNotifications() tries to find the iPhone low memory notification symbol. If it can find it, +// it registers with the default NSNotificationCenter to call the RKLLowMemoryWarningObserver class method +lowMemoryWarning:. +// rkl_RegisterForLowMemoryNotifications() uses an atomic compare and swap to guarentee that it initalizes exactly once. +// +lowMemoryWarning tries to acquire the cache lock. If it gets the lock, it clears the cache. If it can't, it calls performSelector: +// with a delay of half a second to try again. This will hopefully prevent any deadlocks, such as a RegexKitLite request for +// memory triggering a notifcation while the lock is held. + +static void rkl_RegisterForLowMemoryNotifications(void); + +@interface RKLLowMemoryWarningObserver : NSObject +(void)lowMemoryWarning:(id)notification; @end +@implementation RKLLowMemoryWarningObserver ++(void)lowMemoryWarning:(id)notification { + if(OSSpinLockTry(&cacheSpinLock)) { rkl_clearStringCache(); OSSpinLockUnlock(&cacheSpinLock); } + else { [[RKLLowMemoryWarningObserver class] performSelector:@selector(lowMemoryWarning:) withObject:NULL afterDelay:0.5]; } +} +@end + +static int rkl_HaveRegisteredForLowMemoryNotifications = 0; + +__attribute__((constructor)) static void rkl_RegisterForLowMemoryNotifications(void) { + void **memoryWarningNotification = NULL; + + if(OSAtomicCompareAndSwapIntBarrier(0, 1, &rkl_HaveRegisteredForLowMemoryNotifications)) { + if((memoryWarningNotification = dlsym(RTLD_DEFAULT, "UIApplicationDidReceiveMemoryWarningNotification")) != NULL) { + [[NSNotificationCenter defaultCenter] addObserver:[RKLLowMemoryWarningObserver class] selector:@selector(lowMemoryWarning:) name:*memoryWarningNotification object:NULL]; + } + } +} + +#endif + +// IMPORTANT! This code is critical path code. Because of this, it has been written for speed, not clarity. +// IMPORTANT! Should only be called with cacheSpinLock already locked! +// ---------- + +static RKLCacheSlot *getCachedRegex(NSString *regexString, RKLRegexOptions options, NSError **error, id *exception) { + RKLCacheSlot *cacheSlot = NULL; + CFHashCode regexHash = 0; + int32_t status = 0; + + RKLCDelayedAssert(regexString != NULL, exception, exitNow); + + // Fast path the common case where this regex is exactly the same one used last time. + if((lastCacheSlot != NULL) && (lastCacheSlot->options == options) && (lastCacheSlot->icu_regex != NULL) && (lastCacheSlot->regexString != NULL) && (lastCacheSlot->regexString == (CFStringRef)regexString)) { return(lastCacheSlot); } + + regexHash = CFHash((CFTypeRef)regexString); + cacheSlot = &RKLCache[(regexHash % RKL_CACHE_SIZE)]; // Retrieve the cache slot for this regex. + + // Return the cached entry if it's a match, otherwise clear the slot and create a new ICU regex in its place. + if((cacheSlot->options == options) && (cacheSlot->icu_regex != NULL) && (cacheSlot->regexString != NULL) && ((cacheSlot->regexString == (CFStringRef)regexString) || (CFEqual((CFTypeRef)regexString, cacheSlot->regexString) == YES))) { lastCacheSlot = cacheSlot; return(cacheSlot); } + + clearCacheSlotRegex(cacheSlot); + + if((cacheSlot->regexString = CFStringCreateCopy(NULL, (CFStringRef)regexString)) == NULL) { goto exitNow; } ; // Get a cheap immutable copy. + cacheSlot->options = options; + + CFIndex regexStringU16Length = CFStringGetLength(cacheSlot->regexString); // In UTF16 code units. + UParseError parseError = (UParseError){-1, -1, {0}, {0}}; + UniChar *regexUniChar = NULL; + + // Try to quickly obtain regexString in UTF16 format. + if((regexUniChar = (UniChar *)CFStringGetCharactersPtr(cacheSlot->regexString)) == NULL) { // We didn't get the UTF16 pointer quickly and need to perform a full conversion in a temp buffer. + if((regexStringU16Length * sizeof(UniChar)) < RKL_STACK_LIMIT) { if((regexUniChar = alloca(regexStringU16Length * sizeof(UniChar))) == NULL) { goto exitNow; } } // Try to use the stack. + else { if((regexUniChar = rkl_realloc(&scratchBuffer[0], regexStringU16Length * sizeof(UniChar), 0UL)) == NULL) { goto exitNow; } } // Otherwise use the heap. + CFStringGetCharacters(cacheSlot->regexString, CFMakeRange(0, regexStringU16Length), (UniChar *)regexUniChar); // Convert regexString to UTF16. + } + + // Create the ICU regex. + if((cacheSlot->icu_regex = uregex_open(regexUniChar, (int32_t)regexStringU16Length, options, &parseError, &status)) == NULL) { goto exitNow; } + if(status <= 0) { cacheSlot->captureCount = (NSInteger)uregex_groupCount(cacheSlot->icu_regex, &status); } + if(status <= 0) { lastCacheSlot = cacheSlot; } + + exitNow: + if(scratchBuffer[0] != NULL) { free(scratchBuffer[0]); scratchBuffer[0] = NULL; } + if(status > 0) { cacheSlot = NULL; if(error != NULL) { *error = RKLNSErrorForRegex(regexString, options, &parseError, status); } } + return(cacheSlot); +} + +// IMPORTANT! This code is critical path code. Because of this, it has been written for speed, not clarity. +// IMPORTANT! Should only be called with cacheSpinLock already locked! +// ---------- + +static BOOL setCacheSlotToString(RKLCacheSlot *cacheSlot, const NSRange *range, int32_t *status, id *exception) { + RKLCDelayedAssert((cacheSlot != NULL) && (cacheSlot->setToString != NULL) && (range != NULL) && (status != NULL), exception, exitNow); + + if(cacheSlot->setToNeedsConversion == NO) { goto setRegexText; } + + RKLBuffer *buffer = (cacheSlot->setToLength < RKL_FIXED_LENGTH) ? &fixedBuffer : &dynamicBuffer; + if((cacheSlot->setToUniChar != NULL) && ((cacheSlot->setToString == buffer->string) || ((cacheSlot->setToLength == buffer->length) && (cacheSlot->setToHash == buffer->hash)))) { goto setRegexText; } + + clearBuffer(buffer, NO); + + if(cacheSlot->setToLength >= RKL_FIXED_LENGTH) { + RKLCDelayedAssert(buffer == &dynamicBuffer, exception, exitNow); + if((dynamicBuffer.uniChar = rkl_realloc((void *)&dynamicBuffer.uniChar, (cacheSlot->setToLength * sizeof(UniChar)), 0UL)) == NULL) { return(NO); } // Resize the buffer. + } + RKLCDelayedAssert(buffer->uniChar != NULL, exception, exitNow); + CFStringGetCharacters(cacheSlot->setToString, CFMakeRange(0, cacheSlot->setToLength), (UniChar *)buffer->uniChar); // Convert to a UTF16 string. + + if((buffer->string = CFRetain(cacheSlot->setToString)) == NULL) { return(NO); } + buffer->hash = cacheSlot->setToHash; + buffer->length = cacheSlot->setToLength; + + cacheSlot->setToUniChar = buffer->uniChar; + cacheSlot->setToRange = NSNotFoundRange; + + setRegexText: + + if(NSEqualRanges(cacheSlot->setToRange, *range) == NO) { + RKLCDelayedAssert((cacheSlot->icu_regex != NULL) && (cacheSlot->setToUniChar != NULL) && (NSMaxRange(*range) <= (NSUInteger)cacheSlot->setToLength), exception, exitNow); + cacheSlot->lastFindRange = cacheSlot->lastMatchRange = NSNotFoundRange; + cacheSlot->setToRange = *range; + uregex_setText(cacheSlot->icu_regex, cacheSlot->setToUniChar + cacheSlot->setToRange.location, (int32_t)cacheSlot->setToRange.length, status); + if(*status > 0) { return(NO); } + } + + return(YES); + + exitNow: + return(NO); +} + +// IMPORTANT! This code is critical path code. Because of this, it has been written for speed, not clarity. +// IMPORTANT! Should only be called with cacheSpinLock already locked! +// ---------- + +static RKLCacheSlot *getCachedRegexSetToString(NSString *regexString, RKLRegexOptions options, NSString *matchString, NSUInteger *matchLengthPtr, NSRange *matchRange, NSError **error, id *exception, int32_t *status) { + RKLCacheSlot *cacheSlot = NULL; + RKLCDelayedAssert((regexString != NULL) && (exception != NULL) && (status != NULL), exception, exitNow); + + // Fast path the common case where this regex is exactly the same one used last time. + if((lastCacheSlot != NULL) && (lastCacheSlot->regexString == (CFStringRef)regexString) && (lastCacheSlot->options == options)) { cacheSlot = lastCacheSlot; } + else { if((cacheSlot = getCachedRegex(regexString, options, error, exception)) == NULL) { goto exitNow; } } + + // Optimize the case where the string to search (matchString) is immutable and the setToString immutable copy is the same string with its reference count incremented. + BOOL isSetTo = ((cacheSlot->setToString != NULL) && (cacheSlot->setToString == (CFStringRef)matchString)) ? YES : NO; + CFIndex matchLength = ((isSetTo == YES) && (cacheSlot->setToIsImmutable == YES)) ? cacheSlot->setToLength : CFStringGetLength((CFStringRef)matchString); + + *matchLengthPtr = (NSUInteger)matchLength; + if(matchRange->length == NSUIntegerMax) { matchRange->length = matchLength; } // For convenience, allow NSUIntegerMax == string length. + + if((NSUInteger)matchLength < NSMaxRange(*matchRange)) { *exception = [NSException exceptionWithName:NSRangeException reason:@"Range or index out of bounds" userInfo:NULL]; goto exitNow; } + + if((cacheSlot->setToIsImmutable == NO) && (cacheSlot->setToString != NULL) && ((cacheSlot->setToLength != CFStringGetLength(cacheSlot->setToString)) || (cacheSlot->setToHash != CFHash(cacheSlot->setToString)))) { isSetTo = NO; } + else { // If the first pointer equality check failed, check the hash and length. + if(((isSetTo == NO) || (cacheSlot->setToIsImmutable == NO)) && (cacheSlot->setToString != NULL)) { isSetTo = ((cacheSlot->setToLength == matchLength) && (cacheSlot->setToHash == CFHash((CFStringRef)(matchString)))); } + + if((isSetTo == YES)) { // Make sure that the UTF16 conversion cache is set to this string, if conversion is required. + if((cacheSlot->setToNeedsConversion == YES) && (setCacheSlotToString(cacheSlot, matchRange, status, exception) == NO)) { *exception = RKLCAssert(@"Failed to set up UTF16 buffer."); goto exitNow; } + if(NSEqualRanges(cacheSlot->setToRange, *matchRange) == YES) { goto exitNow; } // Verify that the range to search is what the cached regex was prepped for last time. + } + } + + // Sometimes the range that the regex is set to isn't right, in which case we don't want to clear the cache slot. Otherwise, flush it out. + if((cacheSlot->setToString != NULL) && (isSetTo == NO)) { clearCacheSlotSetTo(cacheSlot); } + + if(cacheSlot->setToString == NULL) { + cacheSlot->setToString = CFRetain(matchString); + RKLCDelayedAssert(cacheSlot->setToString != NULL, exception, exitNow); + cacheSlot->setToUniChar = CFStringGetCharactersPtr(cacheSlot->setToString); + cacheSlot->setToNeedsConversion = (cacheSlot->setToUniChar == NULL) ? YES : NO; + cacheSlot->setToIsImmutable = !rkl_CFStringIsMutable(cacheSlot->setToString); // If RKL_FAST_MUTABLE_CHECK is not defined then the result is '0', or in other words mutable.. + cacheSlot->setToHash = CFHash(cacheSlot->setToString); + cacheSlot->setToRange = NSNotFoundRange; + cacheSlot->setToLength = matchLength; + } + + if(setCacheSlotToString(cacheSlot, matchRange, status, exception) == NO) { cacheSlot = NULL; goto exitNow; } + + exitNow: + return(cacheSlot); +} + +// IMPORTANT! This code is critical path code. Because of this, it has been written for speed, not clarity. +// ---------- + +static id performRegexOp(id self, SEL _cmd, RKLRegexOp doRegexOp, NSString *regexString, RKLRegexOptions options, NSInteger capture, id matchString, NSRange *matchRange, NSString *replacementString, NSError **error, void **result) { + BOOL replaceMutable = ((doRegexOp & RKLReplaceMutable) != 0) ? YES : NO; + RKLRegexOp regexOp = (doRegexOp & RKLMaskOp); + + if((error != NULL) && (*error != NULL)) { *error = NULL; } + + if(regexString == NULL) { RKLRaiseException(NSInvalidArgumentException, @"The regular expression argument is NULL."); } + if(matchString == NULL) { RKLRaiseException(NSInternalInconsistencyException, @"The match string argument is NULL."); } + if((regexOp == RKLReplaceOp) && (replacementString == NULL)) { RKLRaiseException(NSInvalidArgumentException, @"The replacement string argument is NULL."); } + + NSUInteger stringU16Length = 0UL, replacementU16Length = (NSUInteger)((replacementString != NULL) ? CFStringGetLength((CFStringRef)replacementString) : 0); // In UTF16 code units. + NSRange stringRange = NSMakeRange(0, NSUIntegerMax), searchRange = (matchRange != NULL) ? *matchRange : NSNotFoundRange; + RKLCacheSlot *cacheSlot = NULL; + id exception = NULL; + id resultObject = NULL; + int32_t status = 0; + + // IMPORTANT! Once we have obtained the lock, code MUST exit via 'goto exitNow;' to unlock the lock! NO EXCEPTIONS! + // ---------- + OSSpinLockLock(&cacheSpinLock); // Grab the lock and get cache entry. + + if(((cacheSlot = getCachedRegexSetToString(regexString, options, matchString, &stringU16Length, (regexOp == RKLRangeOp) ? &stringRange : &searchRange, error, &exception, &status)) == NULL) || (exception != NULL) || (status > 0)) { goto exitNow; } + + if(searchRange.length == NSUIntegerMax) { searchRange.length = stringU16Length; } // For convenience. + if(stringU16Length < NSMaxRange(searchRange)) { exception = [NSException exceptionWithName:NSRangeException reason:@"Range or index out of bounds" userInfo:NULL]; goto exitNow; } + + RKLCDelayedAssert((cacheSlot->icu_regex != NULL) && (exception == NULL), &exception, exitNow); + + if(cacheSlot->setToNeedsConversion != 0) { + RKLBuffer *buffer = (cacheSlot->setToLength < RKL_FIXED_LENGTH) ? &fixedBuffer : &dynamicBuffer; + RKLCDelayedAssert((cacheSlot->setToHash == buffer->hash) && (cacheSlot->setToLength == buffer->length) && (cacheSlot->setToUniChar == buffer->uniChar), &exception, exitNow); + } + + switch(regexOp) { + case RKLRangeOp: rkl_find(cacheSlot, capture, searchRange, (NSRange *)result, &exception, &status); break; + case RKLSplitOp: resultObject = rkl_splitArray(cacheSlot, &exception, &status); break; + case RKLReplaceOp: resultObject = rkl_replaceString(cacheSlot, matchString, stringU16Length, replacementString, replacementU16Length, (NSUInteger *)result, replaceMutable, &exception, &status); break; + default: exception = RKLCAssert(@"Unknown regexOp code."); break; + } + + exitNow: + OSSpinLockUnlock(&cacheSpinLock); + + if((status > 0) && (exception == NULL)) { exception = RKLNSExceptionForRegex(regexString, options, NULL, status); } // If we had a problem, throw an exception. + if(exception != NULL) { + if([exception isKindOfClass:[NSException class]]) { [[NSException exceptionWithName:[exception name] reason:RKLStringFromClassAndMethod(self, _cmd, [exception reason]) userInfo:[exception userInfo]] raise]; } + else { [[NSAssertionHandler currentHandler] handleFailureInFunction:[exception objectForKey:@"function"] file:[exception objectForKey:@"file"] lineNumber:[[exception objectForKey:@"line"] longValue] description:[exception objectForKey:@"description"]]; } + } + if(replaceMutable == YES) { // We're working on a mutable string and if there were successfull matches with replaced text we still have work to do. Done outside the cache lock. + if(*((NSUInteger *)result) > 0) { NSCParameterAssert(resultObject != NULL); [matchString replaceCharactersInRange:searchRange withString:resultObject]; } + } + + return(resultObject); +} + +// IMPORTANT! This code is critical path code. Because of this, it has been written for speed, not clarity. +// IMPORTANT! Should only be called from performRegexOp(). +// ---------- + +static void rkl_find(RKLCacheSlot *cacheSlot, NSInteger capture, NSRange searchRange, NSRange *resultRange, id *exception, int32_t *status) { + NSRange captureRange = NSNotFoundRange; + + RKLCDelayedAssert((cacheSlot != NULL) && (resultRange != NULL) && (exception != NULL) && (status != NULL), exception, exitNow); + + if((capture < 0) || (capture > cacheSlot->captureCount)) { *exception = [NSException exceptionWithName:NSInvalidArgumentException reason:@"The capture argument is not valid." userInfo:NULL]; goto exitNow; } + + if((NSEqualRanges(searchRange, cacheSlot->lastFindRange) == NO)) { // Only perform an expensive 'find' operation iff the current find range is different than the last find range. + RKL_PREFETCH(cacheSlot->setToUniChar, searchRange.location << 1); // Spool up the CPU caches. + + // Using uregex_findNext can be a slight performance win. + BOOL useFindNext = (searchRange.location == (NSMaxRange(cacheSlot->lastMatchRange) + ((cacheSlot->lastMatchRange.length == 0) ? 1 : 0))) ? YES : NO; + + cacheSlot->lastFindRange = NSNotFoundRange; // Cleared the cached search/find range. + if(useFindNext == NO) { if((uregex_find (cacheSlot->icu_regex, (int32_t)searchRange.location, status) == NO) || (*status > 0)) { goto exitNow; } } + else { if((uregex_findNext(cacheSlot->icu_regex, status) == NO) || (*status > 0)) { goto exitNow; } } + + if(RKLGetRangeForCapture(cacheSlot->icu_regex, status, 0, cacheSlot->lastMatchRange) != 0) { goto exitNow; } + if(NSRangeInsideRange(cacheSlot->lastMatchRange, searchRange) == NO) { goto exitNow; } // If the regex matched outside the requested range, exit. + + cacheSlot->lastFindRange = searchRange; // Cache the successful search/find range. + } + + if(capture == 0) { captureRange = cacheSlot->lastMatchRange; } else { RKLGetRangeForCapture(cacheSlot->icu_regex, status, capture, captureRange); } + + exitNow: + *resultRange = captureRange; +} + +// IMPORTANT! This code is critical path code. Because of this, it has been written for speed, not clarity. +// IMPORTANT! Should only be called from performRegexOp(). +// ---------- + +static NSArray *rkl_splitArray(RKLCacheSlot *cacheSlot, id *exception, int32_t *status) { + NSArray *resultArray = NULL; + + RKLCDelayedAssert((cacheSlot != NULL) && (status != NULL), exception, exitNow); + + const char *setToUniCharChar = (const char *)(cacheSlot->setToUniChar + cacheSlot->setToRange.location); + NSUInteger splitRangesCapacity = ((((RKL_STACK_LIMIT / sizeof(NSRange)) / 4) + ((cacheSlot->captureCount + 1) * 2)) + 2), splitRangesIndex = 0, lastLocation = 0, x = 0; + size_t splitRangesSize = (splitRangesCapacity * sizeof(NSRange)), stackUsed = 0; + NSInteger captureCount = cacheSlot->captureCount; + uregex *icu_regex = cacheSlot->icu_regex; + NSRange *splitRanges = NULL; + BOOL copiedStackToHeap = NO; + + if(cacheSlot->setToLength == 0) { resultArray = [NSArray array]; goto exitNow; } // Return an empty array when there is nothing to search. + + if(splitRangesSize < RKL_STACK_LIMIT) { if((splitRanges = alloca(splitRangesSize)) == NULL) { goto exitNow; } stackUsed += splitRangesSize; } + else { if((splitRanges = rkl_realloc(&scratchBuffer[0], splitRangesSize, 0UL)) == NULL) { goto exitNow; } } + + cacheSlot->lastFindRange = cacheSlot->lastMatchRange = NSNotFoundRange; // Clear the cached find information for this regex so a subsequent find works correctly. + uregex_reset(icu_regex, 0, status); // Reset the regex to the start of the string. + + for(splitRangesIndex = 0; splitRangesIndex < splitRangesCapacity; splitRangesIndex++) { + + if(splitRangesIndex >= ((splitRangesCapacity - ((captureCount + 1) * 2)) - 1)) { // Check if we need to grow our NSRanges buffer. + NSUInteger newCapacity = (((splitRangesCapacity + (splitRangesCapacity / 2)) + ((captureCount + 1) * 2)) + 2); + size_t newSize = (newCapacity * sizeof(NSRange)); + NSRange *newRanges = NULL; + + if((newRanges = rkl_realloc(&scratchBuffer[0], newSize, 0UL)) == NULL) { goto exitNow; } // We only try to use the stack the first time, after that, we use the heap. + if((stackUsed > 0) && (copiedStackToHeap == NO)) { memcpy(newRanges, splitRanges, splitRangesSize); copiedStackToHeap = YES; } + + splitRangesCapacity = newCapacity; + splitRangesSize = newSize; + splitRanges = newRanges; + } + + RKL_PREFETCH(setToUniCharChar, lastLocation << 1); // Spool up the CPU caches. + + NSUInteger baseMatchIndex = splitRangesIndex; + NSRange tempRange; + + if((uregex_findNext(icu_regex, status) == NO) || (*status > 0)) { break; } + if(RKLGetRangeForCapture(icu_regex, status, 0, tempRange) > 0) { goto exitNow; } + + splitRanges[splitRangesIndex] = NSMakeRange(lastLocation, tempRange.location - lastLocation); + lastLocation = NSMaxRange(tempRange); + + int32_t capture; + for(capture = 1; capture <= captureCount; capture++) { + RKLCDelayedAssert(splitRangesIndex < (splitRangesCapacity - 2), exception, exitNow); + splitRangesIndex++; + + if(RKLGetRangeForCapture(icu_regex, status, capture, splitRanges[splitRangesIndex]) > 0) { goto exitNow; } + if(splitRanges[splitRangesIndex].location == NSNotFound) { splitRanges[splitRangesIndex] = NSMakeRange(splitRanges[baseMatchIndex].location, 0); } + } + } + + RKLCDelayedAssert(splitRangesIndex < (splitRangesCapacity - 2), exception, exitNow); + splitRanges[splitRangesIndex] = NSMakeRange(lastLocation, (NSMaxRange(cacheSlot->setToRange) - cacheSlot->setToRange.location) - lastLocation); + splitRangesIndex++; + + CFIndex setToLocation = cacheSlot->setToRange.location; + CFStringRef setToString = cacheSlot->setToString; + size_t splitStringsSize = (splitRangesIndex * sizeof(id)); + id *splitStrings = NULL; + + if((stackUsed + splitStringsSize) < RKL_STACK_LIMIT) { if((splitStrings = alloca(splitStringsSize)) == NULL) { goto exitNow; } stackUsed += splitStringsSize; } +#ifdef __OBJC_GC__ + else { if((splitStrings = rkl_realloc(&scratchBuffer[1], splitStringsSize, (NSUInteger)NSScannedOption)) == NULL) { goto exitNow; } } +#else + // http://sourceforge.net/tracker/index.php?func=detail&aid=2050825&group_id=204582&atid=990188 + // This is to get around an iPhone quirk. For whatever reason, the iPhone NSZone.h explicitly removes all NSAllocateCollectable() + // bits and pieces using #if pre-processor conditions. Since NSScannedOption is only really used when the compiler has -fobjc-gc enabled, + // we just chop it out here. + else { if((splitStrings = rkl_realloc(&scratchBuffer[1], splitStringsSize, 0)) == NULL) { goto exitNow; } } +#endif + +#ifdef __OBJC_GC__ + if(rkl_collectingEnabled() == YES) { // I just don't trust the GC system with the faster CF way of doing things... It never seems to work quite the way you expect it to. + for(x = 0; x < splitRangesIndex; x++) { // Optimize the case where the length == 0 by substituting the string @"". + splitStrings[x] = (splitRanges[x].length == 0) ? @"" : [(id)setToString substringWithRange:NSMakeRange(setToLocation + splitRanges[x].location, splitRanges[x].length)]; + } + resultArray = [NSArray arrayWithObjects:splitStrings count:splitRangesIndex]; + } else +#endif + { // This block of code is always compiled in. It is used when not compiled with GC or when compiled with GC but the collector is not enabled. + for(x = 0; x < splitRangesIndex; x++) { // Optimize the case where the length == 0 by substituting the string @"". + splitStrings[x] = (splitRanges[x].length == 0) ? @"" : (id)CFStringCreateWithSubstring(NULL, setToString, CFMakeRange(setToLocation + splitRanges[x].location, (CFIndex)splitRanges[x].length)); + } + resultArray = rkl_CFAutorelease(CFArrayCreate(NULL, (const void **)splitStrings, (CFIndex)splitRangesIndex, &transferOwnershipArrayCallBacks)); // Create the CF/NSArray of the split strings. + } + + exitNow: + if(scratchBuffer[0] != NULL) { free(scratchBuffer[0]); scratchBuffer[0] = NULL; } + if(scratchBuffer[1] != NULL) { free(scratchBuffer[1]); scratchBuffer[1] = NULL; } + + return(resultArray); +} + +// IMPORTANT! This code is critical path code. Because of this, it has been written for speed, not clarity. +// IMPORTANT! Should only be called from performRegexOp(). +// ---------- + +static NSString *rkl_replaceString(RKLCacheSlot *cacheSlot, id searchString, NSUInteger searchU16Length, NSString *replacementString, NSUInteger replacementU16Length, NSUInteger *replacedCountPtr, int replaceMutable, id *exception, int32_t *status) { + int32_t resultU16Length = 0, tempUniCharBufferU16Capacity = 0; + UniChar *tempUniCharBuffer = NULL; + const UniChar *replacementUniChar = NULL; + id resultObject = NULL; + NSUInteger replacedCount = 0; + + // Zero order approximation of the buffer sizes for holding the replaced string or split strings and split strings pointer offsets. As UTF16 code units. + tempUniCharBufferU16Capacity = (int32_t)(16 + (searchU16Length + (searchU16Length >> 1)) + (replacementU16Length * 2)); + + // Buffer sizes converted from native units to bytes. + size_t stackSize = 0, replacementSize = (replacementU16Length * sizeof(UniChar)), tempUniCharBufferSize = (tempUniCharBufferU16Capacity * sizeof(UniChar)); + + // For the various buffers we require, we first try to allocate from the stack if we're not over the RKL_STACK_LIMIT. If we are, switch to using the heap for the buffer. + + if((stackSize + tempUniCharBufferSize) < RKL_STACK_LIMIT) { if((tempUniCharBuffer = alloca(tempUniCharBufferSize)) == NULL) { goto exitNow; } stackSize += tempUniCharBufferSize; } + else { if((tempUniCharBuffer = rkl_realloc(&scratchBuffer[0], tempUniCharBufferSize, 0UL)) == NULL) { goto exitNow; } } + + // Try to get the pointer to the replacement strings UTF16 data. If we can't, allocate some buffer space, then covert to UTF16. + if((replacementUniChar = CFStringGetCharactersPtr((CFStringRef)replacementString)) == NULL) { + if((stackSize + replacementSize) < RKL_STACK_LIMIT) { if((replacementUniChar = alloca(replacementSize)) == NULL) { goto exitNow; } stackSize += replacementSize; } + else { if((replacementUniChar = rkl_realloc(&scratchBuffer[1], replacementSize, 0UL)) == NULL) { goto exitNow; } } + CFStringGetCharacters((CFStringRef)replacementString, CFMakeRange(0, replacementU16Length), (UniChar *)replacementUniChar); // Convert to a UTF16 string. + } + + cacheSlot->lastFindRange = cacheSlot->lastMatchRange = NSNotFoundRange; // Clear the cached find information for this regex so a subsequent find works correctly. + + resultU16Length = rkl_replaceAll(cacheSlot, replacementUniChar, (int32_t)replacementU16Length, tempUniCharBuffer, tempUniCharBufferU16Capacity, &replacedCount, exception, status); + + if(*status == U_BUFFER_OVERFLOW_ERROR) { // Our buffer guess(es) were too small. Resize the buffers and try again. + tempUniCharBufferSize = ((tempUniCharBufferU16Capacity = resultU16Length + 4) * sizeof(UniChar)); + if((stackSize + tempUniCharBufferSize) < RKL_STACK_LIMIT) { if((tempUniCharBuffer = alloca(tempUniCharBufferSize)) == NULL) { goto exitNow; } stackSize += tempUniCharBufferSize; } + else { if((tempUniCharBuffer = rkl_realloc(&scratchBuffer[0], tempUniCharBufferSize, 0UL)) == NULL) { goto exitNow; } } + + *status = 0; // Make sure the status var is cleared and try again. + resultU16Length = rkl_replaceAll(cacheSlot, replacementUniChar, (int32_t)replacementU16Length, tempUniCharBuffer, tempUniCharBufferU16Capacity, &replacedCount, exception, status); + } + + if(*status > 0) { goto exitNow; } // Something went wrong. + + if(resultU16Length == 0) { resultObject = @""; } // Optimize the case where the replaced text length == 0 with a @"" string. + else if(((NSUInteger)resultU16Length == searchU16Length) && (replacedCount == 0)) { // Optimize the case where the replacement == original by creating a copy. Very fast if self is immutable. + if(replaceMutable == NO) { resultObject = rkl_CFAutorelease(CFStringCreateCopy(NULL, (CFStringRef)searchString)); } // .. but only if this is not replacing a mutable self. + } else { resultObject = rkl_CFAutorelease(CFStringCreateWithCharacters(NULL, tempUniCharBuffer, (CFIndex)resultU16Length)); } // otherwise, create a new string. + + // If replaceMutable == YES, we don't do the replacement here. We wait until after we return and unlock the cache lock. + // This is because we may be trying to mutate an immutable string object. + if((replacedCount > 0) && (replaceMutable == YES)) { // We're working on a mutable string and there were successfull matches with replaced text, so there's work to do. + clearBuffer((cacheSlot->setToLength < RKL_FIXED_LENGTH) ? &fixedBuffer : &dynamicBuffer, NO); + clearCacheSlotSetTo(cacheSlot); // Flush any cached information about this string since it will mutate. + } + + exitNow: + if(scratchBuffer[0] != NULL) { free(scratchBuffer[0]); scratchBuffer[0] = NULL; } + if(scratchBuffer[1] != NULL) { free(scratchBuffer[1]); scratchBuffer[1] = NULL; } + if(replacedCountPtr != NULL) { *replacedCountPtr = replacedCount; } + return(resultObject); +} + +// Modified version of the ICU libraries uregex_replaceAll() that keeps count of the number of replacements made. +static int32_t rkl_replaceAll(RKLCacheSlot *cacheSlot, const UniChar *replacementUniChar, int32_t replacementU16Length, UniChar *replacedUniChar, int32_t replacedU16Capacity, NSUInteger *replacedCount, id *exception, int32_t *status) { + NSUInteger replaced = 0; + int32_t u16Length = 0; + RKLCDelayedAssert((cacheSlot != NULL) && (replacementUniChar != NULL) && (replacedUniChar != NULL) && (status != NULL), exception, exitNow); + + uregex_reset(cacheSlot->icu_regex, 0, status); + + // Work around for ICU uregex_reset() bug, see http://bugs.icu-project.org/trac/ticket/6545 + // http://sourceforge.net/tracker/index.php?func=detail&aid=2105213&group_id=204582&atid=990188 + if((cacheSlot->setToLength == 0) && (*status == 8)) { *status = 0; } + + while(uregex_findNext(cacheSlot->icu_regex, status)) { + replaced++; + u16Length += uregex_appendReplacement(cacheSlot->icu_regex, replacementUniChar, replacementU16Length, &replacedUniChar, &replacedU16Capacity, status); + } + u16Length += uregex_appendTail(cacheSlot->icu_regex, &replacedUniChar, &replacedU16Capacity, status); + + if(replacedCount != 0) { *replacedCount = replaced; } + exitNow: + return(u16Length); +} + +static void rkl_clearStringCache(void) { + NSCParameterAssert(cacheSpinLock != 0); + lastCacheSlot = NULL; + NSUInteger x = 0; + for(x = 0; x < SCRATCH_BUFFERS; x++) { if(scratchBuffer[x] != NULL) { free(scratchBuffer[x]); scratchBuffer[x] = NULL; } } + for(x = 0; x < RKL_CACHE_SIZE; x++) { clearCacheSlotRegex(&RKLCache[x]); clearCacheSlotSetTo(&RKLCache[x]); } + clearBuffer(&fixedBuffer, NO); + clearBuffer(&dynamicBuffer, YES); +} + +static void clearBuffer(RKLBuffer *buffer, int freeDynamicBuffer) { + if(buffer == NULL) { return; } + if((freeDynamicBuffer == YES) && (buffer->uniChar != NULL) && (buffer == &dynamicBuffer)) { free(dynamicBuffer.uniChar); dynamicBuffer.uniChar = NULL; } + if(buffer->string != NULL) { CFRelease(buffer->string); buffer->string = NULL; } + buffer->length = 0L; + buffer->hash = 0UL; +} + +static void clearCacheSlotRegex(RKLCacheSlot *cacheSlot) { + if(cacheSlot == NULL) { return; } + if(cacheSlot->regexString != NULL) { CFRelease(cacheSlot->regexString); cacheSlot->regexString = NULL; cacheSlot->options = 0U; } + if(cacheSlot->icu_regex != NULL) { uregex_close(cacheSlot->icu_regex); cacheSlot->icu_regex = NULL; cacheSlot->captureCount = -1L; } + if(cacheSlot->setToString != NULL) { clearCacheSlotSetTo(cacheSlot); } +} + +static void clearCacheSlotSetTo(RKLCacheSlot *cacheSlot) { + if(cacheSlot == NULL) { return; } + if(cacheSlot->icu_regex != NULL) { int32_t status = 0; uregex_setText(cacheSlot->icu_regex, &emptyUniCharString[0], 0, &status); } + if(cacheSlot->setToString != NULL) { CFRelease(cacheSlot->setToString); cacheSlot->setToString = NULL; } + cacheSlot->setToLength = 0L; + cacheSlot->setToHash = 0UL; + cacheSlot->setToIsImmutable = cacheSlot->setToNeedsConversion = 0UL; + cacheSlot->lastFindRange = cacheSlot->lastMatchRange = cacheSlot->setToRange = NSNotFoundRange; + cacheSlot->setToUniChar = NULL; +} + +// Helps to keep things tidy. +#define addKeyAndObject(objs, keys, i, k, o) ({id _o=(o), _k=(k); if((_o != NULL) && (_k != NULL)) { objs[i] = _o; keys[i] = _k; i++; } }) + +static NSDictionary *userInfoDictionary(NSString *regexString, RKLRegexOptions options, const UParseError *parseError, int status, ...) { + va_list varArgsList; + va_start(varArgsList, status); + + if(regexString == NULL) { return(NULL); } + + id objects[64], keys[64]; + NSUInteger count = 0; + + NSString *errorNameString = [NSString stringWithUTF8String:u_errorName(status)]; + + addKeyAndObject(objects, keys, count, RKLICURegexRegexErrorKey, regexString); + addKeyAndObject(objects, keys, count, RKLICURegexRegexOptionsErrorKey, [NSNumber numberWithUnsignedInt:options]); + addKeyAndObject(objects, keys, count, RKLICURegexErrorCodeErrorKey, [NSNumber numberWithInt:status]); + addKeyAndObject(objects, keys, count, RKLICURegexErrorNameErrorKey, errorNameString); + + if((parseError != NULL) && (parseError->line != -1)) { + NSString *preContextString = [NSString stringWithCharacters:&parseError->preContext[0] length:(NSUInteger)u_strlen(&parseError->preContext[0])]; + NSString *postContextString = [NSString stringWithCharacters:&parseError->postContext[0] length:(NSUInteger)u_strlen(&parseError->postContext[0])]; + + addKeyAndObject(objects, keys, count, RKLICURegexLineErrorKey, [NSNumber numberWithInt:parseError->line]); + addKeyAndObject(objects, keys, count, RKLICURegexOffsetErrorKey, [NSNumber numberWithInt:parseError->offset]); + addKeyAndObject(objects, keys, count, RKLICURegexPreContextErrorKey, preContextString); + addKeyAndObject(objects, keys, count, RKLICURegexPostContextErrorKey, postContextString); + addKeyAndObject(objects, keys, count, @"NSLocalizedFailureReason", ([NSString stringWithFormat:@"The error %@ occurred at line %d, column %d: %@<>%@", errorNameString, parseError->line, parseError->offset, preContextString, postContextString])); + } else { + addKeyAndObject(objects, keys, count, @"NSLocalizedFailureReason", ([NSString stringWithFormat:@"The error %@ occurred.", errorNameString])); + } + + while(count < 62) { id obj = va_arg(varArgsList, id), key = va_arg(varArgsList, id); if((obj != NULL) && (key != NULL)) { addKeyAndObject(objects, keys, count, key, obj); } else { break; } } + + return([NSDictionary dictionaryWithObjects:&objects[0] forKeys:&keys[0] count:count]); +} + +static NSError *RKLNSErrorForRegex(NSString *regexString, RKLRegexOptions options, const UParseError *parseError, int status) { + return([NSError errorWithDomain:RKLICURegexErrorDomain code:(NSInteger)status userInfo:userInfoDictionary(regexString, options, parseError, status, @"There was an error compiling the regular expression.", @"NSLocalizedDescription", NULL)]); +} + +static NSException *RKLNSExceptionForRegex(NSString *regexString, RKLRegexOptions options, const UParseError *parseError, int status) { + return([NSException exceptionWithName:RKLICURegexException reason:[NSString stringWithFormat:@"ICU regular expression error #%d, %s", status, u_errorName(status)] userInfo:userInfoDictionary(regexString, options, parseError, status, NULL)]); +} + +static NSDictionary *RKLCAssertDictionary(const char *function, const char *file, int line, NSString *format, ...) { + va_list varArgsList; + va_start(varArgsList, format); + NSString *formatString = [[[NSString alloc] initWithFormat:format arguments:varArgsList] autorelease]; + va_end(varArgsList); + NSString *functionString = [NSString stringWithUTF8String:function], *fileString = [NSString stringWithUTF8String:file]; + return([NSDictionary dictionaryWithObjectsAndKeys:formatString, @"description", functionString, @"function", fileString, @"file", [NSNumber numberWithInt:line], @"line", NSInternalInconsistencyException, @"exceptionName", NULL]); +} + +static NSString *RKLStringFromClassAndMethod(id object, SEL selector, NSString *format, ...) { + va_list varArgsList; + va_start(varArgsList, format); + NSString *formatString = [[[NSString alloc] initWithFormat:format arguments:varArgsList] autorelease]; + va_end(varArgsList); + Class objectsClass = [object class]; + return([NSString stringWithFormat:@"*** %c[%@ %@]: %@", (object == objectsClass) ? '+' : '-', NSStringFromClass(objectsClass), NSStringFromSelector(selector), formatString]); +} + +@implementation NSString (RegexKitLiteAdditions) + +// Class methods + ++ (void)RKL_METHOD_PREPEND(clearStringCache) +{ + OSSpinLockLock(&cacheSpinLock); + rkl_clearStringCache(); + OSSpinLockUnlock(&cacheSpinLock); +} + +// captureCountForRegex: + ++ (NSInteger)RKL_METHOD_PREPEND(captureCountForRegex):(NSString *)regex +{ + return([self RKL_METHOD_PREPEND(captureCountForRegex):regex options:RKLNoOptions error:NULL]); +} + ++ (NSInteger)RKL_METHOD_PREPEND(captureCountForRegex):(NSString *)regex options:(RKLRegexOptions)options error:(NSError **)error +{ + if((error != NULL) && (*error != NULL)) { *error = NULL; } + if(regex == NULL) { RKLRaiseException(NSInvalidArgumentException, @"The regular expression argument is NULL."); } + + NSException *exception = NULL; + RKLCacheSlot *cacheSlot = NULL; + NSInteger captureCount = -1; + + OSSpinLockLock(&cacheSpinLock); + if((cacheSlot = getCachedRegex(regex, options, error, &exception)) != NULL) { captureCount = cacheSlot->captureCount; } + OSSpinLockUnlock(&cacheSpinLock); + + if(exception != NULL) { [exception raise]; } + return(captureCount); +} + +// Instance methods + +// componentsSeparatedByRegex: + +- (NSArray *)RKL_METHOD_PREPEND(componentsSeparatedByRegex):(NSString *)regex +{ + NSRange range = NSMaxiumRange; + return(performRegexOp(self, _cmd, (RKLRegexOp)RKLSplitOp, regex, 0, 0L, self, &range, NULL, NULL, NULL)); +} + +- (NSArray *)RKL_METHOD_PREPEND(componentsSeparatedByRegex):(NSString *)regex range:(NSRange)range +{ + return(performRegexOp(self, _cmd, (RKLRegexOp)RKLSplitOp, regex, 0, 0L, self, &range, NULL, NULL, NULL)); +} + +- (NSArray *)RKL_METHOD_PREPEND(componentsSeparatedByRegex):(NSString *)regex options:(RKLRegexOptions)options range:(NSRange)range error:(NSError **)error +{ + return(performRegexOp(self, _cmd, (RKLRegexOp)RKLSplitOp, regex, options, 0L, self, &range, NULL, error, NULL)); +} + +// isMatchedByRegex: + +- (BOOL)RKL_METHOD_PREPEND(isMatchedByRegex):(NSString *)regex +{ + NSRange result = NSNotFoundRange, range = NSMaxiumRange; + performRegexOp(self, _cmd, (RKLRegexOp)RKLRangeOp, regex, 0, 0L, self, &range, NULL, NULL, (void **)((void *)&result)); + return((result.location == NSNotFound) ? NO : YES); +} + +- (BOOL)RKL_METHOD_PREPEND(isMatchedByRegex):(NSString *)regex inRange:(NSRange)range +{ + NSRange result = NSNotFoundRange; + performRegexOp(self, _cmd, (RKLRegexOp)RKLRangeOp, regex, 0, 0L, self, &range, NULL, NULL, (void **)((void *)&result)); + return((result.location == NSNotFound) ? NO : YES); +} + +- (BOOL)RKL_METHOD_PREPEND(isMatchedByRegex):(NSString *)regex options:(RKLRegexOptions)options inRange:(NSRange)range error:(NSError **)error +{ + NSRange result = NSNotFoundRange; + performRegexOp(self, _cmd, (RKLRegexOp)RKLRangeOp, regex, options, 0L, self, &range, NULL, error, (void **)((void *)&result)); + return((result.location == NSNotFound) ? NO : YES); +} + +// rangeOfRegex: + +- (NSRange)RKL_METHOD_PREPEND(rangeOfRegex):(NSString *)regex +{ + NSRange result = NSNotFoundRange, range = NSMaxiumRange; + performRegexOp(self, _cmd, (RKLRegexOp)RKLRangeOp, regex, 0, 0L, self, &range, NULL, NULL, (void **)((void *)&result)); + return(result); +} + +- (NSRange)RKL_METHOD_PREPEND(rangeOfRegex):(NSString *)regex capture:(NSInteger)capture +{ + NSRange result = NSNotFoundRange, range = NSMaxiumRange; + performRegexOp(self, _cmd, (RKLRegexOp)RKLRangeOp, regex, 0, capture, self, &range, NULL, NULL, (void **)((void *)&result)); + return(result); +} + +- (NSRange)RKL_METHOD_PREPEND(rangeOfRegex):(NSString *)regex inRange:(NSRange)range +{ + NSRange result = NSNotFoundRange; + performRegexOp(self, _cmd, (RKLRegexOp)RKLRangeOp, regex, 0, 0L, self, &range, NULL, NULL, (void **)((void *)&result)); + return(result); +} + +- (NSRange)RKL_METHOD_PREPEND(rangeOfRegex):(NSString *)regex options:(RKLRegexOptions)options inRange:(NSRange)range capture:(NSInteger)capture error:(NSError **)error +{ + NSRange result = NSNotFoundRange; + performRegexOp(self, _cmd, (RKLRegexOp)RKLRangeOp, regex, options, capture, self, &range, NULL, error, (void **)((void *)&result)); + return(result); +} + +// stringByMatching: + +- (NSString *)RKL_METHOD_PREPEND(stringByMatching):(NSString *)regex +{ + return([self RKL_METHOD_PREPEND(stringByMatching):regex options:RKLNoOptions inRange:NSMaxiumRange capture:0L error:NULL]); +} + +- (NSString *)RKL_METHOD_PREPEND(stringByMatching):(NSString *)regex capture:(NSInteger)capture +{ + return([self RKL_METHOD_PREPEND(stringByMatching):regex options:RKLNoOptions inRange:NSMaxiumRange capture:capture error:NULL]); +} + +- (NSString *)RKL_METHOD_PREPEND(stringByMatching):(NSString *)regex inRange:(NSRange)range +{ + return([self RKL_METHOD_PREPEND(stringByMatching):regex options:RKLNoOptions inRange:range capture:0L error:NULL]); +} + +- (NSString *)RKL_METHOD_PREPEND(stringByMatching):(NSString *)regex options:(RKLRegexOptions)options inRange:(NSRange)range capture:(NSInteger)capture error:(NSError **)error +{ + NSRange matchedRange = [self RKL_METHOD_PREPEND(rangeOfRegex):regex options:options inRange:range capture:capture error:error]; + return((matchedRange.location == NSNotFound) ? NULL : rkl_CFAutorelease(CFStringCreateWithSubstring(NULL, (CFStringRef)self, CFMakeRange(matchedRange.location, matchedRange.length)))); +} + +// stringByReplacingOccurrencesOfRegex: + +- (NSString *)RKL_METHOD_PREPEND(stringByReplacingOccurrencesOfRegex):(NSString *)regex withString:(NSString *)replacement +{ + NSRange searchRange = NSMaxiumRange; + return(performRegexOp(self, _cmd, (RKLRegexOp)RKLReplaceOp, regex, 0, 0L, self, &searchRange, replacement, NULL, NULL)); +} + +- (NSString *)RKL_METHOD_PREPEND(stringByReplacingOccurrencesOfRegex):(NSString *)regex withString:(NSString *)replacement range:(NSRange)searchRange +{ + return(performRegexOp(self, _cmd, (RKLRegexOp)RKLReplaceOp, regex, 0, 0L, self, &searchRange, replacement, NULL, NULL)); +} + +- (NSString *)RKL_METHOD_PREPEND(stringByReplacingOccurrencesOfRegex):(NSString *)regex withString:(NSString *)replacement options:(RKLRegexOptions)options range:(NSRange)searchRange error:(NSError **)error +{ + return(performRegexOp(self, _cmd, (RKLRegexOp)RKLReplaceOp, regex, options, 0L, self, &searchRange, replacement, error, NULL)); +} + +@end + + +@implementation NSMutableString (RegexKitLiteAdditions) + +// replaceOccurrencesOfRegex: + +- (NSUInteger)RKL_METHOD_PREPEND(replaceOccurrencesOfRegex):(NSString *)regex withString:(NSString *)replacement +{ + NSRange searchRange = NSMaxiumRange; + NSUInteger replacedCount = 0; + performRegexOp(self, _cmd, (RKLRegexOp)(RKLReplaceOp | RKLReplaceMutable), regex, 0, 0L, self, &searchRange, replacement, NULL, (void **)((void *)&replacedCount)); + return(replacedCount); +} + +- (NSUInteger)RKL_METHOD_PREPEND(replaceOccurrencesOfRegex):(NSString *)regex withString:(NSString *)replacement range:(NSRange)searchRange +{ + NSUInteger replacedCount = 0; + performRegexOp(self, _cmd, (RKLRegexOp)(RKLReplaceOp | RKLReplaceMutable), regex, 0, 0L, self, &searchRange, replacement, NULL, (void **)((void *)&replacedCount)); + return(replacedCount); +} + +- (NSUInteger)RKL_METHOD_PREPEND(replaceOccurrencesOfRegex):(NSString *)regex withString:(NSString *)replacement options:(RKLRegexOptions)options range:(NSRange)searchRange error:(NSError **)error +{ + NSUInteger replacedCount = 0; + performRegexOp(self, _cmd, (RKLRegexOp)(RKLReplaceOp | RKLReplaceMutable), regex, options, 0L, self, &searchRange, replacement, error, (void **)((void *)&replacedCount)); + return(replacedCount); +} + +@end +