source: trunk/ICeCoffEE/ICeCoffEE/ICeCoffEEParser.m @ 388

Last change on this file since 388 was 388, checked in by Nicholas Riley, 12 years ago

APEMain.m: Note missing Xcode 3 support. Add Terminal 2.0 support.

English.lproj/APE Manager plugin.nib:

English.lproj/APEInfo.rtfd:

ICeCoffEE.[hm]: Bring triggering window and app to front if error
dialog displayed in ICCF_HandleException. Remove initial word
selection. Launch preexisting selection if present. Remove 10.3
support. Update for new ICeCoffEETrigger API. Rename downEvent
parameter for consistency.

ICeCoffEE.xcodeproj: Added files.

ICeCoffEEParser.m: Handle multiline URLs.

ICeCoffEETTView.[hm]: Terminal 2.0 support (thank you for having a
usable NSTextInput implementation!)

ICeCoffEETTViewTrigger.[hm]: ICeCoffEETrigger implementation for
TTView. Can't set range as we do for NSTextView because you can't
have an arbitrary empty selection range in Terminal, so we pass it
directly to ICCF_LaunchURLFromTTView.

ICeCoffEETerminal.[hm]: Fix capitalization on ICeCoffEETermSubviewSuper.
Pass downEvent to ICCF_HandleException so it can bring the triggering
window to the front.

ICeCoffEETextViewTrigger.[hm]: Moved from ICeCoffEETrigger.

ICeCoffEETrigger.[hm]: Now an abstract superclass. Replace direct
access to ICCF_sharedTrigger with +cancel. Create 0-character range
(if click outside selectedRange) or save selectedRange. Move
debugging logs here from ICeCoffEE.m. Add description method.

ICeCoffEEWebKit.m: Pass downEvent to ICCF_HandleException so it can
bring the triggering window to the front.

Installer components/ui/ui.plist: Updated for 1.5d3.

TestParser?.m: Handle multiline URLs; newlines are printed as \ and
tabs as >. Implement ICCF_StringByRemovingCharactersInSet, which will
move elsewhere once Internet Config support is removed.

VERSION.xcconfig: Updated for 1.5d3.

urls.plist: Test for multiline URLs.

File size: 10.1 KB
Line 
1//
2//  ICeCoffEEParser.m
3//  ICeCoffEE
4//
5//  Created by Nicholas Riley on 6/21/07.
6//  Copyright 2007 Nicholas Riley. All rights reserved.
7//
8
9#import "ICeCoffEEParser.h"
10#import "ICeCoffEE.h"
11
12void ICCF_Delimiters(NSCharacterSet **leftPtr, NSCharacterSet **rightPtr) {
13    static NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil;
14   
15    if (urlLeftDelimiters == nil || urlRightDelimiters == nil) {
16        NSMutableCharacterSet *set = [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
17        NSMutableCharacterSet *tmpSet;
18        [urlLeftDelimiters release];
19        [urlRightDelimiters release];
20       
21        [set autorelease];
22        [set formUnionWithCharacterSet: [[NSCharacterSet characterSetWithRange: NSMakeRange(0x21, 0x5e)] invertedSet]]; // nonprintable and non-ASCII characters
23        [set formUnionWithCharacterSet: [NSCharacterSet punctuationCharacterSet]];
24        // XXX obsoleted by RFC 3986 now... use §2.1, 2.2, 2.3
25        [set removeCharactersInString: @";/?:@&=+$,-_.!~*'(){}[]%#"]; // RFC 2396 §2.2, 2.3, 2.4, plus % and # from "delims" set and {}, []
26       
27        tmpSet = [[set mutableCopy] autorelease];
28        [tmpSet formUnionWithCharacterSet: [NSCharacterSet characterSetWithCharactersInString: @"<(["]];
29        urlLeftDelimiters = [tmpSet copy]; // make immutable again - for efficiency
30       
31        tmpSet = [[set mutableCopy] autorelease];
32        [tmpSet formUnionWithCharacterSet: [NSCharacterSet characterSetWithCharactersInString: @">)]"]];
33        urlRightDelimiters = [tmpSet copy]; // make immutable again - for efficiency
34    }
35   
36    *leftPtr = urlLeftDelimiters; *rightPtr = urlRightDelimiters;
37}
38
39static ICInstance ICCF_icInst = NULL;
40
41void ICCF_StartIC() {
42    OSStatus err;
43   
44    if (ICCF_icInst != NULL) {
45        ICLog(@"ICCF_StartIC: Internet Config is already running!");
46        ICCF_StopIC();
47    }
48    err = ICStart(&ICCF_icInst, kICCFCreator);
49    NSCAssert1(err == noErr, ICCF_LocalizedString(@"Unable to start Internet Config (error %d)"), err);
50}
51
52void ICCF_StopIC() {
53    if (ICCF_icInst == NULL) {
54        ICLog(@"ICCF_StopIC: Internet Config is not running!");
55    } else {
56        ICStop(ICCF_icInst);
57        ICCF_icInst = NULL;
58    }
59}
60
61ICInstance ICCF_GetInst() {
62    NSCAssert(ICCF_icInst != NULL, @"Internal error: Called ICCF_GetInst without ICCF_StartIC");
63    return ICCF_icInst;
64}
65
66// input/output 'range' is the range of source document which contains 'string'
67void ICCF_ParseURL(NSString *string, NSRange *range) {
68    OSStatus err;
69    Handle h;
70    long selStart = 0, selEnd = range->length; // local offsets within 'string'
71    char *urlData = NULL;
72   
73    NSCAssert(selEnd == [string length], @"Internal error: URL string is wrong length");
74   
75    NS_DURING
76        if ([[NSCharacterSet characterSetWithCharactersInString: @";,."] characterIsMember:
77            [string characterAtIndex: selEnd - 1]]) {
78            selEnd--;
79        }
80        NSCharacterSet *alphanumericCharacterSet = [NSCharacterSet alphanumericCharacterSet];
81        while (![alphanumericCharacterSet characterIsMember: [string characterAtIndex: selStart]]) {
82            selStart++;
83            NSCAssert(selStart < selEnd, @"No URL is selected");
84        }
85       
86        string = [string substringWithRange: NSMakeRange(selStart, selEnd - selStart)];
87       
88        ICLog(@"Parsing URL |%@|", string);
89       
90        NSCAssert([string canBeConvertedToEncoding: NSASCIIStringEncoding], @"No URL is selected");
91       
92        urlData = (char *)malloc( (range->length + 1) * sizeof(char));
93        NSCAssert(urlData != NULL, @"Internal error: can't allocate memory for URL string");
94       
95        // XXX getCString: is deprecated in 10.4, but this is safe and shouldn't assert because we've already verified the string can be converted to ASCII, which should be a subset of any possible system encoding.  The replacement (getCString:maxLength:encoding:) is not available until 10.4, so we leave this until we dump Internet Config and gain IDN friendliness.
96        [string getCString: urlData];
97       
98        h = NewHandle(0);
99        NSCAssert(h != NULL, @"Internal error: can't allocate URL handle");
100       
101        err = ICParseURL(ICCF_GetInst(), "\pmailto", urlData, range->length, &selStart, &selEnd, h);
102        DisposeHandle(h);
103       
104        ICCF_OSErrCAssert(err, @"ICParseURL");
105       
106        range->length = selEnd - selStart;
107        range->location += selStart;
108    NS_HANDLER
109        free(urlData);
110        [localException raise];
111    NS_ENDHANDLER
112   
113    free(urlData);
114}
115
116static BOOL ICCF_StringIncludesCharacter(NSString *s, unichar character, NSRange range) {
117    NSRange result = [s rangeOfCharacterFromSet: [NSCharacterSet characterSetWithCharactersInString:
118                                         [NSString stringWithCharacters: &character length: 1]]
119                                        options: NSLiteralSearch range: range];
120    return (result.location != NSNotFound);
121}
122
123static BOOL ICCF_IsLikelyURI(NSString *s, NSRange range) {
124    return ([s rangeOfCharacterFromSet: [NSCharacterSet characterSetWithCharactersInString: @":/.@"]
125                               options: NSLiteralSearch range: range].location != NSNotFound);
126}
127
128static BOOL ICCF_IsLikelyIPv6Address(NSString *s, NSRange range) {
129    return ([s rangeOfCharacterFromSet:
130             [[NSCharacterSet characterSetWithCharactersInString: @"ABCDEFabcdef0123456789:"] invertedSet]
131                               options: NSLiteralSearch range: range].location == NSNotFound);
132}
133
134NSRange ICCF_URLEnclosingRange(NSString *s, NSRange range) {
135    NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil;
136    NSRange delimiterRange;
137    unsigned extraLen;
138    BOOL multiLine = NO;
139   
140    ICCF_CheckRange(range);
141   
142    ICCF_Delimiters(&urlLeftDelimiters, &urlRightDelimiters);
143   
144    // right delimiter selected?  Yes, this can break with ...)URL(....  Oh well.
145    if (range.location > 0 && [urlRightDelimiters characterIsMember: [s characterAtIndex: range.location]]) {
146        --range.location;
147        ++range.length;
148        ICLog(@"expanding past initial %c, now |%@|", [s characterAtIndex: range.location + 1],
149              [s substringWithRange: range]);
150    }
151       
152expandFront:
153    // XXX instead of 0, make this stop at the max URL length to prevent protracted searches
154    // XXX backport to ICeCoffEETerminal
155    // add 1 to range to trap delimiters that are on the edge of the selection (i.e., <...)
156    delimiterRange = [s rangeOfCharacterFromSet: urlLeftDelimiters
157                                        options: NSLiteralSearch | NSBackwardsSearch
158                                          range: NSMakeRange(0, range.location + (range.location != [s length]))];
159    if (delimiterRange.location == NSNotFound) {
160        // extend to beginning of string
161        range.length += range.location;
162        range.location = 0;
163    } else {
164        NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
165        if ([s characterAtIndex: delimiterRange.location] == '<') { // XXX move to expandBoth to handle clicking in middle
166            multiLine = YES;
167            urlRightDelimiters = [NSCharacterSet characterSetWithCharactersInString: @">"];
168        }
169        range.length += range.location - delimiterRange.location - 1;
170        range.location = delimiterRange.location + 1;
171    }
172   
173
174expandBack:
175    // XXX instead of length of string, make this stop at the max URL length to prevent protracted searches
176    // add 1 to range to trap delimiters that are on the edge of the selection (i.e., ...>)
177    extraLen = [s length] - range.location - range.length;
178    delimiterRange = [s rangeOfCharacterFromSet: urlRightDelimiters
179                                        options: NSLiteralSearch
180                                          range: NSMakeRange(range.location + range.length - (range.length != 0),
181                                                             extraLen + (range.length != 0))];
182    if (delimiterRange.location == NSNotFound) {
183        // extend to end of string
184        range.length += extraLen;
185        extraLen = 0;
186    } else {
187        NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1");
188        range.length += delimiterRange.location - range.location - range.length;
189        extraLen = [s length] - NSMaxRange(range);
190
191        unichar opening, closing = [s characterAtIndex: delimiterRange.location];
192        if (closing == '>' && !multiLine && ICCF_StringIncludesCharacter(s, '<', NSMakeRange(0, range.location))) {
193            urlLeftDelimiters = [NSCharacterSet characterSetWithCharactersInString: @"<"];
194            goto expandFront; // XXX move to expandBoth to handle clicking in middle
195        }
196        // grow URL past closing paren/brace/bracket if we've seen an open paren/brace/bracket
197        if (closing == ')') opening = '(';
198        else if (closing == '}') opening = '{';
199        else if (closing == ']') opening = '[';
200        else goto expandBoth;
201        if (!ICCF_StringIncludesCharacter(s, opening, range))
202            goto expandBoth;
203       
204        if (extraLen == 1) {
205            range.length += 1;
206            --extraLen;
207            ICLog(@"expanding past %c, now |%@|", closing, [s substringWithRange: range]);
208        } else {
209            range.length += 2;
210            ICLog(@"expanding past %c, now |%@|", closing, [s substringWithRange: range]);
211            goto expandBack;
212        }
213    }
214   
215expandBoth:
216    if (range.location <= 1)
217        goto checkRange; // nowhere to expand
218    unichar opening = [s characterAtIndex: range.location - 1], closing;
219    if (opening == '(') closing = ')';
220    else if (opening == '{') closing = '}';
221    else if (opening == '[') closing = ']';
222    else goto checkRange;
223
224    ICLog(@"extraLen = %d", extraLen);
225    // check if we're inside a partial delimited URL: not foolproof, but handles (foo), {UUID} and [IPv6]
226    if (delimiterRange.location != NSNotFound && [s characterAtIndex: delimiterRange.location] == closing &&
227        ((opening == '[' && ICCF_IsLikelyIPv6Address(s, range)) || !ICCF_IsLikelyURI(s, range))) {
228        ICLog(@"expanding past %c...%c, was |%@|", opening, closing, [s substringWithRange: range]);
229        range.location -= 2;
230        if (extraLen > 1)
231            range.length += 4;
232        else
233            range.length += 2 + extraLen;
234        ICLog(@"expanding past %c...%c, now |%@|", opening, closing, [s substringWithRange: range]);
235        goto expandFront;
236    }
237
238    if (ICCF_StringIncludesCharacter(s, closing, range) &&
239        ((opening == '[' &&
240          ICCF_IsLikelyIPv6Address(s, NSMakeRange(range.location,
241                                                  [s rangeOfString: @"]"].location - range.location)))
242         || !ICCF_IsLikelyURI(s, range))) {
243        range.location -= 2;
244        range.length += 2;
245        ICLog(@"expanding past %c, now |%@|", opening, [s substringWithRange: range]);
246        goto expandFront;
247    }
248
249checkRange:
250    ICCF_CheckRange(range);
251   
252    ICCF_ParseURL([s substringWithRange: range], &range);
253   
254    return range;
255}
Note: See TracBrowser for help on using the repository browser.