ftp.nice.ch/pub/next/connectivity/news/Alexandra-0.9.s.tar.gz#/alex/ArticleViewControl.m

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.