clrghouz/app/Classes/BBS/Page/Ansi.php
2023-09-13 20:59:40 +10:00

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;
}
}