sbbs/load/page.js

1116 lines
34 KiB
JavaScript
Raw Normal View History

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