/* Copyright (C) 2004 eXtrapola Srl This file is part of SOPE. SOPE is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. SOPE 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with SOPE; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "StructuredTextBodyElement.h" #include "NSString+STX.h" #include "common.h" #define ST_ESCAPE_CHAR '\\' #define ST_UNDERLINE_CHAR '_' #define ST_DYNAMICKEY_CHAR '@' #define ST_ITALICS_CHAR '*' #define ST_LINK_CHAR '"' #define ST_LINKIMAGE_CHAR '[' static NSString *preprocessorTag = @"##"; @implementation StructuredTextBodyElement static BOOL debugOn = NO; + (void)initialize { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; debugOn = [ud boolForKey:@"STXDebugEnabled"]; } - (id)init { if ((self = [super init])) { self->runPreprocessor = YES; } return self; } - (void)dealloc { [self->_elements release]; [super dealloc]; } /* accessors */ - (NSMutableArray *)elements { if (_elements == nil) _elements = [[NSMutableArray alloc] init]; return _elements; } - (void)addElement:(StructuredTextBodyElement *)anElement { if (anElement == nil) return; [[self elements] addObject:anElement]; } /* operations */ - (NSString *)parseText:(NSString *)_str inContext:(NSDictionary *)_ctx { // TODO: too big a method NSMutableString *result; NSString *text; NSRange range, rangeOut; int i, length, start; if (debugOn) NSLog(@"PARSE TEXT: '%@' (delegate=0x%p)", _str, self->_delegate); _str = [self preprocessText:_str inContext:_ctx]; if (debugOn) NSLog(@" preprocessed: '%@'", _str); result = [NSMutableString stringWithCapacity:[_str length]]; text = _str; for (i = start = 0, length = [text length]; i < length; i++) { unichar c; c = [text characterAtIndex:i]; switch (c) { case ST_ESCAPE_CHAR: if (i - start > 0) { range.location = start; range.length = (i - start); [result appendString:[text substringWithRange:range]]; } start = ++i; break; case '\'': if (i - start > 0) { range.location = start; range.length = i - start; [result appendString:[text substringWithRange:range]]; } if (i + 1 < length) { c = [_str characterAtIndex:i + 1]; if (c == '\'') { start = ++i; break; } } range.location = i + 1; range.length = length - range.location; rangeOut = [text rangeOfString:@"'" options:0 range:range]; if (rangeOut.length > 0) { NSString *s; range.location = i + 1; range.length = rangeOut.location - range.location; s = [[text substringWithRange:range] unescapedString]; s = [self preformattedText:s inContext:_ctx]; [result appendString:s]; start = i = rangeOut.location + 1; } else { start = i; } break; case ST_UNDERLINE_CHAR: // underline if (i - start > 0) { range.location = start; range.length = i - start; [result appendString:[text substringWithRange:range]]; start = i; } range = [self findUnderlineSubstring:[text substringFromIndex:i + 1]]; if (range.length > 0) { NSString *s; range.location = i + 1; s = [[text substringWithRange:range] unescapedString]; s = [self parseText:s inContext:_ctx]; s = [self underlineText:s inContext:_ctx]; if (s) [result appendString:s]; i += range.length + 1; start = i + 1; } break; case ST_DYNAMICKEY_CHAR: // dynamicKey if (i - start > 0) { range.location = start; range.length = i - start; [result appendString:[text substringWithRange:range]]; start = i; } range = [self findDynamicKeySubstring:[text substringFromIndex:i + 1]]; if (range.length > 0) { NSString *s; range.location = i + 1; s = [self parseText:[text substringWithRange:range] inContext:_ctx]; [result appendString:[self dynamicKeyText:s inContext:_ctx]]; i += range.length + 1; start = i + 1; } break; case ST_ITALICS_CHAR: { // italics e bold if (i - start > 0) { range.location = start; range.length = i - start; [result appendString:[text substringWithRange:range]]; start = i; } if (i + 1 < length) { c = [text characterAtIndex:i + 1]; if (c == ST_ITALICS_CHAR) { range = [self findBoldSubstring:[text substringFromIndex:i + 2]]; if (range.length > 0) { NSString *s; range.location = i + 2; s = [[text substringWithRange:range] unescapedString]; s = [self parseText:s inContext:_ctx]; s = [self boldText:s inContext:_ctx]; if (s) [result appendString:s]; i += range.length + 3; start = i + 1; } } else { range = [self findItalicsSubstring:[text substringFromIndex:i + 1]]; if (range.length > 0) { NSString *s; range.location = i + 1; s = [[text substringWithRange:range] unescapedString]; s = [self parseText:s inContext:_ctx]; s = [self italicsText:s inContext:_ctx]; if (s) [result appendString:s]; i += range.length + 1; start = i + 1; } } } break; } case ST_LINKIMAGE_CHAR: // links if (i - start > 0) { range.location = start; range.length = i - start; [result appendString:[text substringWithRange:range]]; start = i; } range = [self findLinkImageSubstring:[text substringFromIndex:i + 1]]; if (range.length > 0) { NSString *s; range.location = i + 1; s = [self linkImage:[text substringWithRange:range] inContext:_ctx]; [result appendString:s]; i += range.length; start = i + 1; } break; case ST_LINK_CHAR: // links if (i - start > 0) { range.location = start; range.length = i - start; [result appendString:[text substringWithRange:range]]; start = i; } range = [self findLinkSubstring:[text substringFromIndex:(i + 1)]]; if (range.length > 0) { NSString *s; range.location = i + 1; s = [self linkText:[text substringWithRange:range] inContext:_ctx]; [result appendString:s]; if (debugOn) NSLog(@"found link substring: '%@'", s); i += range.length; start = i + 1; } break; } } if (i - start > 0) { range.location = start; range.length = i - start; [result appendString:[text substringWithRange:range]]; } if (debugOn) NSLog(@" result: '%@'", result); return result; } - (NSRange)findMarkerSubstring:(NSString *)_str withMarker:(unichar)aMarker markerLength:(int)markLength { NSRange range; int i, h, length; length = [_str length]; markLength--; for (i = 0; i < length; i++) { unichar c; c = [_str characterAtIndex:i]; if (c == aMarker && i + markLength < length) { BOOL foundMarker = YES; for (h = i + 1; h <= i + markLength; h++) { c = [_str characterAtIndex:h]; if (c != aMarker) { foundMarker = NO; break; } } if (foundMarker) { range.location = 0; range.length = i; return range; } } else if (c == ST_ESCAPE_CHAR) { i++; } } range.location = NSNotFound; range.length = 0; return range; } /* find markers */ - (NSRange)findBoldSubstring:(NSString *)_str { return [self findMarkerSubstring:_str withMarker:ST_ITALICS_CHAR markerLength:2]; } - (NSRange)findItalicsSubstring:(NSString *)_str { return [self findMarkerSubstring:_str withMarker:ST_ITALICS_CHAR markerLength:1]; } - (NSRange)findUnderlineSubstring:(NSString *)_str { return [self findMarkerSubstring:_str withMarker:ST_UNDERLINE_CHAR markerLength:1]; } /* operations */ - (NSString *)preformattedText:(NSString *)_s inContext:(NSDictionary *)_ctx { return [self->_delegate insertPreformatted:_s inContext:_ctx]; } - (NSRange)_findLinkBlockTargetSubstring:(NSString *)_str { NSRange range, rangeTarget; int length; unichar c; length = [_str length]; if (debugOn) NSLog(@" find link block target: '%s'", _str); c = [_str characterAtIndex:0]; if (c == ':' && 1 < length) { c = [_str characterAtIndex:1]; range.location = 2; range.length = length - range.location; if (c == '\'') { range = [_str rangeOfString:@"'" options:0 range:range]; if (range.length == 0) return range; range.length = range.location + 1; if (range.length < length) { rangeTarget = [self findLinkTargetFromString: [_str substringFromIndex:range.length]]; if (rangeTarget.length > 0) range.length += rangeTarget.length; } } else if (c == '{') { range = [_str rangeOfString:@"}" options:0 range:range]; if (range.length == 0) return range; range.length = range.location + 1; if (range.length < length) { rangeTarget = [self findLinkTargetFromString: [_str substringFromIndex:range.length]]; if (rangeTarget.length > 0) range.length += rangeTarget.length; } } else { range = [_str rangeOfString:@" " options:0 range:range]; range.length = (range.length == 0) ? length : range.location; } range.location = 0; if (debugOn) NSLog(@" range(0,%d)", range.length); return range; } range.location = NSNotFound; range.length = 0; if (debugOn) NSLog(@" not found."); return range; } - (NSRange)findLinkImageSubstring:(NSString *)_str { NSRange range; int i, length; for (i = 0, length = [_str length]; i < length; i++) { unichar c; c = [_str characterAtIndex:i]; if (!(c == ']' && i + 1 < length)) continue; range = [self _findLinkBlockTargetSubstring: [_str substringFromIndex:i + 1]]; if (range.length > 0) { range.length += (range.location + i + 1); range.location = 0; } return range; } range.location = NSNotFound; range.length = 0; return range; } - (NSRange)findLinkSubstring:(NSString *)_str { NSRange range; int i, length; length = [_str length]; if (debugOn) NSLog(@"find link substring: '%@'(%d)", _str, length); for (i = 0; i < length; i++) { unichar c; c = [_str characterAtIndex:i]; if (!(c == ST_LINK_CHAR && ((i + 1) < length))) continue; range = [self _findLinkBlockTargetSubstring: [_str substringFromIndex:i + 1]]; if (range.length > 0) { range.length += (range.location + i + 1); range.location = 0; } return range; } range.location = NSNotFound; range.length = 0; return range; } - (NSRange)findLinkTargetFromString:(NSString *)_str { int i, length; BOOL tag = NO; NSRange range; for (i = 0, length = [_str length]; i < length; i++) { unichar c; c = [_str characterAtIndex:i]; if (!tag && c == ':' && i + 1 < length) { c = [_str characterAtIndex:i + 1]; if (c == ':') { i++; tag = YES; } } else if (tag) { if (c == '\'') { range.location = i + 1; range.length = length - range.location; range = [_str rangeOfString:@"'" options:0 range:range]; if (range.length == 0) return range; length = range.location + 1; break; } else { range.location = i; range.length = length - i; range = [_str rangeOfString:@" " options:0 range:range]; if (range.length == 0) break; length = range.location; break; } } else { range.location = NSNotFound; range.length = 0; return range; } } range.location = 0; range.length = length; return range; } - (NSRange)findDynamicKeySubstring:(NSString *)_str { NSRange range; int i, length; for (i = 0, length = [_str length]; i < length; i++) { unichar c; c = [_str characterAtIndex:i]; if (c == ST_DYNAMICKEY_CHAR) { range.location = 0; range.length = i; return range; } } range.location = NSNotFound; range.length = 0; return range; } /* processing */ - (NSString *)boldText:(NSString *)_str inContext:(NSDictionary *)_ctx { return (_delegate) ? [_delegate insertBold:_str inContext:_ctx] : _str; } - (NSString *)italicsText:(NSString *)_str inContext:(NSDictionary *)_ctx { return (_delegate) ? [_delegate insertItalics:_str inContext:_ctx] : _str; } - (NSString *)underlineText:(NSString *)_str inContext:(NSDictionary *)_ctx { return (_delegate) ? [_delegate insertUnderline:_str inContext:_ctx] : _str; } /* links */ - (NSString *)_linkBlockTarget:(NSString *)_str withName:(NSString *)aName isImage:(BOOL)isImage inContext:(NSDictionary *)_ctx { NSString *linkName = aName; NSRange range; int length; unichar c; NSArray *components; NSString *link, *linkType = nil, *target = nil; NSRange rangeOut; if (_delegate == nil) return _str; length = [_str length]; if (length < 2) return nil; c = [_str characterAtIndex:0]; if (c != ':') { if (debugOn) NSLog(@"no link in: '%@'", _str); return nil; } c = [_str characterAtIndex:1]; range.location = 2; range.length = length - range.location; if (c == '\'') { rangeOut = [_str rangeOfString:@"'" options:0 range:range]; if (rangeOut.length == 0) return nil; range.length = rangeOut.location - range.location; target = [self linkTargetFromString: [_str substringFromIndex:rangeOut.location + 1]]; } else if (c == '{') { rangeOut = [_str rangeOfString:@"}" options:0 range:range]; if (rangeOut.length == 0) return nil; range.location--; range.length = rangeOut.location - range.location + 1; linkType = @"ibn"; target = [self linkTargetFromString: [_str substringFromIndex:rangeOut.location + 1]]; } else { rangeOut = [_str rangeOfString:@"::" options:0 range:range]; range.location--; if (rangeOut.length == 0) { rangeOut = [_str rangeOfString:@" " options:0 range:range]; range.length = (rangeOut.length == 0) ? length - range.location : rangeOut.location - range.location; } else { range.length = rangeOut.location - range.location; target = [self linkTargetFromString: [_str substringFromIndex:rangeOut.location]]; } } link = [_str substringWithRange:range]; components = [link componentsSeparatedByString:@":"]; if (!linkType) linkType = [components objectAtIndex:0]; if (!isImage) { if ([linkType isEqualToString:@"mailto"]) { return [_delegate insertEmail:linkName withAddress:link inContext:_ctx]; } if ([linkType isEqualToString:@"img"]) { NSString *url; url = [link substringFromIndex:[linkType length] + 1]; return [_delegate insertImage:linkName withUrl:url inContext:_ctx]; } if ([linkType isEqualToString:@"ibn"]) { if (debugOn) NSLog(@"IBN link %@", linkName); return [_delegate insertExtrapolaLink:linkName parameters:[link propertyList] withTarget:target inContext:_ctx]; } if (debugOn) NSLog(@"link %@: %@", linkName, link); return [_delegate insertLink:linkName withUrl:link target:target inContext:_ctx]; } if (debugOn) NSLog(@"img %@: %@", linkName, link); return [_delegate insertImage:linkName withUrl:link inContext:_ctx]; } - (NSString *)linkImage:(NSString *)_str inContext:(NSDictionary *)_ctx { NSString *linkName = nil; NSRange range; int i, length, startOfTarget; if (_delegate == nil) return _str; length = [_str length]; if (length > 1) { unichar c; range.location = 0; range.length = length; range = [_str rangeOfString:@"]" options:0 range:range]; if (range.length == 0) return _str; startOfTarget = range.location + 1; for (i = 1; i < range.location; i++) { c = [_str characterAtIndex:i]; if (c == ST_LINK_CHAR && i + 1 < length) { range.location = 1; range.length = i - 1; linkName = [_str substringWithRange:range]; range.length = startOfTarget - (i + 2); range.location = i + 1; linkName = [self _linkBlockTarget:[_str substringWithRange:range] withName:linkName isImage:YES inContext:_ctx]; break; } } if (linkName) { NSString *result; result = [self _linkBlockTarget: [_str substringFromIndex:startOfTarget] withName:linkName isImage:NO inContext:_ctx]; if (result == nil) result = _str; return result; } } return _str; } - (NSString *)linkText:(NSString *)_str inContext:(NSDictionary *)_ctx { NSString *linkName; int i, length; if (_delegate == nil) return _str; for (i = 0, length = [_str length]; i < length; i++) { NSString *result; unichar c; c = [_str characterAtIndex:i]; if (!(c == ST_LINK_CHAR && (i + 1 < length))) continue; linkName = [_str substringToIndex:i]; result = [self _linkBlockTarget:[_str substringFromIndex:i + 1] withName:linkName isImage:NO inContext:_ctx]; return result ? result : _str; } return _str; } - (NSString *)linkTargetFromString:(NSString *)_str { int i, length, start; BOOL tag = NO; NSRange range; length = [_str length]; for (start = i = 0; i < length; i++) { unichar c; c = [_str characterAtIndex:i]; if (!tag && c == ':' && i + 1 < length) { c = [_str characterAtIndex:i + 1]; if (c == ':') { start = i + 2; i++; tag = YES; } } else if (c == '\'') { range.location = i + 1; range.length = length - range.location; range = [_str rangeOfString:@"'" options:0 range:range]; if (range.length == 0) break; length = range.location; start++; break; } else { break; } } range.location = start; range.length = length - range.location; if (range.length <= 0) { return nil; } return [_str substringWithRange:range]; } - (NSString *)dynamicKeyText:(NSString *)_str inContext:(NSDictionary *)_ctx { if (_delegate) return [_delegate insertDynamicKey:_str inContext:_ctx]; return _str; } - (NSString *)preprocessText:(NSString *)_str inContext:(NSDictionary *)_ctx { // TODO: breaks on libFoundation // TODO: need to find out what this is exactly supposed to do NSMutableString *result; NSRange rangeToCopy, range, rangeEnd; int length; if (debugOn) NSLog(@"preprocess: '%@'", _str); if (!self->runPreprocessor) return _str; self->runPreprocessor = NO; length = [_str length]; result = [NSMutableString stringWithCapacity:length]; range.location = 0; range.length = length; rangeEnd.location = 0; rangeEnd.length = length; rangeToCopy.location = 0; rangeToCopy.length = length; // TODO: the NSNotFound check might make trouble on libFoundation for (; range.location != NSNotFound && range.location < length;) { range = [_str rangeOfString:preprocessorTag options:0 range:range]; if (range.length == 0) { if (rangeEnd.location == 0) return _str; rangeToCopy.location = rangeEnd.location + 2; rangeToCopy.length = (length - rangeToCopy.location); [result appendString:[_str substringWithRange:rangeToCopy]]; continue; } rangeToCopy.location = rangeEnd.location; if (rangeEnd.location > 0) rangeToCopy.location += 2; rangeEnd.location = range.location + 2; rangeEnd.length = length - rangeEnd.location; rangeEnd = [_str rangeOfString:preprocessorTag options:0 range:rangeEnd]; if (rangeEnd.length > 0) { NSRange keyRange; NSString *text; rangeToCopy.length = (range.location - rangeToCopy.location); if (rangeToCopy.length > 0) { NSString *s; s = [_str substringWithRange:rangeToCopy]; [result appendString:s]; } keyRange.location = range.location + 2; keyRange.length = rangeEnd.location - keyRange.location; text = [_delegate insertPreprocessedTextForKey: [_str substringWithRange:keyRange] inContext:_ctx]; if (text) [result appendString:text]; range.location = rangeEnd.location + 2; range.length = length - range.location; self->runPreprocessor = YES; } else { range.location = NSNotFound; rangeToCopy.length = length - rangeToCopy.location; [result appendString:[_str substringWithRange:rangeToCopy]]; } } return result; } /* description */ - (NSString *)description { NSMutableString *ms; ms = [NSMutableString stringWithCapacity:128]; [ms appendFormat:@"<0x%p[%@]:", self, NSStringFromClass([self class])]; if (self->_elements) [ms appendFormat:@" #elements=%d", [self->_elements count]]; if (self->_delegate) { [ms appendFormat:@" delegate=0x%p<%@>", self->_delegate, NSStringFromClass([(id)self->_delegate class])]; } if (self->runPreprocessor) [ms appendFormat:@" run-preprocessor"]; [ms appendString:@">"]; return ms; } @end /* StructuredTextBodyElement */