Added ANSI parsers and rendering ANSI frames

This commit is contained in:
Deon George 2018-12-14 00:02:42 +11:00
parent e0306908bd
commit 6cc793c47f
14 changed files with 333 additions and 119 deletions

View File

@ -35,7 +35,8 @@ use App\Models\CUG;
abstract class Frame
{
protected $frame = NULL;
protected $output = NULL;
protected $output = '';
protected $startline = 1;
// All this vars should be overridden in the child class
/*
@ -71,17 +72,11 @@ abstract class Frame
// @todo Move this to the database
private $header = RED.'T'.BLUE.'E'.GREEN.'S'.YELLOW.'T'.MAGENTA.'!';
public function __construct(\App\Models\Frame $o,string $msg=NULL)
public function __construct(\App\Models\Frame $o)
{
$this->frame = $o;
$this->output = $this->hasFlag('clear') ? CLS : HOME;
// If we have a message to display on the bottom line.
if ($msg)
$this->output .= UP.$msg.HOME;
$startline = 0;
$this->output = $this->frame->cls ? CLS : HOME;
if (! $this->hasFlag('ip') AND (! $this->isCUG(0) OR $this->type() !== self::FRAMETYPE_LOGIN)) {
// Set the page header: CUG/Site Name | Page # | Cost
@ -89,12 +84,16 @@ abstract class Frame
$this->render_page($this->frame->frame,$this->frame->index).
$this->render_cost($this->frame->cost);
$startline = 1;
$this->startline = 2;
} elseif ($this->isCUG(0) AND $this->type() === self::FRAMETYPE_LOGIN) {
$this->startline = 2;
$this->output .= str_repeat(DOWN,$this->startline-1);
}
// Calculate fields and render output.
$this->fields = collect(); // Fields in this frame.
$this->fields($startline);
$this->fields = collect(); // Fields in this frame.
$this->fields($this->startline);
}
/**
@ -403,17 +402,18 @@ abstract class Frame
$o->index = 'a';
$o->access = 1;
$o->closed = 0;
$o->cls = 1;
// Header
$sid = R_RED.'T'.R_BLUE.'E'.R_GREEN.'S'.R_YELLOW.'T';
$o->content .= substr($sid.'-'.str_repeat('12345678901234567890',4),0,static::$header_length+(strlen($sid)-$so->strlenv($sid))).
R_WHITE.'999999999a'.R_RED.sprintf('%07.0f',999).'u';
$o->content .= str_repeat('+-',18).' '.R_RED.'01';
$o->content .= 'Name: '.ESC.str_repeat('u',5).str_repeat('+-',14);
$o->content .= 'Date: '.ESC.str_repeat('d',25).str_repeat('+-',4);
$o->content .= 'Address: '.ESC.str_repeat('a',19).' '.str_repeat('+-',5);
$o->content .= ' : '.ESC.str_repeat('a',19).' '.str_repeat('+-',5);
$o->content .= R_WHITE.str_repeat('+-',static::$frame_width/2-3).' '.R_RED.'01';
$o->content .= R_WHITE.'Name: '.ESC.str_repeat('u',5).' |'.str_repeat('+-',static::$frame_width/2-8).'|';
$o->content .= R_WHITE.'Date: '.ESC.str_repeat('d',17).' |'.str_repeat('+-',static::$frame_width/2-14).'|';
$o->content .= R_WHITE.'Address: '.ESC.str_repeat('t',19).' |'.str_repeat('+-',static::$frame_width/2-17).'|';
$o->content .= R_WHITE.' : '.ESC.str_repeat('t',19).' |'.str_repeat('+-',static::$frame_width/2-17).'|';
return $o;
}

View File

@ -46,6 +46,7 @@ class Login extends Action
return FALSE;
}
$this->so->log('info','User Login: '.$this->uo->name);
$this->page = ['frame'=>1,'index'=>'a']; // @todo Get from DB.
$this->action = 2; // ACTION_GOTO

View File

@ -5,6 +5,7 @@ namespace App\Classes\Frame;
use Illuminate\Support\Facades\Log;
use App\Classes\Frame as AbstractFrame;
use App\Classes\Parser\Ansi as AnsiParser;
class Ansi extends AbstractFrame
{
@ -18,9 +19,21 @@ class Ansi extends AbstractFrame
public static $if_filler = '.';
public function fields($startline=0)
public function __construct(\App\Models\Frame $o,string $msg='')
{
$this->output .= str_replace(LF,CR.LF,$this->frame->content);
parent::__construct($o);
// If we have a message to display on the bottom line.
if ($msg)
$this->output .= ESC.'[24;0f'.$msg.HOME;
}
public function fields($startline=1)
{
$o = new AnsiParser($this->frame->content,$startline);
$this->output .= (string)$o;
$this->fields = $o->fields;
}
public function strlenv($text):int {

View File

@ -19,26 +19,32 @@ class Videotex extends AbstractFrame
public static $if_filler = '.';
public function fields($startline=0)
public function __construct(\App\Models\Frame $o,string $msg='')
{
parent::__construct($o);
// If we have a message to display on the bottom line.
if ($msg)
$this->output .= HOME.UP.$msg.HOME;
}
public function fields($startline=1)
{
$infield = FALSE; // In a field
$fieldtype = NULL; // Type of field
$fieldlength = 0; // Length of field
if ($startline)
$this->output .= str_repeat(DOWN,$startline);
// $fieldadrline = 1;
// Scan the frame for a field start
for ($y=$startline;$y<=static::$frame_length;$y++)
for ($y=$startline-1;$y<=static::$frame_length;$y++)
{
// Fields can only be on a single line
$fieldx = $fieldy = FALSE;
for ($x=0;$x<static::$frame_width;$x++)
{
$posn = $y*40+$x;
$posn = $y*static::$frame_width+$x;
// If the frame is not big enough, fill it with spaces.
$byte = ord(isset($this->frame->content{$posn}) ? $this->frame->content{$posn} : ' ')%128;
@ -62,7 +68,6 @@ class Videotex extends AbstractFrame
}
// Is this a magic field?
// @todo For page redisplay *00, we should show entered contents - for refresh *09 we should show updated contents
if (array_get($this->fieldmap,chr($fieldtype)) ) {
$field = $this->fieldmap[chr($fieldtype)];
//dump(['infield','byte'=>$byte,'fieldtype'=>$fieldtype,'field'=>$field,'strpos'=>strpos($field,'#')]);

View File

@ -15,4 +15,17 @@ class FrameFields
{
return array_get($this->fields,$key);
}
public function output(string $filler)
{
switch ($this->type) {
case 'd':
$out = date('D d M H:ia');
return substr($out.($this->length > strlen($out) ? str_repeat($filler,$this->length-strlen($out)) : ''),0,$this->length);
default:
return str_repeat($filler,$this->length);
}
}
}

7
app/Classes/Parser.php Normal file
View File

@ -0,0 +1,7 @@
<?php
namespace App\Classes;
class Parser
{
}

156
app/Classes/Parser/Ansi.php Normal file
View File

@ -0,0 +1,156 @@
<?php
namespace App\Classes\Parser;
use Illuminate\Support\Facades\Log;
use App\Classes\FrameFields;
use App\Classes\Parser as AbstractParser;
use App\Classes\Frame\Ansi as AnsiFrame;
class Ansi extends AbstractParser {
private $content = '';
private $startline = 0;
public $fields = NULL;
public function __construct(string $content,int $startline=1)
{
$this->content = $content;
$this->startline = $startline;
$this->fields = collect();
}
public function __toString(): string
{
return $this->parse($this->startline);
}
/**
* Parse a string and look for the next character that is not $char
*
* @param string $char
* @param int $start
* @return bool|int
*/
private function findEOF(string $char,int $start)
{
for ($c=$start;$c <= strlen($this->content);$c++)
{
if ($this->content{$c} != $char)
return $c-$start;
}
return FALSE;
}
/**
* @param $startline
* @param int $offset
* @return string
*/
private function parse($startline): string
{
// Our starting coordinates
$x = 1;
$y = $startline;
$output = '';
// Scan the frame for a field start
for ($c=0; $c<=strlen($this->content); $c++)
{
// If the frame is not big enough, fill it with spaces.
$byte = isset($this->content{$c}) ? $this->content{$c} : ' ';
$advance = 0;
switch ($byte) {
case CR:
$x = 1;
break;
case LF:
$y++;
break;
case ESC:
$advance = 1;
// Is the next byte something we know about
$nextbyte = isset($this->content{$c+$advance}) ? $this->content{$c+$advance} : ' ';
switch ($nextbyte) {
case '[':
$advance++;
$chars = $nextbyte;
// Find our end CSI param
$matches = [];
$a = preg_match('/([0-9]+[;]?)+([a-zA-Z])/',$this->content,$matches,NULL,$c+$advance);
if (! $a)
break;
$advance += strlen($matches[0])-1;
$chars .= $matches[0];
switch ($matches[2]) {
// We ignore 'm' they are color CSIs
case 'm': break;
case 'C':
$x += $matches[1]; // Advance our position
break;
default:
dump('Unhandled CSI: '.$matches[2]);
}
break;
case ' ':
dump(['l'=>__LINE__,'LOOSE ESC?']);
break;
default:
$c--; // Allow for the original ESC
$advance++;
$fieldtype = ord($nextbyte)%128; // @todo Do we need the %128 for ANSI?
$fieldlength = $this->findEOF(chr($fieldtype),$c+1)+1;
$byte = '';
$this->fields->push(new FrameFields([
'type'=>chr($fieldtype),
'length'=>$fieldlength,
'x'=>$x, // Adjust for the ESC char
'y'=>$y,
]));
Log::debug(sprintf('Field found at [%s,%s], Type: %s, Length: %s',$x-1,$y,$fieldtype,$fieldlength));
$advance += $fieldlength-2;
$x += $fieldlength;
$chars = $this->fields->last()->output(AnsiFrame::$if_filler);
}
break;
default:
$x++;
}
$output .= $byte;
if ($advance) {
$output .= $chars;
$c += $advance;
}
if ($x > 80) {
$x = 1;
$y++;
}
}
return $output;
}
}

View File

@ -23,7 +23,7 @@ abstract class Server {
$this->mo = $o;
define('MODE_BL', 1); // Typing a * command on the baseline
define('MODE_FIELD', 2); // typing into an imput field
define('MODE_FIELD', 2); // typing into an input field
define('MODE_WARPTO', 3); // awaiting selection of a timewarp
define('MODE_COMPLETE', 4); // Entry of data is complete ..
define('MODE_SUBMITRF', 5); // asking if should send or not.
@ -64,12 +64,12 @@ abstract class Server {
define('TCP_OPT_LINEMODE', chr(34));
define('MSG_SENDORNOT', GREEN.'KEY 1 TO SEND, 2 NOT TO SEND');
define('MSG_SENT', GREEN.'MESSAGE SENT - KEY _ TO CONTINUE');
define('MSG_NOTSENT', GREEN.'MESSAGE NOT SENT - KEY _ TO CONTINUE');
define('MSG_SENT', GREEN.'MESSAGE SENT - KEY '.HASH.' TO CONTINUE');
define('MSG_NOTSENT', GREEN.'MESSAGE NOT SENT - KEY '.HASH.' TO CONTINUE');
define('ERR_DATABASE', RED.'UNAVAILABLE AT PRESENT - PLSE TRY LATER');
define('ERR_NOTSENT', WHITE.'MESSAGE NOT SENT DUE TO AN ERROR');
define('ERR_PRIVATE', WHITE.'PRIVATE PAGE'.GREEN.'- FOR EXPLANATION *37_..');
define('ERR_PRIVATE', WHITE.'PRIVATE PAGE'.GREEN.'- FOR EXPLANATION *37'.HASH.'..');
define('ERR_ROUTE', WHITE.'MISTAKE?'.GREEN.'TRY AGAIN OR TELL US ON *08');
define('ERR_PAGE',ERR_ROUTE);
define('ERR_USER_ALREADYMEMBER', RED.'ALREADY MEMBER OF CUG');
@ -143,16 +143,17 @@ abstract class Server {
// @todo Get the login/start page, and if it is not available, throw the ERR_DATEBASE error.
if (isset($config['loginpage'])) {
$page = ['frame'=>$config['loginpage'],'index'=>'a'];
$page = ['frame'=>$config['loginpage']];
} else if (!empty($service['start_page'])) {
$page = ['frame'=>$service['start_page'],'index'=>'a'];
$page = ['frame'=>$service['start_page']];
} else {
$page = ['frame'=>'980','index'=>'a']; // next page
$page = ['frame'=>'980']; // next page
}
while ($action != ACTION_TERMINATE) {
// Read a character from the client session
$read = $client->read(1);
printf(". Got: %s (%s)\n",$read,ord($read));
// It appears that read will return '' instead of false when a disconnect has occurred.
// We'll set it to NULL so its caught later
@ -181,6 +182,7 @@ abstract class Server {
case TCP_SE:
$session_option = $session_init = FALSE;
$this->log('debug',sprintf('Session Terminal: %s',$session_term));
$read = '';
break;
@ -258,7 +260,7 @@ abstract class Server {
if ($current['field']->type == 'u' AND array_get($fielddata,$current['fieldnum']) == 'NEW')
{
$action = ACTION_GOTO;
$page = ['frame'=>'981','index'=>'a']; // @todo This should be in the DB.
$page = ['frame'=>'981']; // @todo This should be in the DB.
}
}
@ -269,6 +271,7 @@ abstract class Server {
case Frame::FRAMETYPE_ACTION:
switch ($read) {
// End of field entry.
case LF:
case HASH:
// Next Field
$current['fieldnum']++;
@ -391,11 +394,10 @@ abstract class Server {
$route = $fo->route(1);
if ($route == '*' OR is_numeric($route)) {
$this->sendBaseline($client, RED . 'NO action performed');
$this->sendBaseline($client,RED.'NO ACTION PERFORMED');
$mode = MODE_RFSENT;
} elseif ($ao = FrameClass\Action::factory($fo->route(1),$this,$user,$action,$mode)) {
$ao->handle($fielddata);
$mode = $ao->mode;
$action = $ao->action;
@ -431,28 +433,22 @@ abstract class Server {
$client->send(COFF);
if ($read == HASH) {
if (! empty($pagedata['route1'])) {
$action = ACTION_GOTO;
$page['frame'] = $pagedata['route1'];
$page['index'] = 'a';
if ($route = $fo->route(2) AND $route !== '*' AND is_numeric($route)) {
$page = ['frame'=>$route];
} elseif (FrameModel::where('frame',$fo->frame())->where('index',$fo->index_next())->exists()) {
$action = ACTION_GOTO;
$page['frame'] = array_get($current,'page.frame');
$page['index'] = chr(1 + ord($page['index']));
$page = ['frame'=>$fo->frame(),'index'=>$fo->index_next()];
} elseif (! empty($pagedata['route0'])) {
$action = ACTION_GOTO;
$page['frame'] = $pagedata['route0'];
$page['index'] = 'a';
} elseif ($route = $fo->route(0) AND $route !== '*' AND is_numeric($route)) {
$page = ['frame'=>$route];
// No further routes defined, go home.
// No further routes defined, go home.
} else {
$action = ACTION_GOTO;
$page['frame'] = '0';
$page['index'] = 'a';
$page = ['frame'=>0];
}
$action = ACTION_GOTO;
} elseif ($read == STAR) {
$action = ACTION_STAR;
@ -469,28 +465,22 @@ abstract class Server {
$client->send(COFF);
if ($read == HASH) {
if (! empty($pagedata['route2'])) {
$action = ACTION_GOTO;
$page['frame'] = $pagedata['route2'];
$page['index'] = 'a';
if ($route = $fo->route(2) AND $route !== '*' AND is_numeric($route)) {
$page = ['frame'=>$route];
} elseif (FrameModel::where('frame',$fo->frame())->where('index',$fo->index_next())->exists()) {
$action = ACTION_GOTO;
$page['frame'] = $fo->frame();
$page['index'] = $fo->index_next();
$page = ['frame'=>$fo->frame(),'index'=>$fo->index_next()];
} elseif (! empty($pagedata['route0'])) {
$action = ACTION_GOTO;
$page['frame'] = $pagedata['route0'];
$page['index'] = 'a';
} elseif ($route = $fo->route(0) AND $route !== '*' AND is_numeric($route)) {
$page = ['frame'=>$route];
// No further routes defined, go home.
// No further routes defined, go home.
} else {
$action = ACTION_GOTO;
$page['frame'] = '0';
$page['index'] = 'a';
$page = ['frame'=>0];
}
$action = ACTION_GOTO;
} elseif ($read == STAR) {
$action = ACTION_STAR;
@ -554,7 +544,7 @@ abstract class Server {
// Currently accepting baseline input after a * was received
case MODE_BL:
echo "was waiting for page number\n";
echo "- Waiting for Page Number\n";
// if it's a number, continue entry
if (strpos('0123456789', $read) !== FALSE) {
@ -623,7 +613,7 @@ abstract class Server {
}
// Complete request
if ($read === HASH) {
if ($read === HASH or $read === LF) {
$client->send(COFF);
$timewarpalt = FALSE;
@ -657,21 +647,22 @@ abstract class Server {
// This section performs some action if it is deemed necessary
if ($action) {
echo "Performing action $action\n";
printf("+ Performing action: %s\n",$action);
}
switch ($action) {
case ACTION_STAR:
echo " star command started\n";
$this->sendBaseline($client,GREEN.STAR,true);
echo "+ Star command...\n";
$this->sendBaseline($client,GREEN.STAR,TRUE);
$client->send(CON);
$action = false;
$action = FALSE;
$mode = MODE_BL;
break;
case ACTION_SUBMITRF:
$action = false;
$action = FALSE;
$client->send(COFF);
$this->sendBaseline($client,MSG_SENDORNOT);
$mode = MODE_SUBMITRF;
@ -705,7 +696,7 @@ abstract class Server {
try {
$fo = $timewarpalt
? $this->mo->frame(FrameModel::findOrFail($timewarpalt))
: $this->mo->frameLoad($page['frame'],$page['index'],$this);
: $this->mo->frameLoad($page['frame'],array_get($page,'index','a'),$this);
$this->log('debug',sprintf('Fetched frame: %s (%s)',$fo->id(),$fo->page()));
@ -784,19 +775,8 @@ abstract class Server {
// drop into
case ACTION_RELOAD:
// @todo Move this $output into the object.
if ($fo->hasFlag('clear'))
{
$this->blp = 0;
$output = CLS;
} else {
$output = HOME;
// Clear the baseline.
$this->sendBaseline($client,'');
}
$output .= (string)$fo;
$this->sendBaseline($client,'');
$output = (string)$fo;
if ($timewarpalt) {
$this->sendBaseline($client,sprintf(MSG_TIMEWARP_TO,$fo->created() ? $fo->created()->format('Y-m-d H:i:s') : 'UNKNOWN'));
@ -922,21 +902,7 @@ abstract class Server {
/**
* Move the cursor via the shortest path.
*/
function outputPosition($x,$y) {
// Take the shortest path.
if ($y < 12) {
return HOME.
(($x < 21)
? str_repeat(DOWN,$y).str_repeat(RIGHT,$x)
: str_repeat(DOWN,$y+1).str_repeat(LEFT,40-$x));
} else {
return HOME.str_repeat(UP,24-$y).
(($x < 21)
? str_repeat(RIGHT,$x)
: str_repeat(LEFT,40-$x));
}
}
abstract function outputPosition($x,$y);
/**
* Send a message to the base line

View File

@ -16,8 +16,8 @@ class Ansi extends AbstractServer {
define('HOME', ESC.'[0;0f');
define('LEFT', ESC.'[D'); // Move Cursor
define('RIGHT', ESC.'[C'); // Move Cursor
define('DOWN', chr(10)); // Move Cursor
define('UP', chr(11)); // Move Cursor
define('DOWN', ESC.'[B'); // Move Cursor
define('UP', ESC.'[A'); // Move Cursor
define('CR', chr(13));
define('LF', chr(10));
define('CLS', ESC.'[2J');
@ -48,6 +48,10 @@ class Ansi extends AbstractServer {
parent::__construct($o);
}
function outputPosition($x,$y) {
return ESC.'['.$y.';'.$x.'f';
}
// Abstract function
public function sendBaseline($client,$text,$reposition=FALSE) {
$client->send(ESC.'[24;0f'.$text.

View File

@ -48,6 +48,22 @@ class Videotex extends AbstractServer {
parent::__construct($o);
}
public function outputPosition($x,$y) {
// Take the shortest path.
if ($y < 12) {
return HOME.
(($x < 21)
? str_repeat(DOWN,$y).str_repeat(RIGHT,$x)
: str_repeat(DOWN,$y+1).str_repeat(LEFT,40-$x));
} else {
return HOME.str_repeat(UP,24-$y).
(($x < 21)
? str_repeat(RIGHT,$x)
: str_repeat(LEFT,40-$x));
}
}
public function sendBaseline($client,$text,$reposition=FALSE) {
$client->send(HOME.UP.$text.
($this->blp > $this->strlenv($text)

View File

@ -61,6 +61,7 @@ class FrameImport extends Command
try {
$o = $o->where('frame',$this->argument('frame'))
->where('index',$this->argument('index'))
->where('mode_id',$this->option('mode'))
->firstOrFail();
} catch (ModelNotFoundException $e) {
@ -71,6 +72,7 @@ class FrameImport extends Command
} else {
$o->frame = $this->argument('frame');
$o->index = $this->argument('index');
$o->mode_id = $this->option('mode');
}
$o->content = ($this->option('trim'))
@ -80,7 +82,6 @@ class FrameImport extends Command
$o->access = $this->option('access');
$o->closed = $this->option('closed');
$o->cost = $this->option('cost');
$o->mode_id = $this->option('mode');
$o->type = $this->option('type');
$o->save();

View File

@ -44,11 +44,11 @@ class Mode extends Model
*
* @param int $frame
* @param string $index
* @param Server $so
* @return FrameClass
* @throws \Exception
*/
//@todo Move Server $so first
public function frameLoad(int $frame,string $index='a',Server $so): FrameClass
public function frameLoad(int $frame,string $index,Server $so): FrameClass
{
return $this->frame(
// Return our internal test frame.

View File

@ -16,19 +16,19 @@ class CreateFramemeta extends Migration
$this->down();
Schema::create('framemeta', function (Blueprint $table) {
$table->integer('frame_id')->primary();
$table->string('r0')->default('*');
$table->string('r1')->default('*');
$table->string('r2')->default('*');
$table->string('r3')->default('*');
$table->string('r4')->default('*');
$table->string('r5')->default('*');
$table->string('r6')->default('*');
$table->string('r7')->default('*');
$table->string('r8')->default('*');
$table->string('r9')->default('*');
$table->integer('frame_id')->primary();
$table->string('r0')->default('*');
$table->string('r1')->default('*');
$table->string('r2')->default('*');
$table->string('r3')->default('*');
$table->string('r4')->default('*');
$table->string('r5')->default('*');
$table->string('r6')->default('*');
$table->string('r7')->default('*');
$table->string('r8')->default('*');
$table->string('r9')->default('*');
$table->foreign('frame_id')->references('id')->on('frames');
$table->foreign('frame_id')->references('id')->on('frames');
});
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class FrameAddClear extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('frames', function (Blueprint $table) {
$table->boolean('cls')->default(TRUE);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('frames', function (Blueprint $table) {
$table->dropColumn('cls');
});
}
}