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)
|
|
|
|
|
2023-12-21 23:25:32 +00:00
|
|
|
= @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:
|
2023-12-24 09:11:40 +00:00
|
|
|
a = new Page() // root frame 80 x 24 for ANSItex
|
2022-12-09 06:19:33 +00:00
|
|
|
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
|
|
|
|
--:____.____|____.____|____.____|____.____|____.____|____.____|____.____|____.____|
|
|
|
|
*/
|
|
|
|
|
2023-12-21 23:25:32 +00:00
|
|
|
load('ansitex/load/windows.js'); // Our supporting window class
|
2023-12-24 06:44:02 +00:00
|
|
|
require('ansitex/load/defs.js','FRAME_TYPE_INFO'); // FRAME definitions
|
2023-12-21 23:25:32 +00:00
|
|
|
require('ansitex/load/msgbases.js','MAX_PAGE_NUM'); // To read/write to message bases
|
2023-12-24 06:44:02 +00:00
|
|
|
require('sbbsdefs.js','SS_USERON'); // Need for our ANSI colors eg: BG_*
|
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
|
2023-12-21 23:25:32 +00:00
|
|
|
* - 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
|
|
|
*/
|
2023-12-24 09:11:40 +00:00
|
|
|
function Page(debug) {
|
2023-12-21 23:25:32 +00:00
|
|
|
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 (*)
|
|
|
|
|
2023-12-21 23:25:32 +00:00
|
|
|
body: undefined, // Window - Page body
|
|
|
|
};
|
|
|
|
|
|
|
|
this.__properties__ = {
|
|
|
|
name: new PageObject,
|
2023-12-24 06:44:02 +00:00
|
|
|
type: undefined, // Frame type
|
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
|
2023-12-24 06:44:02 +00:00
|
|
|
|
|
|
|
key: [], // Key actions
|
2022-12-09 06:19:33 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
this.__defaults__ = {
|
|
|
|
attr: BG_BLACK|LIGHTGRAY,
|
|
|
|
};
|
|
|
|
|
|
|
|
this.__compiled__ = {
|
2023-12-21 23:25:32 +00:00
|
|
|
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
|
|
|
|
*/
|
2023-12-24 09:11:40 +00:00
|
|
|
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);
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
switch (SESSION_EXT) {
|
2022-12-09 06:19:33 +00:00
|
|
|
case 'tex':
|
2023-12-24 06:44:02 +00:00
|
|
|
require('ansitex/load/session/ansitex.js','SESSION_ANSITEX');
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-24 09:11:40 +00:00
|
|
|
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);
|
2022-12-09 06:19:33 +00:00
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'vtx':
|
2023-12-24 06:44:02 +00:00
|
|
|
require('ansitex/load/session/viewdata.js','SESSION_VIEWDATA');
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-24 09:11:40 +00:00
|
|
|
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);
|
2022-12-09 06:19:33 +00:00
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
2023-12-24 06:44:02 +00:00
|
|
|
throw new Error('INVALID Page Service: '+SESSION_EXT);
|
2022-12-09 06:19:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// @todo change this to Object.defineProperty() - see session.js
|
|
|
|
/**
|
|
|
|
* Determine if this frame is accessible to the current user
|
|
|
|
*/
|
|
|
|
Page.prototype.__defineGetter__('accessible',function() {
|
2023-12-24 06:44:02 +00:00
|
|
|
log(LOG_DEBUG,'- Checking if user can access frame: '+this.name.toString());
|
2022-12-09 06:19:33 +00:00
|
|
|
log(LOG_DEBUG,' - User: '+JSON.stringify(user.number));
|
2023-12-24 06:44:02 +00:00
|
|
|
log(LOG_DEBUG,' - Frame Owner: '+JSON.stringify(this.pageowner)+', System Frame: '+(this.pageowner === SYSTEM_OWNER));
|
|
|
|
log(LOG_DEBUG,' - Accessible: '+JSON.stringify(this.isAccessible));
|
|
|
|
log(LOG_DEBUG,' - Public: '+JSON.stringify(this.isPublic));
|
2022-12-09 06:19:33 +00:00
|
|
|
|
|
|
|
// 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) ||
|
2023-12-21 23:25:32 +00:00
|
|
|
(pageEditor(this.name.frame))
|
2022-12-09 06:19:33 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
} else {
|
2023-12-24 06:44:02 +00:00
|
|
|
return this.__properties__.isAccessible && (this.pageowner === SYSTEM_OWNER) && this.__properties__.isPublic;
|
2022-12-09 06:19:33 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-12-24 09:11:40 +00:00
|
|
|
Page.prototype.__defineGetter__('cost',function() {
|
|
|
|
return Number(this.__properties__.cost);
|
|
|
|
});
|
2022-12-09 06:19:33 +00:00
|
|
|
Page.prototype.__defineSetter__('cost',function(int) {
|
|
|
|
if (typeof int !== 'number')
|
|
|
|
throw new Error('Cost must be a number');
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
this.__properties__.cost = int;
|
|
|
|
|
2023-12-24 09:11:40 +00:00
|
|
|
if ((''+int).length > FRAME_COST_LENGTH-1-FRAME_ATTR_LENGTH)
|
|
|
|
throw new Error('Cost too large');
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
// Populate the cost window
|
|
|
|
switch (SESSION_EXT) {
|
2022-12-09 06:19:33 +00:00
|
|
|
case 'tex':
|
2023-12-24 09:11:40 +00:00
|
|
|
this.__window__.cost.__properties__.content = rawtoattrs(ESC+'[1;32m'+padright(int,FRAME_COST_LENGTH-1-FRAME_ATTR_LENGTH,' ')+FRAME_COSTUNIT).content;
|
2022-12-09 06:19:33 +00:00
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'vtx':
|
2023-12-24 09:11:40 +00:00
|
|
|
this.__window__.cost.__properties__.content = rawtoattrs(VIEWDATA_BIN_GREEN+padright(int,FRAME_COST_LENGTH-1-FRAME_ATTR_LENGTH,' ')+FRAME_COSTUNIT).content;
|
2022-12-09 06:19:33 +00:00
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
2023-12-24 06:44:02 +00:00
|
|
|
throw new Error(SESSION_EXT+' type not implemented');
|
2022-12-09 06:19:33 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
Page.prototype.__defineGetter__('dimensions',function() {
|
2023-12-21 23:25:32 +00:00
|
|
|
return this.__properties__.width+' X '+this.__properties__.height;
|
2022-12-09 06:19:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
Page.prototype.__defineGetter__('dynamic_fields',function() {
|
2023-12-24 06:44:02 +00:00
|
|
|
return this.__properties__.dynamic_fields === undefined ? [] : this.__properties__.dynamic_fields;
|
2022-12-09 06:19:33 +00:00
|
|
|
});
|
2023-12-21 23:25:32 +00:00
|
|
|
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() {
|
2023-12-21 23:25:32 +00:00
|
|
|
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;
|
|
|
|
});
|
2023-12-21 23:25:32 +00:00
|
|
|
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) {
|
2023-12-24 06:44:02 +00:00
|
|
|
if (typeof bool !== 'boolean')
|
2022-12-09 06:19:33 +00:00
|
|
|
throw new Error('isAccessible must be a boolean');
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
this.__properties__.isAccessible = bool;
|
2022-12-09 06:19:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
Page.prototype.__defineSetter__('isPublic',function(bool) {
|
2023-12-24 06:44:02 +00:00
|
|
|
if (typeof bool !== 'boolean')
|
2022-12-09 06:19:33 +00:00
|
|
|
throw new Error('isPublic must be a boolean');
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
this.__properties__.isPublic = bool;
|
|
|
|
});
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
return this.__properties__.key = array;
|
2022-12-09 06:19:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
2023-12-24 09:11:40 +00:00
|
|
|
if ((''+this.__properties__.name.frame).length > FRAME_PAGE_LENGTH-1-FRAME_ATTR_LENGTH)
|
|
|
|
throw new Error('Pagenum too large');
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
switch (SESSION_EXT) {
|
2022-12-09 06:19:33 +00:00
|
|
|
case 'tex':
|
2023-12-24 09:11:40 +00:00
|
|
|
this.__window__.pagenum.__properties__.content = rawtoattrs(ESC+'[1;37m'+this.__properties__.name.toString()).content;
|
2022-12-09 06:19:33 +00:00
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'vtx':
|
2023-12-24 09:11:40 +00:00
|
|
|
this.__window__.pagenum.__properties__.content = rawtoattrs(VIEWDATA_BIN_WHITE+this.__properties__.name.toString()).content;
|
2022-12-09 06:19:33 +00:00
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
2023-12-24 06:44:02 +00:00
|
|
|
throw new Error(SESSION_EXT+' 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() {
|
2023-12-21 23:25:32 +00:00
|
|
|
log(LOG_DEBUG,'Getting page owner for:'+this.__properties__.name.frame);
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-21 23:25:32 +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;
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
switch (SESSION_EXT) {
|
2022-12-09 06:19:33 +00:00
|
|
|
case 'tex':
|
2023-12-24 09:11:40 +00:00
|
|
|
provider = rawtoattrs(ansi+ESC+'[0m').content;
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-24 09:11:40 +00:00
|
|
|
if (provider[1].filter(function(child) { return child.ch; }).length-1 > FRAME_PROVIDER_LENGTH)
|
2022-12-09 06:19:33 +00:00
|
|
|
throw new Error('Provider too large');
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'vtx':
|
2023-12-24 09:11:40 +00:00
|
|
|
provider = rawtoattrs(ansi).content;
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-24 09:11:40 +00:00
|
|
|
if (provider[1].length-1 > FRAME_PROVIDER_LENGTH)
|
2022-12-09 06:19:33 +00:00
|
|
|
throw new Error('Provider too large');
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
2023-12-24 06:44:02 +00:00
|
|
|
throw new Error(SESSION_EXT+' not implemented');
|
2022-12-09 06:19:33 +00:00
|
|
|
}
|
|
|
|
|
2023-12-21 23:25:32 +00:00
|
|
|
this.__window__.provider.__properties__.content = provider;
|
2022-12-09 06:19:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
Page.prototype.__defineSetter__('showHeader',function(bool) {
|
|
|
|
if (typeof bool !== 'boolean')
|
|
|
|
throw new Error('showHeader expected a true/false');
|
|
|
|
|
2023-12-21 23:25:32 +00:00
|
|
|
this.__window__.header.visible = bool;
|
2022-12-09 06:19:33 +00:00
|
|
|
});
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
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;
|
|
|
|
});
|
|
|
|
|
2022-12-09 06:19:33 +00:00
|
|
|
Page.prototype.__defineGetter__('width',function() {
|
2023-12-21 23:25:32 +00:00
|
|
|
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();
|
2023-12-21 23:25:32 +00:00
|
|
|
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; });
|
2023-12-24 06:44:02 +00:00
|
|
|
log(LOG_DEBUG,'We have DF fields:'+fields.length);
|
2022-12-09 06:19:33 +00:00
|
|
|
|
|
|
|
if (fields.length)
|
|
|
|
insert_fields(fields,this.__compiled__.build);
|
|
|
|
|
|
|
|
// Add our dynamic values
|
|
|
|
fields = this.input_fields.filter(function(item) { return item.value !== undefined; });
|
2023-12-24 06:44:02 +00:00
|
|
|
log(LOG_DEBUG,'We have INPUT fields:'+fields.length);
|
2022-12-09 06:19:33 +00:00
|
|
|
|
|
|
|
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;
|
2023-12-24 06:44:02 +00:00
|
|
|
log(LOG_DEBUG,'DISPLAY CALLED:'+last);
|
2022-12-09 06:19:33 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
|
2023-12-24 09:11:40 +00:00
|
|
|
new_screen = BG_BLACK|LIGHTGRAY;
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
switch (SESSION_EXT) {
|
2022-12-09 06:19:33 +00:00
|
|
|
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
|
|
|
|
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)
|
2023-12-21 23:25:32 +00:00
|
|
|
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+'] ------');
|
2023-12-21 23:25:32 +00:00
|
|
|
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)
|
2023-12-24 06:44:02 +00:00
|
|
|
if ((SESSION_EXT === 'vtx') || (last === undefined) || (last !== char.attr)) {
|
2022-12-09 06:19:33 +00:00
|
|
|
// The current attribute for this character
|
2023-12-24 06:44:02 +00:00
|
|
|
attr = (char === undefined) ? undefined : char.attribute(last,SESSION_EXT,debug);
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
switch (SESSION_EXT) {
|
2022-12-09 06:19:33 +00:00
|
|
|
case 'tex':
|
|
|
|
// If the attribute is null, we'll write our default attribute
|
|
|
|
if (attr === null)
|
2023-12-21 23:25:32 +00:00
|
|
|
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:
|
2023-12-24 06:44:02 +00:00
|
|
|
throw new Error('service type:'+SESSION_EXT+' hasnt been implemented.');
|
2022-12-09 06:19:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// For no-color output and ViewData, we'll render a character
|
|
|
|
} else {
|
2023-12-24 06:44:02 +00:00
|
|
|
if ((SESSION_EXT === 'vtx') && char.attr)
|
2022-12-09 06:19:33 +00:00
|
|
|
line += '^';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (char.ch !== undefined) {
|
|
|
|
if (debug)
|
2023-12-21 23:25:32 +00:00
|
|
|
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 += ' ';
|
|
|
|
}
|
|
|
|
|
2023-12-21 23:25:32 +00:00
|
|
|
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);
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
writeln('Dumping Page:'+this.name.toString());
|
2022-12-09 06:19:33 +00:00
|
|
|
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('[');
|
2023-12-21 23:25:32 +00:00
|
|
|
if (display[y][x].attr === undefined) {
|
2022-12-09 06:19:33 +00:00
|
|
|
// NOOP
|
2023-12-21 23:25:32 +00:00
|
|
|
} else if (display[y][x].attr === null) {
|
2022-12-09 06:19:33 +00:00
|
|
|
// NOOP
|
|
|
|
} else {
|
|
|
|
try {
|
2023-12-21 23:25:32 +00:00
|
|
|
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);
|
2023-12-21 23:25:32 +00:00
|
|
|
writeln(JSON.stringify(display[y][x].attr));
|
2022-12-09 06:19:33 +00:00
|
|
|
exit(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
write(':');
|
|
|
|
|
2023-12-21 23:25:32 +00:00
|
|
|
if (display[y][x].ch === undefined) {
|
2022-12-09 06:19:33 +00:00
|
|
|
// NOOP - No window filled a character at this location
|
2023-12-21 23:25:32 +00:00
|
|
|
write((display[y][x].attr === undefined) ? '--' : '');
|
|
|
|
} else if (display[y][x].ch === null) {
|
2022-12-09 06:19:33 +00:00
|
|
|
// NOOP
|
|
|
|
} else {
|
2023-12-21 23:25:32 +00:00
|
|
|
write('_'+display[y][x].ch);
|
2022-12-09 06:19:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
write(']');
|
2023-12-21 23:25:32 +00:00
|
|
|
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]) {
|
|
|
|
|
2023-12-21 23:25:32 +00:00
|
|
|
var attr = display[y][x].attr;
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
writeln('X:'+(xx++)+'|'+attr+':'+display[y][x].ch+'|'+display[y][x].attribute(last,SESSION_EXT,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;
|
|
|
|
}
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
/**
|
|
|
|
* 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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-09 06:19:33 +00:00
|
|
|
/**
|
|
|
|
* Load a specific page
|
|
|
|
*
|
|
|
|
* @param page
|
|
|
|
*/
|
2023-12-24 06:44:02 +00:00
|
|
|
this.get = function(page) {
|
2022-12-09 06:19:33 +00:00
|
|
|
if (!(page instanceof PageObject))
|
|
|
|
throw new Error('page must be a PageObject');
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
// Try to load from the msgbase first
|
|
|
|
if (FRAMES_MSG_BASE && this.load(page))
|
|
|
|
return true;
|
|
|
|
|
|
|
|
return FRAMES_MSG_FILES ? this.import(system.mods_dir+'ansitex/text/'+page.toString()+'.'+SESSION_EXT) : false;
|
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
|
|
|
|
*
|
2023-12-21 23:25:32 +00:00
|
|
|
* @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.
|
|
|
|
*/
|
2023-12-21 23:25:32 +00:00
|
|
|
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);
|
2023-12-21 23:25:32 +00:00
|
|
|
if (! f.exists || ! f.open('rb',true)) {
|
|
|
|
log(LOG_ERROR,' ? File doesnt exist: ['+filename+']');
|
2022-12-09 06:19:33 +00:00
|
|
|
return null;
|
2023-12-21 23:25:32 +00:00
|
|
|
}
|
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);
|
|
|
|
}
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
log(LOG_DEBUG,'*** ['+ext+'].');
|
|
|
|
return this.preload((['vtx','tex'].indexOf(ext) !== -1) ? JSON.parse(contents) : contents,ext,width,height);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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) {
|
2022-12-09 06:19:33 +00:00
|
|
|
switch (ext) {
|
|
|
|
// ANSI files
|
|
|
|
case 'ans':
|
2023-12-24 09:11:40 +00:00
|
|
|
// 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);
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-21 23:25:32 +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
|
2023-12-21 23:25:32 +00:00
|
|
|
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':
|
2023-12-24 06:44:02 +00:00
|
|
|
log(LOG_DEBUG,'Processing FRAME file');
|
2022-12-09 06:19:33 +00:00
|
|
|
|
|
|
|
try {
|
2023-12-24 06:44:02 +00:00
|
|
|
//var load = JSON.parse(contents);
|
|
|
|
var load = contents;
|
|
|
|
|
|
|
|
log(LOG_DEBUG,'*** ['+JSON.stringify(Object.keys(contents))+']['+(typeof contents)+'].');
|
|
|
|
for (var index in contents) {
|
|
|
|
if (FRAME_SAVE_ATTRS.indexOf(index) === -1) {
|
|
|
|
log(LOG_ERROR,'- Unknown index ['+index+'] in input.');
|
|
|
|
continue;
|
|
|
|
}
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
log(LOG_DEBUG,'* Processing ['+index+'] with value ['+JSON.stringify(contents[index])+'].');
|
|
|
|
switch (index) {
|
2022-12-09 06:19:33 +00:00
|
|
|
case 'content':
|
|
|
|
log(LOG_INFO,'- Parsing content');
|
2023-12-24 06:44:02 +00:00
|
|
|
|
2022-12-09 06:19:33 +00:00
|
|
|
if (ext === 'tex')
|
2023-12-24 09:11:40 +00:00
|
|
|
var page = rawtoattrs(base64_decode(contents[index]).replace("\x0a\x0d\x0a\x0d","\x0a\x0d"),this.width,this.__window__.body.y,this.__window__.body.x);
|
2023-12-21 23:25:32 +00:00
|
|
|
else if (ext === 'vtx')
|
2023-12-24 09:11:40 +00:00
|
|
|
var page = rawtoattrs(base64_decode(contents[index]),this.width,this.__window__.body.y,this.__window__.body.x);
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-21 23:25:32 +00:00
|
|
|
this.__window__.body.__properties__.content = page.content;
|
|
|
|
this.dynamic_fields = page.dynamic_fields;
|
2023-12-24 06:44:02 +00:00
|
|
|
|
2022-12-09 06:19:33 +00:00
|
|
|
// Our fields are sorted in x descending order
|
|
|
|
if (page.input_fields.length)
|
2023-12-21 23:25:32 +00:00
|
|
|
this.input_fields = page.input_fields.sort(function(a,b) { return a.x < b.x ? 1 : -1; });
|
2023-12-24 06:44:02 +00:00
|
|
|
|
2022-12-09 06:19:33 +00:00
|
|
|
log(LOG_INFO,'- Parsing content complete');
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
case 'cost':
|
|
|
|
this.cost = contents[index];
|
|
|
|
break;
|
|
|
|
|
2022-12-09 06:19:33 +00:00
|
|
|
case 'date':
|
2023-12-24 06:44:02 +00:00
|
|
|
log(LOG_INFO,'- Frame date : '+contents[index]);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'dynamic_fields':
|
|
|
|
this.dynamic_fields = contents[index];
|
2022-12-09 06:19:33 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 'frame':
|
2023-12-24 06:44:02 +00:00
|
|
|
log(LOG_INFO,'- Frame ID : '+contents[index]);
|
|
|
|
this.name.frame = ''+contents[index];
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'index':
|
|
|
|
log(LOG_INFO,'- Frame Index : '+contents[index]);
|
|
|
|
this.name.index = contents[index];
|
2022-12-09 06:19:33 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 'input_fields':
|
2023-12-24 06:44:02 +00:00
|
|
|
this.input_fields = contents[index];
|
2022-12-09 06:19:33 +00:00
|
|
|
break;
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
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];
|
2022-12-09 06:19:33 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 'version':
|
2023-12-24 06:44:02 +00:00
|
|
|
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
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2023-12-24 09:11:40 +00:00
|
|
|
|
2022-12-09 06:19:33 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
2023-12-24 06:44:02 +00:00
|
|
|
log(LOG_ERROR,'! Frame property not handled: '+index+', value:'+contents[index]);
|
2022-12-09 06:19:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
log(LOG_ERROR,'! Frame error : '+error);
|
|
|
|
|
|
|
|
// Load our system error frame.
|
2023-12-24 06:44:02 +00:00
|
|
|
// @todo If our system error page errors, then we go into a loop
|
|
|
|
this.get(new PageObject(FRAME_SYSTEM_ERROR));
|
2022-12-09 06:19:33 +00:00
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
log(LOG_DEBUG,'= Loaded frame : '+this.name.toString());
|
|
|
|
|
2022-12-09 06:19:33 +00:00
|
|
|
this.loadcomplete();
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
log(LOG_DEBUG,'= Frame complete : '+this.name.toString());
|
2022-12-09 06:19:33 +00:00
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
throw new Error('Unsupported filetype:'+ext);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Successful load
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
this.load = function(page) {
|
|
|
|
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,code+' cannot be opened:'+mb.error);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
log(LOG_ERROR,code+' cannot be opened:'+e.message);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
var msg;
|
|
|
|
// Find existing message with the page number and delete it if defined
|
|
|
|
for(var x in headers) {
|
|
|
|
if ((headers[x].tags === page.toString()) && (!(headers[x].attr&MSG_DELETE)) && (headers[x].from === SESSION_EXT)) {
|
|
|
|
msg = headers[x];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (msg === undefined) {
|
|
|
|
log(LOG_DEBUG,' - Frame not found: ['+page.toString()+'] to ['+FRAMES_MSG_BASE+']');
|
|
|
|
return false;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
log(LOG_DEBUG,' - LOADING frame: ['+page.toString()+'] from ['+msg.number+']');
|
|
|
|
|
|
|
|
var contents = JSON.parse(mb.get_msg_body(false,msg.number,false,false,true,true));
|
|
|
|
|
|
|
|
return this.preload(contents,SESSION_EXT);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-12-09 06:19:33 +00:00
|
|
|
/**
|
|
|
|
* After page load routines
|
|
|
|
*/
|
|
|
|
this.loadcomplete = function() {
|
|
|
|
var po = pageOwner(this.name.frame);
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
switch (SESSION_EXT) {
|
2022-12-09 06:19:33 +00:00
|
|
|
case 'tex':
|
2023-12-24 09:11:40 +00:00
|
|
|
this.__window__.pagenum.__properties__.content = rawtoattrs(ESC+'[1;37m'+this.name.toString()).content;
|
2022-12-09 06:19:33 +00:00
|
|
|
this.provider = base64_decode(po.logoans);
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'vtx':
|
2023-12-24 09:11:40 +00:00
|
|
|
this.__window__.pagenum.__properties__.content = rawtoattrs(VIEWDATA_BIN_WHITE+this.name.toString()).content;
|
2022-12-09 06:19:33 +00:00
|
|
|
this.provider = base64_decode(po.logovtx);
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
2023-12-24 06:44:02 +00:00
|
|
|
throw new Error(SESSION_EXT+' hasnt been implemented');
|
2022-12-09 06:19:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Dont show header on un-authed login frames
|
|
|
|
if (! user.number)
|
|
|
|
this.showHeader = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-12-24 06:44:02 +00:00
|
|
|
* Save the frame to the message base
|
2022-12-09 06:19:33 +00:00
|
|
|
*/
|
|
|
|
this.save = function() {
|
2023-12-24 06:44:02 +00:00
|
|
|
var mb = new MsgBase(FRAMES_MSG_BASE);
|
|
|
|
var headers;
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
try {
|
|
|
|
if (mb.open()) {
|
|
|
|
headers = mb.get_all_msg_headers(false,false) || [];
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
} else {
|
|
|
|
log(LOG_ERROR,FRAMES_MSG_BASE+' cannot be opened:'+mb.error);
|
|
|
|
|
|
|
|
return;
|
2022-12-09 06:19:33 +00:00
|
|
|
}
|
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
} catch (e) {
|
|
|
|
log(LOG_ERROR,FRAMES_MSG_BASE+' cannot be opened:'+e.message);
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
// Build the save content
|
|
|
|
var content = {};
|
2022-12-09 06:19:33 +00:00
|
|
|
|
2023-12-24 06:44:02 +00:00
|
|
|
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+']');
|
2022-12-09 06:19:33 +00:00
|
|
|
}
|
2023-12-24 06:44:02 +00:00
|
|
|
|
|
|
|
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();
|
2022-12-09 06:19:33 +00:00
|
|
|
}
|
2023-12-21 23:25:32 +00:00
|
|
|
|
|
|
|
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;
|
2023-12-21 23:25:32 +00:00
|
|
|
this.index = frame.index;
|
2022-12-09 06:19:33 +00:00
|
|
|
|
|
|
|
} else {
|
|
|
|
this.__properties__.frame = frame;
|
2023-12-21 23:25:32 +00:00
|
|
|
|
|
|
|
if (index)
|
|
|
|
this.index = index;
|
2022-12-09 06:19:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
2023-12-21 23:25:32 +00:00
|
|
|
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
|
|
|
|
2023-12-21 23:25:32 +00:00
|
|
|
next = new PageObject(this.frame,String.fromCharCode(this.index.charCodeAt(0)+1));
|
2022-12-09 06:19:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return next;
|
|
|
|
});
|
2023-12-24 06:44:02 +00:00
|
|
|
|
|
|
|
PageObject.prototype.toString = function() {
|
|
|
|
return (this.frame && this.index) ? this.frame+this.index : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
init.apply(this,arguments);
|
2022-12-09 06:19:33 +00:00
|
|
|
}
|