adium 2610:d6bb9d4249fc: Use NSPredicate for mention detection. ...

commits at adium.im commits at adium.im
Fri Aug 21 19:03:22 UTC 2009


details:	http://hg.adium.im/adium/rev/d6bb9d4249fc
revision:	2610:d6bb9d4249fc
author:		Stephen Holt <sholt at adium.im>
date:		Fri Aug 21 15:08:10 2009 -0400

Use NSPredicate for mention detection.  Fixes #12831.

Refactor our mention filter to search for strings based on NSPredicate matches.  Aside from simplifying the main matching loop, this also lets users seach using ICU regexps like so: ```/[mM]atchng\s[sS]tring/```.

All matches are case and diacritic insensitive.

See http://userguide.icu-project.org/strings/regexp for details on expression syntax.

diffs (269 lines):

diff -r d063b5446352 -r d6bb9d4249fc Frameworks/AIUtilities Framework/Source/AIStringAdditions.h
--- a/Frameworks/AIUtilities Framework/Source/AIStringAdditions.h	Thu Aug 20 11:02:19 2009 -0400
+++ b/Frameworks/AIUtilities Framework/Source/AIStringAdditions.h	Fri Aug 21 15:08:10 2009 -0400
@@ -53,6 +53,8 @@
 - (NSString *)stringByUnescapingFromXMLWithEntities:(NSDictionary *)entities;
 
 - (NSString *)stringByEscapingForShell;
+- (NSString *)stringByEscapingForRegexp;
+
 //- (BOOL)isURLEncoded;
 
 - (NSString *)stringByAddingPercentEscapesForAllCharacters;
diff -r d063b5446352 -r d6bb9d4249fc Frameworks/AIUtilities Framework/Source/AIStringAdditions.m
--- a/Frameworks/AIUtilities Framework/Source/AIStringAdditions.m	Thu Aug 20 11:02:19 2009 -0400
+++ b/Frameworks/AIUtilities Framework/Source/AIStringAdditions.m	Fri Aug 21 15:08:10 2009 -0400
@@ -527,6 +527,7 @@
 enum characterNatureMask {
 	whitespaceNature = 0x1, //space + \t\n\r\f\a 
 	shellUnsafeNature, //backslash + !$`"'
+	regexpUnsafeNature, //blakslash + |.*+?{}()$^
 };
 static enum characterNatureMask characterNature[USHRT_MAX+1] = {
 	//this array is initialised such that the space character (0x20)
@@ -650,6 +651,96 @@
 	return result;
 }
 
+- (NSString *)stringByEscapingForRegexp
+{
+	if (!(characterNature[' '] & whitespaceNature)) {
+		//if space doesn't have the whitespace nature, clearly we need to build the nature array.
+		
+		//first, set all characters to zero.
+		bzero(&characterNature, sizeof(characterNature));
+		
+		//then memorise which characters have the whitespace nature.
+		characterNature[' ']  = whitespaceNature;
+		//NOTE: if you give more characters the whitespace nature, be sure to
+		//	update escapeNames below.
+		
+		//finally, memorise which characters have the unsafe (for regexp) nature.
+		characterNature['\\'] = regexpUnsafeNature;
+		characterNature['/']  = regexpUnsafeNature;
+		characterNature['|']  = regexpUnsafeNature;
+		characterNature['.']  = regexpUnsafeNature;
+		characterNature['*']  = regexpUnsafeNature;
+		characterNature['+']  = regexpUnsafeNature;
+		characterNature['?']  = regexpUnsafeNature;
+		characterNature['{']  = regexpUnsafeNature;
+		characterNature['}']  = regexpUnsafeNature;
+		characterNature['(']  = regexpUnsafeNature;
+		characterNature[')']  = regexpUnsafeNature;
+		characterNature['[']  = regexpUnsafeNature;
+		characterNature['$']  = regexpUnsafeNature;
+		characterNature['^']  = regexpUnsafeNature;
+	}
+	
+	unsigned myLength = [self length];
+	unichar *myBuf = malloc(sizeof(unichar) * myLength);
+	if (!myBuf) return nil;
+	[self getCharacters:myBuf];
+	const unichar *myBufPtr = myBuf;
+	
+	size_t buflen = 0;
+	unichar *buf = NULL;
+	
+	const size_t buflenIncrement = getpagesize() / sizeof(unichar);
+	
+	/*the boundary guard happens everywhere that i increases, and MUST happen
+	 *	at the beginning of the loop.
+	 *
+	 *initialising buflen to 0 and buf to NULL as we have done above means that
+	 *	realloc will act as malloc:
+	 *	-	i is 0 at the beginning of the loop
+	 *	-	so is buflen
+	 *	-	and buf is NULL
+	 *	-	realloc(NULL, ...) == malloc(...)
+	 *
+	 *oh, and 'SBEFR' stands for String By Escaping For Regexp
+	 *	(the name of this method).
+	 */
+#define SBEFR_BOUNDARY_GUARD \
+do { \
+if (i == buflen) { \
+buf = realloc(buf, sizeof(unichar) * (buflen += buflenIncrement)); \
+if (!buf) { \
+NSLog(@"in stringByEscapingForRegexp: could not allocate %lu bytes", (unsigned long)(sizeof(unichar) * buflen)); \
+free(myBuf); \
+return nil; \
+} \
+} \
+} while (0)
+	
+	unsigned i = 0;
+	for (; myLength--; ++i) {
+		SBEFR_BOUNDARY_GUARD;
+		
+		if (characterNature[*myBufPtr] & regexpUnsafeNature) {
+			//escape this character
+			buf[i++] = '\\';
+			SBEFR_BOUNDARY_GUARD;
+		}
+		
+		buf[i] = *myBufPtr;
+		++myBufPtr;
+	}
+	
+#undef SBEFR_BOUNDARY_GUARD
+	
+	free(myBuf);
+	
+	NSString *result = [NSString stringWithCharacters:buf length:i];
+	free(buf);
+	
+	return result;
+}
+
 - (NSString *)volumePath
 {
 	NSEnumerator *pathEnum = [[[NSWorkspace sharedWorkspace] mountedLocalVolumePaths] objectEnumerator];
diff -r d063b5446352 -r d6bb9d4249fc Source/AIMentionAdvancedPreferences.h
--- a/Source/AIMentionAdvancedPreferences.h	Thu Aug 20 11:02:19 2009 -0400
+++ b/Source/AIMentionAdvancedPreferences.h	Fri Aug 21 15:08:10 2009 -0400
@@ -7,6 +7,8 @@
 
 #import <Adium/AIAdvancedPreferencePane.h>
 
+#define PREF_KEY_MENTIONS		@"Saved Mentions"
+
 @interface AIMentionAdvancedPreferences : AIAdvancedPreferencePane {
 	IBOutlet		NSTextField			*label_explanation;
 	
diff -r d063b5446352 -r d6bb9d4249fc Source/AIMentionAdvancedPreferences.m
--- a/Source/AIMentionAdvancedPreferences.m	Thu Aug 20 11:02:19 2009 -0400
+++ b/Source/AIMentionAdvancedPreferences.m	Fri Aug 21 15:08:10 2009 -0400
@@ -11,8 +11,6 @@
 #import <AIUtilities/AIImageAdditions.h>
 #import <AIUtilities/AIArrayAdditions.h>
 
-#define PREF_KEY_MENTIONS		@"Saved Mentions"
-
 @interface AIMentionAdvancedPreferences()
 - (void)saveTerms;
 @end
diff -r d063b5446352 -r d6bb9d4249fc Source/AIMentionEventPlugin.h
--- a/Source/AIMentionEventPlugin.h	Thu Aug 20 11:02:19 2009 -0400
+++ b/Source/AIMentionEventPlugin.h	Fri Aug 21 15:08:10 2009 -0400
@@ -14,12 +14,14 @@
  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
  */
 
+#import <AIUtilities/AIStringAdditions.h>
 #import <Adium/AIContentControllerProtocol.h>
-
+#import <Adium/AIPreferenceControllerProtocol.h>
 #import "AIMentionAdvancedPreferences.h"
 
 @interface AIMentionEventPlugin : AIPlugin <AIContentFilter> {
-	AIMentionAdvancedPreferences *advancedPreferences;
+	AIMentionAdvancedPreferences	*advancedPreferences;
+	NSArray							*mentionPredicates;
 }
-
+ at property(copy, nonatomic) NSArray *mentionPredicates;
 @end
diff -r d063b5446352 -r d6bb9d4249fc Source/AIMentionEventPlugin.m
--- a/Source/AIMentionEventPlugin.m	Thu Aug 20 11:02:19 2009 -0400
+++ b/Source/AIMentionEventPlugin.m	Fri Aug 21 15:08:10 2009 -0400
@@ -25,7 +25,6 @@
 #import <Adium/AIChat.h>
 #import <Adium/AIContactAlertsControllerProtocol.h>
 
-#define PREF_KEY_MENTIONS		@"Saved Mentions"
 
 /*!
  * @class AIMentionEventPlugin
@@ -33,6 +32,8 @@
  */
 @implementation AIMentionEventPlugin
 
+ at synthesize mentionPredicates;
+
 /*!
  * @brief Install
  */
@@ -41,6 +42,9 @@
 	[adium.contentController registerContentFilter:self
 											  ofType:AIFilterContent 
 										   direction:AIFilterIncoming];
+	
+	[adium.preferenceController registerPreferenceObserver:self 
+												  forGroup:PREF_GROUP_GENERAL];
 
 	advancedPreferences = [[AIMentionAdvancedPreferences preferencePaneForPlugin:self] retain];
 }
@@ -68,33 +72,28 @@
 	NSString *messageString = [inAttributedString string];
 			
 	AIAccount *account = (AIAccount *)message.destination;
+	NSString *contactAlias = [chat aliasForContact:[account contactWithUID:account.UID]];
 	
 	// XXX When we fix user lists to contain accounts, fix this too.
-	NSArray *myNames = [NSArray arrayWithObjects:
-						account.UID, 
-						account.displayName, 
-						/* can be nil */ [chat aliasForContact:[account contactWithUID:account.UID]],
-						nil];
+	NSArray *myPredicates = [NSArray arrayWithObjects:
+							 [NSPredicate predicateWithFormat:@"SELF MATCHES[cd] %@", [NSString stringWithFormat:@".*\\b%@\\b.*", [account.UID stringByEscapingForRegexp]]], 
+							 [NSPredicate predicateWithFormat:@"SELF MATCHES[cd] %@", [NSString stringWithFormat:@".*\\b%@\\b.*", [account.displayName stringByEscapingForRegexp]]], 
+							 /* can be nil */ contactAlias? [NSPredicate predicateWithFormat:@"SELF MATCHES[cd] %@", [NSString stringWithFormat:@".*\\b%@\\b.*", [contactAlias stringByEscapingForRegexp]]] : nil,
+							 nil];
 	
-	myNames = [myNames arrayByAddingObjectsFromArray:[adium.preferenceController preferenceForKey:PREF_KEY_MENTIONS group:PREF_GROUP_GENERAL]];
-
-	for(NSString *checkString in myNames) {
-		NSRange range = [messageString rangeOfString:checkString options:NSCaseInsensitiveSearch];
+	myPredicates = [myPredicates arrayByAddingObjectsFromArray:self.mentionPredicates];
 	
-		if(range.location != NSNotFound &&
-		   (range.location == 0 || ![[NSCharacterSet alphanumericCharacterSet] characterIsMember:[messageString characterAtIndex:range.location-1]]) &&
-		   (range.location + range.length >= [messageString length] || ![[NSCharacterSet alphanumericCharacterSet] characterIsMember:[messageString characterAtIndex:range.location+range.length]]))
-		{
+	for(NSPredicate *predicate in myPredicates) {
+		if([predicate evaluateWithObject:messageString]) {
 			if(message.trackContent && adium.interfaceController.activeChat != chat) {
 				[chat incrementUnviewedMentionCount];
 			}
-			
 			[message addDisplayClass:@"mention"];
-			
+			AILog(@"MENTIONED!");
 			break;
 		}
 	}
-	
+
 	return inAttributedString;
 }
 
@@ -106,4 +105,28 @@
 	return LOWEST_FILTER_PRIORITY;
 }
 
+/*!
+ * @brief Rebuild predicates on preference saves.
+ */
+#pragma mark Preference Observing
+- (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
+{
+	if(firstTime || [key isEqualToString:PREF_KEY_MENTIONS]) {
+		AILog(@"building mentions...");
+		NSArray *allMentions = [adium.preferenceController preferenceForKey:PREF_KEY_MENTIONS group:PREF_GROUP_GENERAL];
+		NSMutableArray *predicates = [NSMutableArray arrayWithCapacity:[allMentions count]];
+		NSPredicate *regexPredicate = [NSPredicate predicateWithFormat:@"SELF MATCHES '/.*/'"];
+		
+		for (NSString *mention in allMentions) {
+			if([regexPredicate evaluateWithObject:mention]) {
+				[predicates addObject:[NSPredicate predicateWithFormat:@"SELF MATCHES[cd] %@", [NSString stringWithFormat:@".*%@.*", [mention substringWithRange:NSMakeRange(1, [mention length]-2)]]]];
+			} else {
+				AILog(@"%@ -> %@", mention, [mention stringByEscapingForRegexp]);
+				[predicates addObject:[NSPredicate predicateWithFormat:@"SELF MATCHES[cd] %@", [NSString stringWithFormat:@".*\\b%@\\b.*", [mention stringByEscapingForRegexp]]]];
+			}
+		}
+		self.mentionPredicates = predicates;
+	}
+}
+
 @end




More information about the commits mailing list