libgaim.framework [386] which has extensive debugging in the upnp module to try to...
[adiumx.git] / Source / GBApplescriptFiltersPlugin.m
blob269cbffbeda57679da3d7fded196e63334078281
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import <Adium/AIContentControllerProtocol.h>
18 #import <Adium/AIMenuControllerProtocol.h>
19 #import <Adium/AIToolbarControllerProtocol.h>
20 #import "ESApplescriptabilityController.h"
21 #import "GBApplescriptFiltersPlugin.h"
22 #import <AIUtilities/AIMenuAdditions.h>
23 #import <AIUtilities/AIToolbarUtilities.h>
24 #import <AIUtilities/AIImageAdditions.h>
25 #import <AIUtilities/MVMenuButton.h>
26 #import <AIUtilities/AIExceptionHandlingUtilities.h>
27 #import <Adium/AIContentObject.h>
28 #import <Adium/AIHTMLDecoder.h>
30 #include <sys/errno.h>
31 #include <string.h>
33 #define TITLE_INSERT_SCRIPT             AILocalizedString(@"Insert Script",nil)
34 #define SCRIPT_BUNDLE_EXTENSION @"AdiumScripts"
35 #define SCRIPTS_PATH_NAME               @"Scripts"
36 #define SCRIPT_EXTENSION                @"scpt"
37 #define SCRIPT_IDENTIFIER               @"InsertScript"
39 #define SCRIPT_TIMEOUT                  30
41 @interface GBApplescriptFiltersPlugin (PRIVATE)
43 - (void)_replaceKeyword:(NSString *)keyword
44                          withScript:(NSMutableDictionary *)infoDict
45                            inString:(NSString *)inString
46          inAttributedString:(NSMutableAttributedString *)attributedString
47                            uniqueID:(unsigned long long)uniqueID;
49 - (void)_executeScript:(NSMutableDictionary *)infoDict 
50                  withArguments:(NSArray *)arguments
51                  forAttributedString:(NSMutableAttributedString *)attributedString
52                   keywordRange:(NSRange)keywordRange
53                           uniqueID:(unsigned long long)uniqueID;
55 - (NSArray *)_argumentsFromString:(NSString *)inString forScript:(NSMutableDictionary *)scriptDict;
57 - (void)buildScriptMenu;
58 - (void)_appendScripts:(NSArray *)scripts toMenu:(NSMenu *)menu;
59 - (void)_sortScriptsByTitle:(NSMutableArray *)sortArray;
61 - (void)registerToolbarItem;
63 - (void)scriptDidFinish:(NSNotification *)aNotification;
65 - (void)_replaceKeyword:(NSString *)keyword
66                          withScript:(NSMutableDictionary *)infoDict
67                            inString:(NSString *)inString
68          inAttributedString:(NSMutableAttributedString *)attributedString
69                                 context:(id)context
70                            uniqueID:(unsigned long long)uniqueID;
72 - (void)_executeScript:(NSMutableDictionary *)infoDict 
73                  withArguments:(NSArray *)arguments
74                  forAttributedString:(NSMutableAttributedString *)attributedString
75                   keywordRange:(NSRange)keywordRange
76                            context:(id)context
77                           uniqueID:(unsigned long long)uniqueID;
78 @end
80 int _scriptTitleSort(id scriptA, id scriptB, void *context);
81 int _scriptKeywordLengthSort(id scriptA, id scriptB, void *context);
83 /*!
84  * @class GBApplescriptFiltersPlugin
85  * @brief Filter component to allow .AdiumScripts applescript-based filters for outgoing messages
86  */
87 @implementation GBApplescriptFiltersPlugin
89 /*!
90  * @brief Install
91  */
92 - (void)installPlugin
94         //User scripts
95         [[AIObject sharedAdiumInstance] createResourcePathForName:@"Scripts"];
96         
97         //We have an array of scripts for building the menu, and a dictionary of scripts used for the actual substition
98         scriptArray = nil;
99         flatScriptArray = nil;
100         
101         //Prepare our script menu item (which will have the Scripts menu as its submenu)
102         scriptMenuItem = [[NSMenuItem alloc] initWithTitle:TITLE_INSERT_SCRIPT 
103                                                                                                 target:self
104                                                                                                 action:@selector(dummyTarget:)
105                                                                                  keyEquivalent:@""];
107         //Perform substitutions on outgoing content; we may be slow, so register as a delayed content filter
108         [[adium contentController] registerDelayedContentFilter:self 
109                                                                                                          ofType:AIFilterContent
110                                                                                                   direction:AIFilterOutgoing];
111         
112         //Observe for installation of new scripts
113         [[adium notificationCenter] addObserver:self
114                                                                    selector:@selector(xtrasChanged:)
115                                                                            name:Adium_Xtras_Changed
116                                                                          object:nil];
117         [[NSNotificationCenter defaultCenter] addObserver:self
118                                                                                          selector:@selector(toolbarWillAddItem:)
119                                                                                                  name:NSToolbarWillAddItemNotification
120                                                                                            object:nil]; 
121         
122         //Start building the script menu
123         scriptMenu = nil;
124         [self buildScriptMenu]; //this also sets the submenu for the menu item.
125         
126         [[adium menuController] addMenuItem:scriptMenuItem toLocation:LOC_Edit_Additions];
127         
128         contextualScriptMenuItem = [scriptMenuItem copy];
129         [[adium menuController] addContextualMenuItem:contextualScriptMenuItem toLocation:Context_TextView_Edit];
133  * @brief Deallocate
134  */
135 - (void)dealloc
137         [[NSNotificationCenter defaultCenter] removeObserver:self];
138         [[adium notificationCenter] removeObserver:self];
139         
140         [scriptArray release]; scriptArray = nil;
141     [flatScriptArray release]; flatScriptArray = nil;
142         [scriptMenuItem release]; scriptMenuItem = nil;
143         [contextualScriptMenuItem release]; contextualScriptMenuItem = nil;
144         
145         [super dealloc];
149  * @brief Xtras changes
151  * If the scripts xtras changed, rebuild our menus.
152  */
153 - (void)xtrasChanged:(NSNotification *)notification
155         if ([[notification object] caseInsensitiveCompare:@"AdiumScripts"] == 0) {
156                 [self buildScriptMenu];
157                                 
158                 [self registerToolbarItem];
159                 
160                 //Update our toolbar item's menu
161                 //[self toolbarWillAddItem:nil];
162         }
166 //Script Loading -------------------------------------------------------------------------------------------------------
167 #pragma mark Script Loading
169  * @brief Load our scripts
171  * This will clear out and then load from available scripts (external and internal) into flatScriptArray and scriptArray.
172  */
173 - (void)loadScripts
175         NSEnumerator    *enumerator;
176         NSString                *filePath;
177         NSBundle                *scriptBundle;
179         //
180         [scriptArray release]; scriptArray = [[NSMutableArray alloc] init];
181         [flatScriptArray release]; flatScriptArray = [[NSMutableArray alloc] init];
182         
183         // Load scripts
184         enumerator = [[adium allResourcesForName:@"Scripts" withExtensions:SCRIPT_BUNDLE_EXTENSION] objectEnumerator];
185         while ((filePath = [enumerator nextObject])) {
186                 if ((scriptBundle = [NSBundle bundleWithPath:filePath])) {
187                         
188                         NSString                *scriptsSetName;
189                         NSEnumerator    *scriptEnumerator;
190                         NSDictionary    *scriptDict;
191                         NSDictionary    *infoDict = [scriptBundle infoDictionary];
193                         //Get the name of the set these scripts will go into
194                         scriptsSetName = [infoDict objectForKey:@"Set"];
195                         
196                         //Now enumerate each script the bundle claims as its own
197                         scriptEnumerator = [[infoDict objectForKey:@"Scripts"] objectEnumerator];
198                         
199                         while ((scriptDict = [scriptEnumerator nextObject])) {
200                                 NSString                *scriptFileName, *scriptFilePath, *keyword, *title;
201                                 NSArray                 *arguments;
202                                 NSNumber                *prefixOnlyNumber;
203                                 
204                                 if ((scriptFileName = [scriptDict objectForKey:@"File"]) &&
205                                         (scriptFilePath = [scriptBundle pathForResource:scriptFileName
206                                                                                                                          ofType:SCRIPT_EXTENSION])) {
207                                         
208                                         keyword = [scriptDict objectForKey:@"Keyword"];
209                                         title = [scriptDict objectForKey:@"Title"];
210                                         
211                                         if (keyword && [keyword length] && title && [title length]) {
212                                                 NSMutableDictionary     *infoDict;
213                                                 
214                                                 arguments = [[scriptDict objectForKey:@"Arguments"] componentsSeparatedByString:@","];
215                                                 
216                                                 //Assume "Prefix Only" is NO unless told otherwise or the keyword starts with '/'
217                                                 prefixOnlyNumber = [scriptDict objectForKey:@"Prefix Only"];
218                                                 if (!prefixOnlyNumber) {
219                                                         prefixOnlyNumber = [NSNumber numberWithBool:([keyword characterAtIndex:0] == '/')];
220                                                 }
222                                                 infoDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
223                                                         scriptFilePath, @"Path", keyword, @"Keyword", title, @"Title", 
224                                                         prefixOnlyNumber, @"PrefixOnly", nil];
225                                                 
226                                                 //The bundle may not be part of (or for defining) a set of scripts
227                                                 if (scriptsSetName) {
228                                                         [infoDict setObject:scriptsSetName forKey:@"Set"];
229                                                 }
230                                                 //Arguments may be nil
231                                                 if (arguments) {
232                                                         [infoDict setObject:arguments forKey:@"Arguments"];
233                                                 }
234                                                 
235                                                 //Place the entry in our script arrays
236                                                 [scriptArray addObject:infoDict];
237                                                 [flatScriptArray addObject:infoDict];
238                                                 
239                                                 //Scripts must always be updated via polling
240                                                 [[adium contentController] registerFilterStringWhichRequiresPolling:keyword];
241                                         }
242                                 }
243                         }
244                 }               
245         }
249 //Script Menu ----------------------------------------------------------------------------------------------------------
250 #pragma mark Script Menu
252  * @brief Build the script menu
254  * Loads the scrpts as necessary, sorts them, then builds menus for the menu bar, the contextual menu,
255  * and the toolbar item.
256  */
257 - (void)buildScriptMenu
259         [self loadScripts];
260         
261         //Sort the scripts
262         [scriptArray sortUsingFunction:_scriptTitleSort context:nil];
263         [flatScriptArray sortUsingFunction:_scriptKeywordLengthSort context:nil];
264         
265         //Build the menu
266         [scriptMenu release]; scriptMenu = [[NSMenu alloc] initWithTitle:TITLE_INSERT_SCRIPT];
267         [self _appendScripts:scriptArray toMenu:scriptMenu];
268         [scriptMenuItem setSubmenu:scriptMenu];
269         [contextualScriptMenuItem setSubmenu:[[scriptMenu copy] autorelease]];
270                 
271         [self registerToolbarItem];
275  * @brief Sort first by set, then by title within sets
276  */
277 int _scriptTitleSort(id scriptA, id scriptB, void *context) {
278         NSComparisonResult result;
279         
280         NSString        *setA = [scriptA objectForKey:@"Set"];
281         NSString        *setB = [scriptB objectForKey:@"Set"];
282         
283         if (setA && setB) {
284                 
285                 //If both are within sets, sort by set; if they are within the same set, sort by title
286                 if ((result = [setA caseInsensitiveCompare:setB]) == NSOrderedSame) {
287                         result = [(NSString *)[scriptA objectForKey:@"Title"] caseInsensitiveCompare:[scriptB objectForKey:@"Title"]];
288                 }
289         } else {
290                 //Sort by title if neither is in a set; otherwise sort the one in a set to the top
291                 
292                 if (!setA && !setB) {
293                         result = [(NSString *)[scriptA objectForKey:@"Title"] caseInsensitiveCompare:[scriptB objectForKey:@"Title"]];
294                 
295                 } else if (!setA) {
296                         result = NSOrderedDescending;
297                 } else {
298                         result = NSOrderedAscending;
299                 }
300         }
301         
302         return result;
306  * @brief Sort by descending length so the longest keywords are at the beginning of the array
307  */
308 int _scriptKeywordLengthSort(id scriptA, id scriptB, void *context)
310         NSComparisonResult result;
311         
312         unsigned int lengthA = [(NSString *)[scriptA objectForKey:@"Keyword"] length];
313         unsigned int lengthB = [(NSString *)[scriptB objectForKey:@"Keyword"] length];
314         if (lengthA > lengthB) {
315                 result = NSOrderedAscending;
316         } else if (lengthA < lengthB) {
317                 result = NSOrderedDescending;
318         } else {
319                 result = NSOrderedSame;
320         }
321         
322         return result;
326  * @brief Append an array of scripts to a menu
328  * @param scripts The scripts, each of which is represented by an NSDictionary instance
329  * @param menu The menu to which to add the scripts
330  */
331 - (void)_appendScripts:(NSArray *)scripts toMenu:(NSMenu *)menu
333         NSEnumerator    *enumerator;
334         NSDictionary    *appendDict;
335         NSString                *lastSet = nil;
336         NSString                *set;
337         int                             indentationLevel;
338         
339         enumerator = [scripts objectEnumerator];
340         while ((appendDict = [enumerator nextObject])) {
341                 NSString        *title;
342                 NSMenuItem      *item;
343                 
344                 if ((set = [appendDict objectForKey:@"Set"])) {
345                         indentationLevel = 1;
346                         
347                         if (![set isEqualToString:lastSet]) {
348                                 //We have a new set of scripts; create a section header for them
349                                 item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:set
350                                                                                                                                                          target:nil
351                                                                                                                                                          action:nil
352                                                                                                                                           keyEquivalent:@""] autorelease];
353                                 if ([item respondsToSelector:@selector(setIndentationLevel:)]) [item setIndentationLevel:0];
354                                 [menu addItem:item];
355                                 
356                                 [lastSet release]; lastSet = [set retain];
357                         }
358                 } else {
359                         //Scripts not in sets need not be indented
360                         indentationLevel = 0;
361                         [lastSet release]; lastSet = nil;
362                 }
363         
364                 if ([appendDict objectForKey:@"Title"]) {
365                         title = [NSString stringWithFormat:@"%@ (%@)", [appendDict objectForKey:@"Title"], [appendDict objectForKey:@"Keyword"]];
366                 } else {
367                         title = [appendDict objectForKey:@"Keyword"];
368                 }
369                 
370                 item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
371                                                                                                                                          target:self
372                                                                                                                                          action:@selector(selectScript:)
373                                                                                                                           keyEquivalent:@""] autorelease];
374                 
375                 [item setRepresentedObject:appendDict];
376                 if ([item respondsToSelector:@selector(setIndentationLevel:)]) [item setIndentationLevel:indentationLevel];
377                 [menu addItem:item];
378         }
382  * @brief Insert a script's keyword into the text entry area
384  * This will be called by an NSMenuItem when it is clicked.
385  */
386 - (IBAction)selectScript:(id)sender
388         NSResponder     *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
389         
390         //Append our string into the responder if possible
391         if (responder && [responder isKindOfClass:[NSTextView class]]) {
392                 NSArray         *arguments = [[sender representedObject] objectForKey:@"Arguments"];
393                 NSString        *replacementText = [[sender representedObject] objectForKey:@"Keyword"];
394                 
395                 [(NSTextView *)responder insertText:replacementText];
396                 
397                 //Append arg list to replacement string, to show the user what they can pass
398                 if (arguments) {
399                         NSEnumerator            *argumentEnumerator = [arguments objectEnumerator];
400                         NSDictionary            *originalTypingAttributes = [(NSTextView *)responder typingAttributes];
401                         NSMutableDictionary *italicizedTypingAttributes = [originalTypingAttributes mutableCopy];
402                         NSString                        *anArgument;
403                         BOOL                            insertedFirst = NO;
404                         
405                         [italicizedTypingAttributes setObject:[[NSFontManager sharedFontManager] convertFont:[originalTypingAttributes objectForKey:NSFontAttributeName]
406                                                                                                                                                                          toHaveTrait:NSItalicFontMask]
407                                                                                    forKey:NSFontAttributeName];
408                         
409                         [(NSTextView *)responder insertText:@"{"];
410                         
411                         //Will that be a five minute argument or the full half hour?
412                         while ((anArgument = [argumentEnumerator nextObject])) {
413                                 //Insert a comma after each argument past the first
414                                 if (insertedFirst) {
415                                         [(NSTextView *)responder insertText:@","];                                      
416                                 } else {
417                                         insertedFirst = YES;
418                                 }
419                                 
420                                 //Turn on the italics version, insert the argument, then go back to normal for either the comma or the ending
421                                 [(NSTextView *)responder setTypingAttributes:italicizedTypingAttributes];
422                                 [(NSTextView *)responder insertText:anArgument];
423                                 [(NSTextView *)responder setTypingAttributes:originalTypingAttributes];
424                         }
426                         [(NSTextView *)responder insertText:@"}"];
427                         
428                         [italicizedTypingAttributes release];
429                 }
430         }
434  * @brief Fake target to allow validateMenuItem: to be called
435  */
436 -(IBAction)dummyTarget:(id)sender{
440  * @brief Validate menu item
441  * Disable the insertion if a text field is not active
442  */
443 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
445         if ((menuItem == scriptMenuItem) || (menuItem == contextualScriptMenuItem)) {
446                 return YES; //Always keep the submenu enabled so users can see the available scripts
447         } else {
448                 NSResponder     *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
449                 if (responder && [responder isKindOfClass:[NSText class]]) {
450                         return [(NSText *)responder isEditable];
451                 } else {
452                         return NO;
453                 }
454         }
457 //Message Filtering ----------------------------------------------------------------------------------------------------
458 #pragma mark Message Filtering
460  * @brief Delayed filter messages for keywords to replace
462  * Will eventually replace any script keywords with the result of running the script (with arguments as appropriate).
463  * @result YES if we began a delayed filtration; NO if we did not
464  */
465 - (BOOL)delayedFilterAttributedString:(NSAttributedString *)inAttributedString context:(id)context uniqueID:(unsigned long long)uniqueID
467         BOOL            beganProcessing = NO; 
468         NSString        *stringMessage;
470         if ((stringMessage = [inAttributedString string])) {
471                 NSEnumerator                            *enumerator;
472                 NSMutableDictionary                     *infoDict;
473                 
474                 //Replace all keywords
475                 enumerator = [flatScriptArray objectEnumerator];
476                 while ((infoDict = [enumerator nextObject]) && !beganProcessing) {
477                         NSString        *keyword = [infoDict objectForKey:@"Keyword"];
478                         BOOL            prefixOnly = [[infoDict objectForKey:@"PrefixOnly"] boolValue];
480                         if ((prefixOnly && ([stringMessage rangeOfString:keyword options:(NSCaseInsensitiveSearch | NSAnchoredSearch)].location == 0)) ||
481                            (!prefixOnly && [stringMessage rangeOfString:keyword options:NSCaseInsensitiveSearch].location != NSNotFound)) {
482                                 NSNumber        *shouldSendNumber;
484                                 [self _replaceKeyword:keyword
485                                                    withScript:infoDict
486                                                          inString:stringMessage
487                                    inAttributedString:[[inAttributedString mutableCopy] autorelease]
488                                                           context:context
489                                                          uniqueID:uniqueID];
491                                 shouldSendNumber = [infoDict objectForKey:@"ShouldSend"];
492                                 if ((shouldSendNumber) &&
493                                         (![shouldSendNumber boolValue]) &&
494                                         ([context isKindOfClass:[AIContentObject class]])) {
495                                         [(AIContentObject *)context setSendContent:NO];
496                                 }
497                                 
498                                 beganProcessing = YES;
499                         }
500                 }
501         }
502         
503     return beganProcessing;
507  * @brief Filter priority
509  * Filter earlier than the default
510  */
511 - (float)filterPriority
513         return HIGH_FILTER_PRIORITY;
517  * @brief Replace one instance of a keyword within a string. This will be called once for each instance.
518  */
519 - (void)_replaceKeyword:(NSString *)keyword
520                          withScript:(NSMutableDictionary *)infoDict
521                            inString:(NSString *)inString
522          inAttributedString:(NSMutableAttributedString *)attributedString
523                                 context:(id)context
524                            uniqueID:(unsigned long long)uniqueID
526         NSScanner       *scanner;
527         BOOL            foundKeyword = NO;
529         //Scan for the keyword
530         scanner = [NSScanner scannerWithString:inString];
531         while (![scanner isAtEnd] && !foundKeyword) {
532                 [scanner scanUpToString:keyword intoString:nil];
533                 
534                 if (([scanner scanString:keyword intoString:nil]) &&
535                         ([attributedString attribute:NSLinkAttributeName
536                                                                  atIndex:([scanner scanLocation]-1) /* The scanner ends up one past the keyword */
537                                                   effectiveRange:nil] == nil)) {
538                         //Scan the keyword and ensure it was not found within a link
539                         int             keywordStart, keywordEnd;
540                         NSArray         *argArray = nil;
541                         NSString        *argString;
542                         
543                         //Scan arguments
544                         keywordStart = [scanner scanLocation] - [keyword length];
545                         if ([scanner scanString:@"{" intoString:nil]) {
546                                 if ([scanner scanUpToString:@"}" intoString:&argString]) {
547                                         argArray = [self _argumentsFromString:argString forScript:infoDict];
548                                         [scanner scanString:@"}" intoString:nil];
549                                 }                               
550                         }
551                         keywordEnd = [scanner scanLocation];            
552                         
553                         if (keywordStart != 0 && [inString characterAtIndex:keywordStart - 1] == '\\') {
554                                 //Ignore the script (It was escaped) and delete the escape character
555                                 //XXX This is broken now; escaping scripts is no longer possible. Do we care? I don't. -evands
556                                 [attributedString replaceCharactersInRange:NSMakeRange(keywordStart - 1, 1) withString:@""];
557                                 foundKeyword = YES;
559                         } else {
560                                 //Run the script.
561                                 NSRange keywordRange = NSMakeRange(keywordStart, keywordEnd - keywordStart);
563                                 [self _executeScript:infoDict 
564                                            withArguments:argArray
565                                  forAttributedString:attributedString
566                                                 keywordRange:keywordRange
567                                                          context:context
568                                                         uniqueID:uniqueID];
570                                 foundKeyword = YES;
571                         }
572                 }
573         }
577  * @brief Execute the script as a separate task
579  * When the task is complete, we will be notified, at which point we perform the replacement for the script result
580  * and pass the modified attributed string back to the content controller for use.
581  */
582 - (void)_executeScript:(NSMutableDictionary *)infoDict 
583                            withArguments:(NSArray *)arguments
584                  forAttributedString:(NSMutableAttributedString *)attributedString
585                                 keywordRange:(NSRange)keywordRange
586                                          context:(id)context
587                                         uniqueID:(unsigned long long)uniqueID
589         NSDictionary    *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
590                 attributedString, @"Mutable Attributed String",
591                 NSStringFromRange(keywordRange), @"Range",
592                 [NSNumber numberWithUnsignedLongLong:uniqueID], @"uniqueID",
593                 (context ? context : [NSNull null]), @"context",
594                 nil];
595         
596         [[adium applescriptabilityController] runApplescriptAtPath:[infoDict objectForKey:@"Path"]
597                                                                                                           function:@"substitute"
598                                                                                                          arguments:arguments
599                                                                                            notifyingTarget:self
600                                                                                                           selector:@selector(applescriptDidRun:resultString:)
601                                                                                                           userInfo:userInfo];
605  * @brief A script finished running
606  */
607 - (void)applescriptDidRun:(id)userInfo resultString:(NSString *)resultString
609         NSMutableAttributedString       *attributedString = [userInfo objectForKey:@"Mutable Attributed String"];
610         NSRange                                         keywordRange = NSRangeFromString([userInfo objectForKey:@"Range"]);
611         unsigned long long                      uniqueID = [[userInfo objectForKey:@"uniqueID"] unsignedLongLongValue];
613         //If the script fails, eat the keyword
614         if (!resultString) resultString = @"";
616         //Replace the substring with script result
617         if (NSMaxRange(keywordRange) <= [attributedString length]) {
618                 if (([resultString hasPrefix:@"<HTML>"])) {
619                         //Obtain the attributed string version of the HTML, passing our current attributes as the default ones
620                         NSAttributedString *attributedScriptResult = [AIHTMLDecoder decodeHTML:resultString
621                                                                                                                          withDefaultAttributes:[attributedString attributesAtIndex:keywordRange.location
622                                                                                                                                                                                                                 effectiveRange:nil]];
623                         [attributedString replaceCharactersInRange:keywordRange
624                                                                   withAttributedString:attributedScriptResult];
625                         
626                 } else {
627                         [attributedString replaceCharactersInRange:keywordRange
628                                                                                         withString:resultString];
629                 }
630         }
632         //Inform the content controller that we're done if we don't need to do any more filtering
633         if (![self delayedFilterAttributedString:attributedString
634                                                                          context:[userInfo objectForKey:@"context"]
635                                                                         uniqueID:uniqueID]) {
636                 [[adium contentController] delayedFilterDidFinish:attributedString
637                                                                                                  uniqueID:uniqueID];
638         }
642  * @brief Determine the arguments for a script execution
644  * @param inString The string of potential arguments
645  * @param scriptDict The script being executed
647  * @result An NSArray of NSString instances
648  */
649 - (NSArray *)_argumentsFromString:(NSString *)inString forScript:(NSMutableDictionary *)scriptDict
651         NSArray                 *scriptArguments = [scriptDict objectForKey:@"Arguments"];
652         NSMutableArray  *argArray = [NSMutableArray array];
653         NSArray                 *inStringComponents = [inString componentsSeparatedByString:@","];
654         
655         unsigned                i = 0;
656         unsigned                count = (scriptArguments ? [scriptArguments count] : 0);
657         unsigned                inStringComponentsCount = [inStringComponents count];
658         
659         //Add each argument of inString to argArray so long as the number of arguments is less
660         //than the number of expected arguments for the script and the number of supplied arguments
661         while ((i < count) && (i < inStringComponentsCount)) {
662                 [argArray addObject:[inStringComponents objectAtIndex:i]];
663                 i++;
664         }
665         
666         //If more components were passed than were actually requested, the last argument gets the
667         //remainder
668         if (i < inStringComponentsCount) {
669                 NSRange remainingRange;
670                 
671                 //i was incremented to end the while loop if i > 0, so subtract 1 to reexamine the last object
672                 remainingRange.location = ((i > 0) ? i-1 : 0);
673                 remainingRange.length = (inStringComponentsCount - remainingRange.location);
675                 if (remainingRange.location >= 0) {
676                         NSString        *lastArgument;
678                         //Remove that last, incomplete argument if it was added
679                         if ([argArray count]) [argArray removeLastObject];
681                         //Create the last argument by joining all remaining comma-separated arguments with a comma
682                         lastArgument = [[inStringComponents subarrayWithRange:remainingRange] componentsJoinedByString:@","];
684                         [argArray addObject:lastArgument];
685                 }
686         }
687         
688         return argArray;
691 #pragma mark Toolbar item
693  * @brief Register our insert script toolbar item
694  */
695 - (void)registerToolbarItem
697         MVMenuButton *button;
698         
699         //Unregister the existing toolbar item first
700         if (toolbarItem) {
701                 [[adium toolbarController] unregisterToolbarItem:toolbarItem forToolbarType:@"TextEntry"];
702                 [toolbarItem release]; toolbarItem = nil;
703         }
704         
705         //Register our toolbar item
706         button = [[[MVMenuButton alloc] initWithFrame:NSMakeRect(0,0,32,32)] autorelease];
707         [button setImage:[NSImage imageNamed:@"scriptToolbar" forClass:[self class]]];
708         toolbarItem = [[AIToolbarUtilities toolbarItemWithIdentifier:SCRIPT_IDENTIFIER
709                                                                                                                    label:AILocalizedString(@"Scripts",nil)
710                                                                                                         paletteLabel:TITLE_INSERT_SCRIPT
711                                                                                                                  toolTip:AILocalizedString(@"Insert a script",nil)
712                                                                                                                   target:self
713                                                                                                  settingSelector:@selector(setView:)
714                                                                                                          itemContent:button
715                                                                                                                   action:@selector(selectScript:)
716                                                                                                                         menu:nil] retain];
717         [toolbarItem setMinSize:NSMakeSize(32,32)];
718         [toolbarItem setMaxSize:NSMakeSize(32,32)];
719         [button setToolbarItem:toolbarItem];
720     [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"TextEntry"];
724  * @brief After the toolbar has added the item we can set up the submenus
725  */
726 - (void)toolbarWillAddItem:(NSNotification *)notification
728         NSToolbarItem   *item = [[notification userInfo] objectForKey:@"item"];
729         
730         if (!notification || ([[item itemIdentifier] isEqualToString:SCRIPT_IDENTIFIER])) {
731                 NSMenu          *menu = [[[scriptMenuItem submenu] copy] autorelease];
732                 
733                 //Add menu to view
734                 [[item view] setMenu:menu];
735                 
736                 //Add menu to toolbar item (for text mode)
737                 NSMenuItem      *mItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] init] autorelease];
738                 [mItem setSubmenu:menu];
739                 [mItem setTitle:[menu title]];
740                 [item setMenuFormRepresentation:mItem];
741         }
744 @end