This is ArticleViewControl.m in view mode; [Download] [Up]
// Most of this implementation is an ugly hack to integrate the new // message framework into the old Alexandra codebase. #import "Alexandra.h" #import "AlexandraApp.h" #import "ArticleViewControl.h" #import <misckit/MiscAppDefaults.h> #import "response_codes.h" #import "FaceView.h" #import <misckit/MiscClockView.h> #import "parse-header.h" #import "rfc822realname.h" #include <ctype.h> #import <foundation/foundation.h> #import "LFCompatibility.h" #import "Message.h" #import "UrlGraphicCell.h" #import "ImageCell.h" #import "AttachmentCell.h" #import "NSString+MessageUtils.h" #define URLIFIER_DIRECTIVE "URLAdornment" #define IMAGE_DIRECTIVE "Image" #define ATTACHMENT_DIRECTIVE "Attachment" #define QUOTESTRING "> " //#define ISEMPTY(string) ((string==NULL)||(*(string)=='\0')) #define FORCEBREAK "\n\t -·1234567890>#}:" static void writeNSStringToRTFStream(NXStream *stream, void *item, void *data) { NSString *string = item; unsigned char c; const char *cp; int i, l; if(string == nil) { NXPrintf(stream, "*nil*"); } else { for(cp = [string cString]; *cp != '\0'; cp++) { c = *cp; if(NXIsPrint(c) || NXIsSpace(c)) { if((c == '\n') || (c == '\\') || (c == '{') || (c == '}')) NXPutc(stream, '\\'); NXPutc(stream, c); } } } } static void writeNSStringToASCIIStream(NXStream *stream, void *item, void *data) { NSString *string = item; NSData *cstring; if(string == nil) { NXPrintf(stream, "*nil*"); } else { cstring = [string dataUsingEncoding:NSNEXTSTEPStringEncoding allowLossyConversion:NO]; NXWrite(stream, [cstring bytes], [cstring length]); } } @implementation ArticleViewControl + initialize { NXRegisterPrintfProc('w', &writeNSStringToRTFStream, [(Object *)self zone]); NXRegisterPrintfProc('v', &writeNSStringToASCIIStream, [(Object *)self zone]); return self; } - init { [super init]; noArticle=TRUE; rot13=FALSE; [ERROR_MANAGER addObserver:self selector:@selector(updateText) forError:ENOTEPrefsChanged]; [Text registerDirective: URLIFIER_DIRECTIVE forClass:[UrlGraphicCell class]]; [Text registerDirective:IMAGE_DIRECTIVE forClass:[ImageCell class]]; [Text registerDirective:ATTACHMENT_DIRECTIVE forClass:[AttachmentCell class]]; [self updateText]; return self; } - awakeFromNib { [theText setFontPanelEnabled:FALSE]; [clockView setMilitaryTime:[NXApp defaultBoolValue:"24HourClock"]]; [clockView setHide:YES]; return self; } - free { [article release]; [ERROR_MANAGER removeObserver:self forError:ENOTEPrefsChanged]; return [super free]; } - updateText { [self displayArticleScrollUp:NO]; return self; } - (int)loadArticle:(Article *)theArticle fromGroup:(const char *)theGroup { int statusCode; NSData *articleData; NSString *hfList[FIELD_COUNT - XOVER_COUNT] = {@"Reply-To", @"Followup-To", @"Newsgroups", @"Organization"}; NSString *hfValue; int i; time_t t; [article release]; article = nil; statusCode = [nntpServer loadArticleWithNumber:[theArticle number] intoData:&articleData]; if(statusCode != OK_ARTICLE) return statusCode; article = [[MessagePart messagePartWithData:articleData] retain]; for(i = 0; i < FIELD_COUNT - XOVER_COUNT; i++) if((hfValue = [article stringValueOfHeaderFieldNamed:hfList[i]]) != nil) [theArticle header]->fieldBody[XOVER_COUNT + i] = NXCopyStringBuffer([hfValue cString]); noArticle=FALSE; rot13 = FALSE; urlZaps = TRUE; [self displayArticleScrollUp:YES]; if(![imageView showFaceForName:[theArticle header]->fieldBody[FROM]]) { const char *r = [theArticle header]->fieldBody[REPLY_TO]; if((r != NULL) && (*r != '\0')) [imageView showFaceForName:r]; } t = [theArticle time]; [[[clockView setHide:FALSE] setTime:localtime(&t)] display]; return OK_ARTICLE; } - displayArticleScrollUp:(BOOL)scroll { Font *textFont, *sigFont, *nptitleFont; NXSize visibleSize; NXRect theUpperRect; NXStream *stream; if(noArticle) return self; textFont = [NXApp defaultFont:DEFAULT_ARTICLE_FONT]; sigFont = [Font userFixedPitchFontOfSize:0 matrix:NX_FLIPPEDMATRIX]; nptitleFont = [Font systemFontOfSize:24 matrix:NX_FLIPPEDMATRIX]; signatureDetection = [NXApp defaultBoolValue:DEFAULT_SIG_DETECTION]; rewrapping = [NXApp defaultBoolValue:DEFAULT_REWRAP_ARTICLE_TEXT]; headerMode = [NXApp defaultIntValue:DEFAULT_HEADER_MODE]; rtfCellCounter = 0; stream = NXOpenMemory(NULL, 0, NX_READWRITE); NXPrintf(stream, "{\\rtf0\\ansi{\\fonttbl\\f0\\fnil %s;\\f1\\fnil %s;\\f2\\fnil %s;}\n\\paperw11040\\paperh9800\\margl120\\margr120\\pard\\tx520\\tx1060\\f0\\b0\\i0\\ulnone\\fc0\\cf0\\fs%d ", [textFont name], [nptitleFont name], [sigFont name], (int)([textFont pointSize] * 2)); if(headerMode==NEWSPAPER_HEADER) [self writeNewspaperHeaderOntoStream:stream]; else if(headerMode==FULL_HEADER) [self writeCompleteHeaderOntoStream:stream]; else if(headerMode==SMALL_HEADER) [self writeFilteredHeaderOntoStream:stream]; NXPrintf(stream, "\\\n\\f0\\fs%d ", (int)([textFont pointSize] * 2)); [self writeMessagePart:article ontoStream:stream type:StreamTypeRTF]; NXPrintf(stream, "}\n"); [[theText window] disableDisplay]; [theText setAutodisplay:NO]; #if DEBUG NXSeek(stream, 0, NX_FROMSTART); NXSaveToFile(stream, "/tmp/article.rtf"); #endif NXSeek(stream, 0, NX_FROMSTART); chdir([NXApp scratchDirectoryName]); [theText readRichText:stream]; NXCloseMemory(stream, NX_FREEBUFFER); if(scroll) { [[[theText superview] superview] getContentSize:&visibleSize]; NXSetRect(&theUpperRect,0.0,0.0,visibleSize.width,visibleSize.height); [theText scrollRectToVisible:&theUpperRect]; } [theText setAutodisplay:YES]; [[theText window] reenableDisplay]; [[theText window] display]; return self; } //--------------------------------------------------------------------------------------- // Writing the header //--------------------------------------------------------------------------------------- - (void)writeCompleteHeaderOntoStream:(NXStream *)stream; { NSEnumerator *fieldEnum; NSString *fieldName; fieldEnum = [[article headerFieldNames] objectEnumerator]; while((fieldName = [fieldEnum nextObject]) != nil) { NXPrintf(stream, "{\\b %w:} ", fieldName); [self writeString:[article stringValueOfHeaderFieldNamed:fieldName] ontoStream:stream type:StreamTypeRTF]; NXPrintf(stream, "\\\n"); } } - (void)writeFilteredHeaderOntoStream:(NXStream *)stream; { const char *ddbValue; NSArray *requestedFields; NSEnumerator *fieldEnum; NSString *fieldName, *fieldValue; ddbValue = [NXApp defaultValue:DEFAULT_HEADER_FILTER]; if(*ddbValue == '\0') return; requestedFields = [[NSString stringWithCString:ddbValue] componentsSeparatedByString:@":"]; fieldEnum = [requestedFields objectEnumerator]; while((fieldName = [fieldEnum nextObject]) != nil) { if([fieldName isEqualToString:@""]) continue; if((fieldValue = [article stringValueOfHeaderFieldNamed:fieldName]) == nil) continue; NXPrintf(stream, "{\\b %w:} ", fieldName); [self writeString:fieldValue ontoStream:stream type:StreamTypeRTF]; NXPrintf(stream, "\\\n"); } } - (void)writeNewspaperHeaderOntoStream:(NXStream *)stream; { NSString *subject, *from, *organization; subject = [article stringValueOfHeaderFieldNamed:@"Subject"]; if([subject isEqualToString:@""] == NO) { Font *titleFont; int titleSize = 24; NXRect visibleRect; titleFont = [Font systemFontOfSize:titleSize matrix:NX_FLIPPEDMATRIX]; titleFont = [[FontManager new] convert:titleFont toHaveTrait:NX_BOLD]; [theText getVisibleRect:&visibleRect]; while(([titleFont getWidthOf:[subject cString]] > NX_WIDTH(&visibleRect) - 10) && (titleSize > 12)) { titleSize -= 2; titleFont = [[FontManager new] convert:titleFont toSize:titleSize]; } NXPrintf(stream, "\\f1\\b\\fs%d %w\\b0\\\n", titleSize * 2, subject); } from = [article stringValueOfHeaderFieldNamed:@"From"]; if((from != nil) && ([from isEqualToString:@""] == NO)) { NSString *realname; realname = [from realnameFromEMailAddress]; NXPrintf(stream, "\\f1\\b\\i0\\ulnone\\fs28 by %w", realname); organization = [article stringValueOfHeaderFieldNamed:@"Organization"]; if((organization != nil) && ([organization isEqualToString:@""] == NO)) NXPrintf(stream, ", %w", organization); NXPrintf(stream, "\\b0\\\n"); } } //--------------------------------------------------------------------------------------- // writing the body //--------------------------------------------------------------------------------------- - writeBody:(NXStream *)aStream { [self writeMessagePart:article ontoStream:aStream type:StreamTypeASCII]; return self; } - writeQuotedText:(NXStream *)aStream { NXStream *tmpStream; NSString *quotePrefixString, *string; const char *quotePrefixCString; char *buffer; int length, maxlength, width; quotePrefixCString=[NXApp defaultValue:DEFAULT_QUOTING_PREFIX]; if(!quotePrefixCString){ [NXApp setDefault:DEFAULT_QUOTING_PREFIX to:QUOTESTRING]; quotePrefixCString=[NXApp defaultValue:DEFAULT_QUOTING_PREFIX]; } quotePrefixString = [NSString stringWithCString:quotePrefixCString]; // this is awkward... tmpStream = NXOpenMemory(NULL, 0, NX_READWRITE); [self writeMessagePart:article ontoStream:tmpStream type:StreamTypeASCII]; NXGetMemoryBuffer(tmpStream, &buffer, &length, &maxlength); string = [[[NSString alloc] initWithCStringNoCopy:buffer length:length freeWhenDone:NO] autorelease]; width = 72 - [quotePrefixString length]; string = [[string stringByWrappingToLineLength:width] stringByPrefixingLinesWithString:quotePrefixString]; NXWrite(aStream, [string cString], [string cStringLength]); NXCloseMemory(tmpStream, NX_FREEBUFFER); return self; } //--------------------------------------------------------------------------------------- // writing a message part //--------------------------------------------------------------------------------------- - (void)writeMessagePart:(MessagePart *)part ontoStream:(NXStream *)stream type:(int)streamType; { NSString *contentType = [part contentType]; NSString *contentSubtype = [part contentSubtype]; if(contentType == MIMEMultipartContentType) { NSEnumerator *subpartEnum; MessagePart *subpart, *richestAcceptableVersion; if([contentSubtype isEqualToString:MIMEAlternativeMPSubtype]) { // this is not perfect yet. no nested parts allowed! also assumes that // subparts are textual. who would post two versions of, say, an image // anyway?! richestAcceptableVersion = nil; subpartEnum = [(NSArray *)[part contents] reverseObjectEnumerator]; while((subpart = [subpartEnum nextObject]) != nil) { if([subpart contentType] == MIMETextContentType) { NSString *subtype = [subpart contentSubtype]; if([subtype isEqualToString:@"plain"] || [subtype isEqualToString:@"enriched"]) break; else if([subtype isEqualToString:@"html"]) richestAcceptableVersion = subpart; } } if(subpart == nil) if(richestAcceptableVersion != nil) subpart = richestAcceptableVersion; else subpart = [(NSArray *)[part contents] lastObject]; [self writeMessagePart:subpart ontoStream:stream type:streamType]; } else { subpartEnum = [(NSArray *)[part contents] objectEnumerator]; while((subpart = [subpartEnum nextObject]) != nil) [self writeMessagePart:subpart ontoStream:stream type:streamType]; } } else if(contentType == MIMETextContentType) { if([contentSubtype isEqualToString:@"plain"]) [self writePlainText:[part contents] ontoStream:stream type:streamType]; else if([contentSubtype isEqualToString:@"enriched"]) [self writeEnrichedText:[part contents] ontoStream:stream type:streamType]; else if([contentSubtype isEqualToString:@"html"]) [self writeAttachment: [(NSString *)[part contents] dataUsingEncoding:NSISOLatin1StringEncoding] withName:[part filename] ontoStream:stream type:streamType]; else [self writeAttachment: [(NSString *)[part contents] dataUsingEncoding:[NSString defaultCStringEncoding]] withName:[part filename] ontoStream:stream type:streamType]; } else if(contentType == MIMEMessageContentType) { if([contentSubtype isEqualToString:@"rfc822"]) [self writeAttachment:[part contents] withName:[part filename] ontoStream:stream type:streamType]; else if([contentSubtype isEqualToString:@"external-body"]) [self writeAttachment:[part transferRepresentation] withName:[part filename] ontoStream:stream type:streamType]; else NXRunAlertPanel(NULL, "Cannot handle MIME type message/%@ yet.", "Cancel", NULL, NULL, contentSubtype); } else if((contentType == MIMEImageContentType) || ((contentType == MIMEApplicationContentType) && [contentSubtype isEqualToString:@"postscript"])) { NSString *dispositionType; if((contentType != MIMEImageContentType) && ([NXApp defaultBoolValue:DEFAULT_ALLOW_PS_INLINE] == NO)) dispositionType = MIMEAttachmentContentDisposition; else if((dispositionType = [part contentDisposition]) == nil) dispositionType = MIMEInlineContentDisposition; if(dispositionType == MIMEInlineContentDisposition) [self writeImage:[part contents] withName:[part filename] ontoStream:stream type:streamType]; else [self writeAttachment:[part contents] withName:[part filename] ontoStream:stream type:streamType]; } else // cannot display anything inline { [self writeAttachment:[part contents] withName:[part filename] ontoStream:stream type:streamType]; } } //--------------------------------------------------------------------------------------- // writing textual contents //--------------------------------------------------------------------------------------- - (void)writePlainText:(NSString *)string ontoStream:(NXStream *)stream type:(int)streamType; { if(rewrapping) string = [string stringByUnwrappingParagraphs]; [self writeString:string ontoStream:stream type:streamType]; } - (void)writeEnrichedText:(NSString *)text ontoStream:(NXStream *)stream type:(int)streamType; { static NSCharacterSet *etSpecialSet = nil, *newlineSet; NSScanner *scanner; NSMutableString *buffer, *output; NSString *string, *command; int nofillct, paramct; char *attribChange; BOOL scanNormal = YES; if(etSpecialSet == nil) { etSpecialSet = [[NSCharacterSet characterSetWithCharactersInString:@"\n<"] retain]; newlineSet = [[NSCharacterSet characterSetWithCharactersInString:@"\n"] retain]; } buffer = [NSMutableString string]; nofillct = paramct = 0; scanner = [NSScanner scannerWithString:text]; [scanner setCharactersToBeSkipped:nil]; while([scanner isAtEnd] == NO) { output = (paramct > 0) ? nil : buffer; if(scanNormal) { if([scanner scanUpToCharactersFromSet:etSpecialSet intoString:&string]) [output appendString:string]; scanNormal = NO; } else if([scanner scanString:@"<" intoString:NULL]) { if([scanner scanString:@"<" intoString:NULL]) { [output appendString:@"<"]; } else { if([scanner scanUpToString:@">" intoString:&string] == NO) [NSException raise:MIMEFormatException format:@"text/enriched"]; [scanner scanString:@">" intoString:NULL]; command = string = [string lowercaseString]; if([command hasPrefix:@"/"]) command = [command substringFromIndex:1]; attribChange = NULL; if([string isEqualToString:@"param"]) paramct += 1; else if([string isEqualToString:@"/param"]) paramct -= 1; else if([string isEqualToString:@"nofill"]) nofillct += 1; else if([string isEqualToString:@"/nofill"]) nofillct -= 1; else if([command isEqualToString:@"bold"]) attribChange = "b"; else if([command isEqualToString:@"italic"]) attribChange = "i"; if((attribChange != NULL) && (streamType == StreamTypeRTF)) { [self writeString:buffer ontoStream:stream type:streamType]; buffer = [NSMutableString string]; if([string hasPrefix:@"/"] == NO) NXPrintf(stream, "\\%s ", attribChange); else NXPrintf(stream, "\\%s0 ", attribChange); } } } else if([scanner scanCharactersFromSet:newlineSet intoString:&string]) { if(nofillct > 0) [output appendString:string]; else if([string length] == 1) [output appendString:@" "]; else [output appendString:[string substringFromIndex:1]]; } else { scanNormal = YES; } } [self writeString:buffer ontoStream:stream type:streamType]; } //--------------------------------------------------------------------------------------- // primitives //--------------------------------------------------------------------------------------- - (void)writeString:(NSString *)string ontoStream:(NXStream *)stream type:(int)streamType; { static NSCharacterSet *colon = nil, *alpha, *urlstop; static NSArray *services; static unsigned int maxServLength; NSRange r, remainingRange, possServRange, servRange, urlRange; NSEnumerator *serviceEnumerator; NSString *service; unsigned int outputLocation, nextLocation; if(rot13) string = [string stringByApplyingROT13]; if(streamType == StreamTypeASCII) { NXPrintf(stream, "%v", string); return; } else if(urlZaps == NO) { NXPrintf(stream, "%w", string); return; } if(colon == nil) { colon = [[NSCharacterSet characterSetWithCharactersInString:@":"] retain]; alpha = [[NSCharacterSet alphanumericCharacterSet] retain]; urlstop = [[NSCharacterSet characterSetWithCharactersInString: @"\"<>()[]',; \t\n\r"] retain]; services = [[NSArray arrayWithObjects:@"http", @"ftp", @"mailto", @"gopher", @"news", nil] retain]; maxServLength = 6; } nextLocation = outputLocation = 0; while(1) { remainingRange = NSMakeRange(nextLocation, [string length] - nextLocation); r = [string rangeOfCharacterFromSet:colon options:0 range:remainingRange]; if(r.length == 0) break; nextLocation = r.location + r.length; if(r.location < maxServLength) possServRange = NSMakeRange(0, r.location); else possServRange = NSMakeRange(r.location - 6, 6); // no need to clean up composed chars becasue they are not allowed in URLs anyway serviceEnumerator = [services objectEnumerator]; // b/c while((service = [serviceEnumerator nextObject]) != nil) { servRange = [string rangeOfString:service options: (NSBackwardsSearch | NSAnchoredSearch | NSLiteralSearch) range:possServRange]; if(servRange.length != 0) { r.length = [string length] - r.location; r = [string rangeOfCharacterFromSet:urlstop options:0 range:r]; urlRange.location = servRange.location; if(r.length == 0) // not found, assume URL extends to end of string r.location = [string length]; urlRange = NSMakeRange(servRange.location, r.location - servRange.location); nextLocation = urlRange.location + urlRange.length; r = NSMakeRange(outputLocation, urlRange.location - outputLocation); NXPrintf(stream, "%w", [string substringWithRange:r]); [self writeURL:[string substringWithRange:urlRange] ontoStream:stream type:streamType]; outputLocation = nextLocation; break; } } } if(outputLocation < [string length]) { r = NSMakeRange(outputLocation, [string length] - outputLocation); NXPrintf(stream, "%w", [string substringWithRange:r]); } } - (void)writeImage:(NSData *)data withName:(NSString *)name ontoStream:(NXStream *)stream type:(int)streamType; { if(streamType == StreamTypeRTF) { NSString *pathName; pathName = [NSString stringWithFormat:@"%s/%@", [NXApp scratchDirectoryName], name]; [data writeToFile:pathName atomically:NO]; NXPrintf(stream, "{{\\%s%d %w}\n\254}", IMAGE_DIRECTIVE, rtfCellCounter++, pathName); } else { NXPrintf(stream, "[An image was included here]"); } } - (void)writeAttachment:(NSData *)data withName:(NSString *)name ontoStream:(NXStream *)stream type:(int)streamType; { if(streamType == StreamTypeRTF) { // file + file.tiff ablegen NSString *pathName; pathName = [NSString stringWithFormat:@"%s/%@", [NXApp scratchDirectoryName], name]; [data writeToFile:pathName atomically:NO]; NXPrintf(stream, "{{\\%s%d %w}\n\254}", ATTACHMENT_DIRECTIVE, rtfCellCounter++, pathName); } else { NXPrintf(stream, "[An attachment was included here]"); } } - (void)writeURL:(NSString *)url ontoStream:(NXStream *)stream type:(int)streamType; { if(streamType == StreamTypeRTF) { NXPrintf(stream, "{{\\%s%d %w}%c}%w", URLIFIER_DIRECTIVE, rtfCellCounter++, url, 254, url); } else { NXPrintf(stream, "%v", url); } } //--------------------------------------------------------------------------------------- // target methods //--------------------------------------------------------------------------------------- - clear { [theText setAutodisplay:NO]; [theText setText:""]; [[theText setAutodisplay:YES] display]; noArticle=TRUE; [imageView showFaceForName:NULL]; [[clockView setHide:TRUE] display]; if(article != nil) [article release]; article = nil; return self; } - saveAs:sender { SavePanel *panel; int fd; NXStream *theStream; static char *dir=NULL; const char *articleHeader; if(dir==NULL) dir=NXCopyStringBuffer([NXApp defaultValue:DEFAULT_SAVE_PATH]); if([theText textLength]==0 || noArticle) return self; panel=[SavePanel new]; if([panel runModalForDirectory:dir file:""]!=NX_CANCELTAG){ free(dir); dir=NXCopyStringBuffer([panel filename]); } else return nil; //Save fd=open([panel filename],O_WRONLY|O_CREAT|O_TRUNC,0666); if(fd<0){ NXRunAlertPanel("ALEXANDRA","Can't save file: %s",NULL,NULL,NULL,strerror(errno)); return self; } articleHeader = [self articleHeader]; theStream=NXOpenFile(fd,NX_WRITEONLY); NXWrite(theStream,articleHeader,strlen(articleHeader)); NXWrite(theStream,"\n",1); [self writeBody:theStream]; NXClose(theStream); close(fd); return self; } - printText:sender { if([theText textLength]!=0){ [[NXApp printInfo] setHorizPagination:NX_FITPAGINATION]; [theText printPSCode:self]; } return self; } - (const char *)articleHeader { NSEnumerator *headerEnum; NSString *headerFieldName; NSMutableString *allHeaderFields; if(noArticle) return NULL; allHeaderFields = [[[NSMutableString alloc] init] autorelease]; headerEnum = [[article headerFieldNames] objectEnumerator]; while((headerFieldName = [headerEnum nextObject]) != nil) { [allHeaderFields appendString:headerFieldName]; [allHeaderFields appendString:@": "]; [allHeaderFields appendString: [article stringValueOfHeaderFieldNamed:headerFieldName]]; [allHeaderFields appendString:@"\n"]; } return [allHeaderFields cString]; // is autoreleased... } -(id)theText { return theText; } - setModeTo:(int)mode { [NXApp setDefault:DEFAULT_HEADER_MODE toInt:mode]; if(!noArticle) [self displayArticleScrollUp:NO]; return self; } - showHeader:sender { [self setModeTo:FULL_HEADER]; return self; } - hideHeader:sender { [self setModeTo:NO_HEADER]; return self; } - smallHeader:sender { [self setModeTo:SMALL_HEADER]; return self; } - newspaperHeader:sender { [self setModeTo:NEWSPAPER_HEADER]; return self; } - (BOOL)headermodeCellEnabled:menuCell { if((headerMode!=FULL_HEADER)&&([menuCell action]==@selector(showHeader:))) return TRUE; else if((headerMode!=NO_HEADER)&&([menuCell action]==@selector(hideHeader:))) return TRUE; else if((headerMode!=SMALL_HEADER)&&([menuCell action]==@selector(smallHeader:))) return TRUE; else if((headerMode!=NEWSPAPER_HEADER)&&([menuCell action]==@selector(newspaperHeader:))) return TRUE; return FALSE; } - rot13:sender { rot13=!rot13; [self displayArticleScrollUp:NO]; return self; } @end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.