/* * Grouch.app Copyright (C) 2006 Andy Sveikauskas * ------------------------------------------------------------------------ * This program is free software under the GNU General Public License * -- * This parses and generates very minimal, very non-standards-conforming HTML. * The idea here is that (1) AIM doesn't use very many tags and (2) nobody * generates "good" HTML in IMs, so we can't be strict. * * This is pretty dirty, but such is the state of things. */ #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #include #include #include static NSAttributedString *parseHtml( NSString *str ); static NSString *generateHtml( NSAttributedString *str ); /* * Tries to find a match for a font family using case insensitive string * compares. */ static NSString *checkFontFamily( NSString *str ) { NSFontManager *fnt = [NSFontManager sharedFontManager]; // Is this the correct case? if( [[fnt availableMembersOfFontFamily:str] count] ) return str; else // No; look for it. { NSArray *families = [fnt availableFontFamilies]; int i; str = [str lowercaseString]; for( i=0; i<[families count]; ++i ) { NSString *family = [families objectAtIndex:i]; if( [[family lowercaseString] isEqual:str] ) return family; } return nil; } } @implementation NSString (GrouchHtml) - (NSAttributedString*)parseHtml { // This makes a lot of use of temporary objects, so we might // as well put this in. NSAutoreleasePool *pool = [NSAutoreleasePool new]; NSAttributedString *str = parseHtml(self); [str retain]; [pool release]; [str autorelease]; return str; } @end @implementation NSAttributedString (GrouchHtml) - (NSString*)generateHtml { NSAutoreleasePool *pool = [NSAutoreleasePool new]; NSString *str = generateHtml(self); [str retain]; [pool release]; [str autorelease]; return str; } @end #import // XXX figure out how to do these on non-Mac OS X. #if !defined(__APPLE__) || defined(GNUSTEP) #define NSToolTipAttributeName @"NSToolTip" #define NSCursorAttributeName @"NSCursor" #define NSStrikethroughStyleAttributeName @"NSStrikethrough" #endif /**************************************************************************** * PARSING HTML ***************************************************************************/ #define attrib(str) [[[NSAttributedString alloc] initWithString:str] \ autorelease] /* * Get an HTML color, such as @"red" or @"#ff0000" */ @interface NSColor (GrouchExtension) + colorFromHtml:(NSString*)color; @end @implementation NSColor (GrouchExtensions) + colorFromHtml:(NSString*)color { if( !color ) return nil; if( [color characterAtIndex:0] == '#' ) { int r = 0, g = 0, b = 0; color = [color substringFromIndex:1]; NS_DURING NSString *tmp; tmp = [color substringWithRange:NSMakeRange(0,2)]; sscanf( [tmp cString], "%x", &r ); tmp = [color substringWithRange:NSMakeRange(2,2)]; sscanf( [tmp cString], "%x", &g ); tmp = [color substringWithRange:NSMakeRange(4,2)]; sscanf( [tmp cString], "%x", &b ); NS_HANDLER NS_ENDHANDLER return [NSColor colorWithDeviceRed:r/255.0f green:g/255.0f blue:b/255.0f alpha:1.0f]; } else { static NSDictionary *plist = nil; if( !plist ) { NSBundle *b = [NSBundle mainBundle]; NSString *path = [b pathForResource:@"HtmlColors" ofType:@"plist"]; if( !path ) return nil; plist = [NSPropertyListSerialization propertyListFromData: [NSData dataWithContentsOfFile:path] mutabilityOption:NSPropertyListImmutable format:NULL errorDescription:NULL]; if( !plist ) return nil; [plist retain]; } color = [color lowercaseString]; return [self colorFromHtml:[plist objectForKey:color]]; } } @end @interface NSMutableAttributedString (GrouchHtmlPrivate) /* * Add an attributed in a given range, but only if it's not there already. * This took much longer to get right than it should have. */ - (void)addAttributeWhereNotPresent:(NSString*)attrib value:val range:(NSRange)range; /* * Add a link, complete with blue/underline/etc attributes. */ - (void)addLink:(NSString*)url range:(NSRange)range; @end @implementation NSMutableAttributedString (GrouchHtmlPrivate) - (void)addAttributeWhereNotPresent:(NSString*)attrib value:val range:(NSRange)range { int n = range.length; while( range.length > 0 && range.location < [self length] ) { NSRange backup = range; BOOL present = [self attribute:attrib atIndex:range.location effectiveRange:&range] ? YES : NO; // Is this range larger than the one we are considering? // If so, chop it down. if( range.location < backup.location ) { int diff = backup.location-range.location; range.location = backup.location; range.length -= diff; } if( range.length > backup.length ) range.length = backup.length; if( !present ) [self addAttribute:attrib value:val range:range]; range.location += range.length; range.length = (n -= range.length); } } - (void)addLink:(NSString*)url range:(NSRange)range { NSDictionary *attrs = [NSDictionary dictionaryWithObjectsAndKeys: // URL [NSURL URLWithString:url], NSLinkAttributeName, // Tooltip url, NSToolTipAttributeName, // blue [NSColor blueColor], NSForegroundColorAttributeName, // underline [NSNumber numberWithInt:NSSingleUnderlineStyle], NSUnderlineStyleAttributeName, // cursor [NSCursor pointingHandCursor], NSCursorAttributeName, nil]; [self addAttributes:attrs range:range]; } @end @implementation NSMutableAttributedString (GrouchHtml) - attribute:(NSString*)str atIndex:(int)i { return [self attribute:str atIndex:i effectiveRange:NULL]; } - (void)_inferLinks:(NSString*)hdr badChars:(NSCharacterSet*)badSet { NSRange searchRange = NSMakeRange(0, [self length]); NSRange found; goto check; do { if(![self attribute:NSLinkAttributeName atIndex:found.location]) { int i, end = -1; for(i=found.location+[hdr length]; i<[self length]; ++i) if([badSet characterIsMember:[[self string] characterAtIndex:i]]) { end = i; break; } if(end < 0) end = [self length]; found.length = end - found.location; if(found.length > [hdr length]) [self addLink:[[self string] substringWithRange:found] range:found]; } searchRange.location += found.length; searchRange.length -= found.length; check: found = [[self string] rangeOfString:hdr options:NSCaseInsensitiveSearch range:searchRange]; } while(found.length); } - (void)inferLinks { NSCharacterSet *invalidUrlChars = [NSCharacterSet characterSetWithCharactersInString:@" \t"]; NSCharacterSet *invalidEmailChars = [NSCharacterSet characterSetWithCharactersInString: @" \t#$%^&*()+=\\|/<>,:;'\"[]{}"]; [self _inferLinks:@"http://" badChars:invalidUrlChars]; [self _inferLinks:@"ftp://" badChars:invalidUrlChars]; [self _inferLinks:@"mailto:" badChars:invalidEmailChars]; } @end /* * Set the font if there is none. We need to do this if we are to set * italic or bold. */ static void setDefaultFont( NSMutableAttributedString *r, NSRange range ) { [r addAttributeWhereNotPresent:NSFontAttributeName value:[NSFont userFontOfSize:[NSFont systemFontSize]] range:range]; } /* * Process an HTML tag. * r - The NSAttributedString * range - Where this tag applies in r * tag - The name of the tag * props - The attributes of this tag, as NSStrings (keys in lower case) */ static void processTagWithRange( r, range, tag, props ) NSMutableAttributedString *r; NSRange range; NSString *tag; NSDictionary *props; { if( [tag isEqual:@"b"] ) { setDefaultFont( r, range ); [r applyFontTraits:NSBoldFontMask range:range]; } else if( [tag isEqual:@"i"] ) { setDefaultFont( r, range ); [r applyFontTraits:NSItalicFontMask range:range]; } else if( [tag isEqual:@"u"] ) [r addAttribute:NSUnderlineStyleAttributeName value:[NSNumber numberWithInt:NSSingleUnderlineStyle] range:range]; else if( [tag isEqual:@"s"] || [tag isEqual:@"strike"] ) [r addAttribute:NSStrikethroughStyleAttributeName value:[NSNumber numberWithInt:1] range:range]; else if( [tag isEqual:@"font"] ) { NSColor *c = [NSColor colorFromHtml: [props objectForKey:@"color"]]; NSString *face = [props objectForKey:@"face"]; NSString *size = [props objectForKey:@"size"]; if( c ) [r addAttributeWhereNotPresent: NSForegroundColorAttributeName value:c range:range]; if( face || size ) { NSFont *current; setDefaultFont( r, range ); int i = range.location; while( i < range.location + range.length ) { NSRange range2; current = [r attribute:NSFontAttributeName atIndex:i effectiveRange:&range2]; if( range2.location < i ) { range2.length -= (i-range2.location); range2.location = i; } if( range2.length > range.length ) range2.length = range.length; if( face ) { face = checkFontFamily(face); current = [[NSFontManager sharedFontManager] convertFont:current toFamily:face]; } if( size ) { // TODO } [r addAttribute:NSFontAttributeName value:current range:range2]; i = range2.location + range2.length; } } } else if( [tag isEqual:@"body"] ) { NSColor *c = [NSColor colorFromHtml: [props objectForKey:@"bgcolor"]]; if( c ) [r addAttributeWhereNotPresent: NSBackgroundColorAttributeName value:c range:range]; } else if( [tag isEqual:@"a"] ) { NSString *url = [props objectForKey:@"href"]; if( url ) [r addLink:url range:range]; } } /* * Process a tag that does not need to be closed (e.g.
) */ static BOOL processSingle( r, tagName, tag ) NSMutableAttributedString *r; NSString *tagName, *tag; { if( [tagName isEqual:@"br"] || [tagName isEqual:@"hr"] ) { [r appendAttributedString:attrib(@"\n")]; return YES; } return NO; } static int skipWhitespace( NSString *str, int i ) { NSCharacterSet *whitespace = [NSCharacterSet whitespaceCharacterSet]; while( i < [str length] && [whitespace characterIsMember:[str characterAtIndex:i]] ) ++i; return i; } static int endOfSymbol( NSString *str, int i ) { NSCharacterSet *whitespace = [NSCharacterSet whitespaceCharacterSet]; while( i < [str length] && [str characterAtIndex:i] != '=' && ![whitespace characterIsMember:[str characterAtIndex:i]] ) ++i; return i; } static NSString *parseSymbol( NSString *str, int *i ) { int start = skipWhitespace(str, *i); *i = endOfSymbol(str, start); [str substringWithRange:NSMakeRange(start, *i-start)]; return [str substringWithRange:NSMakeRange(start, *i-start)]; } static NSString *parseAttribute( NSString *str, int *i ) { NSCharacterSet *whitespace = [NSCharacterSet whitespaceCharacterSet]; unichar first; int start; if( *i >= [str length] ) return @""; switch( (first=[str characterAtIndex:*i]) ) { case '\'': case '"': start = ++*i; while( *i < [str length] && [str characterAtIndex:*i] != first ) ++*i; return [str substringWithRange:NSMakeRange(start, *i++-start)]; break; default: start = *i; while( *i < [str length] && ![whitespace characterIsMember:[str characterAtIndex:*i]]) ++*i; return [str substringWithRange:NSMakeRange(start, *i-start)]; } } /* * Process a tag that needs to be closed */ static void processDouble( r, tagName, tag, range ) NSMutableAttributedString *r; NSString *tagName, *tag; NSRange range; { int i = 0; NSMutableDictionary *props = [NSMutableDictionary new]; parseSymbol( tag, &i ); while( i < [tag length] ) { NSString *attribute = parseSymbol(tag, &i); NSString *value = @""; if( i < [tag length] && [tag characterAtIndex:i] == '=' ) { ++i; value = parseAttribute(tag, &i); } [props setObject:value forKey:[attribute lowercaseString]]; } processTagWithRange( r, range, tagName, props ); [props release]; } struct node { NSString *tag; NSString *tagFull; int start; struct node *next; }; static void endTag( list, r, tag ) struct node **list; NSMutableAttributedString *r; NSString *tag; { struct node *n = *list, *last = NULL; while( n && ![n->tag isEqual:tag] ) { last = n; n = n->next; } if( n ) { if( last ) last->next = n->next; else *list = n->next; processDouble ( r, tag, n->tagFull, NSMakeRange(n->start,[r length]-n->start) ); [n->tag release]; [n->tagFull release]; free( n ); } } static NSString *getTagName( NSString *tag ) { int start = 0; int i; if( [tag characterAtIndex:0] == '/' ) ++start; for( i=start; i<[tag length]; ++i ) { NSCharacterSet *an = [NSCharacterSet alphanumericCharacterSet]; unichar c = [tag characterAtIndex:i]; if( ![an characterIsMember:c] ) break; } return [[tag substringWithRange:NSMakeRange(start,i-start)] lowercaseString]; } static void processTag( list, r, tag ) struct node **list; NSMutableAttributedString *r; NSString *tag; { BOOL on = [tag characterAtIndex:0] != '/'; NSString *tagName = getTagName(tag); if( on ) { if( !processSingle(r, tagName, tag) ) { struct node n; [n.tag = tagName retain]; [n.tagFull = tag retain]; n.start = [r length]; n.next = *list; *list = malloc(sizeof(n)); if( !*list ) [GrouchException raiseMemoryException]; memcpy( *list, &n, sizeof(n) ); } } else endTag( list, r, tagName ); } static BOOL lookUpInPlist( NSMutableAttributedString *r, NSString *subst ) { static NSDictionary *plist = nil; static NSString *dict = @"HtmlSubstitutions"; if( [subst characterAtIndex:0] == '#' ) { unichar c; if( [subst length] == 1 ) return NO; c = [[subst substringFromIndex:1] intValue]; [r appendAttributedString:attrib( [NSString stringWithCharacters:&c length:1] )]; return YES; } if( !plist ) { NSBundle *b = [NSBundle mainBundle]; NSString *path = [b pathForResource:dict ofType:@"plist"]; if( !path ) return NO; plist = [NSPropertyListSerialization propertyListFromData: [NSData dataWithContentsOfFile:path] mutabilityOption:NSPropertyListImmutable format:NULL errorDescription:NULL]; if( !plist ) return NO; [plist retain]; } subst = [plist objectForKey:subst]; if( subst ) { [r appendAttributedString:attrib(subst)]; return YES; } else return NO; } static BOOL processAmpSequence( r, str, off ) NSMutableAttributedString *r; NSString *str; int *off; { int i; for( i=*off+1; i<[str length]; ++i ) { NSCharacterSet *an = [NSCharacterSet alphanumericCharacterSet]; unichar c = [str characterAtIndex:i]; if( c == ';' ) { NSRange range = NSMakeRange(*off+1, i-(*off+1)); NSString *which = [str substringWithRange:range]; if( lookUpInPlist(r, which) ) { *off = i; return YES; } else return NO; } if( c == '#' && i == *off+1 ) continue; if( ![an characterIsMember:c] ) break; } return NO; } static BOOL validate( NSString *str, int *start ) { enum { FIRST, NAME, ARG_EQUALS, ARG_QUOTED, ARG_UNQUOTED } state = FIRST; BOOL slash = NO; NSCharacterSet *space = [NSCharacterSet whitespaceCharacterSet]; NSCharacterSet *alnum = [NSCharacterSet alphanumericCharacterSet]; unichar c, quot = '"'; int i; for( i=*start+1; i<[str length]; ++i ) { c = [str characterAtIndex:i]; switch( state ) { case FIRST: state = NAME; if( c == '/' ) continue; case NAME: if( c == '=' ) { state = ARG_EQUALS; slash = NO; break; } if( c == '>' ) { accept: *start = i-1; return YES; } if( ![space characterIsMember:c] && ![alnum characterIsMember:c] && c != '/' ) return NO; break; case ARG_EQUALS: if( c == '\'' || c == '\"' ) { quot = c; state = ARG_QUOTED; break; } else state = ARG_UNQUOTED; case ARG_UNQUOTED: if( c == '>' ) goto accept; if( [space characterIsMember:c] ) state = FIRST; break; case ARG_QUOTED: if( c == quot ) state = FIRST; break; } } return NO; } static NSAttributedString *parseHtml( NSString *str ) { NSMutableAttributedString *r = [NSMutableAttributedString new]; unichar c; int i, j, tagStart = 0; BOOL inTag = NO; struct node *list = NULL; [r beginEditing]; for( i=j=0; i<[str length]; j=++i ) switch( (c=[str characterAtIndex:i]) ) { case '\r': case '\n': break; case '&': if( processAmpSequence(r, str,&i) ) continue; else if(1); else case '<': if( !inTag && i+1 < [str length] && validate(str, &j) ) { inTag = YES; tagStart = i+1; i = j; continue; } else if(1); else case '>': if( inTag ) { NSRange range; NSString *tag; range = NSMakeRange(tagStart,i-tagStart); tag = [str substringWithRange:range]; if( [tag length] ) processTag(&list, r, tag); inTag = NO; continue; } default: if( !inTag ) [r appendAttributedString: attrib([NSString stringWithCharacters: &c length:1])]; } while( list ) endTag( &list, r, list->tag ); [r endEditing]; return r; } /********************************************************************** * Code to generate *********************************************************************/ #define node node2 struct node { NSString *openTag, *closeTag; int start, end; struct node *next1, *next2; int ref; }; static struct node *allocateNode() { struct node *n = malloc(sizeof(struct node)); if( n ) memset( n, 0, sizeof(struct node) ); else [GrouchException raiseMemoryException]; return n; } static NSString *link_attribute() { return NSLinkAttributeName; } static struct node *link_handler( str, range, obj ) NSAttributedString *str; NSRange range; id obj; { struct node *n = allocateNode(); NSURL *url = obj; n->openTag = [NSString stringWithFormat:@"", [url absoluteString]]; n->closeTag = @""; return n; } static NSString *fg_attribute() { return NSForegroundColorAttributeName; } static struct node *fg_handler( str, range, obj ) NSAttributedString *str; NSRange range; id obj; { if( ![str attribute:link_attribute() atIndex:range.location effectiveRange:NULL] ) { NSColor *c = obj; struct node *n = allocateNode(); NS_DURING n->openTag = [NSString stringWithFormat: @"", (int)([c redComponent]*255.0f), (int)([c greenComponent]*255.0f), (int)([c blueComponent]*255.0f)]; n->closeTag = @""; NS_HANDLER free( n ); n = NULL; NS_ENDHANDLER return n; } else return NULL; } static NSString *bg_attribute() { return NSBackgroundColorAttributeName; } static struct node *bg_handler( str, range, obj ) NSAttributedString *str; NSRange range; id obj; { struct node *n = allocateNode(); NSColor *c = obj; NS_DURING n->openTag = [NSString stringWithFormat: @"", (int)([c redComponent]*255.0f), (int)([c greenComponent]*255.0f), (int)([c blueComponent]*255.0f)]; n->closeTag = @""; NS_HANDLER free( n ); n = NULL; NS_ENDHANDLER return n; } static NSString *font_attribute() { return NSFontAttributeName; } static struct node *font_handler( str, range, obj ) NSAttributedString *str; NSRange range; id obj; { NSFont *font = obj; NSFontTraitMask traits = [[NSFontManager sharedFontManager] traitsOfFont:font] & (NSItalicFontMask | NSBoldFontMask); struct node *n = NULL; switch( traits ) { case NSItalicFontMask: n = allocateNode(); n->openTag = @""; n->closeTag = @""; break; case NSBoldFontMask: n = allocateNode(); n->openTag = @""; n->closeTag = @""; break; case (NSItalicFontMask | NSBoldFontMask): n = allocateNode(); n->openTag = @""; n->closeTag = @""; } return n; } static struct node *nodeForFont( thisFont, thisSize, i, list1, list2 ) NSString *thisFont; float thisSize; int i; struct node **list1, **list2; { struct node *n = allocateNode(); n->openTag = [NSString stringWithFormat:@"", thisFont]; n->closeTag = @""; n->ref = 2; n->next1 = *list1; n->next2 = *list2; *list1 = n; *list2 = n; n->start = i; return n; } static void scanForFonts( str, list1, list2 ) NSAttributedString *str; struct node **list1, **list2; { NSFont *font = [NSFont userFontOfSize:[NSFont systemFontSize]]; NSString *lastFont = [font familyName]; float lastSize = [font pointSize]; int i; NSRange range; struct node *n = nodeForFont(lastFont, lastSize, 0, list1, list2); for( i=0; i<[str length]; ++i ) { font = [str attribute:NSFontAttributeName atIndex:i longestEffectiveRange:&range inRange:NSMakeRange(i, [str length]-i)]; if( font ) { NSString *thisFont = [font familyName]; float thisSize = [font pointSize]; if(![lastFont isEqual:thisFont] || lastSize != thisSize) { n->end = i; lastFont = thisFont; lastSize = thisSize; n = nodeForFont(lastFont, lastSize, i, list1, list2); } i = range.location + range.length - 1; } } n->end = [str length]; } static NSString *underline_attribute() { return NSUnderlineStyleAttributeName; } static struct node *underline_handler( str, range, obj ) NSAttributedString *str; NSRange range; id obj; { struct node *n = allocateNode(); n->openTag = @""; n->closeTag = @""; return n; } static NSString *strike_attribute() { return NSStrikethroughStyleAttributeName; } static struct node *strike_handler( str, range, obj ) NSAttributedString *str; NSRange range; id obj; { struct node *n = allocateNode(); n->openTag = @""; n->closeTag = @""; return n; } #define ATTRIB(x) {x##_attribute, x##_handler} static struct tag_information { NSString *(*name)(); struct node *(*handler)( NSAttributedString *, NSRange, id ); } tag_info[] = { ATTRIB(link), ATTRIB(fg), ATTRIB(bg), ATTRIB(font), ATTRIB(underline), ATTRIB(strike), {NULL,NULL} }; static void processAttribute( str, list1, list2, name, handler ) NSAttributedString *str; struct node **list1, **list2; NSString *name; struct node *(handler)(NSAttributedString *, NSRange, id); { int i; for( i=0; i<[str length]; ++i ) { NSRange range; id o = [str attribute:name atIndex:i longestEffectiveRange:&range inRange:NSMakeRange(i, [str length]-i)]; if( o ) { struct node *n = handler(str, range, o); if( n ) { n->next1 = *list1; n->next2 = *list2; *list1 = *list2 = n; n->ref = 2; n->start = range.location; n->end = n->start + range.length; i = range.location + range.length - 1; } } } } static struct node *split( n, get_next, set_next ) struct node *n; struct node *(*get_next)(struct node*); void (*set_next)(struct node*, struct node*); { struct node *m; if( !n ) return n; m = get_next(n); set_next( n, get_next(m) ); set_next( m, split(get_next(m), get_next, set_next) ); return m; } static struct node *merge( a, b, get_next, set_next, cmp ) struct node *a, *b; struct node *(*get_next)(struct node*); void (*set_next)(struct node*, struct node*); int (*cmp)(struct node *, struct node *); { int c; if( !a ) return b; if( !b ) return a; c = cmp(a, b); if( c < 0 ) { set_next( a, merge(get_next(a), b, get_next, set_next, cmp) ); return a; } else { set_next( b, merge(get_next(b), a, get_next, set_next, cmp) ); return b; } } struct node *mergeSort( n, get_next, set_next, cmp ) struct node *n; struct node *(*get_next)(struct node*); void (*set_next)(struct node*, struct node*); int (*cmp)(struct node *, struct node *); { struct node *m = split(n, get_next, set_next); if( !m ) return n; n = mergeSort( n, get_next, set_next, cmp ); m = mergeSort( m, get_next, set_next, cmp ); return merge( n, m, get_next, set_next, cmp ); } static int integer_cmp( int a, int b ) { if( a < b ) return -1; else if( a > b ) return 1; else return 0; } static struct node *list1_get( struct node *n ) { return n ? n->next1 : n; } static void list1_set( struct node *n, struct node *m ) { if( n ) n->next1 = m; } static int list1_cmp( struct node *a, struct node *b ) { int r = integer_cmp(a->start, b->start); if( r ) return r; else if( a < b ) return -1; else if( a > b ) return 1; else return r; } static struct node *list2_get( struct node *n ) { return n ? n->next2 : n; } static void list2_set( struct node *n, struct node *m ) { if( n ) n->next2 = m; } static int list2_cmp( struct node *a, struct node *b ) { int r = integer_cmp(a->end, b->end); if( r ) return r; else return -list1_cmp(a, b); } static NSString *generateHtml( NSAttributedString *str ) { NSMutableString *r = [NSMutableString string]; struct node *startList = NULL, *endList = NULL; struct tag_information *p; int i; for( p = tag_info; p->name && p->handler; ++p ) processAttribute ( str, &startList, &endList, p->name(), p->handler ); scanForFonts( str, &startList, &endList ); startList = mergeSort( startList, list1_get, list1_set, list1_cmp ); endList = mergeSort( endList, list2_get, list2_set, list2_cmp ); for( i=0; i<[str length]; ++i ) { unichar c; while( endList && endList->end == i ) { struct node *n = endList; endList = endList->next2; if( n->start != n->end && n->closeTag ) [r appendString:n->closeTag]; if( !--(n->ref) ) free(n); } while( startList && startList->start == i ) { struct node *n = startList; startList = startList->next1; if( n->start != n->end && n->openTag ) [r appendString:n->openTag]; if( !--(n->ref) ) free(n); } c = [[str string] characterAtIndex:i]; switch(c) { case '\r': if( i+1<[str length] ) if([[str string] characterAtIndex:i+1] == '\n') break; case '\n': [r appendString:@"
"]; break; case '<': [r appendString:@"<"]; break; case '>': [r appendString:@">"]; break; case '&': [r appendString:@"&"]; break; default: [r appendString: [NSString stringWithCharacters:&c length:1]]; } } while( startList ) { struct node *n = startList; startList = startList->next1; if( !--(n->ref) ) free(n); } while( endList ) { struct node *n = endList; endList = endList->next2; if( n->start != n->end && n->closeTag ) [r appendString:n->closeTag]; if( !--(n->ref) ) free(n); } return r; }