1099 lines
34 KiB
JavaScript
1099 lines
34 KiB
JavaScript
|
/**
|
||
|
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)
|
||
|
|
||
|
= When scrolling is disabled, and the canvas is greater than the window, then "nextpage" returns the next frame
|
||
|
= Pageable windows cannot have children, only "CONTENT" is paged
|
||
|
= 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');
|
||
|
|
||
|
/**
|
||
|
* 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
|
||
|
* - load - Load a frame from a file source
|
||
|
*/
|
||
|
function Page(service,debug) {
|
||
|
this.__properties__ = {
|
||
|
type: undefined, // Viewdata or ANSItex frame
|
||
|
name: new PageObject,
|
||
|
|
||
|
layout: undefined, // Window - Full page content
|
||
|
header: undefined, // Window - Page Title
|
||
|
|
||
|
provider: undefined, // Page provider (*)
|
||
|
pagenum: undefined, // Our page number (*)
|
||
|
cost: undefined, // Page cost (*)
|
||
|
|
||
|
content: undefined, // Window - Page Content
|
||
|
|
||
|
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,
|
||
|
};
|
||
|
|
||
|
/*
|
||
|
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.__properties__.layout = new Window(1,1,ANSI_FRAME_WIDTH,ANSI_FRAME_HEIGHT+1,'LAYOUT',this,debug);
|
||
|
this.__properties__.content = new Window(1,2,ANSI_FRAME_WIDTH,ANSI_FRAME_HEIGHT,'CONTENT',this.__properties__.layout,debug);
|
||
|
|
||
|
this.__properties__.header = new Window(1,1,ANSI_FRAME_WIDTH,1,'HEADER',this.__properties__.layout,debug);
|
||
|
this.__properties__.provider = new Window(1,1,ANSI_FRAME_PROVIDER_LENGTH,1,'PROVIDER',this.__properties__.header,debug);
|
||
|
this.__properties__.pagenum = new Window(57,1,ANSI_FRAME_PAGE_LENGTH,1,'#',this.__properties__.header,debug);
|
||
|
this.__properties__.cost = new Window(71,1,ANSI_FRAME_COST_LENGTH,1,'$',this.__properties__.header,debug);
|
||
|
|
||
|
break;
|
||
|
|
||
|
case 'vtx':
|
||
|
// @todo VTX hasnt been worked on at all - need at last a viewdata2attrs function
|
||
|
this.__properties__.layout = new Window(1,1,VIEWDATA_FRAME_WIDTH,VIEWDATA_FRAME_HEIGHT+1,'LAYOUT',this,debug);
|
||
|
this.__properties__.content = new Window(1,2,VIEWDATA_FRAME_WIDTH,VIEWDATA_FRAME_HEIGHT,'CONTENT',this.__properties__.layout,debug)
|
||
|
|
||
|
this.__properties__.header = new Window(1,1,VIEWDATA_FRAME_WIDTH,1,'HEADER',this.__properties__.layout,debug);
|
||
|
this.__properties__.provider = new Window(1,1,VIEWDATA_FRAME_PROVIDER_LENGTH,1,'PROVIDER',this.__properties__.header,debug);
|
||
|
this.__properties__.pagenum = new Window(24,1,VIEWDATA_FRAME_PAGE_LENGTH,1,'#',this.__properties__.header,debug);
|
||
|
this.__properties__.cost = new Window(35,1,VIEWDATA_FRAME_COST_LENGTH,1,'$',this.__properties__.header,debug);
|
||
|
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
throw new Error('INVALID Page Service: '+service);
|
||
|
}
|
||
|
|
||
|
this.service = service;
|
||
|
}
|
||
|
|
||
|
init.apply(this,arguments);
|
||
|
|
||
|
// @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.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.service) {
|
||
|
case 'tex':
|
||
|
if ((''+int).length > ANSI_FRAME_COST_LENGTH-1)
|
||
|
throw new Error('Cost too large');
|
||
|
|
||
|
this.__properties__.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.__properties__.cost.__properties__.content = bintoattrs(VIEWDATA_BIN_GREEN+padright(int,VIEWDATA_FRAME_COST_LENGTH-2,' ')+'c').content;
|
||
|
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
throw new Error(this.service+' type not implemented');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
Page.prototype.__defineGetter__('dimensions',function() {
|
||
|
return this.width+' X '+this.height;
|
||
|
});
|
||
|
|
||
|
Page.prototype.__defineGetter__('dynamic_fields',function() {
|
||
|
return this.__properties__.dynamic_fields;
|
||
|
});
|
||
|
|
||
|
Page.prototype.__defineGetter__('height',function() {
|
||
|
return Number(this.__properties__.layout.height);
|
||
|
});
|
||
|
|
||
|
Page.prototype.__defineGetter__('input_fields',function() {
|
||
|
return this.__properties__.input_fields;
|
||
|
});
|
||
|
|
||
|
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.service) {
|
||
|
case 'tex':
|
||
|
if ((''+this.name.frame).length > ANSI_FRAME_PAGE_LENGTH-1)
|
||
|
throw new Error('Pagenum too large');
|
||
|
|
||
|
this.__properties__.pagenum.__properties__.content = anstoattrs(ESC+'[1;37m'+this.name.name).content;
|
||
|
|
||
|
break;
|
||
|
|
||
|
case 'vtx':
|
||
|
if ((''+this.name.frame).length > VIEWDATA_FRAME_PAGE_LENGTH-2)
|
||
|
throw new Error('Pagenum too large');
|
||
|
|
||
|
this.__properties__.pagenum.__properties__.content = bintoattrs(VIEWDATA_BIN_WHITE+this.name.name).content;
|
||
|
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
throw new Error(this.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.__properties__.frame);
|
||
|
|
||
|
return pageOwner(this.__properties__.name.__properties__.frame).prefix;
|
||
|
});
|
||
|
|
||
|
Page.prototype.__defineSetter__('provider',function(ansi) {
|
||
|
var provider;
|
||
|
|
||
|
switch (this.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.service+' not implemented');
|
||
|
}
|
||
|
|
||
|
this.__properties__.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.__properties__.header.__properties__.visible = bool;
|
||
|
});
|
||
|
|
||
|
Page.prototype.__defineGetter__('width',function() {
|
||
|
return Number(this.__properties__.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.__properties__.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<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 !== 'p' ? content[index] : '*';
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Build in our dynamic_fields that can be populated automatically
|
||
|
*/
|
||
|
this.build_system_fields = function() {
|
||
|
// Fields we can process automatically
|
||
|
const auto = ['nodeid','DATETIME','TIME','REALNAME','BBS','STATS.LTODAY','BYTESLEFT','MAILW','STATS.TTODAY','ON','STATS.NUSERS'];
|
||
|
|
||
|
var df = this.dynamic_fields.filter(function(item) { return item.value === undefined; });
|
||
|
|
||
|
if (! df.length)
|
||
|
return;
|
||
|
|
||
|
var that = this;
|
||
|
|
||
|
df.forEach(function(field) {
|
||
|
if (auto.indexOf(field.name) >= 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.__properties__.ch : 'undefined')+', ATTR:'+(char !== undefined ? char.__properties__.attr : 'undefined')+', LAST:'+last);
|
||
|
|
||
|
if (debug) {
|
||
|
writeln();
|
||
|
writeln('-------- ['+x+'] ------');
|
||
|
writeln('y:'+y+',x:'+x+', attr:'+(char !== undefined ? char.__properties__.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.__properties__.attr)) {
|
||
|
// The current attribute for this character
|
||
|
attr = (char === undefined) ? undefined : char.attr(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.__defaults__.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.__properties__.attr)
|
||
|
line += '^';
|
||
|
}
|
||
|
|
||
|
if (char.ch !== undefined) {
|
||
|
if (debug)
|
||
|
log(LOG_DEBUG,' = SEND CHAR :'+char.ch+', attr:'+char.__properties__.attr+', last:'+last);
|
||
|
|
||
|
line += (char.ch !== null) ? char.ch : '';
|
||
|
|
||
|
} else {
|
||
|
if (debug)
|
||
|
log(LOG_DEBUG,' = CHAR UNDEFINED');
|
||
|
line += ' ';
|
||
|
}
|
||
|
|
||
|
last = (char.__properties__.attr === undefined) ? undefined : char.__properties__.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].__properties__.attr === undefined) {
|
||
|
// NOOP
|
||
|
} else if (display[y][x].__properties__.attr === null) {
|
||
|
// NOOP
|
||
|
} else {
|
||
|
try {
|
||
|
write((last === display[y][x].__properties__.attr) ? '' : display[y][x].__properties__.attr);
|
||
|
} catch (e) {
|
||
|
writeln();
|
||
|
writeln('error:'+e);
|
||
|
writeln(' y:'+y);
|
||
|
writeln(' x:'+x);
|
||
|
writeln(JSON.stringify(display[y][x].__properties__.attr));
|
||
|
exit(1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
write(':');
|
||
|
|
||
|
if (display[y][x].__properties__.ch === undefined) {
|
||
|
// NOOP - No window filled a character at this location
|
||
|
write((display[y][x].__properties__.attr === undefined) ? '--' : '');
|
||
|
} else if (display[y][x].__properties__.ch === null) {
|
||
|
// NOOP
|
||
|
} else {
|
||
|
write('_'+display[y][x].__properties__.ch);
|
||
|
}
|
||
|
|
||
|
write(']');
|
||
|
last = display[y][x].__properties__.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].__properties__.attr;
|
||
|
|
||
|
writeln('X:'+(xx++)+'|'+attr+':'+display[y][x].__properties__.ch+'|'+display[y][x].attr(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<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 (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;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Load a specific page
|
||
|
*
|
||
|
* @param page
|
||
|
* @param ext
|
||
|
*/
|
||
|
this.get = function(page,ext) {
|
||
|
if (!(page instanceof PageObject))
|
||
|
throw new Error('page must be a PageObject');
|
||
|
|
||
|
return this.load(system.mods_dir+'ansitex/text/'+page.name+'.'+ext);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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
|
||
|
* @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.load = function(filename,width,height) {
|
||
|
log(LOG_DEBUG,'Loading frame: ['+filename+']');
|
||
|
|
||
|
var f = new File(filename);
|
||
|
if (! f.exists || ! f.open('rb',true))
|
||
|
return null;
|
||
|
|
||
|
var contents = f.read();
|
||
|
f.close();
|
||
|
|
||
|
var valid_sauce = false;
|
||
|
var ext = file_getext(filename).substr(1).toLowerCase();
|
||
|
|
||
|
if (contents.substr(-128, 7) === 'SAUCE00') {
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
switch (ext) {
|
||
|
// ANSI files
|
||
|
case 'ans':
|
||
|
var page = anstoattrs(contents,this.width,this.__properties__.content.y,this.__properties__.content.x);
|
||
|
|
||
|
this.__properties__.content.__properties__.content = page.content;
|
||
|
this.__properties__.dynamic_fields = page.dynamic_fields;
|
||
|
// Our fields are sorted in x descending order
|
||
|
this.__properties__.input_fields = page.input_fields.sort(function(a,b) { return a.x < b.x ? 1 : -1; });
|
||
|
|
||
|
break;
|
||
|
|
||
|
// ANSItex files
|
||
|
case 'tex':
|
||
|
case 'vtx':
|
||
|
log(LOG_DEBUG,'Loading FRAME from: '+filename);
|
||
|
|
||
|
try {
|
||
|
var load = JSON.parse(contents);
|
||
|
|
||
|
for (property in SAVED_FRAME_ATTRS) {
|
||
|
switch (SAVED_FRAME_ATTRS[property]) {
|
||
|
case 'content':
|
||
|
log(LOG_INFO,'- Parsing content');
|
||
|
//log(LOG_ERROR,'Frame content: '+JSON.stringify(base64_decode(load[SAVED_FRAME_ATTRS[property]])));
|
||
|
if (ext === 'tex')
|
||
|
var page = anstoattrs(base64_decode(load[SAVED_FRAME_ATTRS[property]]).replace("\x0a\x0d\x0a\x0d","\x0a\x0d"),this.width,this.__properties__.content.y,this.__properties__.content.x);
|
||
|
else if (ext == 'vtx')
|
||
|
var page = bintoattrs(base64_decode(load[SAVED_FRAME_ATTRS[property]]),this.width,this.__properties__.content.y,this.__properties__.content.x);
|
||
|
//log(LOG_ERROR,'Frame content: '+JSON.stringify(page));
|
||
|
|
||
|
this.__properties__.content.__properties__.content = page.content;
|
||
|
this.__properties__.dynamic_fields = page.dynamic_fields;
|
||
|
// Our fields are sorted in x descending order
|
||
|
if (page.input_fields.length)
|
||
|
this.__properties__.input_fields = page.input_fields.sort(function(a,b) { return a.x < b.x ? 1 : -1; });
|
||
|
log(LOG_INFO,'- Parsing content complete');
|
||
|
|
||
|
break;
|
||
|
|
||
|
case 'date':
|
||
|
log(LOG_INFO,'- Frame date : '+load[SAVED_FRAME_ATTRS[property]]);
|
||
|
break;
|
||
|
|
||
|
case 'frame':
|
||
|
this.__properties__.name.frame = ''+load[SAVED_FRAME_ATTRS[property]];
|
||
|
break;
|
||
|
|
||
|
case 'input_fields':
|
||
|
if (load[SAVED_FRAME_ATTRS[property]])
|
||
|
this.__properties__.input_fields = load[SAVED_FRAME_ATTRS[property]];
|
||
|
break;
|
||
|
|
||
|
case 'index':
|
||
|
this.__properties__.name.index = load[SAVED_FRAME_ATTRS[property]];
|
||
|
break;
|
||
|
|
||
|
case 'version':
|
||
|
log(LOG_INFO,'- Frame version : '+load[SAVED_FRAME_ATTRS[property]]);
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
log(LOG_DEBUG,'- Frame property: '+SAVED_FRAME_ATTRS[property]+', value:'+load[SAVED_FRAME_ATTRS[property]]);
|
||
|
this[SAVED_FRAME_ATTRS[property]] = load[SAVED_FRAME_ATTRS[property]];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If the page doesnt match the filename, throw an error
|
||
|
// @todo This needs to be tested.
|
||
|
// @todo doesnt work on command line
|
||
|
/*
|
||
|
if (this.name.name !== filename.replace(system.mods_dir+'ansitex/text/','').replace('.'+ext,''))
|
||
|
throw new Error('Frame '+this.name.name+' doesnt match filename:'+filename.replace(system.mods_dir+'ansitex/text/','').replace('.'+ext,''));
|
||
|
*/
|
||
|
|
||
|
} catch (error) {
|
||
|
log(LOG_ERROR,'! Frame error : '+error);
|
||
|
|
||
|
// Load our system error frame.
|
||
|
this.get(new PageObject(FRAME_SYSTEM_ERROR),ext);
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
this.loadcomplete();
|
||
|
|
||
|
log(LOG_DEBUG,'= Loaded frame : '+this.name.name);
|
||
|
|
||
|
break;
|
||
|
|
||
|
// ViewData files
|
||
|
case 'bin':
|
||
|
var page = bintoattrs(contents,this.width,this.__properties__.content.y,this.__properties__.content.x,debug);
|
||
|
|
||
|
this.__properties__.content.__properties__.content = page.content;
|
||
|
this.__properties__.dynamic_fields = page.dynamic_fields;
|
||
|
// Our fields are sorted in x descending order
|
||
|
this.__properties__.input_fields = page.input_fields.sort(function(a,b) { return a.x < b.x ? 1 : -1; });
|
||
|
|
||
|
break;
|
||
|
|
||
|
/*
|
||
|
case 'ASC':
|
||
|
case 'MSG':
|
||
|
case 'TXT':
|
||
|
lines = contents.split(/\r*\n/);
|
||
|
|
||
|
while (lines.length > 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.__properties__.pagenum.__properties__.content = anstoattrs(ESC+'[1;37m'+this.name.name).content;
|
||
|
this.provider = base64_decode(po.logoans);
|
||
|
|
||
|
break;
|
||
|
|
||
|
case 'vtx':
|
||
|
this.__properties__.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.__properties__.layout.drawline(1,this.width,y,false);
|
||
|
write(line.content);
|
||
|
|
||
|
write('\x1b[0m');
|
||
|
|
||
|
writeln();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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.__properties__.index = frame.index;
|
||
|
|
||
|
} else {
|
||
|
this.__properties__.frame = frame;
|
||
|
this.__properties__.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.__properties__.frame && this.__properties__.index) ? this.frame+this.index : null;
|
||
|
});
|
||
|
|
||
|
PageObject.prototype.__defineGetter__('next',function() {
|
||
|
var next = undefined;
|
||
|
|
||
|
if (this.__properties__.index !== 'z') {
|
||
|
log(LOG_DEBUG,'page_next: Current page:'+this.__properties__.frame+', current index:'+this.__properties__.index);
|
||
|
|
||
|
next = new PageObject(this.__properties__.frame,String.fromCharCode(this.__properties__.index.charCodeAt(0)+1));
|
||
|
}
|
||
|
|
||
|
return next;
|
||
|
});
|
||
|
}
|