Error: Initializer element not constant

Discussion in 'Mac Programming' started by iMasterWeb, Sep 19, 2009.

  1. iMasterWeb macrumors regular

    Joined:
    Mar 15, 2009
    #1
    Hi! So basically I'm working on a code that will act as a Twitter Shortener. I am taking user input, then shortening it to 140 characters using a search and replace command. I know the code might not be as pretty, or effecient as it could be, but I'm just a begginer. The problem is that I keep getting an error message:
    Code:
    Error: Initializer element not constant
    And I have no idea how to fix it. Any tips? Here's the part of the code where the error is:
    Code:
    
    #import "tweet.h"
    #import <Foundation/NSString.h>
    #import <Foundation/NSArray.h>
    #import <Foundation/NSObject.h>
    #import <Foundation/NSAutoreleasePool.h>
    
    @implementation tweet
    
    NSMutableArray *shortWords = [NSMutableArray arrayWithObjects: @"b/c", @"4", @"2", @"2", @"sum", @"w/", @"w/o" , nil]; 
    //error here
    NSMutableArray *longWords = [NSMutableArray arrayWithObjects: @"because", @"for" , @"two", @"to", @"some", @"with", @"without" , nil];
    //and here
    NSMutableString *reply = [[NSMutableString alloc] init];
    //and here
    NSMutableString *shortTweet = [[NSMutableString alloc] init];
    //and here
    
    BOOL shorten;
    
    -(void) shorten {
    	int overflow = [self length] - 140;
    	if([self length] > 140)
    		NSLog(@"Your tweet is &i characters long, %i more than 140. At this point, you can choose to shorten your tweet with the built in shortener or keep it as is.", [self length], overflow); 
    	scanf("%@", &reply);
    	do{
    		if( reply = @"shorten" ) shorten = YES;
    	else if( reply = @"publish" ) shorten = NO;
    	else NSLog(@"Please enter either 'shorten' or 'publish' to continue");
    	} while (shorten != YES || NO);
    	if( shorten == YES ) {
    	[self findAndReplace];
    		NSLog(@"Your shortened tweet is:\n");
    		NSLog(@"%@", shortTweet);	
    	}
    	if( shorten == NO ) {
    		NSLog(@"Your tweet is:\n");
    		NSLog(@"%@", self);
    	}
    }
    -(void) findAndReplace {
    	int search;
    	int replace;
    	int i;
    	for(i = 0; i < 7; ++i) {
    		search = [longWords objectAtIndex: i];
    		replace = [shortWords objectAtIndex: i];
    		[shortTweet replaceOccurrencesOfString: search 
    									withString: replace 
    									   options: nil 
    										 range: NSMakeRange (0, [shortTweet length])];
    	}
    }
    
    @end
    
    If you'd like to give any tips to actually making the code more efficient, feel free to. But I really just want to figure out this error. Thanks!!
     
  2. Catfish_Man macrumors 68030

    Catfish_Man

    Joined:
    Sep 13, 2001
    Location:
    Portland, OR
    #2
    You'll need to either make your arrays instance variables and initialize then in your -init method, or if you really want them static, use the lazy initializer pattern and check if they're set up before use:
    Code:
    if (!shortWords) {
        shortWords = bla bla;
    }
    /* code using shortWords */
    
     
  3. iMasterWeb thread starter macrumors regular

    Joined:
    Mar 15, 2009
    #3
    Like this? (I'm not exactly sure what you mean by "lazy initializer pattern" so that could be my problem)
    Code:
    NSMutableArray *shortWords = [[NSMutableArray alloc] init];
    if (!shortWords) {
        shortWords = @"b/c", @"4", @"2", @"2", @"sum", @"w/", @"w/o";
    }
    Because that didn't fix the error...but I do appreciate the help! I probably did something wrong though....
     
  4. Catfish_Man macrumors 68030

    Catfish_Man

    Joined:
    Sep 13, 2001
    Location:
    Portland, OR
    #4
    The problem is that you're trying to call [[NSMutableArray alloc] init]; *outside* of any method or function. You can't do that, so you need to do it either in your init method, or when you first use the object (aka lazy initialization, waiting until you absolutely have to).

    <edit> Also, comma separated lists of objects are not NSMutableArrays. That syntax won't work regardless of where it is. </edit>
     
  5. iMasterWeb thread starter macrumors regular

    Joined:
    Mar 15, 2009
    #5
    Ok, so I think I'm gonna try doing it in the init method. To do this, would I have to override the default init method? If so.....how? I know how to override methods but what would I include in it? Would it simply be something like
    Code:
    -(void) init {
    NSMutableArray *shortWords = [NSMutableArray arrayWithObjects: @"b/c", @"4", @"2", @"2", @"sum", @"w/", @"w/o" , nil];
    NSMutableArray *longWords = [NSMutableArray arrayWithObjects: @"because", @"for" , @"two", @"to", @"some", @"with", @"without" , nil];
    NSMutableString *reply = [[NSMutableString alloc] init];
    NSMutableString *shortTweet = [[NSMutableString alloc] init];
    }
    Thanks for all the help so far!!
     
  6. gnasher729 macrumors P6

    gnasher729

    Joined:
    Nov 25, 2005
    #6
    Instead of poking around in the dark, could you get a book about the C language and learn the basics?

    You replaced your global variables with local variables. Local variables in your init method will promptly disappear as soon as you exit the init method. You really, really need to learn the difference between global variables (two variants: extern and static), local variables and instance variables, and then you need to decide what kind of variables you need. How long to you expect these variables to live? Who should have access to them?

    And I just noticed that catfish_man gave you advice that was correct and would have worked in his first post, but you had to do it in a different way that didn't work, and then you changed it to something completely different that compiles but will quite likely not work at all.
     
  7. iMasterWeb thread starter macrumors regular

    Joined:
    Mar 15, 2009
    #7
    I do have a book on Obj-C : Programming in Objective-C 2.0 by Stephen Kochan. But sometimes I just want to try my own stuff and have fun, but I run into problems so I come here :D

    Anyways, I read bout extern and static, but I didn't know they could be used on strings, I thought they could only be used on ints, bools, etc.

    Anyways I'll play around a little more.

    As for the life of these variables, they will need to be used throughout the entire program.
     
  8. iMasterWeb thread starter macrumors regular

    Joined:
    Mar 15, 2009
    #8
    I din't deliberately do something other than what he told me to do, I was confused about what he wanted me to do, so I tried to as best I could. As I said, I'm still learning and I'm slowly but surely making my way to the book mentioned above, so I don't always understand the terminology.
     
  9. iMasterWeb thread starter macrumors regular

    Joined:
    Mar 15, 2009
    #9
    Ok. here are some edited @interface and @implementation files:

    @interface:
    Code:
    #import <Foundation/Foundation.h>
    
    
    @interface tweet : NSString {
    
    	BOOL shorten;	
    	NSMutableArray *longWords;
    	NSMutableArray *shortWords;
    	NSString *reply;
    	NSMutableString *shortTweet;
    }
    
    
    -(void) shorten;
    -(void) findAndReplace;
    -(void) initObjects;
    @end
    
    @implementation:
    Code:
    #import "tweet.h"
    #import <Foundation/NSString.h>
    #import <Foundation/NSArray.h>
    #import <Foundation/NSObject.h>
    #import <Foundation/NSAutoreleasePool.h>
    
    @implementation tweet
    
    -(void) initObjects {
    	longWords = [NSMutableArray arrayWithObjects: @"because", @"for" , @"two", @"to", @"some", @"with", @"without"];
    	shortWords = [NSMutableArray arrayWithObjects: @"b/c", @"4", @"2", @"2", @"sum", @"w/", @"w/o"];
    }
    
    
    
    -(void) shorten {
            [self initObjects]
    	shortTweet = self;
    	int overflow = [self length] - 140;
    	if([self length] > 140)
    		NSLog(@"Your tweet is &i characters long, %i more than 140. At this point, you can choose to shorten your tweet with our built in shortener or publish as is.", [self length], overflow); 
    	scanf("%@", &reply);
    	do{
    		if( reply = @"shorten" ) shorten = YES;
    	else if( reply = @"publish" ) shorten = NO;
    	else NSLog(@"Please enter either 'shorten' or 'publish' to continue");
    	} while (shorten != YES || NO);
    	if( shorten == YES ) {
    	[self findAndReplace];
    		NSLog(@"Your shortened tweet is:\n");
    		NSLog(@"%@", shortTweet);	
    	}
    	if( shorten == NO ) {
    		NSLog(@"Your tweet is:\n");
    		NSLog(@"%@", self);
    	}
    }
    -(void) findAndReplace {
    	int search;
    	int replace;
    	int i;
    	for(i = 0; i < 7; ++i) {
    		search = [longWords objectAtIndex: i];
    		replace = [shortWords objectAtIndex: i];
    		[shortTweet replaceOccurrencesOfString: search 
    									withString: replace 
    									   options: nil 
    										 range: NSMakeRange (0, [shortTweet length])];
    	}
    }
    
    @end
    
    Am I on the right track here? Or am I dead wrong? It now builds and launches without any errors (however there are a few warnings) but terminates due to an uncaught exception.

    Thanks for all the help so far!
     
  10. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #10
    Your uses of arrayWithObjects: needs to terminate with a nil.

    Those aren't the only errors (pay attention to those warnings), but those could be the crashers.
     
  11. iMasterWeb thread starter macrumors regular

    Joined:
    Mar 15, 2009
    #11
    Ok, I made some changes and got it down to only one warning, but it still terminates. Here is the new code (the only part I changed is the search and replace part because that's the only place there were errors)
    Code:
    -(void) findAndReplace {
    	int i;
    	for(i = 0; i < 7; ++i) {
    		NSString *search = [NSString stringWithString: [longWords objectAtIndex: i]];
    		NSString *replace = [NSString stringWithString: [longWords objectAtIndex: i]];
    		[shortTweet replaceOccurrencesOfString: search 
    									withString: replace 
    									   options: nil 
    										 range: NSMakeRange (0, [shortTweet length])]; \\error here
    	}
    I took out the int definitions for search and replace because I realized that objectAtIndex would return a string, not an int, so I made search and replace into strings.

    As I mentioned I still get one pesky little warning that apparently is a common one, but I'm stumped because I got the search and replace code straight from the book. The warning is
    Code:
    warning: passing argument 3 of 'replaceOccurrencesOfString:withString:options:range:' makes integer from pointer without a cast
    I'm a little confused because I'm not trying to make an integer at all...

    NOTE: I also changed the part that used to say
    Code:
    shortTweet = self;
    to
    Code:
    shortTweet = [NSString stringWithString: self];
    Thanks for all previous and any future help!!
     
  12. lee1210 macrumors 68040

    lee1210

    Joined:
    Jan 10, 2005
    Location:
    Dallas, TX
    #12
    Haven't tried to run any of this, so i don't know why it's crashing, but NSStringCompareOptions isn't an object, it is typedef'd to an NSUInteger, which is typedef'd to an int or long int depending on your platform. You should be able to pass 0 for this, but I will admit to not having used this method, so I'm not 100% sure... however, this is what is causing your warning. nil is (void *)0, so it is a pointer, not an integer.

    -Lee

    Edit: Also, you are not changing anything, because you are using longwords to get search and replace.

    Edit 2: You should loop from 0 to [shortWords count] or [longWords count] so you don't have to change this if you add replacements.
     
  13. iMasterWeb thread starter macrumors regular

    Joined:
    Mar 15, 2009
    #13
    Thanks Lee...but where do I use NSStringCompareOptions? Or is it fancy termniology for something else? Anyways, I got it to work without any errors..but it still doesn't "work". Here's what show up in the console:

    Code:
    2009-09-20 18:02:52.962 Twitter Shortener[27361:903] -[NSCFString shorten]: unrecognized selector sent to instance 0x3030
    2009-09-20 18:02:52.965 Twitter Shortener[27361:903] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSCFString shorten]: unrecognized selector sent to instance 0x3030'
    *** Call stack at first throw:
    (
    	0   CoreFoundation                      0x9898f58a __raiseError + 410
    	1   libobjc.A.dylib                     0x91661f49 objc_exception_throw + 56
    	2   CoreFoundation                      0x989db9db -[NSObject(NSObject) doesNotRecognizeSelector:] + 187
    	3   CoreFoundation                      0x98937026 ___forwarding___ + 950
    	4   CoreFoundation                      0x98936bf2 _CF_forwarding_prep_0 + 50
    	5   Twitter Shortener                   0x0000288b main + 161
    	6   Twitter Shortener                   0x000027be start + 54
    	7   ???                                 0x00000001 0x0 + 1
    )
    Can anyone help me to understand what this jumble of words means?? :eek:

    Thanks!
     
  14. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #14
    After that change, shortTweet is assigned an immutable NSString instance. Immutable NSStrings don't recognize this selector:

    replaceOccurrencesOfString:withString:eek:ptions:range

    That selector is only defined for NSMutableString.

    You're not retaining the returned NSString*, even though you're storing it in shortTweet and implicitly taking ownership. Since stringWithString: returns an auto-released value, you're breaking the memory management rules.


    You're also apparently calling shorten on a plain NSString, rather than an instance of your tweet class. The exception is coming from main(), where you apparently try to send a shorten message to a plain NSString. That's what "-[NSCFString shorten]: unrecognized selector.." is telling you.

    That's just a guess, though, without seeing the source for main().

    You should rethink the "tweet is a subclass of NSString" decision. For one thing, having it subclass NSString instead of NSMutableString breaks the contract of NSString that it's immutable. Call the class TweetShortener, and have it take an NSString* argument to a shorten method, returning a shortened NSString* result.


    You should also post all your code again, including main(), so we can see the entire context.
     
  15. lee1210 macrumors 68040

    lee1210

    Joined:
    Jan 10, 2005
    Location:
    Dallas, TX
    #15
  16. iMasterWeb thread starter macrumors regular

    Joined:
    Mar 15, 2009
    #16
    Thanks Chown and Lee! Anyways, here's the code in its entirety. I made "tweet" a subclass of NSMutableString rather than NSString and changed the part
    Code:
    shortTweet = [NSString stringWithString: self];
    to
    Code:
    shortTweet = [NSMutableString stringWithString: self];
    Here the @interface
    Code:
    #import <Foundation/Foundation.h>
    
    
    @interface tweet : NSMutableString {
    
    	BOOL shorten;	
    	NSMutableArray *longWords;
    	NSMutableArray *shortWords;
    	NSString *reply;
    	NSMutableString *shortTweet;
    }
    
    
    -(void) shorten;
    -(void) findAndReplace;
    -(void) initObjects;
    @end
    
    and the @implementation
    Code:
    #import "tweet.h"
    #import <Foundation/NSString.h>
    #import <Foundation/NSArray.h>
    #import <Foundation/NSObject.h>
    #import <Foundation/NSAutoreleasePool.h>
    
    @implementation tweet
    
    -(void) initObjects {
    	longWords = [NSMutableArray arrayWithObjects: @"because", @"for" , @"two", @"to", @"some", @"with", @"without", nil];
    	shortWords = [NSMutableArray arrayWithObjects: @"b/c", @"4", @"2", @"2", @"sum", @"w/", @"w/o", nil];
    }
    
    
    
    -(void) shorten {
    	[self initObjects];
    	shortTweet = [NSMutableString stringWithString: self];
    	int overflow = [self length] - 140;
    	if([self length] > 140)
    		NSLog(@"Your tweet is &i characters long, %i more than 140. At this point, you can choose to shorten your tweet with our built in shortener or publish as is.", [self length], overflow); 
    	scanf("%@", &reply);
    	do{
    		if( reply = @"shorten" ) shorten = YES;
    	else if( reply = @"publish" ) shorten = NO;
    	else NSLog(@"Please enter either 'shorten' or 'publish' to continue");
    	} while (shorten != YES || NO);
    	if( shorten == YES ) {
    	[self findAndReplace];
    		NSLog(@"Your shortened tweet is:\n");
    		NSLog(@"%@", shortTweet);	
    	}
    	if( shorten == NO ) {
    		NSLog(@"Your tweet is:\n");
    		NSLog(@"%@", self);
    	}
    }
    -(void) findAndReplace {
    	int i;
    	for(i = 0; i < 7; ++i) {
    		NSString *search = [NSString stringWithString: [longWords objectAtIndex: i]];
    		NSString *replace = [NSString stringWithString: [shortWords objectAtIndex: i]];
    		[shortTweet replaceOccurrencesOfString: search 
    									withString: replace 
    									   options: 0 
    										 range: NSMakeRange (0, [shortTweet length])];
    	}
    }
    
    @end
    and main
    Code:
    #import <Foundation/Foundation.h>
    #import "tweet.h"
    #import <Foundation/NSString.h>
    #import <Foundation/NSArray.h>
    
    int main (int argc, const char * argv[]) {
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    	tweet *myTweet = [[tweet alloc]init];
    	scanf("%@", myTweet);
    	[myTweet shorten];
    	[myTweet release];
    	[pool drain];
        return 0;
    }
    Thanks for the help guys!
     
  17. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #17
    The above line in main() can't possibly work. If it's not clear why, review the docs for scanf(). In particular, consider whether "%@" is a valid scan format, and whether myTweet is an appropriate pointer value.

    You also have a call to scanf() in -shorten that won't work.

    Fundamentally, you're mixing C Standard I/O functions (scanf) with Objective-C objects, but C I/O functions have no concept of Objective-C objects.
     
  18. iMasterWeb thread starter macrumors regular

    Joined:
    Mar 15, 2009
    #18
    Ok, I've poked around on the internet and my book, and found that scanf() doesn't scan NSStrings..only characters (correct?) so what I did was create a character string and then convert it to an NSString whose value is then assigned to the appropriate object. So far so good, I got rid of everything in the console...except there now is NOTHING in the console..nothing happens. Here's the codes again:

    @implementation:
    Code:
    #import "tweet.h"
    #import <Foundation/NSString.h>
    #import <Foundation/NSArray.h>
    #import <Foundation/NSObject.h>
    #import <Foundation/NSAutoreleasePool.h>
    
    @implementation tweet
    
    -(void) initObjects {
    	longWords = [NSMutableArray arrayWithObjects: @"because", @"for" , @"two", @"to", @"some", @"with", @"without", nil];
    	shortWords = [NSMutableArray arrayWithObjects: @"b/c", @"4", @"2", @"2", @"sum", @"w/", @"w/o", nil];
    }
    
    
    
    -(void) shorten {
    	[self initObjects];
    	shortTweet = [NSMutableString stringWithString: self];
    	int overflow = [self length] - 140;
    	if([self length] > 140)
    		NSLog(@"Your tweet is &i characters long, %i more than 140. At this point, you can choose to shorten your tweet with our built in shortener or publish as is.", [self length], overflow); 
    	char *firstReply;
    	scanf("%s", firstReply); \\CHANGES HERE!
    	reply = [NSMutableString stringWithUTF8String: firstReply]; \\AND HERE!
    	do{
    		if( reply = @"shorten" ) shorten = YES;
    	else if( reply = @"publish" ) shorten = NO;
    	else NSLog(@"Please enter either 'shorten' or 'publish' to continue");
    	} while (shorten != YES || NO);
    	if( shorten == YES ) {
    	[self findAndReplace];
    		NSLog(@"Your shortened tweet is:\n");
    		NSLog(@"%@", shortTweet);	
    	}
    	if( shorten == NO ) {
    		NSLog(@"Your tweet is:\n");
    		NSLog(@"%@", self);
    	}
    }
    -(void) findAndReplace {
    	int i;
    	for(i = 0; i < 7; ++i) {
    		NSString *search = [NSString stringWithString: [longWords objectAtIndex: i]];
    		NSString *replace = [NSString stringWithString: [shortWords objectAtIndex: i]];
    		[shortTweet replaceOccurrencesOfString: search 
    									withString: replace 
    									   options: 0 
    										 range: NSMakeRange (0, [shortTweet length])];
    	}
    }
    
    @end
    
    main:
    Code:
    #import <Foundation/Foundation.h>
    #import "tweet.h"
    #import <Foundation/NSString.h>
    #import <Foundation/NSArray.h>
    
    int main (int argc, const char * argv[]) {
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    	tweet *myTweet = [[tweet alloc]init];
    	char *firstTweet;
    	scanf("%s", firstTweet); \\CHANGES HERE!
    	myTweet = [tweet stringWithUTF8String: firstTweet]; \\AND HERE!
    	[myTweet shorten];
    	[myTweet release];
    	[pool drain];
        return 0;
    }
    
    Thanks guys!!
     
  19. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #19
    You're still doing the scanf() wrong. I suggest a different function, such as fgets() on the stdin stream, or fgetln().

    If you insist on using scanf(), then consider that both firstReply and firstTweet are uninitialized pointers. Try char[] instead of char*. If that doesn't make sense, review the correspondence in C between an array and a pointer to the first element of an array.

    Also note that "%s" only reads one whitespace-delimited word from the input (i.e. "a sequence of non-white-space characters" as quoted from the scanf man page). It doesn't read a complete line. That doesn't seem sensible given the code.
     
  20. iMasterWeb thread starter macrumors regular

    Joined:
    Mar 15, 2009
    #20
    Ok, I see what you're saying, sorry for my ignorance :(. Anyways I did what you suggested and am now using fgets() This WORKS! It actually asked for my input!!:D Althought there seems to be a problem when I convert it from a C string (which it returns...right?) to an NSMutableString (or in my case a "tweet") I get an error mesaage from the console and it says
    Code:
     *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[tweet initWithBytes:length:encoding:]: unrecognized selector sent to instance 0x105010'
    I deducted that it was referring to this line:
    Code:
    myTweet = [tweet stringWithUTF8String: firstTweet];
    so here is my main..I got the conversion method straight from Apple's docs...so I'm not sure what's going wrong...
    Code:
    #import <Foundation/Foundation.h>
    #import "tweet.h"
    #import <Foundation/NSString.h>
    #import <Foundation/NSArray.h>
    
    int main (int argc, const char * argv[]) {
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    	tweet *myTweet = [[tweet alloc]init];
    	char firstTweet[250];
    	fgets(firstTweet, 250, stdin);
    	if(firstTweet[strlen(firstTweet) - 1] == '\n') {
    		firstTweet[strlen(firstTweet) - 1] = 0;
    	}
    	myTweet = [tweet stringWithUTF8String: firstTweet];
    	[myTweet shorten];
    	[myTweet release];
    	[pool drain];
        return 0;
    }
    
    Also, do I need to import stdio.h, or is that included in Foundation/Foundation.h ?

    Thanks! :D:D:D:D
     
  21. Catfish_Man macrumors 68030

    Catfish_Man

    Joined:
    Sep 13, 2001
    Location:
    Portland, OR
    #21
    I suspect you want [NSString stringWith...] rather than [tweet stringWith...]

    Although that will get you an NSString. You probably want an init method on Tweet that takes an NSString as a parameter. Or a -setString: method or something.
     
  22. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #22
    The short answer: you're subclassing NSMutableString, but you're doing it wrong.

    Subclassing NSString or NSMutableString must follow certain rules, and you're not following those rules. See lee1210's post from yesterday for a link that explains your obligations. If you don't understand what that link is telling you, then you're in over your head and you need to change something.

    I strongly suggest that you remove the subclassing of NSMutableString, and just make your class a subclass of NSObject. It will take less time for you to figure that out than for you to figure out how to subclass NSMutableString correctly.

    You're not only struggling with the language and the Cocoa library, you're also struggling against your incorrect subclassing of NSMutableString. You're unintentionally making it more difficult for yourself, although I suspect you think that subclassing NSMutableString is making it easier.

    You will need a setup method that takes one NSString* parameter: the text to be shortened. You will also need a results method that returns an NSString*: the shortened text. Or you can think about making a single method with one NSString* parameter and returning a shortened NSString*.
     
  23. iMasterWeb thread starter macrumors regular

    Joined:
    Mar 15, 2009
    #23
    AAAAAAAAHHH That would be soooooooo much easier! For some reason I thought that because I was dealing with a string (or a tweet) I had to subclass NSString. But now I realize that that is incorrect. Thanks so much for all your help! I'll work on that tomorrow and post the results (and hopefully not more errors) THANK YOU GUYS SO MUCH! :D :D
     

Share This Page