'use strict'; /** 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() // 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 load('ansitex/load/msgbases.js'); // To read/write to message bases require('sbbsdefs.js','SS_USERON'); // Need for our ANSI colors eg: BG_* /** * 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(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__ = { name: new PageObject, //type: undefined, // Frame type (deprecated, see attrs) attrs: 0, // (Type 0, CUG 0 (public), Hidden=false) input_fields: [], // Array of our input fields dynamic_fields: [], // Array of our dynamic fields //isAccessible: undefined, // Is this page visible to all users (deprecated, see attrs) // If FALSE, only the SP can view/edit the frame //isPublic: undefined, // Is this page visible to public (not CUG) users (deprecated, see attrs) // If FALSE user must be a member of the CUG to view the frame // All users, including unauthenticated, are members of 'system' (owner = 0) // Frame's owned by the system where: // isPublic is FALSE - the user must be logged in to view it // isPublic is TRUE - can be viewed by non-logged in users // Frame's owned by Service Providers where: // isPublic is FALSE - can only be viewed if a user is // a member of the Service Providers CUG // isPublic is TRUE - can be viewed by users (if logged in) key: [], // Key actions raw: {}, // Page raw content for each session type }; 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(debug) { log(LOG_DEBUG,'- PAGE::init(): type ['+SESSION_EXT+']'); this.__window__.layout = new Window(1,1,FRAME_WIDTH,FRAME_HEIGHT+1,'LAYOUT',this,debug); this.__window__.body = new Window(1,2,FRAME_WIDTH,FRAME_HEIGHT,'CONTENT',this.__window__.layout,debug); this.__window__.header = new Window(1,1,FRAME_WIDTH,1,'HEADER',this.__window__.layout,debug); this.__window__.provider = new Window(1,1,FRAME_PROVIDER_LENGTH,1,'PROVIDER',this.__window__.header,debug); switch (SESSION_EXT) { case 'tex': require('ansitex/load/session/ansitex.js','SESSION_ANSITEX'); this.__window__.pagenum = new Window(57,1,FRAME_PAGE_LENGTH,1,'#',this.__window__.header,debug); this.__window__.cost = new Window(71,1,FRAME_COST_LENGTH,1,'$',this.__window__.header,debug); break; case 'vtx': require('ansitex/load/session/viewdata.js','SESSION_VIEWDATA'); this.__window__.pagenum = new Window(24,1,FRAME_PAGE_LENGTH,1,'#',this.__window__.header,debug); this.__window__.cost = new Window(35,1,FRAME_COST_LENGTH,1,'$',this.__window__.header,debug); break; default: throw new Error('INVALID Page Service: '+SESSION_EXT); } } /** * Determine if this frame is accessible to the current user */ Object.defineProperty(this,'accessible',{ get: function() { log(LOG_DEBUG,'- Checking if user ['+user.number+'] can access frame: '+this.name.toString()); log(LOG_DEBUG,'|* Frame Owner: '+this.owner+', System Frame: '+this.isSystemPage); log(LOG_DEBUG,'|* Accessible: '+this.isAccessible); log(LOG_DEBUG,'|* Public: '+this.isPublic); log(LOG_DEBUG,'|* Member: '+this.isMember); // user.number 0 is unidentified user. if (user.number) { return ( (this.isAccessible && this.isSystemPage && ! this.isPublic) || (this.isAccessible && this.isPublic) || (this.isAccessible && ! this.isPublic && this.isMember) || (pageEditor(this.name.frame)) ); } else { return this.isAccessible && this.isSystemPage && this.isPublic; } } }); // The page attributes, which determine accessible, public, cug and frame type Object.defineProperty(this,'attrs',{ get: function() { return this.__properties__.attrs; }, /* Frame attributes are bit items, bits: * 1-4 TYPE * 5-7 CUG * 8 accessible */ set: function(int) { if (int === undefined) int = 0; this.__properties__.attrs = int; } }); Object.defineProperty(this,'cost',{ get: function() { return Number(this.__properties__.cost); }, set: function(int) { if (int === undefined) int = 0; if (typeof int !== 'number') throw new Error('Cost must be a number'); this.__properties__.cost = int; if ((''+int).length > FRAME_COST_LENGTH-1-FRAME_ATTR_LENGTH) throw new Error('Cost too large'); // Populate the cost window switch (SESSION_EXT) { case 'tex': this.__window__.cost.__properties__.content = rawtoattrs(ESC+'[1;32m'+padright(int,FRAME_COST_LENGTH-1-FRAME_ATTR_LENGTH,' ')+FRAME_COSTUNIT).content; break; case 'vtx': this.__window__.cost.__properties__.content = rawtoattrs(VIEWDATA_BIN_GREEN+padright(int,FRAME_COST_LENGTH-1-FRAME_ATTR_LENGTH,' ')+FRAME_COSTUNIT).content; break; default: throw new Error(SESSION_EXT+' type not implemented'); } } }); Object.defineProperty(this,'cug',{ get: function() { return ((this.__properties__.attrs >> 4) & 0x7); } }); Page.prototype.__defineGetter__('dimensions',function() { return this.__properties__.width+' X '+this.__properties__.height; }); Page.prototype.__defineGetter__('dynamic_fields',function() { return this.__properties__.dynamic_fields === undefined ? [] : 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; }); Object.defineProperty(this,'isAccessible',{ get: function() { return (this.__properties__.attrs >> 7 !== 1); } }); /** * Check if the user is already a member of the CUG */ Object.defineProperty(this,'isMember',{ get: function() { // @todo Implement CUGs, this would be "=== SERVICE_PROVIDER" and user is a member of SERVICE_PROVIDER return (user.number > 0) && this.isSystemPage; } }); // Is the page public or CUG Object.defineProperty(this,'isPublic',{ get: function() { return (this.cug === 0); } }); // Is this a system page Object.defineProperty(this,'isSystemPage',{ get: function() { return (this.owner === SYSTEM_OWNER); } }); // Key Array Page.prototype.__defineGetter__('key',function() { return this.__properties__.key; }); Page.prototype.__defineSetter__('key',function(array) { if (typeof array !== 'object') throw new Error('key must be an array :'+typeof array); if (array.length !== 10) throw new Error('key must contain 10 items :'+array); return this.__properties__.key = array; }); 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; if ((''+this.__properties__.name.frame).length > FRAME_PAGE_LENGTH-1-FRAME_ATTR_LENGTH) throw new Error('Pagenum too large'); switch (SESSION_EXT) { case 'tex': this.__window__.pagenum.__properties__.content = rawtoattrs(ESC+'[1;37m'+this.__properties__.name.toString()).content; break; case 'vtx': this.__window__.pagenum.__properties__.content = rawtoattrs(VIEWDATA_BIN_WHITE+this.__properties__.name.toString()).content; break; default: throw new Error(SESSION_EXT+' type not implemented'); } }); // Display who owns the page Object.defineProperty(this,'owner',{ get: function() { return pageOwner(this.__properties__.name.frame).prefix; }, }); Page.prototype.__defineGetter__('pagenext',function() { return this.__properties__.name.next; }); /** * Determine who the owner of a page is * @deprecated use owner */ Page.prototype.__defineGetter__('pageowner',function() { return pageOwner(this.__properties__.name.frame).prefix; }); Page.prototype.__defineSetter__('provider',function(ansi) { var provider; switch (SESSION_EXT) { case 'tex': provider = rawtoattrs(ansi+ESC+'[0m').content; if (provider[1].filter(function(child) { return child.ch; }).length-1 > FRAME_PROVIDER_LENGTH) throw new Error('Provider too large'); break; case 'vtx': provider = rawtoattrs(ansi).content; if (provider[1].length-1 > FRAME_PROVIDER_LENGTH) throw new Error('Provider too large'); break; default: throw new Error(SESSION_EXT+' not implemented'); } this.__window__.provider.__properties__.content = provider; }); Object.defineProperty(this,'raw',{ get: function() { return this.__properties__.raw }, set: function(value) { this.__properties__.raw[SESSION_EXT] = value; } }); Page.prototype.__defineSetter__('showHeader',function(bool) { if (typeof bool !== 'boolean') throw new Error('showHeader expected a true/false'); this.__window__.header.visible = bool; }); Object.defineProperty(this,'type',{ get: function() { return this.__properties__.attrs & 0xf; }, }); Page.prototype.__defineGetter__('type',function() { return this.__properties__.type; }); Page.prototype.__defineSetter__('type',function(string) { if (typeof string !== 'string') throw new Error('type must be an string :'+typeof string); return this.__properties__.type = string; }); Page.prototype.__defineGetter__('width',function() { return Number(this.__window__.layout.width); }); /** * Build the screen layout * * @returns {*} */ this.build = function(force) { log(LOG_DEBUG,'* Building frame...'); 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; }); log(LOG_DEBUG,'|* 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; }); log(LOG_DEBUG,'|* 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 (var x=fields[i].x;x 0) throw new Error('Dynamic fields ['+df.length+'] without values:'+(df.map(function(item) { return item.name; }).join('|'))); // Render the display for (var y=1;y<=this.height;y++) { var line = ''; if (new_line) last = new_line; if (debug) writeln('============== ['+y+'] ==============='); for (var 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 ((SESSION_EXT === 'vtx') || (last === undefined) || (last !== char.attr)) { // The current attribute for this character attr = (char === undefined) ? undefined : char.attribute(last,SESSION_EXT,debug); switch (SESSION_EXT) { 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:'+SESSION_EXT+' hasnt been implemented.'); } } // For no-color output and ViewData, we'll render a character } else { if ((SESSION_EXT === '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.toString()); 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,SESSION_EXT,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 (var x=0;x