433 lines
12 KiB
PHP
433 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Classes\BBS\Page;
|
|
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
use App\Classes\BBS\Frame\{Char,Field};
|
|
use App\Classes\BBS\Page;
|
|
use App\Models\BBS\Mode;
|
|
|
|
class Ansi extends Page
|
|
{
|
|
protected const FRAME_WIDTH = 80;
|
|
protected const FRAME_HEIGHT = 22;
|
|
protected const FRAME_PROVIDER_LENGTH = 55;
|
|
protected const FRAME_PAGE_LENGTH = 17; // Full space for page number + space at beginning (as would be displayed by viewdata)
|
|
protected const FRAME_COST_LENGTH = 8; // Full space for cost + space at beginning (as would be displayed by viewdata)
|
|
protected const FRAME_SPACE = 1; // Since colors dont take a space, this is to buffer a space
|
|
|
|
public const ESC = 27;
|
|
public const I_CLEAR_CODE = 0;
|
|
public const I_HIGH_CODE = 1;
|
|
public const I_BLINK_CODE = 5;
|
|
public const FG_WHITE_CODE = self::FG_LIGHTGRAY_CODE;
|
|
public const FG_YELLOW_CODE = self::FG_BROWN_CODE;
|
|
public const FG_BLACK_CODE = 30;
|
|
public const FG_RED_CODE = 31;
|
|
public const FG_GREEN_CODE = 32;
|
|
public const FG_BROWN_CODE = 33;
|
|
public const FG_BLUE_CODE = 34;
|
|
public const FG_MAGENTA_CODE = 35;
|
|
public const FG_CYAN_CODE = 36;
|
|
public const FG_LIGHTGRAY_CODE = 37;
|
|
public const BG_BLACK_CODE = 40;
|
|
public const BG_RED_CODE = 41;
|
|
public const BG_GREEN_CODE = 42;
|
|
public const BG_BROWN_CODE = 43;
|
|
public const BG_YELLOW_CODE = self::BG_BROWN_CODE;
|
|
public const BG_BLUE_CODE = 44;
|
|
public const BG_MAGENTA_CODE = 45;
|
|
public const BG_CYAN_CODE = 46;
|
|
public const BG_LIGHTGRAY_CODE = 47;
|
|
|
|
public static function strlenv($text): int
|
|
{
|
|
return strlen($text ? preg_replace('/'.ESC.'\[[0-9;?]+[a-zA-Z]/','',$text) : $text);
|
|
}
|
|
|
|
public function __construct(int $frame,string $index='a')
|
|
{
|
|
parent::__construct($frame,$index);
|
|
|
|
$this->mo = Mode::where('name','Ansi')->single();
|
|
}
|
|
|
|
public function __get(string $key): mixed
|
|
{
|
|
switch ($key) {
|
|
case 'color_page':
|
|
return sprintf(" %s[%d;%dm",chr(self::ESC),self::I_HIGH_CODE,self::FG_WHITE_CODE);
|
|
case 'color_unit':
|
|
return sprintf(" %s[%d;%dm",chr(self::ESC),self::I_HIGH_CODE,self::FG_GREEN_CODE);
|
|
|
|
default:
|
|
return parent::__get($key);
|
|
}
|
|
}
|
|
|
|
public function attr(array $field): string
|
|
{
|
|
return sprintf('%s[%d;%d;%dm',ESC,$field['i'],$field['f'],$field['b']);
|
|
}
|
|
|
|
/**
|
|
* 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 string $contents Our ANSI content to convert
|
|
* @param int $width Canvas width before we wrap to the next line
|
|
* @param int $yoffset fields offset when rendered (based on main window)
|
|
* @param int $xoffset fields offset when rendered (based on main window)
|
|
* @param int|null $debug Enable debug mode
|
|
* @return array
|
|
* @throws \Exception
|
|
*/
|
|
public function parse(string $contents,int $width,int $yoffset=0,int $xoffset=0,?int $debug=NULL): array
|
|
{
|
|
$result = [];
|
|
|
|
$lines = collect(explode("\r\n",$contents));
|
|
if ($debug)
|
|
dump(['lines'=>$lines]);
|
|
|
|
$i = 0; // Intensity
|
|
$bg = self::BG_BLACK; // Background color
|
|
$fg = self::LIGHTGRAY; // Foreground color
|
|
$attr = $fg + $bg + $i; // Attribute int
|
|
$default = ['i'=>0,'f'=>self::FG_LIGHTGRAY_CODE,'b'=>self::BG_BLACK_CODE];
|
|
|
|
$y = 0; // Line
|
|
$saved_x = NULL; // Cursor saved
|
|
$saved_y = NULL; // Cursor saved
|
|
|
|
$ansi = $default; // Our current attribute used for input fields
|
|
|
|
while ($lines->count() > 0) {
|
|
$x = 0;
|
|
$line = $lines->shift();
|
|
|
|
$result[$y+1] = [];
|
|
|
|
if ($this->debug) dump(['next line'=>$line,'length'=>strlen($line)]);
|
|
|
|
if (is_numeric($debug) && ($y > $debug)) {
|
|
dump(['exiting'=>serialize($debug)]);
|
|
exit(1);
|
|
}
|
|
|
|
while (strlen($line) > 0) {
|
|
if ($debug)
|
|
dump(['y:'=>$y,'attr'=>$attr,'line'=>$line,'length'=>strlen($line)]);
|
|
|
|
if ($x >= $width) {
|
|
$x = 0;
|
|
$y++;
|
|
}
|
|
|
|
/* parse an attribute sequence*/
|
|
$m = [];
|
|
preg_match('/^\x1b\[((\d+)+(;(\d+)+)*)m/U',$line,$m);
|
|
if (count($m)) {
|
|
$line = substr($line,strlen(array_shift($m)));
|
|
|
|
// Are values separated by ;
|
|
$m = array_map(function($item) { return (int)$item; },explode(';',$m[0]));
|
|
// Sort our numbers
|
|
sort($m);
|
|
|
|
// Reset
|
|
if ($m[0] === self::I_CLEAR_CODE) {
|
|
$bg = self::BG_BLACK;
|
|
$fg = self::LIGHTGRAY;
|
|
$i = 0;
|
|
$ansi = $default;
|
|
array_shift($m);
|
|
}
|
|
|
|
// High Intensity
|
|
if (count($m) && ($m[0] === self::I_HIGH_CODE)) {
|
|
$i += ((($i === 0) || ($i === self::BLINK)) ? self::HIGH : 0);
|
|
$ansi['i'] = self::I_HIGH_CODE;
|
|
array_shift($m);
|
|
}
|
|
|
|
// Blink
|
|
if (count($m) && ($m[0] === self::I_BLINK_CODE)) {
|
|
$i += ((($i === 0) || ($i === self::HIGH)) ? self::BLINK : 0);
|
|
array_shift($m);
|
|
}
|
|
|
|
// Foreground
|
|
if (count($m) && ($m[0] >= self::FG_BLACK_CODE) && ($m[0] <= self::FG_LIGHTGRAY_CODE)) {
|
|
$ansi['f'] = $m[0];
|
|
|
|
switch (array_shift($m)) {
|
|
case self::FG_BLACK_CODE:
|
|
$fg = self::BLACK;
|
|
break;
|
|
|
|
case self::FG_RED_CODE:
|
|
$fg = self::RED;
|
|
break;
|
|
|
|
case self::FG_GREEN_CODE:
|
|
$fg = self::GREEN;
|
|
break;
|
|
|
|
case self::FG_YELLOW_CODE:
|
|
$fg = self::BROWN;
|
|
break;
|
|
|
|
case self::FG_BLUE_CODE:
|
|
$fg = self::BLUE;
|
|
break;
|
|
|
|
case self::FG_MAGENTA_CODE:
|
|
$fg = self::MAGENTA;
|
|
break;
|
|
|
|
case self::FG_CYAN_CODE:
|
|
$fg = self::CYAN;
|
|
break;
|
|
|
|
case self::FG_LIGHTGRAY_CODE:
|
|
$fg = self::LIGHTGRAY;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Background
|
|
if (count($m) && ($m[0] >= self::BG_BLACK_CODE) && ($m[0] <= self::BG_LIGHTGRAY_CODE)) {
|
|
$ansi['b'] = $m[0];
|
|
|
|
switch (array_shift($m)) {
|
|
case self::BG_BLACK_CODE:
|
|
$bg = self::BG_BLACK;
|
|
break;
|
|
|
|
case self::BG_RED_CODE:
|
|
$bg = self::BG_RED;
|
|
break;
|
|
|
|
case self::BG_GREEN_CODE:
|
|
$bg = self::BG_GREEN;
|
|
break;
|
|
|
|
case self::BG_BROWN_CODE:
|
|
$bg = self::BG_BROWN;
|
|
break;
|
|
|
|
case self::BG_BLUE_CODE:
|
|
$bg = self::BG_BLUE;
|
|
break;
|
|
|
|
case self::BG_MAGENTA_CODE:
|
|
$bg = self::BG_MAGENTA;
|
|
break;
|
|
|
|
case self::BG_CYAN_CODE:
|
|
$bg = self::BG_CYAN;
|
|
break;
|
|
|
|
case self::BG_LIGHTGRAY_CODE:
|
|
$bg = self::BG_LIGHTGRAY;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$attr = $bg + $fg + $i;
|
|
continue;
|
|
}
|
|
|
|
/* parse absolute character position */
|
|
$m = [];
|
|
preg_match('/^\x1b\[(\d*);?(\d*)[Hf]/',$line,$m);
|
|
if (count($m)) {
|
|
dump(['Hf'=>$m]); // @todo Remove once validated
|
|
$line = substr($line,strlen(array_shift($m)));
|
|
|
|
$y = (int)array_shift($m);
|
|
|
|
if (count($m))
|
|
$x = (int)array_shift($m)-1;
|
|
|
|
continue;
|
|
}
|
|
|
|
/* ignore an invalid sequence */
|
|
$m = [];
|
|
preg_match('/^\x1b\[\?7h/',$line,$m);
|
|
if (count($m)) {
|
|
$line = substr($line,strlen(array_shift($m)));
|
|
continue;
|
|
}
|
|
|
|
/* parse positional sequences */
|
|
$m = [];
|
|
preg_match('/^\x1b\[(\d+)([A-D])/',$line,$m);
|
|
if (count($m)) {
|
|
$line = substr($line,strlen(array_shift($m)));
|
|
|
|
switch ($m[1]) {
|
|
/* parse an up positional sequence */
|
|
case 'A':
|
|
$y -= ($m[0] < 1) ? 0 : $m[0];
|
|
break;
|
|
|
|
/* parse a down positional sequence */
|
|
case 'B':
|
|
$y += ($m[0] < 1) ? 0 : $m[0];
|
|
break;
|
|
|
|
/* parse a forward positional sequence */
|
|
case 'C':
|
|
$x += ($m[0] < 1) ? 0 : $m[0];
|
|
break;
|
|
|
|
/* parse a backward positional sequence */
|
|
case 'D':
|
|
$x -= ($m[0] < 1) ? 0 : $m[0];
|
|
break;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
/* parse a clear screen sequence - we ignore them */
|
|
$m = [];
|
|
preg_match('/^\x1b\[2J/',$line,$m);
|
|
if (count($m)) {
|
|
$line = substr($line,strlen(array_shift($m)));
|
|
|
|
continue;
|
|
}
|
|
|
|
/* parse cursor sequences */
|
|
$m = [];
|
|
preg_match('/^\x1b\[([su])/',$line,$m);
|
|
if (count($m)) {
|
|
$line = substr($line,strlen(array_shift($m)));
|
|
|
|
switch ($m[0]) {
|
|
/* parse save cursor sequence */
|
|
case 's':
|
|
$saved_x = $x;
|
|
$saved_y = $y;
|
|
break;
|
|
|
|
/* parse restore cursor sequence */
|
|
case 'u':
|
|
$x = $saved_x;
|
|
$y = $saved_y;
|
|
break;
|
|
}
|
|
|
|
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
|
|
$m = [];
|
|
preg_match('/^\x1b_([A-Z]+;[0-9a-z]+)([;]?.+)?\x1b\\\/',$line,$m);
|
|
if (count($m)) {
|
|
$line = substr($line,strlen(array_shift($m)));
|
|
|
|
// We are interested in our field match
|
|
$f = explode(';',array_shift($m));
|
|
|
|
// First value is the field name
|
|
$field = array_shift($f);
|
|
|
|
// Second value is the length/type of the field, nnX nn=size in chars, X=type (lower case)
|
|
$c = [];
|
|
preg_match('/([0-9]+)([a-z])/',$xx=array_shift($f),$c);
|
|
if (! count($c)) {
|
|
Log::channel('bbs')->alert(sprintf('! IF FAILED PARSING FIELD LENGTH/TYPE [%02dx%02d] (%s)',$y,$x,$xx));
|
|
break;
|
|
}
|
|
|
|
// Third field is the char to use
|
|
$fieldpad = count($f) ? array_shift($f) : '.';
|
|
Log::channel('bbs')->info(sprintf('- IF [%02dx%02d], Field: [%s], Length: [%d], Char: [%s]',$y,$x,$c[2],$c[1],$fieldpad));
|
|
|
|
// Any remaining fields are junk
|
|
if (count($f))
|
|
Log::channel('bbs')->alert(sprintf('! IGNORING ADDITIONAL IF FIELDS [%02dx%02d] (%s)',$y,$x,join('',$f)));
|
|
|
|
// 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 ($c[1])
|
|
$line = str_repeat($fieldpad,$c[1]).$line;
|
|
|
|
$this->fields_input->push(new Field([
|
|
'attribute' => $ansi,
|
|
'name' => $field,
|
|
'pad' => $fieldpad,
|
|
'size' => $c[1],
|
|
'type' => $c[2],
|
|
'value' => NULL,
|
|
'x' => $x+$xoffset,
|
|
'y' => $y+$yoffset,
|
|
]));
|
|
}
|
|
|
|
/* 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)
|
|
$m = [];
|
|
preg_match('/^\x1bX([a-zA-Z._:^;]+[0-9]?;-?[0-9^;]+)([;]?[^;]+)?\x1b\\\/',$line,$m);
|
|
if (count($m)) {
|
|
$line = substr($line,strlen(array_shift($m)));
|
|
|
|
// We are interested in our field match
|
|
$f = explode(';',array_shift($m));
|
|
$pad = Arr::get($f,2,' ');
|
|
|
|
Log::channel('bbs')->info(sprintf('- DF [%02dx%02d], Field: [%s], Length: [%d], Char: [%s]',$y,$x,$f[0],$f[1],$pad));
|
|
// 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 = str_repeat($pad,abs($f[1])).$line;
|
|
|
|
$this->fields_dynamic->push(new Field([
|
|
'name' => $f[0],
|
|
'pad' => $pad,
|
|
'type' => NULL,
|
|
'size' => $f[1],
|
|
'value' => NULL,
|
|
'x' => $x+$xoffset,
|
|
'y' => $y+$yoffset,
|
|
]));
|
|
}
|
|
|
|
/* set character and attribute */
|
|
$ch = $line[0];
|
|
$line = substr($line,1);
|
|
|
|
/* validate position */
|
|
if ($y < 0)
|
|
$y = 0;
|
|
if ($x < 0)
|
|
$x = 0;
|
|
|
|
if ($attr === null)
|
|
throw new \Exception('Attribute is null?');
|
|
|
|
$result[$y+1][$x+1] = new Char($ch,$attr);
|
|
|
|
$x++;
|
|
}
|
|
|
|
// If we got a self::BG_BLACK|self::LIGHTGRAY ESC [0m, but not character, we include it as it resets any background that was going on
|
|
if (($attr === self::BG_BLACK|self::LIGHTGRAY) && isset($result[$y+1][$x]) && ($result[$y+1][$x]->attr !== $attr))
|
|
$result[$y+1][$x+1] = new Char(NULL,$attr);
|
|
|
|
$y++;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
} |