William Bowling is sharing code with you

Bitbucket is a code hosting site. Unlimited public and private repositories. Free for small teams.

Don't show this again

wbowling / adium (fork of adium / adium)

Fork of Adium for patches/improvements

Clone this repository (size: 338.7 MB): HTTPS / SSH
hg clone https://bitbucket.org/wbowling/adium
hg clone ssh://hg@bitbucket.org/wbowling/adium

adium / Source / GBApplescriptFiltersPlugin.m

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
/* 
 * Adium is the legal property of its developers, whose names are listed in the copyright file included
 * with this source distribution.
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software Foundation; either version 2 of the License,
 * or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
 * Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program; if not,
 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#import <Adium/AIContentControllerProtocol.h>
#import <Adium/AIMenuControllerProtocol.h>
#import <Adium/AIToolbarControllerProtocol.h>
#import "ESApplescriptabilityController.h"
#import "GBApplescriptFiltersPlugin.h"
#import <AIUtilities/AIMenuAdditions.h>
#import <AIUtilities/AIToolbarUtilities.h>
#import <AIUtilities/AIImageAdditions.h>
#import <AIUtilities/MVMenuButton.h>
#import <Adium/AIContentObject.h>
#import <Adium/AIHTMLDecoder.h>

#import <sys/errno.h>
#import <string.h>

#define TITLE_INSERT_SCRIPT             AILocalizedString(@"Insert Script",nil)
#define SCRIPT_BUNDLE_EXTENSION @"AdiumScripts"
#define SCRIPTS_PATH_NAME               @"Scripts"
#define SCRIPT_EXTENSION                @"scpt"
#define SCRIPT_IDENTIFIER               @"InsertScript"

#define SCRIPT_TIMEOUT                  30

@interface GBApplescriptFiltersPlugin ()
- (NSArray *)_argumentsFromString:(NSString *)inString forScript:(NSMutableDictionary *)scriptDict;
- (void)buildScriptMenu;
- (void)_appendScripts:(NSArray *)scripts toMenu:(NSMenu *)menu;
- (void)registerToolbarItem;

- (void)_replaceKeyword:(NSString *)keyword
                         withScript:(NSMutableDictionary *)infoDict
                           inString:(NSString *)inString
         inAttributedString:(NSMutableAttributedString *)attributedString
                                context:(id)context
                           uniqueID:(unsigned long long)uniqueID;

- (void)_executeScript:(NSMutableDictionary *)infoDict 
                 withArguments:(NSArray *)arguments
                 forAttributedString:(NSMutableAttributedString *)attributedString
                  keywordRange:(NSRange)keywordRange
                           context:(id)context
                          uniqueID:(unsigned long long)uniqueID;
@end

NSInteger _scriptTitleSort(id scriptA, id scriptB, void *context);
NSInteger _scriptKeywordLengthSort(id scriptA, id scriptB, void *context);

/*!
 * @class GBApplescriptFiltersPlugin
 * @brief Filter component to allow .AdiumScripts applescript-based filters for outgoing messages
 */
@implementation GBApplescriptFiltersPlugin

/*!
 * @brief Install
 */
- (void)installPlugin
{
        //User scripts
        [adium createResourcePathForName:@"Scripts"];
        
        //We have an array of scripts for building the menu, and a dictionary of scripts used for the actual substition
        scriptArray = nil;
        flatScriptArray = nil;
        
        //Prepare our script menu item (which will have the Scripts menu as its submenu)
        scriptMenuItem = [[NSMenuItem alloc] initWithTitle:TITLE_INSERT_SCRIPT 
                                                                                                target:self
                                                                                                action:@selector(dummyTarget:)
                                                                                 keyEquivalent:@""];

        //Perform substitutions on outgoing content; we may be slow, so register as a delayed content filter
        [adium.contentController registerDelayedContentFilter:self 
                                                                                                         ofType:AIFilterContent
                                                                                                  direction:AIFilterOutgoing];
        
        //Observe for installation of new scripts
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                                   selector:@selector(xtrasChanged:)
                                                                           name:AIXtrasDidChangeNotification
                                                                         object:nil];
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                                                         selector:@selector(toolbarWillAddItem:)
                                                                                                 name:NSToolbarWillAddItemNotification
                                                                                           object:nil]; 
        
        //Start building the script menu
        scriptMenu = nil;
        [self buildScriptMenu]; //this also sets the submenu for the menu item.
        
        [adium.menuController addMenuItem:scriptMenuItem toLocation:LOC_Edit_Additions];
        
        contextualScriptMenuItem = [scriptMenuItem copy];
        [adium.menuController addContextualMenuItem:contextualScriptMenuItem toLocation:Context_TextView_Edit];
}

/*!
 * @brief Deallocate
 */
- (void)dealloc
{
        [[NSNotificationCenter defaultCenter] removeObserver:self];
        
        [scriptArray release]; scriptArray = nil;
    [flatScriptArray release]; flatScriptArray = nil;
        [scriptMenuItem release]; scriptMenuItem = nil;
        [contextualScriptMenuItem release]; contextualScriptMenuItem = nil;
        
        [super dealloc];
}

/*!
 * @brief Xtras changes
 *
 * If the scripts xtras changed, rebuild our menus.
 */
- (void)xtrasChanged:(NSNotification *)notification
{
        if ([[notification object] caseInsensitiveCompare:@"AdiumScripts"] == NSOrderedSame) {
                [self buildScriptMenu];
                                
                [self registerToolbarItem];
                
                //Update our toolbar item's menu
                //[self toolbarWillAddItem:nil];
        }
}


//Script Loading -------------------------------------------------------------------------------------------------------
#pragma mark Script Loading
/*!
 * @brief Load our scripts
 *
 * This will clear out and then load from available scripts (external and internal) into flatScriptArray and scriptArray.
 */
- (void)loadScripts
{
        //
        [scriptArray release]; scriptArray = [[NSMutableArray alloc] init];
        [flatScriptArray release]; flatScriptArray = [[NSMutableArray alloc] init];
        
        // Load scripts
        for (NSString *filePath in [adium allResourcesForName:@"Scripts" withExtensions:SCRIPT_BUNDLE_EXTENSION]) {
                NSBundle                *scriptBundle;

                if ((scriptBundle = [NSBundle bundleWithPath:filePath])) {
                        NSString                *scriptsSetName;
                        NSDictionary    *infoDict = [NSDictionary dictionaryWithContentsOfFile:[[scriptBundle bundlePath] stringByAppendingPathComponent:@"Info.plist"]];
                        if (!infoDict) infoDict= [scriptBundle infoDictionary];

                        NSDictionary    *localizedInfoDict = [scriptBundle localizedInfoDictionary];

                        //Get the name of the set these scripts will go into
                        scriptsSetName = [localizedInfoDict objectForKey:@"Set"];
                        if (!scriptsSetName) scriptsSetName = [infoDict objectForKey:@"Set"];

                        //Now enumerate each script the bundle claims as its own
                        for (NSDictionary *scriptDict in [infoDict objectForKey:@"Scripts"]) {
                                NSString                *scriptFileName, *scriptFilePath, *keyword, *title;
                                NSArray                 *arguments;
                                NSNumber                *prefixOnlyNumber;
                                
                                if ((scriptFileName = [scriptDict objectForKey:@"File"]) &&
                                        (scriptFilePath = [scriptBundle pathForResource:scriptFileName
                                                                                                                         ofType:SCRIPT_EXTENSION])) {
                                        
                                        keyword = [scriptDict objectForKey:@"Keyword"];
                                        title = [scriptDict objectForKey:@"Title"];

                                        //The keywords titles are keyed by their English version in the localized info dict
                                        NSString *localizedKeyword = [localizedInfoDict objectForKey:keyword];
                                        if (localizedKeyword) keyword = localizedKeyword;

                                        NSString *localizedTitle = [localizedInfoDict objectForKey:title];
                                        if (localizedTitle) title = localizedTitle;

                                        if (keyword && [keyword length] && title && [title length]) {
                                                NSMutableDictionary     *infoDict;
                                                
                                                arguments = [[scriptDict objectForKey:@"Arguments"] componentsSeparatedByString:@","];
                                                
                                                //Assume "Prefix Only" is NO unless told otherwise or the keyword starts with '/'
                                                prefixOnlyNumber = [scriptDict objectForKey:@"Prefix Only"];
                                                if (!prefixOnlyNumber) {
                                                        prefixOnlyNumber = [NSNumber numberWithBool:[keyword hasPrefix:@"/"]];
                                                }

                                                infoDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                                        scriptFilePath, @"Path", keyword, @"Keyword", title, @"Title", 
                                                        prefixOnlyNumber, @"PrefixOnly", nil];
                                                
                                                //The bundle may not be part of (or for defining) a set of scripts
                                                if (scriptsSetName) {
                                                        [infoDict setObject:scriptsSetName forKey:@"Set"];
                                                }
                                                //Arguments may be nil
                                                if (arguments) {
                                                        [infoDict setObject:arguments forKey:@"Arguments"];
                                                }
                                                
                                                //Place the entry in our script arrays
                                                [scriptArray addObject:infoDict];
                                                [flatScriptArray addObject:infoDict];
                                                
                                                //Scripts must always be updated via polling
                                                [adium.contentController registerFilterStringWhichRequiresPolling:keyword];
                                        }
                                }
                        }
                } else {
                        NSLog(@"Warning: Could not load Adium script bundle at %@",filePath);
                }
        }
}


//Script Menu ----------------------------------------------------------------------------------------------------------
#pragma mark Script Menu
/*!
 * @brief Build the script menu
 *
 * Loads the scrpts as necessary, sorts them, then builds menus for the menu bar, the contextual menu,
 * and the toolbar item.
 */
- (void)buildScriptMenu
{
        [self loadScripts];
        
        //Sort the scripts
        [scriptArray sortUsingFunction:_scriptTitleSort context:nil];
        [flatScriptArray sortUsingFunction:_scriptKeywordLengthSort context:nil];
        
        //Build the menu
        [scriptMenu release]; scriptMenu = [[NSMenu alloc] initWithTitle:TITLE_INSERT_SCRIPT];
        [self _appendScripts:scriptArray toMenu:scriptMenu];
        [scriptMenuItem setSubmenu:scriptMenu];
        [contextualScriptMenuItem setSubmenu:[[scriptMenu copy] autorelease]];
                
        [self registerToolbarItem];
}

/*!
 * @brief Sort first by set, then by title within sets
 */
NSInteger _scriptTitleSort(id scriptA, id scriptB, void *context) {
        NSComparisonResult result;
        
        NSString        *setA = [scriptA objectForKey:@"Set"];
        NSString        *setB = [scriptB objectForKey:@"Set"];
        
        if (setA && setB) {
                
                //If both are within sets, sort by set; if they are within the same set, sort by title
                if ((result = [setA caseInsensitiveCompare:setB]) == NSOrderedSame) {
                        result = [(NSString *)[scriptA objectForKey:@"Title"] caseInsensitiveCompare:[scriptB objectForKey:@"Title"]];
                }
        } else {
                //Sort by title if neither is in a set; otherwise sort the one in a set to the top
                
                if (!setA && !setB) {
                        result = [(NSString *)[scriptA objectForKey:@"Title"] caseInsensitiveCompare:[scriptB objectForKey:@"Title"]];
                
                } else if (!setA) {
                        result = NSOrderedDescending;
                } else {
                        result = NSOrderedAscending;
                }
        }
        
        return result;
}

/*!
 * @brief Sort by descending length so the longest keywords are at the beginning of the array
 */
NSInteger _scriptKeywordLengthSort(id scriptA, id scriptB, void *context)
{
        NSComparisonResult result;
        
        NSUInteger lengthA = [(NSString *)[scriptA objectForKey:@"Keyword"] length];
        NSUInteger lengthB = [(NSString *)[scriptB objectForKey:@"Keyword"] length];
        if (lengthA > lengthB) {
                result = NSOrderedAscending;
        } else if (lengthA < lengthB) {
                result = NSOrderedDescending;
        } else {
                result = NSOrderedSame;
        }
        
        return result;
}

/*!
 * @brief Append an array of scripts to a menu
 *
 * @param scripts The scripts, each of which is represented by an NSDictionary instance
 * @param menu The menu to which to add the scripts
 */
- (void)_appendScripts:(NSArray *)scripts toMenu:(NSMenu *)menu
{
        NSDictionary    *appendDict;
        NSString                *lastSet = nil;
        NSString                *set;
        NSInteger                               indentationLevel;
        
        for (appendDict in scripts) {
                NSString        *title;
                NSMenuItem      *item;
                
                if ((set = [appendDict objectForKey:@"Set"])) {
                        indentationLevel = 1;
                        
                        if (![set isEqualToString:lastSet]) {
                                //We have a new set of scripts; create a section header for them
                                item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:set
                                                                                                                                                         target:nil
                                                                                                                                                         action:nil
                                                                                                                                          keyEquivalent:@""] autorelease];
                                if ([item respondsToSelector:@selector(setIndentationLevel:)]) [item setIndentationLevel:0];
                                [menu addItem:item];
                                
                                [lastSet release]; lastSet = [set retain];
                        }
                } else {
                        //Scripts not in sets need not be indented
                        indentationLevel = 0;
                        [lastSet release]; lastSet = nil;
                }
        
                if ([appendDict objectForKey:@"Title"]) {
                        title = [NSString stringWithFormat:@"%@ (%@)", [appendDict objectForKey:@"Title"], [appendDict objectForKey:@"Keyword"]];
                } else {
                        title = [appendDict objectForKey:@"Keyword"];
                }
                
                item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
                                                                                                                                         target:self
                                                                                                                                         action:@selector(selectScript:)
                                                                                                                          keyEquivalent:@""] autorelease];
                
                [item setRepresentedObject:appendDict];
                if ([item respondsToSelector:@selector(setIndentationLevel:)]) [item setIndentationLevel:indentationLevel];
                [menu addItem:item];
        }
}

/*!
 * @brief Insert a script's keyword into the text entry area
 *
 * This will be called by an NSMenuItem when it is clicked.
 */
- (IBAction)selectScript:(id)sender
{
        NSResponder     *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
        
        //Append our string into the responder if possible
        if (responder && [responder isKindOfClass:[NSTextView class]]) {
                NSArray         *arguments = [[sender representedObject] objectForKey:@"Arguments"];
                NSString        *replacementText = [[sender representedObject] objectForKey:@"Keyword"];
                
                [(NSTextView *)responder insertText:replacementText];
                
                //Append arg list to replacement string, to show the user what they can pass
                if (arguments) {
                        NSDictionary            *originalTypingAttributes = [(NSTextView *)responder typingAttributes];
                        NSMutableDictionary *italicizedTypingAttributes = [originalTypingAttributes mutableCopy];
                        NSString                        *anArgument;
                        BOOL                            insertedFirst = NO;
                        
                        [italicizedTypingAttributes setObject:[[NSFontManager sharedFontManager] convertFont:[originalTypingAttributes objectForKey:NSFontAttributeName]
                                                                                                                                                                         toHaveTrait:NSItalicFontMask]
                                                                                   forKey:NSFontAttributeName];
                        
                        [(NSTextView *)responder insertText:@"{"];
                        
                        //Will that be a five minute argument or the full half hour?
                        for (anArgument in arguments) {
                                //Insert a comma after each argument past the first
                                if (insertedFirst) {
                                        [(NSTextView *)responder insertText:@","];                                      
                                } else {
                                        insertedFirst = YES;
                                }
                                
                                //Turn on the italics version, insert the argument, then go back to normal for either the comma or the ending
                                [(NSTextView *)responder setTypingAttributes:italicizedTypingAttributes];
                                [(NSTextView *)responder insertText:anArgument];
                                [(NSTextView *)responder setTypingAttributes:originalTypingAttributes];
                        }

                        [(NSTextView *)responder insertText:@"}"];
                        
                        [italicizedTypingAttributes release];
                }
        }
}

/*!
 * @brief Fake target to allow validateMenuItem: to be called
 */
-(IBAction)dummyTarget:(id)sender{
}

/*!
 * @brief Validate menu item
 * Disable the insertion if a text field is not active
 */
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
{
        if ((menuItem == scriptMenuItem) || (menuItem == contextualScriptMenuItem)) {
                return YES; //Always keep the submenu enabled so users can see the available scripts
        } else {
                NSResponder     *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
                if (responder && [responder isKindOfClass:[NSText class]]) {
                        return [(NSText *)responder isEditable];
                } else {
                        return NO;
                }
        }
}

//Message Filtering ----------------------------------------------------------------------------------------------------
#pragma mark Message Filtering
/*!
 * @brief Delayed filter messages for keywords to replace
 *
 * Will eventually replace any script keywords with the result of running the script (with arguments as appropriate).
 * @result YES if we began a delayed filtration; NO if we did not
 */
- (BOOL)delayedFilterAttributedString:(NSAttributedString *)inAttributedString context:(id)context uniqueID:(unsigned long long)uniqueID
{
        BOOL            beganProcessing = NO; 
        NSString        *stringMessage;

        if ((stringMessage = [inAttributedString string])) {
                //Replace all keywords
                for (NSMutableDictionary *infoDict in flatScriptArray) {
                        NSString        *keyword = [infoDict objectForKey:@"Keyword"];
                        BOOL            prefixOnly = [[infoDict objectForKey:@"PrefixOnly"] boolValue];

                        if ((prefixOnly && ([stringMessage rangeOfString:keyword options:(NSCaseInsensitiveSearch | NSAnchoredSearch)].location == 0)) ||
                           (!prefixOnly && [stringMessage rangeOfString:keyword options:NSCaseInsensitiveSearch].location != NSNotFound)) {
                                NSNumber        *shouldSendNumber;

                                [self _replaceKeyword:keyword
                                                   withScript:infoDict
                                                         inString:stringMessage
                                   inAttributedString:[[inAttributedString mutableCopy] autorelease]
                                                          context:context
                                                         uniqueID:uniqueID];

                                shouldSendNumber = [infoDict objectForKey:@"ShouldSend"];
                                if ((shouldSendNumber) &&
                                        (![shouldSendNumber boolValue]) &&
                                        ([context isKindOfClass:[AIContentObject class]])) {
                                        [(AIContentObject *)context setSendContent:NO];
                                }
                                
                                beganProcessing = YES;
                                break;
                        }
                }
        }
        
    return beganProcessing;
}

/*!
 * @brief Filter priority
 *
 * Filter earlier than the default
 */
- (CGFloat)filterPriority
{
        return HIGH_FILTER_PRIORITY;
}

/*!
 * @brief Replace one instance of a keyword within a string. This will be called once for each instance.
 */
- (void)_replaceKeyword:(NSString *)keyword
                         withScript:(NSMutableDictionary *)infoDict
                           inString:(NSString *)inString
         inAttributedString:(NSMutableAttributedString *)attributedString
                                context:(id)context
                           uniqueID:(unsigned long long)uniqueID
{
        NSScanner       *scanner;
        BOOL            foundKeyword = NO;

        //Scan for the keyword
        scanner = [NSScanner scannerWithString:inString];
        while (![scanner isAtEnd] && !foundKeyword) {
                [scanner scanUpToString:keyword intoString:nil];
                
                if (([scanner scanString:keyword intoString:nil]) &&
                        ([attributedString attribute:NSLinkAttributeName
                                                                 atIndex:([scanner scanLocation]-1) /* The scanner ends up one past the keyword */
                                                  effectiveRange:nil] == nil)) {
                        //Scan the keyword and ensure it was not found within a link
                        NSInteger               keywordStart, keywordEnd;
                        NSArray         *argArray = nil;
                        NSString        *argString;
                        
                        //Scan arguments
                        keywordStart = [scanner scanLocation] - [keyword length];
                        if ([scanner scanString:@"{" intoString:nil]) {
                                if ([scanner scanUpToString:@"}" intoString:&argString]) {
                                        argArray = [self _argumentsFromString:argString forScript:infoDict];
                                        [scanner scanString:@"}" intoString:nil];
                                }                               
                        }
                        keywordEnd = [scanner scanLocation];            
                        
                        //Run the script.
                        NSRange keywordRange = NSMakeRange(keywordStart, keywordEnd - keywordStart);
                        [self _executeScript:infoDict 
                                   withArguments:argArray
                         forAttributedString:attributedString
                                        keywordRange:keywordRange
                                                 context:context
                                                uniqueID:uniqueID];
                        
                        foundKeyword = YES;
                }
        }
}

/*!
 * @brief Execute the script as a separate task
 *
 * When the task is complete, we will be notified, at which point we perform the replacement for the script result
 * and pass the modified attributed string back to the content controller for use.
 */
- (void)_executeScript:(NSMutableDictionary *)infoDict 
                           withArguments:(NSArray *)arguments
                 forAttributedString:(NSMutableAttributedString *)attributedString
                                keywordRange:(NSRange)keywordRange
                                         context:(id)context
                                        uniqueID:(unsigned long long)uniqueID
{
        NSDictionary    *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
                attributedString, @"Mutable Attributed String",
                NSStringFromRange(keywordRange), @"Range",
                [NSNumber numberWithUnsignedLongLong:uniqueID], @"uniqueID",
                (context ? context : [NSNull null]), @"context",
                nil];
        
        [adium.applescriptabilityController runApplescriptAtPath:[infoDict objectForKey:@"Path"]
                                                                                                          function:@"substitute"
                                                                                                         arguments:arguments
                                                                                           notifyingTarget:self
                                                                                                          selector:@selector(applescriptDidRun:resultString:)
                                                                                                          userInfo:userInfo];
}

/*!
 * @brief A script finished running
 */
- (void)applescriptDidRun:(id)userInfo resultString:(NSString *)resultString
{
        NSMutableAttributedString       *attributedString = [userInfo objectForKey:@"Mutable Attributed String"];
        NSRange                                         keywordRange = NSRangeFromString([userInfo objectForKey:@"Range"]);
        unsigned long long                      uniqueID = [[userInfo objectForKey:@"uniqueID"] unsignedLongLongValue];

        //If the script fails, eat the keyword
        if (!resultString) resultString = @"";

        //Replace the substring with script result
        if (NSMaxRange(keywordRange) <= [attributedString length]) {
                if (([resultString hasPrefix:@"<HTML>"])) {
                        //Obtain the attributed string version of the HTML, passing our current attributes as the default ones
                        NSAttributedString *attributedScriptResult = [AIHTMLDecoder decodeHTML:resultString
                                                                                                                         withDefaultAttributes:[attributedString attributesAtIndex:keywordRange.location
                                                                                                                                                                                                                effectiveRange:nil]];
                        [attributedString replaceCharactersInRange:keywordRange
                                                                  withAttributedString:attributedScriptResult];
                        
                } else {
                        [attributedString replaceCharactersInRange:keywordRange
                                                                                        withString:resultString];
                }
        }

        //Inform the content controller that we're done if we don't need to do any more filtering
        if (![self delayedFilterAttributedString:attributedString
                                                                         context:[userInfo objectForKey:@"context"]
                                                                        uniqueID:uniqueID]) {
                [adium.contentController delayedFilterDidFinish:attributedString
                                                                                                 uniqueID:uniqueID];
        }
}

/*!
 * @brief Determine the arguments for a script execution
 *
 * @param inString The string of potential arguments
 * @param scriptDict The script being executed
 *
 * @result An NSArray of NSString instances
 */
- (NSArray *)_argumentsFromString:(NSString *)inString forScript:(NSMutableDictionary *)scriptDict
{
        NSArray                 *scriptArguments = [scriptDict objectForKey:@"Arguments"];
        NSMutableArray  *argArray = [NSMutableArray array];
        NSArray                 *inStringComponents = [inString componentsSeparatedByString:@","];
        
        NSUInteger              i = 0;
        NSUInteger              count = (scriptArguments ? [scriptArguments count] : 0);
        NSUInteger              inStringComponentsCount = [inStringComponents count];
        
        //Add each argument of inString to argArray so long as the number of arguments is less
        //than the number of expected arguments for the script and the number of supplied arguments
        while ((i < count) && (i < inStringComponentsCount)) {
                [argArray addObject:[inStringComponents objectAtIndex:i]];
                i++;
        }
        
        //If more components were passed than were actually requested, the last argument gets the
        //remainder
        if (i < inStringComponentsCount) {
                NSRange remainingRange;
                
                //i was incremented to end the while loop if i > 0, so subtract 1 to reexamine the last object
                remainingRange.location = ((i > 0) ? i-1 : 0);
                remainingRange.length = (inStringComponentsCount - remainingRange.location);

                if (remainingRange.location >= 0) {
                        NSString        *lastArgument;

                        //Remove that last, incomplete argument if it was added
                        if ([argArray count]) [argArray removeLastObject];

                        //Create the last argument by joining all remaining comma-separated arguments with a comma
                        lastArgument = [[inStringComponents subarrayWithRange:remainingRange] componentsJoinedByString:@","];

                        [argArray addObject:lastArgument];
                }
        }
        
        return argArray;
}

#pragma mark Toolbar item
/*!
 * @brief Register our insert script toolbar item
 */
- (void)registerToolbarItem
{
        MVMenuButton *button;
        
        //Unregister the existing toolbar item first
        if (toolbarItem) {
                [adium.toolbarController unregisterToolbarItem:toolbarItem forToolbarType:@"TextEntry"];
                [toolbarItem release]; toolbarItem = nil;
        }
        
        //Register our toolbar item
        button = [[[MVMenuButton alloc] initWithFrame:NSMakeRect(0,0,32,32)] autorelease];
        [button setImage:[NSImage imageNamed:@"scriptToolbar" forClass:[self class] loadLazily:YES]];
        toolbarItem = [[AIToolbarUtilities toolbarItemWithIdentifier:SCRIPT_IDENTIFIER
                                                                                                                   label:AILocalizedString(@"Scripts",nil)
                                                                                                        paletteLabel:TITLE_INSERT_SCRIPT
                                                                                                                 toolTip:AILocalizedString(@"Insert a script",nil)
                                                                                                                  target:self
                                                                                                 settingSelector:@selector(setView:)
                                                                                                         itemContent:button
                                                                                                                  action:@selector(selectScript:)
                                                                                                                        menu:nil] retain];
        [toolbarItem setMinSize:NSMakeSize(32,32)];
        [toolbarItem setMaxSize:NSMakeSize(32,32)];
        [button setToolbarItem:toolbarItem];
    [adium.toolbarController registerToolbarItem:toolbarItem forToolbarType:@"TextEntry"];
}

/*!
 * @brief After the toolbar has added the item we can set up the submenus
 */
- (void)toolbarWillAddItem:(NSNotification *)notification
{
        NSToolbarItem   *item = [[notification userInfo] objectForKey:@"item"];
        
        if (!notification || ([[item itemIdentifier] isEqualToString:SCRIPT_IDENTIFIER])) {
                NSMenu          *menu = [[[scriptMenuItem submenu] copy] autorelease];
                
                //Add menu to view
                [[item view] setMenu:menu];
                
                //Add menu to toolbar item (for text mode)
                NSMenuItem      *mItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] init] autorelease];
                [mItem setSubmenu:menu];
                [mItem setTitle:[menu title]];
                [item setMenuFormRepresentation:mItem];
        }
}

@end