/** PAGE.js handles ANSItex and ViewData page frames It also handles windows, horizontal/vertical scrolling and content paging. This is inspired by frame.js provided by Synchronet Objects: + Page - our display page, build with Windows - Line 1 - Title (Fixed) - Line 2..23 - Content (Scrollable) - Line 24 - Status/Command Input (Fixed) = @todo When scrolling is disabled, and the canvas is greater than the window, then "nextpage" returns the next frame = Pageable windows cannot have children [b-z frames], only "CONTENT" is paged = @todo Pageable windows are pagable when scrolling is false and window canvas.height > window.height and canvas.width = window.width + Window - size W x H, where W/H can be larger than the Screen - Window holds all the content to be shown - x,y - attributes define the position of the window in it's parent [1..] - z - determines which layer the window is on, higher z is shown [0..] - width/height - determines the physical size of the window (cannot be larger than it's parent) - canvas width/height - determines the logical size of the window, which if larger than physical enables scrolling - ox/oy - determines the start of the canvas that is shown, relative to canvas width/height - service - Current supported are ANSItex (80x24) and ViewData (40x24) - content - array of Chars height/width order - visible - determines if the window (and it's children) are renderable = Windows can be have children, and the z determines the layer shown relative to its parent = Swapping z values determines which windows are hidden by others + Char - object holding each character, and it's color when rendered = Rendering - ANSItex + Each attribute can have it's own color (colors take up no positional space) + We only change render control colors to change attributes when it actually changes, otherwise we render just the character - ViewData + An attribute Foreground or Background or Special Function takes up a character + Character must be set to NULL when it's a control character = EXAMPLE: a = new Page('TEX') // root frame 80 x 24 for ANSItex b = new Window(1,1,40,22,a.content) // b frame 40 x 22 - starting at 1,1 c = new Window(41,1,40,22,a.content) // c frame 40 x 22 - starting at 41,1 (child of a) d = new Window(1,1,21,10,c) // d frame 20 x 11 - starting at 1,1 of c e = new Window(25,12,10,5,c) // e frame 10 x 5 - starting at 25,12 of c f = new Window(15,8,13,7,c) // f frame 13 x 7 - starting at 15,8 of c --:____.____|____.____|____.____|____.____|____.____|____.____|____.____|____.____| 01:TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT 02:22222222222222222222222222222222222222224444444444444444444443333333333333333333 03:2 24 43 3 04:2 24 43 3 05:2 24 43 3 06:2 24 43 3 07:2 24 43 3 08:2 24 444444443333333 3 09:2 24 466666666666663 3 10:2 24 46 63 3 11:2 2444444444444446 63 3 12:2 2333333333333336 633333333 3 13:2 23 36 665555553 3 14:2 23 36 65 53 3 15:2 23 366666666666665 53 3 16:2 23 333333333335555 53 3 17:2 23 355555555553 3 18:2 23 333333333333 3 19:2 23 3 20:2 23 3 21:2 23 3 22:2 23 3 23:22222222222222222222222222222222222222223333333333333333333333333333333333333333 24:PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP --:____.____|____.____|____.____|____.____|____.____|____.____|____.____|____.____| */ load('ansitex/load/windows.js'); // Our supporting window class require('sbbsdefs.js','SS_USERON'); // Need for our ANSI colors eg: BG_* require('ansitex/load/msgbases.js','MAX_PAGE_NUM'); // To read/write to message bases require('ansitex/load/session-ansitex.js','SESSION_ANSITEX'); // @todo Only load this if it is an ANSI page we are loading /** * This object represents a full page that interacts with the user * * @param service - Type of page (tex=ANSI, vtx=VIEWDATA) * @param debug - Whether to dump some debug information. This is an int, and will start debugging on line debug * @constructor * * Pages have the following attributes: * - dimensions - (string) representation of their width x height * - dynamic_fields - (array) Location of fields that are dynamically filled * - height - (Number) page height * - input_fields - (array) Location of fields that take input * - page - (string) Full page number (frame+index) * - width - (Number) page width * * Pages have the following settings: * - cost - (int) Cost to view the page * - page - (object) Frame/index of the page * - provider - (string) Name of the frame provider * - showHeader - (boolean) Whether to show the header when rendering the frame * - type - (TEX/VTX) Type of frame * * Pages have the following public functions * - build - Compile the frame for rendering * - display - Display the compiled frame * - import - Load a frame from a file source * - save - Save the frame to the msgbase * - load - Load the frame from the msgbase */ function Page(service,debug) { this.__window__ = { layout: undefined, // Window - Full page content header: undefined, // Window - Page Title provider: undefined, // Page provider (*) pagenum: undefined, // Our page number (*) cost: undefined, // Page cost (*) body: undefined, // Window - Page body }; this.__properties__ = { type: undefined, // Viewdata or ANSItex frame name: new PageObject, input_fields: [], // Array of our input fields dynamic_fields: [], // Array of our dynamic fields isAccessible: undefined, // Is this page visible to all users isPublic: undefined, // Is this page visible to public (not CUG) users }; this.__defaults__ = { attr: BG_BLACK|LIGHTGRAY, }; this.__compiled__ = { build: undefined, // Our page compiled content }; /* this.__settings__ = { pageable: false, // If the virtual window is larger that height (and width is the same) next page is the next content contenttitle: undefined, // Template (window) for 1st page (a) contentsubtitle: undefined, // Template (window) for subsequent pages (b..z) } */ /** * @todo borders for each window * @param service * @param debug */ function init(service,debug) { log(LOG_DEBUG,'- PAGE::init(): type ['+service+']'); switch (service) { case 'tex': this.__window__.layout = new Window(1,1,ANSI_FRAME_WIDTH,ANSI_FRAME_HEIGHT+1,'LAYOUT',this,debug); this.__window__.body = new Window(1,2,ANSI_FRAME_WIDTH,ANSI_FRAME_HEIGHT,'CONTENT',this.__window__.layout,debug); this.__window__.header = new Window(1,1,ANSI_FRAME_WIDTH,1,'HEADER',this.__window__.layout,debug); this.__window__.provider = new Window(1,1,ANSI_FRAME_PROVIDER_LENGTH,1,'PROVIDER',this.__window__.header,debug); this.__window__.pagenum = new Window(57,1,ANSI_FRAME_PAGE_LENGTH,1,'#',this.__window__.header,debug); this.__window__.cost = new Window(71,1,ANSI_FRAME_COST_LENGTH,1,'$',this.__window__.header,debug); break; case 'vtx': // @todo VTX hasnt been worked on at all - need at last a viewdata2attrs function this.__window__.layout = new Window(1,1,VIEWDATA_FRAME_WIDTH,VIEWDATA_FRAME_HEIGHT+1,'LAYOUT',this,debug); this.__window__.body = new Window(1,2,VIEWDATA_FRAME_WIDTH,VIEWDATA_FRAME_HEIGHT,'CONTENT',this.__window__.layout,debug) this.__window__.header = new Window(1,1,VIEWDATA_FRAME_WIDTH,1,'HEADER',this.__window__.layout,debug); this.__window__.provider = new Window(1,1,VIEWDATA_FRAME_PROVIDER_LENGTH,1,'PROVIDER',this.__window__.header,debug); this.__window__.pagenum = new Window(24,1,VIEWDATA_FRAME_PAGE_LENGTH,1,'#',this.__window__.header,debug); this.__window__.cost = new Window(35,1,VIEWDATA_FRAME_COST_LENGTH,1,'$',this.__window__.header,debug); break; default: throw new Error('INVALID Page Service: '+service); } this.service = service; } // @todo change this to Object.defineProperty() - see session.js /** * Determine if this frame is accessible to the current user */ Page.prototype.__defineGetter__('accessible',function() { log(LOG_DEBUG,'- Checking if user can access frame: '+this.name.name); log(LOG_DEBUG,' - User: '+JSON.stringify(user.number)); log(LOG_DEBUG,' - Frame Owner: '+JSON.stringify(this.owner)+', System Frame: '+(this.pageowner === SYSTEM_OWNER)); // user.number 0 is unidentified user. if (user.number) { return ( (this.__properties__.isAccessible && this.pageowner === SYSTEM_OWNER && ! this.__properties__.isPublic) || (this.__properties__.isAccessible && this.__properties__.isPublic) || (this.__properties__.isAccessible && ! this.__properties__.isPublic && this.isMember) || (pageEditor(this.name.frame)) ); } else { return (this.__properties__.isAccessible && this.pageowner === SYSTEM_OWNER && this.__properties__.isPublic); } }); Page.prototype.__defineSetter__('cost',function(int) { if (typeof int !== 'number') throw new Error('Cost must be a number'); switch (this.__properties__.service) { case 'tex': if ((''+int).length > ANSI_FRAME_COST_LENGTH-1) throw new Error('Cost too large'); this.__window__.cost.__properties__.content = anstoattrs(ESC+'[1;32m'+padright(int,ANSI_FRAME_COST_LENGTH-1,' ')+'c').content; break; case 'vtx': if ((''+int).length > VIEWDATA_FRAME_COST_LENGTH-2) throw new Error('Cost too large'); this.__window__.cost.__properties__.content = bintoattrs(VIEWDATA_BIN_GREEN+padright(int,VIEWDATA_FRAME_COST_LENGTH-2,' ')+'c').content; break; default: throw new Error(this.__properties__.service+' type not implemented'); } }); Page.prototype.__defineGetter__('dimensions',function() { return this.__properties__.width+' X '+this.__properties__.height; }); Page.prototype.__defineGetter__('dynamic_fields',function() { return this.__properties__.dynamic_fields; }); Page.prototype.__defineSetter__('dynamic_fields',function(array) { this.__properties__.dynamic_fields = array; }); Page.prototype.__defineGetter__('height',function() { return Number(this.__window__.layout.height); }); Page.prototype.__defineGetter__('input_fields',function() { return this.__properties__.input_fields; }); Page.prototype.__defineSetter__('input_fields',function(array) { this.__properties__.input_fields = array; }); Page.prototype.__defineSetter__('isAccessible',function(bool) { if ((typeof bool !== 'boolean') && (typeof bool !== 'number')) throw new Error('isAccessible must be a boolean'); this.__properties__.isAccessible = (bool === 1); }); Page.prototype.__defineSetter__('isPublic',function(bool) { if ((typeof bool !== 'boolean') && (typeof bool !== 'number')) throw new Error('isPublic must be a boolean'); this.__properties__.isPublic = (bool === 1); }); Page.prototype.__defineGetter__('name',function() { return this.__properties__.name; }); Page.prototype.__defineSetter__('name',function(object) { if (!(object instanceof PageObject)) throw new Error('Page must be PageObject'); this.__properties__.name = object; switch (this.__properties__.service) { case 'tex': if ((''+this.__properties__.name.frame).length > ANSI_FRAME_PAGE_LENGTH-1) throw new Error('Pagenum too large'); this.__window__.pagenum.__properties__.content = anstoattrs(ESC+'[1;37m'+this.__properties__.name.name).content; break; case 'vtx': if ((''+this.__properties__.name.frame).length > VIEWDATA_FRAME_PAGE_LENGTH-2) throw new Error('Pagenum too large'); this.__window__.pagenum.__properties__.content = bintoattrs(VIEWDATA_BIN_WHITE+this.__properties__.name.name).content; break; default: throw new Error(this.__properties__.service+' type not implemented'); } }); Page.prototype.__defineGetter__('pagenext',function() { return this.__properties__.name.next; }); /** * Determine who the owner of a page is */ Page.prototype.__defineGetter__('pageowner',function() { log(LOG_DEBUG,'Getting page owner for:'+this.__properties__.name.frame); return pageOwner(this.__properties__.name.frame).prefix; }); Page.prototype.__defineSetter__('provider',function(ansi) { var provider; switch (this.__properties__.service) { case 'tex': provider = anstoattrs(ansi+ESC+'[0m').content; if (provider[1].filter(function(child) { return child.ch; }).length-1 > ANSI_FRAME_PROVIDER_LENGTH) throw new Error('Provider too large'); break; case 'vtx': provider = bintoattrs(ansi).content; if (provider[1].length-1 > VIEWDATA_FRAME_PROVIDER_LENGTH) throw new Error('Provider too large'); break; default: throw new Error(this.__properties__.service+' not implemented'); } this.__window__.provider.__properties__.content = provider; }); Page.prototype.__defineGetter__('service',function() { return this.__properties__.service; }); Page.prototype.__defineSetter__('service',function(string) { if (this.__properties__.service) throw new Error('service already DEFINED'); if (['VTX','TEX'].indexOf(string) === undefined) throw new Error('Unknown SERVICE:'+string); return this.__properties__.service = string; }); Page.prototype.__defineSetter__('showHeader',function(bool) { if (typeof bool !== 'boolean') throw new Error('showHeader expected a true/false'); this.__window__.header.visible = bool; }); Page.prototype.__defineGetter__('width',function() { return Number(this.__window__.layout.width); }); /** * Build the screen layout * * @returns {*} */ this.build = function(force) { if (this.__compiled__.build && ! force) throw new Error('Refusing to build without force.'); this.build_system_fields(); this.__compiled__.build = this.__window__.layout.build(1,1,false); // Add our dynamic values var fields = this.dynamic_fields.filter(function(item) { return item.value !== undefined; }); // writeln('We have DF fields:'+fields.length); if (fields.length) insert_fields(fields,this.__compiled__.build); // Add our dynamic values fields = this.input_fields.filter(function(item) { return item.value !== undefined; }); // writeln('We have INPUT fields:'+fields.length); if (fields.length) insert_fields(fields,this.__compiled__.build); // Insert our *_field data (if it is set) function insert_fields(fields,build) { for (var i in fields) { // writeln('- adding:'+fields[i].name+', with value:'+fields[i].value); var content = fields[i].value.split(''); for (x=fields[i].x;x= 0) that.dynamic_field(field.name,atcode(field.name,field.length,field.pad,undefined)); }); } /** * Return the compiled screen as an array of lines * * @param last - the last attribute sent to the screen * @param color - whether to render the color attributes */ this.display = function(last,color) { var debug = false; if (! this.__compiled__.build) this.build(); // Our built display var display = this.__compiled__.build; // Default attribute when the screen is cleared var new_screen; // Default attribute when a new line is started var new_line; var result = []; var attr; switch (this.service) { case 'tex': new_screen = BG_BLACK|LIGHTGRAY; break; case 'vtx': new_screen = BG_BLACK|LIGHTGRAY; new_line = BG_BLACK|LIGHTGRAY; break; default: throw new Error(SESSION_EXT+' dump processing not implemented'); } if (last === undefined) last = new_screen; // Check all our dynamic fields have been placed df = this.dynamic_fields.filter(function(item) { return item.value === undefined; }); // If our dynamic fields havent been filled in if (df.length > 0) throw new Error('Dynamic fields without values:'+(df.map(function(item) { return item.name; }).join('|'))); // Render the display for (y=1;y<=this.height;y++) { var line = ''; if (new_line) last = new_line; if (debug) writeln('============== ['+y+'] ==============='); for (x=1;x<=this.width;x++) { if (debug) log(LOG_DEBUG,'* CELL : y:'+y+', x:'+x); // The current char value var char = (display[y] !== undefined && display[y][x] !== undefined) ? display[y][x] : undefined; if (debug) log(LOG_DEBUG,' - CHAR : '+(char !== undefined ? char.ch : 'undefined')+', ATTR:'+(char !== undefined ? char.attr : 'undefined')+', LAST:'+last); if (debug) { writeln(); writeln('-------- ['+x+'] ------'); writeln('y:'+y+',x:'+x+', attr:'+(char !== undefined ? char.attr : 'undefined')); } if ((color === undefined) || color) { // Only write a new attribute if it has changed (and not Videotex) if ((this.service === 'vtx') || (last === undefined) || (last !== char.attr)) { // The current attribute for this character attr = (char === undefined) ? undefined : char.attribute(last,this.service,debug); switch (this.service) { case 'tex': // If the attribute is null, we'll write our default attribute if (attr === null) line += this.attr else line += (attr !== undefined) ? attr : ''; break; case 'vtx': // If the attribute is null, we'll ignore it since we are drawing a character if ((attr !== undefined) && (attr !== null)) { if (debug) log(LOG_DEBUG,' = SEND ATTR :'+attr+', attr length:'+attr.length+', last:'+last); line += attr; } break; default: throw new Error('service type:'+this.service+' hasnt been implemented.'); } } // For no-color output and ViewData, we'll render a character } else { if ((this.service === 'vtx') && char.attr) line += '^'; } if (char.ch !== undefined) { if (debug) log(LOG_DEBUG,' = SEND CHAR :'+char.ch+', attr:'+char.attr+', last:'+last); line += (char.ch !== null) ? char.ch : ''; } else { if (debug) log(LOG_DEBUG,' = CHAR UNDEFINED'); line += ' '; } last = (char.attr === undefined) ? undefined : char.attr; } result.push(line); if (debug && (y > debug)) exit(1); } return result; } /** * Dump a page in an axis grid to view that it renders correctly * * @param last - (int) The current cursor color * @param color - (bool) Optionally show color * @param debug - (int) Debugging mode starting at line * * @note When drawing a Char: * * | CH | ATTR | RESULT | * |------------|------------|--------------------------------------| * | undefined | undefined | no output (cursor advances 1) | NOOP * | null | undefined | invalid | * | defined | undefined | invalid | * | undefined | null | invalid | * | null | null | invalid | * | defined | null | render ch only (cursor advances 1) | Viewdata * | undefined | defined | render attr only (no cursor move) | ANSItex (used to close the edge of a window) * | null | defined | render attr only (cursor advances 1) | Viewdata * | defined | defined | render attr + ch (cursor advances 1) | ANSItex * |------------|------------|--------------------------------------| * * + for ANSItex, attribute(s) dont advance the cursor, clear screen sets the default to BG_BLACK|LIGHTGRAY * + for ViewData, an attribute does advance the cursor, and each attribute advances the cursor, also each new line starts with a default BG_BLACK|WHITE */ this.dump = function(last,color,debug) { if (! this.__compiled__.build) this.build(); // Our built display var display = this.__compiled__.build; color = (color === undefined) || (color === '1') || (color === true); writeln('Dumping Page:'+this.name.name); writeln('= Size :'+this.dimensions); writeln('- Last :'+last); writeln('- Color:'+color); writeln('- Debug:'+debug); if (last === undefined) last = new_screen; if (debug) { writeln('==== content dump ===='); var yy = 1; for (var y in display) { write(padright(yy,2,0)+':'); var xx = 1; for (var x in display[y]) { if (debug && (y === debug)) { writeln(JSON.stringify(display[y][x])); writeln() } write('['); if (display[y][x].attr === undefined) { // NOOP } else if (display[y][x].attr === null) { // NOOP } else { try { write((last === display[y][x].attr) ? '' : display[y][x].attr); } catch (e) { writeln(); writeln('error:'+e); writeln(' y:'+y); writeln(' x:'+x); writeln(JSON.stringify(display[y][x].attr)); exit(1); } } write(':'); if (display[y][x].ch === undefined) { // NOOP - No window filled a character at this location write((display[y][x].attr === undefined) ? '--' : ''); } else if (display[y][x].ch === null) { // NOOP } else { write('_'+display[y][x].ch); } write(']'); last = display[y][x].attr; xx++; } writeln('|'+padright(xx-1,2,0)); xx = 0; yy++; } // Detail dump when debug is a line number if (debug && (y > debug)) { writeln('=========================='); for (var y in display) { writeln ('------ ['+y+'] -------'); var xx = 1; for (var x in display[y]) { var attr = display[y][x].attr; writeln('X:'+(xx++)+'|'+attr+':'+display[y][x].ch+'|'+display[y][x].attribute(last,this.service,debug)); // Only write a new attribute if it has changed if ((this.last === undefined) || (this.last !== attr)) { this.last = attr; } } } } writeln('==== END content dump ===='); } // Dump Header write('--:'); for (x=0;x 0) this.putmsg(lines.shift()+"\r\n"); break; */ default: throw new Error('Unsupported filetype:'+ext); } // Successful load return true; } /** * After page load routines */ this.loadcomplete = function() { var po = pageOwner(this.name.frame); switch (this.service) { case 'tex': this.__window__.pagenum.__properties__.content = anstoattrs(ESC+'[1;37m'+this.name.name).content; this.provider = base64_decode(po.logoans); break; case 'vtx': this.__window__.pagenum.__properties__.content = bintoattrs(VIEWDATA_BIN_WHITE+this.name.name).content; this.provider = base64_decode(po.logovtx); break; default: throw new Error(this.service+' hasnt been implemented'); } // Dont show header on un-authed login frames if (! user.number) this.showHeader = false; } /** * Save the frame for later retrieval * @todo Inject back all input_fields and dynamic_fields * @todo this is not complete? */ this.save = function() { var line; // If we have any input fields, we need to insert them back inside ESC .... ESC \ control codes // @todo avoid the ending ESC \ with a control code. this.input_fields.filter(function(child) { if (child.y === y) { line.content = line.content.substring(0,child.x-1) + 'FIELD:'+child.name + line.content.substring(child.x+child.length,80) + 'END'; } }) // We draw line by line. for (var y=1;y<=this.height;y++) { // Line intro write('\x1b[0m'); line = this.__window__.layout.drawline(1,this.width,y,false); write(line.content); write('\x1b[0m'); writeln(); } } init.apply(this,arguments); } function PageObject(frame,index) { this.__properties__ = { frame: '0', // Frame number index: 'a', // Frame index } function init(frame,index) { if (typeof frame === 'object') { this.__properties__.frame = frame.frame; this.index = frame.index; } else { this.__properties__.frame = frame; if (index) this.index = index; } } init.apply(this,arguments); PageObject.prototype.__defineGetter__('frame',function() { return this.__properties__.frame; }); // @todo validate that string only has digits PageObject.prototype.__defineSetter__('frame',function(string) { if (typeof string !== 'string') throw new Error('Page.number must be a string'); this.__properties__.frame = string; }); PageObject.prototype.__defineGetter__('index',function() { return this.__properties__.index; }); PageObject.prototype.__defineSetter__('index',function(string) { if (typeof string !== 'string') throw new Error('Page.index must be a string'); if (string.length !== 1) throw new Error('Page.index can only be 1 char'); this.__properties__.index = string; }); PageObject.prototype.__defineGetter__('name',function() { return (this.frame && this.index) ? this.frame+this.index : null; }); PageObject.prototype.__defineGetter__('next',function() { var next = undefined; if (this.index !== 'z') { log(LOG_DEBUG,'page_next: Current page:'+this.frame+', current index:'+this.index); next = new PageObject(this.frame,String.fromCharCode(this.index.charCodeAt(0)+1)); } return next; }); }