sbbs/load/page.js

1497 lines
43 KiB
JavaScript

'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
require('ansitex/load/msgbases.js','PAGE_LENGTH'); // 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: undefined, // Page raw content
};
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;
});
Page.prototype.__defineGetter__('raw',function() {
return this.__properties__.raw;
});
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<fields[i].x+Math.abs(fields[i].length);x++) {
var index = x-fields[i].x;
if (content[index])
build[fields[i].y][x].ch = fields[i].type !== FIELD_PASSWORD ? content[index] : FIELD_PASSWORD_MASK;
}
}
}
}
/**
* Build in any input_fields with values
*/
this.build_input_fields = function() {
var that = this;
var f = this.input_fields.filter(function(item) { return item.value.length; });
log(LOG_DEBUG,'* INPUT_FIELDS WITH VALUES:'+f.length);
if (f.length) {
f.forEach(function(field) {
that.input_field(field.name,field.value);
});
this.__compiled__.build = null;
}
}
/**
* Build in our dynamic_fields that can be populated automatically
*/
this.build_system_fields = function(context) {
var that = this;
var f = this.dynamic_fields.filter(function(item) { return item.value === undefined; });
if (f.length) {
f.forEach(function(field) {
that.dynamic_field(field.name,atcode(field.name,field.length,field.pad,context));
});
}
}
/**
* 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;
new_screen = BG_BLACK|LIGHTGRAY;
switch (SESSION_EXT) {
case 'tex':
break;
case 'vtx':
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
var 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 ['+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<this.width;x+=10) {
write('_'.repeat(4)+'.'+'_'.repeat(4)+'|');
}
writeln();
var result = this.display(last,color);
// We draw line by line.
for (var y=1;y<=this.height;y++) {
// Line intro
if (color)
write('\x1b[0m');
write(padright(y,2,0)+':');
writeln(result[y-1]);
}
// Dump Header
write('--:');
for (var x=0;x<this.width;x+=10) {
write('_'.repeat(4)+'.'+'_'.repeat(4)+'|');
}
writeln();
if (this.input_fields.length) {
writeln('= Input Fields:')
this.input_fields.forEach(function(x) {
writeln(' - '+x.name+', type:'+x.type+', length:'+x.length+', value:'+x.value);
})
}
if (this.dynamic_fields.length) {
writeln('= Dynamic Fields:')
this.dynamic_fields.forEach(function(x) {
writeln(' - '+x.name+', length:'+Math.abs(x.length)+', pad:'+x.pad+', value:'+x.value);
})
}
// Reset our color
if (color)
write('\x1b[0m');
}
/**
* Set the value for a dynamic field
*
* @param field
* @param value
*/
this.dynamic_field = function(field,value) {
var fields = this.dynamic_fields.filter(function(item) { return item.name === field; });
if (fields.length !== 1)
throw new Error('Dynamic field: '+field+', doesnt exist?');
// Store our value
this.dynamic_fields[this.dynamic_fields.indexOf(fields[0])].value = value;
}
/**
* Save the frame for later retrieval
* @todo Inject back all input_fields and dynamic_fields
* @todo this is not complete?
*/
this.export = 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();
}
}
/**
* Load a specific page
*
* @param page
*/
this.get = function(page) {
if (!(page instanceof PageObject))
throw new Error('page must be a PageObject');
this.name = page;
// Load a page from disk first if it exists
if (FRAMES_MSG_FILES && this.import(FRAMES_HOME+page.toString(),SESSION_EXT))
return true;
// Fall back to loading from msgbase
return FRAMES_MSG_BASE ? this.load(page) : false;
}
/**
* Set the value for an input field
*
* @param field
* @param value
*/
this.input_field = function(field,value) {
var fields = this.input_fields.filter(function(item) { return item.name === field; });
if (fields.length !== 1)
throw new Error('Input field: '+field+', doesnt exist?');
// Store our value
this.input_fields[this.input_fields.indexOf(fields[0])].value = value;
}
/**
* Load a frame from a file
*
* @param filename - Name of file to load page from (SBBS default dir is CTRL, so relative to it)
* @param width - Width to build window (not required for ANS)
* @param height - Height to build window (not required for ANS)
* @returns {boolean}
* @todo Dont allow load() to load a Viewdata frame for an ANSItex session and visa-versa.
*/
this.import = function(filename,ext,width,height) {
log(LOG_DEBUG,'|- Importing frame: ['+filename+']');
var f = new File(filename);
if (! f.exists || ! f.open('rb',true)) {
log(LOG_ERROR,'|? File doesnt exist: ['+filename+']');
return null;
}
var contents = f.read();
f.close();
var valid_sauce = false;
if (contents.substr(-128, 7) === 'SAUCE00') {
ext = file_getext(filename).substr(1).toLowerCase();
var sauceless_size = ascii(contents.substr(-35,1));
sauceless_size <<= 8;
sauceless_size |= ascii(contents.substr(-36,1));
sauceless_size <<= 8;
sauceless_size |= ascii(contents.substr(-37,1));
sauceless_size <<= 8;
sauceless_size |= ascii(contents.substr(-38,1));
var data_type = ascii(contents.substr(-34,1));
var file_type = ascii(contents.substr(-33,1));
var tinfo1 = ascii(contents.substr(-31,1));
tinfo1 <<= 8;
tinfo1 |= ascii(contents.substr(-32,1));
var tinfo2 = ascii(contents.substr(-29,1));
tinfo2 <<= 8;
tinfo2 |= ascii(contents.substr(-30,1));
switch(data_type) {
case 1:
switch(file_type) {
// Plain ASCII
case 0:
ext = 'TXT';
if (tinfo1)
width = tinfo1;
if (tinfo2)
height = tinfo2;
break;
// ANSI
case 1:
ext = 'ANS';
if (tinfo1)
width = tinfo1;
if (tinfo2)
height = tinfo2;
break;
// Source
case 7:
ext = 'TXT';
break;
}
valid_sauce = true;
break;
case 5:
ext = 'BIN';
width = file_type * 2;
height = (sauceless_size / 2) / width;
valid_sauce = true;
break;
}
if (valid_sauce)
contents = contents.substr(0, sauceless_size);
}
return this.preload((['vtx','tex'].indexOf(ext) !== -1) ? JSON.parse(contents) : contents,ext,width,height);
}
this.load = function(page) {
var headers;
var mb = new MsgBase(FRAMES_MSG_BASE);
try {
if (mb.open()) {
headers = mb.get_all_msg_headers(false,false) || [];
} else {
log(LOG_ERROR,'! ['+FRAMES_MSG_BASE+'] cannot be opened ['+mb.error+']');
return false;
}
// @todo It appears if the message base doesnt exist, we dont error?
} catch (e) {
log(LOG_ERROR,'! ['+FRAMES_MSG_BASE+'] cannot be opened ['+e.message+']');
return false;
}
var msg;
// Find existing message with the page number
for (var x in headers) {
if ((!(headers[x].attr&MSG_DELETE)) && (headers[x].to === page.toString()) && (headers[x].from === SESSION_EXT)) {
msg = headers[x];
//break; @todo We'll take the last one that matches, if there are more than one.
// @todo In the case of frames coming via FTN packets, we are not currently deleting old entries
}
}
if (msg === undefined) {
log(LOG_DEBUG,'|- Frame not found: ['+page.toString()+'] in ['+FRAMES_MSG_BASE+']');
return false;
} else {
log(LOG_DEBUG,'|- Loading frame: ['+page.toString()+'] from msgbase ['+msg.number+']');
var contents = mb.get_msg_body(false,msg.number,false,false,true,true).split("\r\n");
var i;
for (var i=0; i<contents.length; i++) {
// Echomail tag line
if (contents[i] === '---' || contents[i].substring(0,4) === '--- ')
break;
}
contents.length = i;
try {
var result = JSON.parse(contents.join(''));
} catch(e) {
alert('Error ' + e + ' parsing JSON');
}
return this.preload(result,SESSION_EXT);
}
return false;
}
/**
* After page load routines
*/
this.loadcomplete = function() {
var po = pageOwner(this.name.frame);
switch (SESSION_EXT) {
case 'tex':
this.__window__.pagenum.__properties__.content = rawtoattrs(ESC+'[1;37m'+this.name.toString()).content;
this.provider = base64_decode(po.logoans);
break;
case 'vtx':
this.__window__.pagenum.__properties__.content = rawtoattrs(VIEWDATA_BIN_WHITE+this.name.toString()).content;
this.provider = base64_decode(po.logovtx);
break;
default:
throw new Error(SESSION_EXT+' hasnt been implemented');
}
// Dont show header on un-authed login frames
if (! user.number)
this.showHeader = false;
}
/**
* Process a loaded frame from either a file or message base
*
* @param contents
* @param ext
* @param width
* @param height
* @returns {boolean|null}
*/
this.preload = function(contents,ext,width,height) {
switch (ext) {
// Messages
case 'txt':
log(LOG_DEBUG,'Processing txt');
var page = rawtoattrs(contents,this.width,this.__window__.body.y,this.__window__.body.x,debug);
this.__window__.body.__properties__.content = page.content;
this.__properties__.raw = contents;
// ANSI files
case 'ans':
// ViewData files
case 'bin':
log(LOG_DEBUG,'Processing ANSI/VIEWDATA file');
var page = rawtoattrs(contents,this.width,this.__window__.body.y,this.__window__.body.x,debug);
this.__window__.body.__properties__.content = page.content;
this.dynamic_fields = page.dynamic_fields;
// Our fields are sorted in x descending order
this.input_fields = page.input_fields.sort(function(a,b) { return a.x < b.x ? 1 : -1; });
this.__properties__.raw = contents;
break;
// ANSItex files
case 'tex':
case 'vtx':
log(LOG_DEBUG,'|-- Processing FRAME file. V:'+contents.version);
switch (contents.version) {
case 1:
try {
for (var index in contents) {
if (FRAME_SAVE_ATTRS.indexOf(index) === -1) {
log(LOG_ERROR,'|-! Unknown index ['+index+'] in input.');
continue;
}
log(LOG_DEBUG,'|-* Processing ['+index+'] with value ['+JSON.stringify(contents[index])+'].');
switch (index) {
case 'content':
//if (ext === 'tex')
// var page = rawtoattrs(base64_decode(contents[index]).replace("\x0a\x0d\x0a\x0d","\x0a\x0d"),this.width,this.__window__.body.y,this.__window__.body.x);
//else if (ext === 'vtx')
var page = rawtoattrs(base64_decode(contents[index]),this.width,this.__window__.body.y,this.__window__.body.x);
this.__window__.body.__properties__.content = page.content;
this.dynamic_fields = page.dynamic_fields;
// Our fields are sorted in x descending order
if (page.input_fields.length)
this.input_fields = page.input_fields.sort(function(a,b) { return a.x < b.x ? 1 : -1; });
this.__properties__.raw = base64_decode(contents[index]);
break;
case 'cost':
this.cost = contents[index];
break;
case 'date':
log(LOG_INFO,'|-/ Frame date : '+contents[index]);
break;
case 'dynamic_fields':
this.dynamic_fields = contents[index];
break;
case 'frame':
this.name.frame = ''+contents[index];
break;
case 'index':
this.name.index = contents[index];
break;
case 'input_fields':
this.input_fields = contents[index];
break;
case 'isAccessible':
this.isAccessible = ((contents[index] === 1) || (contents[index] === true));
break;
case 'isPublic':
this.isPublic = ((contents[index] === 1) || (contents[index] === true));
break;
case 'key':
this.key = contents[index];
break;
case 'type':
this.type = contents[index];
break;
case 'version':
log(LOG_INFO,'|-/ Frame version : '+contents[index]);
break;
case 'window':
for (var y in contents[index]) {
//log(LOG_DEBUG,' - Y: '+y+', '+JSON.stringify(contents[index][y]));
if (contents[index][y] === null)
continue;
for (var x in contents[index][y]) {
//log(LOG_DEBUG,' - X: '+x+', '+JSON.stringify(contents[index][y][x]));
if (contents[index][y][x] === null)
continue;
this.__window__.body.__properties__.content[y][x] = new Char(
contents[index][y][x].__properties__.ch,
contents[index][y][x].__properties__.attr
);
}
}
break;
default:
log(LOG_ERROR,'|-! Frame property not handled: '+index+', value:'+contents[index]);
}
}
} catch (error) {
log(LOG_ERROR,'|-! Frame error : '+error);
// Load our system error frame.
// @todo If our system error page errors, then we go into a loop
this.get(new PageObject(FRAME_SYSTEM_ERROR));
}
break;
case 2:
// Load the page content
var content_file = FRAMES_HOME+SESSION_EXT+'/'+this.name.toString()+'.'+CONTENT_EXT;
log(LOG_DEBUG,'|-- Importing frame content: ['+content_file+']');
var f = new File(content_file);
if (! f.exists || ! f.open('rb',true)) {
log(LOG_ERROR,'|? File doesnt exist: ['+content_file+']');
this.get(new PageObject(FRAME_SYSTEM_ERROR));
break;
}
this.__properties__.raw = f.read();
f.close();
var page = rawtoattrs(this.__properties__.raw,this.width,this.__window__.body.y,this.__window__.body.x);
this.__window__.body.__properties__.content = page.content;
this.dynamic_fields = page.dynamic_fields;
// Our fields are sorted in x descending order
if (page.input_fields.length)
this.input_fields = page.input_fields.sort(function(a,b) { return a.x < b.x ? 1 : -1; });
// Work out frame type
this.attrs = contents.attrs;
this.cost = contents.cost;
this.key = contents.key;
break;
default:
writeln('here');
log(LOG_ERROR,'|-! Unknown frame version : '+contents.version);
this.get(new PageObject(FRAME_SYSTEM_ERROR));
return null;
}
this.loadcomplete();
log(LOG_DEBUG,'|= Frame complete : '+this.name.toString());
break;
default:
throw new Error('Unsupported filetype:'+ext);
}
// Successful load
return true;
}
/**
* Save the frame to the message base
*/
this.save = function() {
var mb = new MsgBase(FRAMES_MSG_BASE);
var headers;
try {
if (mb.open()) {
headers = mb.get_all_msg_headers(false,false) || [];
} else {
log(LOG_ERROR,FRAMES_MSG_BASE+' cannot be opened:'+mb.error);
return;
}
} catch (e) {
log(LOG_ERROR,FRAMES_MSG_BASE+' cannot be opened:'+e.message);
return;
}
// Build the save content
var content = {};
for (var index in FRAME_SAVE_ATTRS) {
switch (FRAME_SAVE_ATTRS[index]) {
case 'cost':
content[FRAME_SAVE_ATTRS[index]] = this.cost;
break;
case 'dynamic_fields':
content[FRAME_SAVE_ATTRS[index]] = this.dynamic_fields;
break;
case 'frame':
content[FRAME_SAVE_ATTRS[index]] = this.name.frame;
break;
case 'index':
content[FRAME_SAVE_ATTRS[index]] = this.name.index;
break;
case 'input_fields':
content[FRAME_SAVE_ATTRS[index]] = this.input_fields;
break;
case 'isAccessible':
content[FRAME_SAVE_ATTRS[index]] = this.__properties__.isAccessible;
break;
case 'isPublic':
content[FRAME_SAVE_ATTRS[index]] = this.__properties__.isPublic;
break;
case 'key':
content[FRAME_SAVE_ATTRS[index]] = this.key;
break;
case 'type':
content[FRAME_SAVE_ATTRS[index]] = this.type;
break;
case 'version':
content[FRAME_SAVE_ATTRS[index]] = 1;
break;
case 'window':
content[FRAME_SAVE_ATTRS[index]] = this.__window__.body.__properties__.content;
break;
default:
log(LOG_ERROR,' ! NOTE Index ['+FRAME_SAVE_ATTRS[index]+'] has been ignored.');
continue;
}
log(LOG_DEBUG,' / Storing ['+FRAME_SAVE_ATTRS[index]+'] with value:'+content[FRAME_SAVE_ATTRS[index]]);
}
// Find existing message with the page number and delete it if defined
var msg;
for (var x in headers) {
if ((headers[x].tags === this.name.toString()) && (!(headers[x].attr&MSG_DELETE))) {
msg = headers[x];
break;
}
}
if (msg === undefined) {
log(LOG_DEBUG,' - Saving NEW frame: ['+this.name.toString()+'] to ['+FRAMES_MSG_BASE+']');
} else {
log(LOG_DEBUG,' - REPLACING frame: ['+this.name.toString()+'] at ['+msg.number+']');
if (! mb.remove_msg(msg.number))
log(LOG_ERROR,' ! Error removing frame: ['+this.name.toString()+'] to ['+msg.number+']');
}
log(LOG_DEBUG,'** Save frame with keys'+JSON.stringify(Object.keys(content)));
if (! mb.save_msg(
{
subject: this.name.toString(),
to: this.name.toString(),
from: SESSION_EXT,
tags: this.name.toString(),
},
JSON.stringify(content)
))
log(LOG_ERROR,' ! Error saving frame: ['+this.name.toString()+']');
mb.close();
}
this.scroll = function(x,y) {
this.__compiled__.build = null;
// @todo Check that we can scroll and if we are out of bounds.
this.__window__.body.scroll(x,y);
}
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.toString();
this.index = frame.index;
} else if ((frame !== undefined) && (index === undefined)) {
if (/^\d+[a-z]$/.test(frame)) {
var split = frame.split(/(\d+)/);
this.__properties__.frame = split[1];
this.index = split[2];
} else {
this.__properties__.frame = frame;
}
} else if ((frame !== undefined) && (index !== undefined)) {
this.__properties__.frame = frame;
this.index = index;
}
}
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__('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;
});
PageObject.prototype.toString = function() {
return (this.frame && this.index) ? this.frame+this.index : null;
}
init.apply(this,arguments);
}