sbbs/load/session/ansitex.js

595 lines
14 KiB
JavaScript
Raw Normal View History

const SESSION_ANSITEX = (1<<1);
const SESSION_EXT = 'tex';
2022-12-09 06:19:33 +00:00
const FRAME_WIDTH = 80;
const FRAME_HEIGHT = 22;
const FRAME_PROVIDER_LENGTH = 55;
const FRAME_PAGE_LENGTH = 13;
const FRAME_COST_LENGTH = 10;
const FRAME_ATTR_LENGTH = 0; // Space that an attribute takes
2022-12-09 06:19:33 +00:00
/**
* This function converts ANSI text into an array of attributes
*
* We include the attribute for every character, so that if a window is placed on top of this window, the edges
* render correctly.
*
* @param contents - Our ANSI content to convert
* @param width - The width before wrapping to the next line
* @param yoffset - fields offset as discovered
* @param xoffset - fields offset as discovered
* @param debug - Enable debug mode
*/
function rawtoattrs(contents,width,yoffset,xoffset,debug) {
2022-12-09 06:19:33 +00:00
if (debug)
writeln('DEBUG active: '+debug);
lines = (''+contents).split(/\r\n/);
var i = 0;
var bg = BG_BLACK;
var fg = LIGHTGRAY;
var attr = fg + bg + i;
var y = 0;
// Saving cursor positions
var saved = {};
var frame = {
content: [],
dynamic_fields: [],
input_fields: [],
};
// @todo temp hack, rework ansi variable - perhaps have a function that converts an attribute back into an ANSI sequence
var ansi = { i: 0, f: 37, b: 40 };
while (lines.length > 0) {
var x = 0;
var line = lines.shift();
if ((debug !== undefined) && (y > debug)) {
exit(1);
}
if (debug) {
log(LOG_DEBUG,'y:'+y);
log(LOG_DEBUG,'line:'+line);
}
while (line.length > 0) {
/* parse an attribute sequence*/
var m = line.match(/^\x1b\[((\d+)(;?(\d+))*)+m/);
if (m !== null) {
line = line.substr(m.shift().length);
m = m.shift().split(';').sort(function(a,b) { return Number(a) < Number(b) ? -1 : 1; });
if ((debug !== undefined) && debug === y) {
//writeln();
log(LOG_DEBUG,'m:'+JSON.stringify(m));
}
// Reset
if (Number(m[0]) === 0) {
bg = BG_BLACK;
fg = LIGHTGRAY;
i = 0;
ansi = { i:0, b:37, f:40};
m.shift();
if (debug)
log(LOG_DEBUG,' - RESET');
}
// High Intensity
if (Number(m[0]) === 1) {
i += (((i === 0) || (i === BLINK)) ? HIGH : 0);
m.shift();
ansi.i = 1;
if (debug && (debug === y))
writeln('fg:'+fg+', bg:'+bg+' i:'+i);
}
// Blink
if (Number(m[0]) === 5) {
i += (((i === 0) || (i === HIGH)) ? BLINK : 0);
m.shift();
}
// Foreground
if ((Number(m[0]) >= 30) && (Number(m[0]) <= 37)) {
ansi.f = Number(m[0]);
switch(Number(m.shift())) {
case 30:
fg = BLACK;
break;
case 31:
fg = RED;
break;
case 32:
fg = GREEN;
break;
case 33:
fg = BROWN;
break;
case 34:
fg = BLUE;
break;
case 35:
fg = MAGENTA;
break;
case 36:
fg = CYAN;
break;
case 37:
fg = LIGHTGRAY;
break;
}
}
// Background
if ((Number(m[0]) >= 40) && (Number(m[0]) <= 47)) {
ansi.b = Number(m[0]);
switch (Number(m.shift())) {
case 40:
bg = BG_BLACK;
break;
case 41:
bg = BG_RED;
break;
case 42:
bg = BG_GREEN;
break;
case 43:
bg = BG_BROWN;
break;
case 44:
bg = BG_BLUE;
break;
case 45:
bg = BG_MAGENTA;
break;
case 46:
bg = BG_CYAN;
break;
case 47:
bg = BG_LIGHTGRAY;
break;
}
}
if (debug) {
log(LOG_DEBUG,'fg:'+fg+', bg:'+bg+' i:'+i);
log(LOG_DEBUG,'ansi:'+JSON.stringify(ansi));
}
attr = bg + fg + i;
continue;
}
if (debug)
log(LOG_DEBUG,'= Current attr:'+attr);
/* parse absolute character position */
var m = line.match(/^\x1b\[(\d*);?(\d*)[Hf]/);
if (m !== null) {
line = line.substr(m.shift().length);
if (m.length === 0) {
x = 0;
y = 0;
} else {
if(m[0])
y = Number(m.shift())-1;
if(m[0])
x = Number(m.shift())-1;
}
continue;
}
/* ignore a bullshit sequence */
var n = line.match(/^\x1b\[\?7h/);
if (n !== null) {
line = line.substr(n.shift().length);
continue;
}
/* parse an up positional sequence */
var n = line.match(/^\x1b\[(\d*)A/);
if (n !== null) {
line = line.substr(n.shift().length);
var chars = n.shift();
y -= (chars < 1) ? 0 : Number(chars);
continue;
}
/* parse a down positional sequence */
var n = line.match(/^\x1b\[(\d*)B/);
if (n !== null) {
line = line.substr(n.shift().length);
var chars = n.shift();
y += (chars < 1) ? 0 : Number(chars);
continue;
}
/* parse a forward positional sequence */
var n = line.match(/^\x1b\[(\d*)C/);
if (n !== null) {
line = line.substr(n.shift().length);
var chars = n.shift();
x += (chars < 1) ? 0 : Number(chars);
continue;
}
/* parse a backward positional sequence */
var n = line.match(/^\x1b\[(\d*)D/);
if (n !== null) {
line = line.substr(n.shift().length);
var chars = n.shift()
x -= (chars < 1) ? 0 : Number(chars);
continue;
}
/* parse a clear screen sequence */
var n = line.match(/^\x1b\[2J/);
if (n !== null) {
line = line.substr(n.shift().length);
continue;
}
/* parse save cursor sequence */
var n = line.match(/^\x1b\[s/);
if (n !== null) {
line = line.substr(n.shift().length);
saved.x = x;
saved.y = y;
continue;
}
/* parse restore cursor sequence */
var n = line.match(/^\x1b\[u/);
if (n !== null) {
line = line.substr(n.shift().length);
x = saved.x;
y = saved.y;
continue;
}
/* parse an input field */
// Input field 'FIELD;valueTYPE;input char'
// @todo remove the trailing ESC \ to end the field, just use a control code ^B \x02 (Start of Text) and ^C \x03
var m = line.match(/^\x1b_(([A-Z]+;[0-9a-z]+)([;]?.+)?)\x1b\\/);
if (m !== null) {
log(LOG_DEBUG,'Got input field: '+JSON.stringify(m));
log(LOG_DEBUG,'ansi:'+JSON.stringify(ansi));
// full string that matched
match = m.shift();
// thus, the rest of the line
line = line.substr(match.length);
//writeln('rest of line:'+JSON.stringify(line));
// We are interested in our field match
var sos = m.shift().split(';');
//writeln('sos:'+JSON.stringify(sos));
for (var num in sos) {
switch (num) {
// First value is the field name
case '0':
field = sos[num];
break;
// Second value is the length/type of the field, nnX nn=size in chars, T=type (lower case)
case '1':
var c = sos[num].match(/([0-9]+)([a-z])/);
if (! c) {
log(LOG_ERROR,'SOS FAILED PARSING FIELD LENGTH/TYPE. ['+r+'x'+c+'] '+sos[num]);
break;
}
//log(LOG_DEBUG,'SOS ['+r+'x'+c+'] NUM CHARS: '+x[1]+', TYPE: '+x[2]);
fieldlen = c[1];
fieldtype = c[2];
break;
// Third field is the char to to use
case '2':
fieldchar = sos[num];
break;
default:
log(LOG_ERROR,'IGNORING ADDITIONAL SOS FIELDS. ['+r+'x'+c+'] '+sos[num]);
}
}
// If we are padding our field with a char, we need to add that back to line
// @todo validate if this goes beyond our width (and if scrolling not enabled)
if (fieldlen)
line = fieldchar.repeat(fieldlen)+line;
frame.input_fields.push({
type: fieldtype,
length: Number(fieldlen),
char: fieldchar,
name: field,
attribute: JSON.parse(JSON.stringify(ansi)),
x: Number(x+(xoffset !== undefined ? xoffset : 0)),
y: Number(y+(yoffset !== undefined ? yoffset : 0)),
value: '',
});
log(LOG_DEBUG,'input_field:'+JSON.stringify(frame.input_fields.last));
}
/* parse dynamic value field */
// @todo remove the trailing ESC \ to end the field, just use a control code ie: ^E \x05 (Enquiry) or ^Z \x26 (Substitute)
var m = line.match(/^\x1bX(([a-zA-Z._:^;]+[0-9]?;-?[0-9^;]+)([;]?[^;]+)?)\x1b\\/);
if (m !== null) {
// full string that matched
match = m.shift();
// thus, the rest of the line
line = line.substr(match.length);
//writeln('rest of line:'+JSON.stringify(line));
// We are interested in our field match
var df = m.shift().split(';');
log(LOG_DEBUG,'- DF found at ['+x+'x'+y+'], Field: '+df[0]+', Length: '+df[1]+', Pad:'+df[2]);
// If we are padding our field with a char, we need to add that back to line
// @todo validate if this goes beyond our width (and if scrolling not enabled)
line = (df[2] ? df[2] : '_').repeat(Math.abs(df[1]))+line;
frame.dynamic_fields.push({
name: df[0],
length: df[1],
pad: df[2],
x: x+(xoffset !== undefined ? xoffset : 0),
y: y+(yoffset !== undefined ? yoffset : 0),
value: undefined,
});
}
/* set character and attribute */
var ch = line[0];
line = line.substr(1);
if (debug && debug === y) {
log(LOG_DEBUG,'Got char: '+ch);
//write(ch);
}
/* validate position */
if (y < 0)
y = 0;
if (x < 0)
x = 0;
if (x >= width) {
x = 0;
y++;
}
/* set character and attribute */
if (! frame.content[y+1])
frame.content[y+1]=[];
if (attr === null)
throw new Error('Attribute is null?');
frame.content[y+1][x+1] = new Char(ch,attr);
x++;
}
// If we got a BG_BLACK|LIGHTGRAY ESC [0m, but not character, we include it as it resets any background that was going on
if ((attr === BG_BLACK|LIGHTGRAY) && frame.content[y+1] && (frame.content[y+1][x].__properties__.attr !== attr))
frame.content[y+1][x+1] = new Char(undefined,attr);
y++;
}
return frame;
}
load('ansitex/load/session.js');
// Our frame object
function SessionAnsitex() {
Session.apply(this,arguments);
this.settings.MSG_SENDORNOT = '\1n\1h\1GKEY 1 TO SEND, 2 NOT TO SEND';
this.settings.MSG_LOGON = '\1n\1h\1GKEY 1 TO LOGON, 2 TO RETURN';
this.settings.MSG_SENT = '\1n\1h\1GMESSAGE SENT - KEY # TO CONTINUE';
this.settings.MSG_NOTSENT = '\1n\1h\1GMESSAGE NOT SENT - KEY # TO CONTINUE';
this.settings.ERR_NO_PARENT = '\1n\1h\1RPARENT FRAME DOESNT EXIST';
this.settings.ERR_NOT_IMPLEMENTED = '\1n\1h\1RNOT IMPLEMENTED YET?';
this.settings.ERR_ROUTE = '\1n\1h\1WMISTAKE? \1GTRY AGAIN OR TELL US ON *08';
this.settings.ERR_METHOD_NOT_EXIST = '\1n\1h\1WMISTAKE? \1GTRY AGAIN OR TELL US ON *08';
this.settings.ACCESS_DENIED = '\1n\1h\1RACCESS DENIED. MISTAKE? TRY AGAIN OR TELL US *08';
this.settings.ALREADY_MEMBER = '\1n\1h\1RALREADY MEMBER OF CUG'
this.settings.INACTIVITY = '\1n\1h\1RINACTIVITY ALERT, DISCONNECT PENDING...';
this.settings.INACTIVE = '\1n\1h\1RINACTIVITY DISCONNECT';
this.settings.NOACTION = '\1n\1h\1RNO ACTION PERFORMED';
this.settings.BASESTAR = '\1N\1G\1H*';
this.settings.INVALID_CODE = '\1n\1h\1RINVAID CODE, PLEASE TRY AGAIN *00';
this.settings.TOKEN_EMAIL = '\1n\1h\1RTOKEN EMAILED TO YOU...';
this.settings.TOKEN_SENT = '\1n\1h\1RTOKEN SENT, PLEASE ENTER TOKEN';
this.settings.INVALID_EMAIL = '\1n\1h\1RINVAID EMAIL, PLEASE TRY AGAIN *00';
this.settings.INVALID_UID = '\1n\1h\1RINVAID USER ID, PLEASE TRY AGAIN *00';
this.settings.CANNOT_SEND_TOKEN = '\1n\1h\1RCANNOT SEND VALIDATION CODE, PLEASE TRY AGAIN *00';
this.settings.USER_EXISTS = '\1n\1h\1RERROR USER EXISTS, PLEASE TRY AGAIN *00';
this.settings.USER_CREATE_ERROR = '\1n\1h\1RERROR CREATING USER, PLEASE TRY AGAIN *00';
this.settings.LOGIN_ERROR = '\1n\1h\1RERROR LOGGING IN, PLEASE TRY AGAIN *00';
this.settings.CANCEL_MSG = '\1n\1h\1GPRESS 2 TO CANCEL';
this.settings.SYS_ERROR = '\1n\1h\1RSYSTEM ERROR DETECTED - TRY AGAIN OR TELL US *08';
this.settings.LOADING = '\1n\1h\1Kloading...';
this.settings.PROCESSING = '\1n\1h\1Kprocessing...';
/**
* Set the attribute at the current position
*/
this.attr = function(field) {
write('\x1b['+field.i+';'+field.f+';'+field.b+'m');
}
this.baselineClear = function(reposition) {
this.cursorSave();
this.gotoxy(0,24);
this.cleareol();
if (! reposition)
this.cursorRestore();
}
/**
* Send a message to the baseline.
*
* @param text
* @param reposition
*/
this.baselineSend = function(text,reposition) {
this.cursorSave();
this.gotoxy(0,24);
write(this.getMessage(text));
this.cleareol();
if (! reposition)
this.cursorRestore();
}
/**
* Turn off the cursor
*/
this.cursorOff = function() {
write('\x1b[?25l');
write('\x1b[24;0H');
}
/**
* Turn on cursor
* @param x
* @param y
*/
this.cursorOn = function(x,y) {
write('\x1b[?25h');
if (x && y)
this.gotoxy(x,y);
}
this.cursorRestore = function() {
write('\x1b[u');
}
this.cursorSave = function() {
write('\x1b[s');
}
this.cleareol = function() {
write('\x1b[0K');
}
// Field backspace, that leaves the field filler char
this.fieldbs = function(char) {
write('\x1b[D'+char+'\x1b[D');
}
/**
* Position the cursor in a specific location
*
* @param x
* @param y
*/
this.gotoxy = function(x,y) {
write('\x1b['+y+';'+x+'H');
}
this.qrcode = function(qr,subframe) {
2022-12-09 06:19:33 +00:00
// SMALL Image
var full = ascii(0xdb);
var top = ascii(0xdf);
var bot = ascii(0xdc);
var blank = ' ';
var qrcode = '';
/*
// Render the top line
var line = ascii(27)+'[1;37m'+bot;
for (var y = 0; y < qr.size; y++) {
line += bot;
}
qrcode += line+bot+bot+ascii(27)+'[0m'+"\r\n";
*/
// Render the body
for (var x = -1; x < qr.size; x=x+2) {
line = ascii(27)+'[1;37m'+full;
for (var y = 0; y < qr.size; y++) {
// Top is white
if (! ((x===-1)? 0 : qr.getModule(x, y))) {
line += (qr.getModule(x+1, y)) ? top : full;
// Top is black
} else {
line += (qr.getModule(x+1, y)) ? blank : bot;
}
}
qrcode += line+full+ascii(27)+'[0m'+"\r\n";
}
// Render the bottom
line = ascii(27)+'[1;37m'+top;
for (var y = 0; y < qr.size; y++) {
line += top;
}
qrcode += line+top+ascii(27)+'[0m'+"\r\n";
ans2bin(fo.parse(qrcode),subframe);
subframe.open();
subframe.cycle();
};
}
SessionAnsitex.prototype = Session.prototype;
SessionAnsitex.prototype.constructor = SessionAnsitex;